Feat/Add Project Scanner (#34)
* feat: support base_dir option for searching for git repos * style: fix indentation * fix: only close file if exists * fix: explicitly set to false if base_dir not set * refactor: always initialize project file * refactor: separate project extraction and project writing * fix: iterate with pairs and no arg for io.lines * refactor: restructure, test, and lint * refactor: use standard telescope file structure * refactor: utilize utility functions for actions module * refactor: support max_depth arg for recursive searching * chore: add Makefile for linting and testing * test: add utils general tests * refactor: do not need file_exists function * test: add tests for creating, writing and reading projects * chore: move location of tests * test: add tests for git project scanning * test: do not need minimal_init.vim now * test: clean up tests * refactor: update git projects on setup
This commit is contained in:
7
.luacheckrc
Normal file
7
.luacheckrc
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Rerun tests only if their modification time changed.
|
||||
cache = true
|
||||
|
||||
-- Global objects defined by the C code
|
||||
read_globals = {
|
||||
"vim",
|
||||
}
|
||||
7
Makefile
Normal file
7
Makefile
Normal file
@@ -0,0 +1,7 @@
|
||||
.PHONY: lint tests
|
||||
|
||||
lint:
|
||||
luacheck ./lua/telescope
|
||||
|
||||
tests:
|
||||
nvim --headless -c "PlenaryBustedDirectory lua/tests/"
|
||||
77
README.md
77
README.md
@@ -15,51 +15,78 @@ You can setup the extension by adding the following to your config:
|
||||
require'telescope'.load_extension('project')
|
||||
```
|
||||
|
||||
You may skip explicitly loading extensions (they will then be lazy-loaded), but tab completions will not be available right away.
|
||||
|
||||
## Available functions:
|
||||
|
||||
### Project
|
||||
|
||||
The projects picker.
|
||||
The `projects` picker:
|
||||
|
||||
```lua
|
||||
require'telescope'.extensions.project.project{}
|
||||
```
|
||||
|
||||
## Example config:
|
||||
## Default mappings (normal mode):
|
||||
|
||||
| Key | Description |
|
||||
|-----|---------------------------------------------------------------|
|
||||
| `d` | delete currently selected project |
|
||||
| `r` | rename currently selected project |
|
||||
| `c` | create a project\* |
|
||||
| `s` | search inside files within your project |
|
||||
| `b` | browse inside files within your project |
|
||||
| `w` | change to the selected project's directory without opening it |
|
||||
| `R` | find a recently opened file within your project |
|
||||
| `f` | find a file within your project (same as \<CR\>) |
|
||||
|
||||
\* *defaults to your git root if used inside a git project, otherwise, it will use your current working directory*
|
||||
|
||||
Example key map config:
|
||||
|
||||
```lua
|
||||
vim.api.nvim_set_keymap(
|
||||
'n',
|
||||
'<C-p>',
|
||||
":lua require'telescope'.extensions.project.project{}<CR>",
|
||||
{noremap = true, silent = true}
|
||||
'n',
|
||||
'<C-p>',
|
||||
":lua require'telescope'.extensions.project.project{}<CR>",
|
||||
{noremap = true, silent = true}
|
||||
)
|
||||
```
|
||||
|
||||
## Default mappings (normal mode):
|
||||
|
||||
d: delete currently selected project
|
||||
r: rename currently selected project
|
||||
c: create a project (defaults to your git root if used inside a git project,
|
||||
otherwise it will use your current working directory)
|
||||
s: search inside files within your project
|
||||
b: browse inside files within your project
|
||||
w: change to the selected project's directory without opening it
|
||||
r: find a recently opened file within your project
|
||||
f: find a file within your project (this works the same as \<CR\>)
|
||||
|
||||
|
||||
## Available options:
|
||||
|
||||
Options can be added when requiring telescope project, as shown below:
|
||||
```lua require'telescope'.extensions.project.project{ display_type = 'full' }```
|
||||
| Keys | Description | Options |
|
||||
|----------------|---------------------------------------------|-------------------------------|
|
||||
| `display_type` | Show the title and the path of the project | 'full' or 'minimal' (default) |
|
||||
|
||||
display_type:
|
||||
Options can be added when requiring telescope-project, as shown below:
|
||||
|
||||
- 'full' (Show the title and the path of the project)
|
||||
- 'minimal' (Default. Show the title of the project only)
|
||||
```lua
|
||||
lua require'telescope'.extensions.project.project{ display_type = 'full' }
|
||||
```
|
||||
|
||||
## Available setup settings:
|
||||
|
||||
| Keys | Description | Options |
|
||||
|-------------|--------------------------------------------------|------------------------|
|
||||
| `base_dir` | path to projects (all git repos will be added) | string (default: nil) |
|
||||
| `max_depth` | maximum depth to recursively search for projects | integer (default: 3) |
|
||||
|
||||
Setup settings can be added when requiring telescope, as shown below:
|
||||
|
||||
```lua
|
||||
require('telescope').setup {
|
||||
extensions = {
|
||||
project = {
|
||||
base_dir = '~/projects',
|
||||
max_depth = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Roadmap :blue_car:
|
||||
|
||||
- order projects by last opened :heavy_check_mark:
|
||||
- add all (git-enabled) subdirectories automatically :heavy_check_mark:
|
||||
- workspaces :construction:
|
||||
- add all (git-enabled) subdirectories automatically :construction:
|
||||
|
||||
@@ -1,155 +1,14 @@
|
||||
local has_telescope, telescope = pcall(require, 'telescope')
|
||||
local main = require('telescope._extensions.project.main')
|
||||
local utils = require('telescope._extensions.project.utils')
|
||||
|
||||
if not has_telescope then
|
||||
error('This plugins requires nvim-telescope/telescope.nvim')
|
||||
end
|
||||
|
||||
local actions = require("telescope.actions")
|
||||
local action_state = require("telescope.actions.state")
|
||||
local finders = require("telescope.finders")
|
||||
local pickers = require("telescope.pickers")
|
||||
local conf = require("telescope.config").values
|
||||
local entry_display = require("telescope.pickers.entry_display")
|
||||
local utils = require("telescope.utils")
|
||||
utils.init_file()
|
||||
|
||||
local project_actions = require("telescope._extensions.project_actions")
|
||||
|
||||
local project_dirs_file = vim.fn.stdpath('data') .. '/telescope-projects.txt'
|
||||
|
||||
-- Checks if the file containing the list of project
|
||||
-- directories already exists.
|
||||
-- If it doesn't exist, it creates it.
|
||||
local function check_for_project_dirs_file()
|
||||
local f = io.open(project_dirs_file, "r")
|
||||
if f ~= nil then
|
||||
io.close(f)
|
||||
return true
|
||||
else
|
||||
local newFile = io.open(project_dirs_file, "w")
|
||||
newFile:write()
|
||||
newFile:close()
|
||||
end
|
||||
end
|
||||
|
||||
-- Creates a Telescope `finder` based on the given options
|
||||
-- and list of projects
|
||||
local create_finder = function(opts, projects)
|
||||
local display_type = opts.display_type
|
||||
local widths = {
|
||||
title = 0,
|
||||
dir = 0,
|
||||
}
|
||||
|
||||
-- Loop over all of the projects and find the maximum length of
|
||||
-- each of the keys
|
||||
for _,entry in pairs(projects) do
|
||||
if display_type == 'full' then
|
||||
entry.dir = '[' .. entry.path .. ']'
|
||||
else
|
||||
entry.dir = ''
|
||||
end
|
||||
for key, value in pairs(widths) do
|
||||
widths[key] = math.max(value,utils.strdisplaywidth(entry[key] or ''))
|
||||
end
|
||||
end
|
||||
|
||||
local displayer = entry_display.create {
|
||||
separator = " ",
|
||||
items = {
|
||||
{ width = widths.title },
|
||||
{ width = widths.dir },
|
||||
}
|
||||
}
|
||||
local make_display = function(entry)
|
||||
return displayer {
|
||||
{ entry.title },
|
||||
{ entry.dir }
|
||||
}
|
||||
end
|
||||
|
||||
return finders.new_table {
|
||||
results = projects,
|
||||
entry_maker = function(entry)
|
||||
entry.value = entry.path
|
||||
entry.ordinal = entry.title
|
||||
entry.display = make_display
|
||||
return entry
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local get_last_accessed_time = function(path)
|
||||
local expanded_path = vim.fn.expand(path)
|
||||
local fs_stat = vim.loop.fs_stat(expanded_path)
|
||||
if fs_stat then
|
||||
return fs_stat.atime.sec
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Get information on all of the projects in the
|
||||
-- `project_dirs_file` and output it as a list
|
||||
local get_projects = function()
|
||||
check_for_project_dirs_file()
|
||||
local projects = {}
|
||||
|
||||
for line in io.lines(project_dirs_file) do
|
||||
local title, path = line:match("^(.-)=(.-)$")
|
||||
local last_accessed = get_last_accessed_time(path)
|
||||
table.insert(projects, {
|
||||
title = title,
|
||||
path = path,
|
||||
last_accessed = last_accessed
|
||||
})
|
||||
end
|
||||
|
||||
table.sort(projects, function(a,b)
|
||||
return a.last_accessed > b.last_accessed
|
||||
end)
|
||||
|
||||
return projects
|
||||
end
|
||||
|
||||
-- The main function.
|
||||
-- This creates a picker with a list of all of the projects,
|
||||
-- and attaches the appropriate mappings for associated
|
||||
-- actions.
|
||||
local project = function(opts)
|
||||
opts = opts or {}
|
||||
|
||||
local projects = get_projects()
|
||||
local new_finder = create_finder(opts, projects)
|
||||
|
||||
pickers.new(opts, {
|
||||
prompt_title = 'Select a project',
|
||||
results_title = 'Projects',
|
||||
finder = new_finder,
|
||||
sorter = conf.file_sorter(opts),
|
||||
attach_mappings = function(prompt_bufnr, map)
|
||||
local refresh_projects = function()
|
||||
local picker = action_state.get_current_picker(prompt_bufnr)
|
||||
picker:refresh(create_finder(opts,get_projects()), {reset_prompt=true})
|
||||
end
|
||||
project_actions.add_project:enhance({ post = refresh_projects })
|
||||
project_actions.delete_project:enhance({ post = refresh_projects })
|
||||
project_actions.rename_project:enhance({ post = refresh_projects })
|
||||
|
||||
map('n', 'd', project_actions.delete_project)
|
||||
map('n', 'r', project_actions.rename_project)
|
||||
map('n', 'c', project_actions.add_project)
|
||||
map('n', 'f', project_actions.find_project_files)
|
||||
map('n', 'b', project_actions.browse_project_files)
|
||||
map('n', 's', project_actions.search_in_project_files)
|
||||
map('n', 'R', project_actions.recent_project_files)
|
||||
map('n', 'w', project_actions.change_working_directory)
|
||||
local on_project_selected = function()
|
||||
project_actions.find_project_files(prompt_bufnr)
|
||||
end
|
||||
actions.select_default:replace(on_project_selected)
|
||||
return true
|
||||
end
|
||||
}):find()
|
||||
end
|
||||
|
||||
return telescope.register_extension {exports = {project = project}}
|
||||
return telescope.register_extension{
|
||||
setup = main.setup,
|
||||
exports = { project = main.project }
|
||||
}
|
||||
|
||||
121
lua/telescope/_extensions/project/actions.lua
Normal file
121
lua/telescope/_extensions/project/actions.lua
Normal file
@@ -0,0 +1,121 @@
|
||||
local builtin = require("telescope.builtin")
|
||||
local actions = require("telescope.actions")
|
||||
local transform_mod = require('telescope.actions.mt').transform_mod
|
||||
|
||||
local _git = require("telescope._extensions.project.git")
|
||||
local _utils = require("telescope._extensions.project.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Extracts project title from current buffer selection
|
||||
M.get_selected_title = function(prompt_bufnr)
|
||||
return actions.get_selected_entry(prompt_bufnr).ordinal
|
||||
end
|
||||
|
||||
-- Extracts project path from current buffer selection
|
||||
M.get_selected_path = function(prompt_bufnr)
|
||||
return actions.get_selected_entry(prompt_bufnr).value
|
||||
end
|
||||
|
||||
-- Create a new project and add it to the list in the `telescope_projects_file`
|
||||
M.add_project = function()
|
||||
local path = _git.try_and_find_git_path()
|
||||
local projects = _utils.get_project_objects()
|
||||
local path_not_in_projects = true
|
||||
|
||||
local file = io.open(_utils.telescope_projects_file, "w")
|
||||
for _, project in pairs(projects) do
|
||||
if project.path == path then
|
||||
project.activated = 1
|
||||
path_not_in_projects = false
|
||||
end
|
||||
_utils.store_project(file, project)
|
||||
end
|
||||
|
||||
if path_not_in_projects then
|
||||
local new_project = _utils.get_project_from_path(path)
|
||||
_utils.store_project(file, new_project)
|
||||
end
|
||||
|
||||
io.close(file)
|
||||
print('Project added: ' .. path)
|
||||
end
|
||||
|
||||
-- Rename the selected project within the `telescope_projects_file`.
|
||||
M.rename_project = function(prompt_bufnr)
|
||||
local selected_title = M.get_selected_title(prompt_bufnr)
|
||||
local new_title = vim.fn.input('Rename ' ..selected_title.. ' to: ', selected_title)
|
||||
local projects = _utils.get_project_objects()
|
||||
|
||||
local file = io.open(_utils.telescope_projects_file, "w")
|
||||
for _, project in pairs(projects) do
|
||||
if project.title == selected_title then
|
||||
project.title = new_title
|
||||
end
|
||||
_utils.store_project(file, project)
|
||||
end
|
||||
|
||||
io.close(file)
|
||||
end
|
||||
|
||||
-- Delete (deactivate) the selected project from the `telescope_projects_file`
|
||||
M.delete_project = function(prompt_bufnr)
|
||||
local projects = _utils.get_project_objects()
|
||||
local selected_title = M.get_selected_title(prompt_bufnr)
|
||||
|
||||
local file = io.open(_utils.telescope_projects_file, "w")
|
||||
for _, project in pairs(projects) do
|
||||
if project.title == selected_title then
|
||||
project.activated = 0
|
||||
end
|
||||
_utils.store_project(file, project)
|
||||
end
|
||||
|
||||
io.close(file)
|
||||
print('Project deleted: ' .. selected_title)
|
||||
end
|
||||
|
||||
-- Find files within the selected project using the
|
||||
-- Telescope builtin `find_files`.
|
||||
M.find_project_files = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions._close(prompt_bufnr, true)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
builtin.find_files({cwd = dir})
|
||||
end
|
||||
|
||||
-- Browse through files within the selected project using
|
||||
-- the Telescope builtin `file_browser`.
|
||||
M.browse_project_files = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions._close(prompt_bufnr, true)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
builtin.file_browser({cwd = dir})
|
||||
end
|
||||
|
||||
-- Search within files in the selected project using
|
||||
-- the Telescope builtin `live_grep`.
|
||||
M.search_in_project_files = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions._close(prompt_bufnr, true)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
builtin.live_grep({cwd = dir})
|
||||
end
|
||||
|
||||
-- Search the recently used files within the selected project
|
||||
-- using the Telescope builtin `oldfiles`.
|
||||
M.recent_project_files = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions._close(prompt_bufnr, true)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
builtin.oldfiles({cwd_only = true})
|
||||
end
|
||||
|
||||
-- Change working directory to the selected project and close the picker.
|
||||
M.change_working_directory = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions.close(prompt_bufnr)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
end
|
||||
|
||||
return transform_mod(M)
|
||||
54
lua/telescope/_extensions/project/finders.lua
Normal file
54
lua/telescope/_extensions/project/finders.lua
Normal file
@@ -0,0 +1,54 @@
|
||||
local finders = require("telescope.finders")
|
||||
local utils = require("telescope.utils")
|
||||
local entry_display = require("telescope.pickers.entry_display")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Creates a Telescope `finder` based on the given options
|
||||
-- and list of projects
|
||||
M.project_finder = function(opts, projects)
|
||||
local display_type = opts.display_type
|
||||
local widths = {
|
||||
title = 0,
|
||||
dir = 0,
|
||||
}
|
||||
|
||||
-- Loop over all of the projects and find the maximum length of
|
||||
-- each of the keys
|
||||
for _, project in pairs(projects) do
|
||||
if display_type == 'full' then
|
||||
project.display_path = '[' .. project.path .. ']'
|
||||
else
|
||||
project.display_path = ''
|
||||
end
|
||||
for key, value in pairs(widths) do
|
||||
widths[key] = math.max(value, utils.strdisplaywidth(project[key] or ''))
|
||||
end
|
||||
end
|
||||
|
||||
local displayer = entry_display.create {
|
||||
separator = " ",
|
||||
items = {
|
||||
{ width = widths.title },
|
||||
{ width = widths.dir },
|
||||
}
|
||||
}
|
||||
local make_display = function(project)
|
||||
return displayer {
|
||||
{ project.title },
|
||||
{ project.display_path }
|
||||
}
|
||||
end
|
||||
|
||||
return finders.new_table {
|
||||
results = projects,
|
||||
entry_maker = function(project)
|
||||
project.value = project.path
|
||||
project.ordinal = project.title
|
||||
project.display = make_display
|
||||
return project
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
60
lua/telescope/_extensions/project/git.lua
Normal file
60
lua/telescope/_extensions/project/git.lua
Normal file
@@ -0,0 +1,60 @@
|
||||
local _utils = require("telescope._extensions.project.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Temporary store for git repo list
|
||||
M.tmp_path = "/tmp/found_projects.txt"
|
||||
|
||||
-- Find and store git repos if base_dir provided
|
||||
M.update_git_repos = function(base_dir, max_depth)
|
||||
M.search_for_git_repos(base_dir, max_depth)
|
||||
local git_projects = M.parse_git_repo_paths()
|
||||
M.save_git_repos(git_projects)
|
||||
end
|
||||
|
||||
-- Recurses directories under base directory to find all git projects
|
||||
M.search_for_git_repos = function(base_dir, max_depth)
|
||||
if base_dir then
|
||||
local max_depth_arg = " -maxdepth " .. max_depth
|
||||
local find_args = " -type d -name .git -printf '%h\n'"
|
||||
local shell_cmd = "find " .. base_dir .. max_depth_arg .. find_args
|
||||
os.execute(shell_cmd .. " > " .. M.tmp_path)
|
||||
end
|
||||
end
|
||||
|
||||
-- Reads tmp file, converting paths to projects
|
||||
M.parse_git_repo_paths = function()
|
||||
local git_projects = {}
|
||||
for path in io.lines(M.tmp_path) do
|
||||
local project = _utils.get_project_from_path(path)
|
||||
table.insert(git_projects, project)
|
||||
end
|
||||
return git_projects
|
||||
end
|
||||
|
||||
-- Write project to telescope projects file
|
||||
M.save_git_repos = function(git_projects)
|
||||
local project_paths = _utils.get_project_paths()
|
||||
local file = io.open(_utils.telescope_projects_file, "a")
|
||||
|
||||
for _, project in pairs(git_projects) do
|
||||
local path_exists = _utils.has_value(project_paths, project.path)
|
||||
if not path_exists then
|
||||
_utils.store_project(file, project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Attempt to locate git directory, else return cwd
|
||||
M.try_and_find_git_path = function()
|
||||
local git_cmd = "git -C " .. vim.loop.cwd() .. " rev-parse --show-toplevel"
|
||||
local git_root = vim.fn.systemlist(git_cmd)[1]
|
||||
local git_root_fatal = _utils.string_starts_with(git_root, 'fatal')
|
||||
|
||||
if not git_root or git_root_fatal then
|
||||
return vim.loop.cwd()
|
||||
end
|
||||
return git_root
|
||||
end
|
||||
|
||||
return M
|
||||
63
lua/telescope/_extensions/project/main.lua
Normal file
63
lua/telescope/_extensions/project/main.lua
Normal file
@@ -0,0 +1,63 @@
|
||||
-- telescope modules
|
||||
local actions = require("telescope.actions")
|
||||
local action_state = require("telescope.actions.state")
|
||||
local pickers = require("telescope.pickers")
|
||||
local conf = require("telescope.config").values
|
||||
|
||||
-- telescope-project modules
|
||||
local _actions = require("telescope._extensions.project.actions")
|
||||
local _finders = require("telescope._extensions.project.finders")
|
||||
local _git = require("telescope._extensions.project.git")
|
||||
local _utils = require("telescope._extensions.project.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Variables that setup can change
|
||||
local base_dir
|
||||
local max_depth
|
||||
|
||||
-- Allow user to set base_dir in setup
|
||||
M.setup = function(setup_config)
|
||||
base_dir = setup_config.base_dir or nil
|
||||
max_depth = setup_config.max_depth or 3
|
||||
_git.update_git_repos(base_dir, max_depth)
|
||||
end
|
||||
|
||||
-- This creates a picker with a list of all of the projects
|
||||
M.project = function(opts)
|
||||
pickers.new(opts or {}, {
|
||||
prompt_title = 'Select a project',
|
||||
results_title = 'Projects',
|
||||
finder = _finders.project_finder(opts, _utils.get_projects()),
|
||||
sorter = conf.file_sorter(opts),
|
||||
attach_mappings = function(prompt_bufnr, map)
|
||||
|
||||
local refresh_projects = function()
|
||||
local picker = action_state.get_current_picker(prompt_bufnr)
|
||||
local finder = _finders.project_finder(opts, _utils.get_projects())
|
||||
picker:refresh(finder, { reset_prompt = true })
|
||||
end
|
||||
|
||||
_actions.add_project:enhance({ post = refresh_projects })
|
||||
_actions.delete_project:enhance({ post = refresh_projects })
|
||||
_actions.rename_project:enhance({ post = refresh_projects })
|
||||
|
||||
map('n', 'd', _actions.delete_project)
|
||||
map('n', 'r', _actions.rename_project)
|
||||
map('n', 'c', _actions.add_project)
|
||||
map('n', 'f', _actions.find_project_files)
|
||||
map('n', 'b', _actions.browse_project_files)
|
||||
map('n', 's', _actions.search_in_project_files)
|
||||
map('n', 'R', _actions.recent_project_files)
|
||||
map('n', 'w', _actions.change_working_directory)
|
||||
|
||||
local on_project_selected = function()
|
||||
_actions.find_project_files(prompt_bufnr)
|
||||
end
|
||||
actions.select_default:replace(on_project_selected)
|
||||
return true
|
||||
end
|
||||
}):find()
|
||||
end
|
||||
|
||||
return M
|
||||
103
lua/telescope/_extensions/project/utils.lua
Normal file
103
lua/telescope/_extensions/project/utils.lua
Normal file
@@ -0,0 +1,103 @@
|
||||
local M = {}
|
||||
|
||||
-- The file path to telescope projects
|
||||
M.telescope_projects_file = vim.fn.stdpath('data') .. '/telescope-projects.txt'
|
||||
|
||||
-- Initialize file if does not exist
|
||||
M.init_file = function()
|
||||
local file_path = require'plenary'.path:new(M.telescope_projects_file)
|
||||
if not file_path:exists() then
|
||||
file_path:touch()
|
||||
end
|
||||
end
|
||||
|
||||
-- Fetches project information to be passed to picker
|
||||
M.get_projects = function()
|
||||
local filtered_projects = {}
|
||||
for _, project in pairs(M.get_project_objects()) do
|
||||
local is_activated = tonumber(project.activated) == 1
|
||||
if is_activated then
|
||||
table.insert(filtered_projects, project)
|
||||
end
|
||||
end
|
||||
table.sort(filtered_projects, function(a,b)
|
||||
return a.last_accessed > b.last_accessed
|
||||
end)
|
||||
return filtered_projects
|
||||
end
|
||||
|
||||
-- Get project info for all (de)activated projects
|
||||
M.get_project_objects = function()
|
||||
local projects = {}
|
||||
for line in io.lines(M.telescope_projects_file) do
|
||||
local project = M.parse_project_line(line)
|
||||
table.insert(projects, project)
|
||||
end
|
||||
return projects
|
||||
end
|
||||
|
||||
-- Extract paths from all project objects
|
||||
M.get_project_paths = function()
|
||||
local paths = {}
|
||||
for _, project in pairs(M.get_project_objects()) do
|
||||
table.insert(paths, project.path)
|
||||
end
|
||||
return paths
|
||||
end
|
||||
|
||||
-- Extracts information from telescope projects line
|
||||
M.parse_project_line = function(line)
|
||||
local title, path, activated = line:match("^(.-)=(.-)=(.-)$")
|
||||
if not activated then
|
||||
title, path = line:match("^(.-)=(.-)$")
|
||||
activated = 1
|
||||
end
|
||||
return {
|
||||
title = title,
|
||||
path = path,
|
||||
last_accessed = M.get_last_accessed_time(path),
|
||||
activated = activated
|
||||
}
|
||||
end
|
||||
|
||||
-- Parses path into project object (activated by default)
|
||||
M.get_project_from_path = function(path)
|
||||
local title = path:match("[^/]+$")
|
||||
local activated = 1
|
||||
local line = title .. "=" .. path .. "=" .. activated
|
||||
return M.parse_project_line(line)
|
||||
end
|
||||
|
||||
-- Checks the last time a directory was last accessed
|
||||
M.get_last_accessed_time = function(path)
|
||||
local expanded_path = vim.fn.expand(path)
|
||||
local fs_stat = vim.loop.fs_stat(expanded_path)
|
||||
return fs_stat and fs_stat.atime.sec or 0
|
||||
end
|
||||
|
||||
-- Standardized way of storing project to file
|
||||
M.store_project = function(file, project)
|
||||
local line = project.title .. "=" .. project.path .. "=" .. project.activated .. "\n"
|
||||
file:write(line)
|
||||
end
|
||||
|
||||
-- Trim whitespace for strings
|
||||
M.trim = function(s)
|
||||
return s:match( "^%s*(.-)%s*$" )
|
||||
end
|
||||
|
||||
-- Check if value exists in table
|
||||
M.has_value = function(tbl, val)
|
||||
for _, value in ipairs(tbl) do
|
||||
if M.trim(value) == M.trim(val) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
M.string_starts_with = function(text, start)
|
||||
return string.sub(text, 1, string.len(start)) == start
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,135 +0,0 @@
|
||||
local builtin = require("telescope.builtin")
|
||||
local actions = require("telescope.actions")
|
||||
local transform_mod = require('telescope.actions.mt').transform_mod
|
||||
|
||||
local project_actions = {}
|
||||
|
||||
local project_dirs_file = vim.fn.stdpath('data') .. '/telescope-projects.txt'
|
||||
|
||||
function string.starts(String,Start)
|
||||
return string.sub(String,1,string.len(Start))==Start
|
||||
end
|
||||
|
||||
-- Create a new project and add it to the list in the `project_dirs_file`
|
||||
project_actions.add_project = function(prompt_bufnr)
|
||||
local git_root = vim.fn.systemlist("git -C " .. vim.loop.cwd() .. " rev-parse --show-toplevel")[
|
||||
1
|
||||
]
|
||||
|
||||
local project_directory = git_root
|
||||
if not git_root or string.starts(git_root,'fatal') then
|
||||
project_directory = vim.loop.cwd()
|
||||
end
|
||||
|
||||
local project_title = project_directory:match("[^/]+$")
|
||||
local project_to_add = project_title .. "=" .. project_directory .. "\n"
|
||||
|
||||
local file = assert(
|
||||
io.open(project_dirs_file, "a"),
|
||||
"No project file exists"
|
||||
)
|
||||
|
||||
local project_already_added = false
|
||||
for line in io.lines(project_dirs_file) do
|
||||
local project_exists_check = line .. "\n" == project_to_add
|
||||
if project_exists_check then
|
||||
project_already_added = true
|
||||
print('This project already exists.')
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if not project_already_added then
|
||||
io.output(file)
|
||||
io.write(project_to_add)
|
||||
print('project added: ' .. project_title)
|
||||
end
|
||||
io.close(file)
|
||||
end
|
||||
|
||||
-- Rename the selected project within the `project_dirs_file`.
|
||||
-- Uses a name provided by the user.
|
||||
project_actions.rename_project = function(prompt_bufnr)
|
||||
local oldName = actions.get_selected_entry(prompt_bufnr).ordinal
|
||||
local newName = vim.fn.input('Rename ' ..oldName.. ' to: ', oldName)
|
||||
local newLines = ""
|
||||
for line in io.lines(project_dirs_file) do
|
||||
local title, path = line:match("^(.-)=(.-)$")
|
||||
if title ~= oldName then
|
||||
newLines = newLines .. title .. '=' .. path .. '\n'
|
||||
else
|
||||
newLines = newLines .. newName .. '=' .. actions.get_selected_entry(prompt_bufnr).value .. '\n'
|
||||
end
|
||||
end
|
||||
local file = assert(
|
||||
io.open(project_dirs_file, "w"),
|
||||
"No project file exists"
|
||||
)
|
||||
file:write(newLines)
|
||||
file:close()
|
||||
print('Project renamed: ' .. actions.get_selected_entry(prompt_bufnr).ordinal .. ' -> ' .. newName)
|
||||
end
|
||||
|
||||
-- Delete the selected project from the `project_dirs_file`
|
||||
project_actions.delete_project = function(prompt_bufnr)
|
||||
local newLines = ""
|
||||
for line in io.lines(project_dirs_file) do
|
||||
local title, path = line:match("^(.-)=(.-)$")
|
||||
if title ~= actions.get_selected_entry(prompt_bufnr).ordinal then
|
||||
newLines = newLines .. title .. '=' .. path .. "\n"
|
||||
end
|
||||
end
|
||||
local file = assert(
|
||||
io.open(project_dirs_file, "w"),
|
||||
"No project file exists"
|
||||
)
|
||||
file:write(newLines)
|
||||
file:close()
|
||||
print('Project deleted: ' .. actions.get_selected_entry(prompt_bufnr).ordinal)
|
||||
end
|
||||
|
||||
-- Find files within the selected project using the
|
||||
-- Telescope builtin `find_files`.
|
||||
project_actions.find_project_files = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions._close(prompt_bufnr, true)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
builtin.find_files({cwd = dir})
|
||||
end
|
||||
|
||||
-- Browse through files within the selected project using
|
||||
-- the Telescope builtin `file_browser`.
|
||||
project_actions.browse_project_files = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions._close(prompt_bufnr, true)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
builtin.file_browser({cwd = dir})
|
||||
end
|
||||
|
||||
-- Search within files in the selected project using
|
||||
-- the Telescope builtin `live_grep`.
|
||||
project_actions.search_in_project_files = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions._close(prompt_bufnr, true)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
builtin.live_grep({cwd = dir})
|
||||
end
|
||||
|
||||
-- Search the recently used files within the selected project
|
||||
-- using the Telescope builtin `oldfiles`.
|
||||
project_actions.recent_project_files = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions._close(prompt_bufnr, true)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
builtin.oldfiles({cwd_only = true})
|
||||
end
|
||||
|
||||
-- Change working directory to the selected project and close the picker.
|
||||
project_actions.change_working_directory = function(prompt_bufnr)
|
||||
local dir = actions.get_selected_entry(prompt_bufnr).value
|
||||
actions.close(prompt_bufnr)
|
||||
vim.fn.execute("cd " .. dir, "silent")
|
||||
end
|
||||
|
||||
project_actions = transform_mod(project_actions);
|
||||
return project_actions
|
||||
34
lua/tests/git_spec.lua
Normal file
34
lua/tests/git_spec.lua
Normal file
@@ -0,0 +1,34 @@
|
||||
local path = require("plenary.path")
|
||||
|
||||
describe("git", function()
|
||||
|
||||
local git = require("telescope._extensions.project.git")
|
||||
local path_to_projects = path:new("/tmp/git_spec_projects")
|
||||
local example_project_path = path:new(path_to_projects.filename .. "/example")
|
||||
example_project_path:mkdir({ parents = true })
|
||||
os.execute("git init --quiet " .. example_project_path.filename)
|
||||
|
||||
it("try and find path", function()
|
||||
vim.fn.execute("cd " .. example_project_path.filename, "silent")
|
||||
local git_path = git.try_and_find_git_path()
|
||||
assert.equal(example_project_path.filename, git_path)
|
||||
end)
|
||||
|
||||
it("search for repos", function()
|
||||
git.tmp_path = "/tmp/found_projects_git_spec.txt"
|
||||
git.search_for_git_repos(path_to_projects.filename, 2)
|
||||
local git_projects = git.parse_git_repo_paths()
|
||||
|
||||
local found_git_project = false
|
||||
for _, git_project in pairs(git_projects) do
|
||||
if git_project.path == example_project_path.filename then
|
||||
found_git_project = true
|
||||
end
|
||||
end
|
||||
assert.equal(true, found_git_project)
|
||||
end)
|
||||
|
||||
path:new(git.tmp_path):rm()
|
||||
path_to_projects:rm({ recursive = true })
|
||||
|
||||
end)
|
||||
66
lua/tests/utils_spec.lua
Normal file
66
lua/tests/utils_spec.lua
Normal file
@@ -0,0 +1,66 @@
|
||||
local path = require("plenary.path")
|
||||
|
||||
describe("utils", function()
|
||||
|
||||
local utils = require("telescope._extensions.project.utils")
|
||||
|
||||
describe("general", function()
|
||||
|
||||
it("trim whitespace (left and right)", function()
|
||||
local input_str = " text "
|
||||
assert.equal("text", utils.trim(input_str))
|
||||
end)
|
||||
|
||||
it("string start with 'fatal'", function()
|
||||
-- expected text when running git.try_and_find_git_path()
|
||||
local input_str = "fatal: not a git repository (or any parent up to mount point /)"
|
||||
assert.equal(true, utils.string_starts_with(input_str, "fatal"))
|
||||
end)
|
||||
|
||||
it("table has a value (whitespace ignored)", function()
|
||||
local paths = {"/projects/A", "/projects/B"}
|
||||
assert.equal(true, utils.has_value(paths, "/projects/A"))
|
||||
assert.equal(true, utils.has_value(paths, "/projects/B "))
|
||||
assert.equal(false, utils.has_value(paths, "/projects/C"))
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
describe("project", function()
|
||||
|
||||
it("create, save, and read from file", function()
|
||||
|
||||
-- initialize projects file where projects are stored
|
||||
local test_projects_file = "/tmp/telescope-projects-test.txt"
|
||||
utils.telescope_projects_file = test_projects_file
|
||||
utils.init_file()
|
||||
local test_projects_path = path:new(test_projects_file)
|
||||
|
||||
-- extract project information from path
|
||||
local example_project_path = "/projects/my_project"
|
||||
local project = utils.get_project_from_path(example_project_path)
|
||||
assert.equal(project.path, example_project_path)
|
||||
assert.equal(project.title, "my_project")
|
||||
assert.equal(project.activated, "1")
|
||||
|
||||
-- store project in test file
|
||||
local file = io.open(test_projects_path.filename, "w")
|
||||
utils.store_project(file, project)
|
||||
io.close(file)
|
||||
|
||||
-- check that test project was found
|
||||
local projects = utils.get_projects()
|
||||
local found_test_project = false
|
||||
for _, stored_project in pairs(projects) do
|
||||
if stored_project.path == example_project_path then
|
||||
found_test_project = true
|
||||
end
|
||||
end
|
||||
|
||||
assert.equal(true, found_test_project)
|
||||
test_projects_path:rm()
|
||||
end)
|
||||
|
||||
end)
|
||||
|
||||
end)
|
||||
Reference in New Issue
Block a user