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')
|
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:
|
## Available functions:
|
||||||
|
|
||||||
### Project
|
### Project
|
||||||
|
|
||||||
The projects picker.
|
The `projects` picker:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
require'telescope'.extensions.project.project{}
|
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
|
```lua
|
||||||
vim.api.nvim_set_keymap(
|
vim.api.nvim_set_keymap(
|
||||||
'n',
|
'n',
|
||||||
'<C-p>',
|
'<C-p>',
|
||||||
":lua require'telescope'.extensions.project.project{}<CR>",
|
":lua require'telescope'.extensions.project.project{}<CR>",
|
||||||
{noremap = true, silent = true}
|
{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:
|
## Available options:
|
||||||
|
|
||||||
Options can be added when requiring telescope project, as shown below:
|
| Keys | Description | Options |
|
||||||
```lua require'telescope'.extensions.project.project{ display_type = 'full' }```
|
|----------------|---------------------------------------------|-------------------------------|
|
||||||
|
| `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)
|
```lua
|
||||||
- 'minimal' (Default. Show the title of the project only)
|
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:
|
## Roadmap :blue_car:
|
||||||
|
|
||||||
- order projects by last opened :heavy_check_mark:
|
- order projects by last opened :heavy_check_mark:
|
||||||
|
- add all (git-enabled) subdirectories automatically :heavy_check_mark:
|
||||||
- workspaces :construction:
|
- workspaces :construction:
|
||||||
- add all (git-enabled) subdirectories automatically :construction:
|
|
||||||
|
|||||||
@@ -1,155 +1,14 @@
|
|||||||
local has_telescope, telescope = pcall(require, 'telescope')
|
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
|
if not has_telescope then
|
||||||
error('This plugins requires nvim-telescope/telescope.nvim')
|
error('This plugins requires nvim-telescope/telescope.nvim')
|
||||||
end
|
end
|
||||||
|
|
||||||
local actions = require("telescope.actions")
|
utils.init_file()
|
||||||
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")
|
|
||||||
|
|
||||||
local project_actions = require("telescope._extensions.project_actions")
|
return telescope.register_extension{
|
||||||
|
setup = main.setup,
|
||||||
local project_dirs_file = vim.fn.stdpath('data') .. '/telescope-projects.txt'
|
exports = { project = main.project }
|
||||||
|
}
|
||||||
-- 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}}
|
|
||||||
|
|||||||
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