diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..dd84ea78
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..bbcbbe7d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..0d8e1703
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,22 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: check-shebang-scripts-are-executable
+ - id: check-executables-have-shebangs
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+
+ - repo: https://github.com/Lucas-C/pre-commit-hooks
+ rev: v1.5.5
+ hooks:
+ - id: remove-crlf
+
+ - repo: local
+ hooks:
+ - id: stylua
+ name: StyLua
+ language: system
+ entry: stylua
+ types: [lua]
+ verbose: true
diff --git a/.stylua.toml b/.stylua.toml
index 9393cf9a..bd987d8d 100644
--- a/.stylua.toml
+++ b/.stylua.toml
@@ -1,5 +1,10 @@
-column_width = 120
-line_endings = "Unix"
indent_type = "Spaces"
-indent_width = 2
-quote_style = "AutoPreferDouble"
+indent_width = 4
+column_width = 100
+line_endings = "Unix"
+quote_style = "AutoPreferSingle"
+call_parentheses = "Always"
+collapse_simple_statement = "FunctionOnly"
+
+[sort_requires]
+enabled = false
diff --git a/README.md b/README.md
index 69594fc4..7734c5dc 100644
--- a/README.md
+++ b/README.md
@@ -1,214 +1,333 @@
-# 🗃️ project.nvim
+
+
🗃️ project.nvim
+
-**project.nvim** is an all in one neovim plugin written in lua that provides
-superior project management.
+
+
+`project.nvim` is an all in one [Neovim](neovim/neovim) plugin written in Lua
+that provides superior project management.

-## ⚡ Requirements
-
-- Neovim >= 0.5.0
-
-## ✨ Features
-
-- Automagically cd to project directory using nvim lsp
- - Dependency free, does not rely on lspconfig
-- If no lsp then uses pattern matching to cd to root directory
-- Telescope integration `:Telescope projects`
- - Access your recently opened projects from telescope!
- - Asynchronous file io so it will not slow down vim when reading the history
- file on startup.
-- ~~Nvim-tree.lua support/integration~~
- - Please add the following to your config instead:
- ```vim
- " Vim Script
- lua << EOF
- require("nvim-tree").setup({
- sync_root_with_cwd = true,
- respect_buf_cwd = true,
- update_focused_file = {
- enable = true,
- update_root = true
- },
- })
- EOF
- ```
- ```lua
- -- lua
- require("nvim-tree").setup({
- sync_root_with_cwd = true,
- respect_buf_cwd = true,
- update_focused_file = {
- enable = true,
- update_root = true
- },
- })
- ```
+---
+
+## Table of Contents
+
+1. [Features](#features)
+2. [Installation](#installation)
+ 1. [Requirements](#requirements)
+ 2. [`vim-plug`](#vim-plug)
+ 3. [`lazy.nvim`](#lazy-nvim)
+ 4. [`pckr.nvim`](#pckr-nvim)
+3. [Configuration](#configuration)
+ 1. [Pattern Matching](#pattern-matching)
+ 2. [`nvim-tree.lua` Integration](#nvim-tree-integration)
+ 3. [`telescope.nvim` Integration](#telescope-integration)
+ 1. [Telescope Projects Picker](#telescope-projects-picker)
+ 2. [Telescope Mappings](#telescope-mappings)
+4. [API](#api)
+5. [Contributing](#contributing)
+6. [Addendum](#addendum)
+
+---
+
+## Features
+
+- Automagically `cd` to the project root directory using `vim.lsp`
+- If no LSP is available then it'll try using pattern matching to `cd` to the project root directory instead
+- [Telescope Integration](#telescope-integration) `:Telescope projects`
+- Asynchronous file IO so it will not slow down neovim when reading the history file on startup.
+- [`nvim-tree` integration](#nvim-tree-integration)
+
+## Installation
+
+### Requirements
+
+- Neovim >= 0.11.0
+- [`telescope.nvim`](nvim-telescope/telescope.nvim) **(optional)**
+- [`nvim-tree.lua`](nvim-tree/nvim-tree.lua) **(optional)**
+
+---
+
+
+WARNING: DO NOT LAZY-LOAD THIS PLUGIN
-## 📦 Installation
+The cwd might not update otherwise.
+
+
+---
Install the plugin with your preferred package manager:
-### [vim-plug](https://github.com/junegunn/vim-plug)
+
```vim
-" Vim Script
-Plug 'ahmedkhalf/project.nvim'
+Plug 'DrKJeff16/project.nvim'
+
+" OPTIONAL
+Plug 'nvim-telescope/telescope.nvim' | Plug 'plenary.nvim'
lua << EOF
- require("project_nvim").setup {
+ require('project_nvim').setup({
-- your configuration comes here
-- or leave it empty to use the default settings
-- refer to the configuration section below
- }
+ })
EOF
```
-### [packer](https://github.com/wbthomason/packer.nvim)
+
```lua
--- Lua
-use {
- "ahmedkhalf/project.nvim",
- config = function()
- require("project_nvim").setup {
- -- your configuration comes here
- -- or leave it empty to use the default settings
- -- refer to the configuration section below
- }
- end
-}
+require('lazy').setup({
+ spec = {
+ -- Other plugins
+ {
+ 'DrKJeff16/project.nvim',
+ lazy = false, -- WARN: IMPORTANT NOT TO LAZY-LOAD THIS PLUGIN
+ dependencies = {
+ 'plenary.nvim',
+ 'nvim-telescope/telescope.nvim',
+ }, -- OPTIONAL
+ config = function()
+ require('project_nvim').setup({
+ -- your configuration comes here
+ -- or leave it empty to use the default settings
+ -- refer to the configuration section below
+ })
+ end,
+ },
+ },
+})
+```
+
+
+
+```lua
+require('pckr').add({
+ -- Other plugins
+ {
+ 'DrKJeff16/project.nvim',
+ requires = {
+ 'nvim-lua/plenary.nvim',
+ 'nvim-telescope/telescope.nvim',
+ }, -- OPTIONAL
+ config = function()
+ require('project_nvim').setup({
+ -- your configuration comes here
+ -- or leave it empty to use the default settings
+ -- refer to the configuration section below
+ })
+ end,
+ };
+})
```
-## ⚙️ Configuration
+---
-**project.nvim** comes with the following defaults:
+## Configuration
+
+To enable the plugin you must call `setup()`:
+
+```lua
+require('project_nvim').setup({
+ -- Options
+})
+```
+
+`project.nvim` comes with the following defaults:
```lua
{
-- Manual mode doesn't automatically change your root directory, so you have
-- the option to manually do so using `:ProjectRoot` command.
+ ---@type boolean
manual_mode = false,
- -- Methods of detecting the root directory. **"lsp"** uses the native neovim
- -- lsp, while **"pattern"** uses vim-rooter like glob pattern matching. Here
+ -- Methods of detecting the root directory. **'lsp'** uses the native neovim
+ -- LSP, while **'pattern'** uses vim-rooter like glob pattern matching. Here
-- order matters: if one is not detected, the other is used as fallback. You
- -- can also delete or rearangne the detection methods.
- detection_methods = { "lsp", "pattern" },
-
- -- All the patterns used to detect root dir, when **"pattern"** is in
- -- detection_methods
- patterns = { ".git", "_darcs", ".hg", ".bzr", ".svn", "Makefile", "package.json" },
-
- -- Table of lsp clients to ignore by name
- -- eg: { "efm", ... }
+ -- can also delete or rearrange the detection methods.
+ ---@type ('lsp'|'pattern')[]|table
+ detection_methods = { 'lsp', 'pattern' },
+
+ -- All the patterns used to detect root dir, when `'pattern'` is in
+ -- `detection_methods`
+ ---@type string[]
+ patterns = {
+ '.git',
+ '.github',
+ '_darcs',
+ '.hg',
+ '.bzr',
+ '.svn',
+ 'package.json',
+ },
+
+ -- Table of LSP clients to ignore by name
+ -- eg: { 'efm', ... }
+ ---@type string[]|table
ignore_lsp = {},
-- Don't calculate root dir on specific directories
- -- Ex: { "~/.cargo/*", ... }
+ -- Ex: { '~/.cargo/*', ... }
+ ---@type string[]|table
exclude_dirs = {},
-- Show hidden files in telescope
+ ---@type boolean
show_hidden = false,
-- When set to false, you will get a message when project.nvim changes your
-- directory.
+ ---@type boolean
silent_chdir = true,
-- What scope to change the directory, valid options are
-- * global (default)
-- * tab
-- * win
+ ---@type 'global'|'tab'|'win'
scope_chdir = 'global',
-- Path where project.nvim will store the project history for use in
-- telescope
- datapath = vim.fn.stdpath("data"),
+ ---@type string
+ datapath = vim.fn.stdpath('data'),
}
```
-Even if you are pleased with the defaults, please note that `setup {}` must be
+Even if you are pleased with the defaults, please note that `setup()` must be
called for the plugin to start.
### Pattern Matching
-**project.nvim**'s pattern engine uses the same expressions as vim-rooter, but
-for your convenience, I will copy paste them here:
-
-To specify the root is a certain directory, prefix it with `=`.
+`project.nvim` comes with a pattern matching engine that uses the same expressions
+as `vim-rooter`, but for your convenience here come some examples:
+
+- To specify the root is a certain directory, prefix it with `=`:
+ ```lua
+ patterns = { '=src' }
+ ```
+- To specify the root has a certain directory or file (which may be a glob), just
+ add it to the pattern list:
+ ```lua
+ patterns = { '.git', '.github', '*.sln', 'build/env.sh' }
+ ```
+- To specify the root has a certain directory as an ancestor (useful for
+ excluding directories), prefix it with `^`:
+ ```lua
+ patterns = { '^fixtures' }
+ ```
+- To specify the root has a certain directory as its direct ancestor / parent
+ (useful when you put working projects in a common directory), prefix it with `>`:
+ ```lua
+ patterns = { '>Latex' }
+ ```
+- To exclude a pattern, prefix it with `!`:
+ ```lua
+ patterns = { '!.git/worktrees', '!=extras', '!^fixtures', '!build/env.sh' }
+ ```
+
+**NOTE**: Make sure to put your pattern exclusions first, and then the patterns you do want included.
+
+
+
+Make sure these flags are enabled to support [`nvim-tree.lua`](nvim-tree/nvim-tree.lua):
```lua
-patterns = { "=src" }
+require('nvim-tree').setup({
+ sync_root_with_cwd = true,
+ respect_buf_cwd = true,
+ update_focused_file = {
+ enable = true,
+ update_root = true,
+ },
+})
```
-To specify the root has a certain directory or file (which may be a glob), just
-give the name:
+
+
+To enable [`telescope.nvim`](nvim-telescope/telescope.nvim) integration use the following
+code in your config:
```lua
-patterns = { ".git", "Makefile", "*.sln", "build/env.sh" }
+require('telescope').setup(...)
+-- Other stuff may come here...
+require('telescope').load_extension('projects')
```
-To specify the root has a certain directory as an ancestor (useful for
-excluding directories), prefix it with `^`:
+After that you can now call it from the command line:
-```lua
-patterns = { "^fixtures" }
+```vim
+:Telescope projects
```
-To specify the root has a certain directory as its direct ancestor / parent
-(useful when you put working projects in a common directory), prefix it with
-`>`:
+#### Telescope Projects Picker
+
+To use the projects picker execute the following Lua code:
```lua
-patterns = { ">Latex" }
+require('telescope').extensions.projects.projects()
```
-To exclude a pattern, prefix it with `!`.
+#### Telescope Mappings
-```lua
-patterns = { "!.git/worktrees", "!=extras", "!^fixtures", "!build/env.sh" }
-```
+`project.nvim` comes with the following mappings for Telescope:
-List your exclusions before the patterns you do want.
+| Normal mode | Insert mode | Action |
+| ----------- | ----------- | -------------------------- |
+| f | \ | `find_project_files` |
+| b | \ | `browse_project_files` |
+| d | \ | `delete_project` |
+| s | \ | `search_in_project_files` |
+| r | \ | `recent_project_files` |
+| w | \ | `change_working_directory` |
-### Telescope Integration
+---
-To enable telescope integration:
-```lua
-require('telescope').load_extension('projects')
-```
+## API
+
+You can get a list of recent projects by running the code below:
-#### Telescope Projects Picker
-To use the projects picker
```lua
-require'telescope'.extensions.projects.projects{}
+-- Using `vim.notify()`
+vim.notify(
+ vim.inspect(require('project_nvim').get_recent_projects()),
+ vim.log.levels.INFO
+)
+
+-- Using `vim.print()`
+vim.print(
+ vim.inspect(require('project_nvim').get_recent_projects())
+)
```
-#### Telescope mappings
+Where `get_recent_projects()` returns either an empty table `{}` or a string array `{ '/path/to/project', ... }`
-**project.nvim** comes with the following mappings:
+---
-| Normal mode | Insert mode | Action |
-| ----------- | ----------- | -------------------------- |
-| f | \ | find\_project\_files |
-| b | \ | browse\_project\_files |
-| d | \ | delete\_project |
-| s | \ | search\_in\_project\_files |
-| r | \ | recent\_project\_files |
-| w | \ | change\_working\_directory |
-
-## API
+## Contributing
-Get a list of recent projects:
+- All pull requests are welcome
+- If you encounter bugs please open an issue
-```lua
-local project_nvim = require("project_nvim")
-local recent_projects = project_nvim.get_recent_projects()
+---
-print(vim.inspect(recent_projects))
-```
+## Addendum
-## 🤝 Contributing
+(DrKJeff16) Thanks for the support to this fork <3
-- All pull requests are welcome.
-- If you encounter bugs please open an issue.
+Also, thanks to the original creator, [ahmedkhalf](https://github.com/ahmedkhalf)!
diff --git a/lua/project_nvim/config.lua b/lua/project_nvim/config.lua
index 36e3027f..66d1c042 100644
--- a/lua/project_nvim/config.lua
+++ b/lua/project_nvim/config.lua
@@ -1,66 +1,147 @@
-local M = {}
+---@diagnostic disable:missing-fields
----@class ProjectOptions
-M.defaults = {
- -- Manual mode doesn't automatically change your root directory, so you have
- -- the option to manually do so using `:ProjectRoot` command.
- manual_mode = false,
+local glob = require('project_nvim.utils.globtopattern')
- -- Methods of detecting the root directory. **"lsp"** uses the native neovim
- -- lsp, while **"pattern"** uses vim-rooter like glob pattern matching. Here
- -- order matters: if one is not detected, the other is used as fallback. You
- -- can also delete or rearangne the detection methods.
- detection_methods = { "lsp", "pattern" },
+---@class Project.Config.Options
+-- If `true` your root directory won't be changed automatically,
+-- so you have the option to manually do so using `:ProjectRoot` command.
+-- ---
+-- Default: `false`
+-- ---
+---@field manual_mode? boolean
+-- Methods of detecting the root directory. `'lsp'` uses the native neovim
+-- lsp, while `'pattern'` uses vim-rooter like glob pattern matching. Here
+-- order matters: if one is not detected, the other is used as fallback. You
+-- can also delete or rearrange the detection methods.
+-- ---
+-- Default: `{ 'lsp' , 'pattern' }`
+-- ---
+---@field detection_methods? ('lsp'|'pattern')[]
+-- All the patterns used to detect root dir, when **'pattern'** is in
+-- detection_methods
+-- ---
+-- Default:
+-- ```lua
+-- {
+-- '.git',
+-- '.github',
+-- '_darcs',
+-- '.hg',
+-- '.bzr',
+-- '.svn',
+-- 'package.json',
+-- '.stylua.toml',
+-- 'stylua.toml',
+-- }
+-- ```
+-- ---
+-- See `:h project.nvim-pattern-matching`
+-- ---
+---@field patterns? string[]
+-- Table of lsp clients to ignore by name
+-- e.g. `{ 'efm', ... }`
+-- ---
+-- Default: `{}`
+-- ---
+-- If you have `nvim-lspconfig` installed **see** `:h lspconfig-all`
+-- for a list of servers
+-- ---
+---@field ignore_lsp? string[]
+-- Don't calculate root dir on specific directories
+-- e.g. `{ '~/.cargo/*', ... }`
+-- ---
+-- Default: `{}`
+-- ---
+---@field exclude_dirs? string[]
+-- Make hidden files visible when using the `telescope` picker
+-- ---
+-- Default: `false`
+-- ---
+---@field show_hidden? boolean
+-- If `false`, you'll get a _notification_ every time
+-- `project.nvim` changes directory
+-- ---
+-- Default: `true`
+-- ---
+---@field silent_chdir? boolean
+-- What scope to change the directory, valid options are
+-- * `global`
+-- * `tab`
+-- * `win`
+-- ---
+-- Default: `'global'`
+-- ---
+---@field scope_chdir? 'global'|'tab'|'win'
+-- Path where `project.nvim` will store the project history for
+-- future use with the `telescope` picker
+-- ---
+-- Default: `vim.fn.stdpath('data')`
+-- ---
+---@field datapath? string
- -- All the patterns used to detect root dir, when **"pattern"** is in
- -- detection_methods
- patterns = { ".git", "_darcs", ".hg", ".bzr", ".svn", "Makefile", "package.json" },
+---@class Project.Config
+---@field defaults Project.Config.Options
+-- Options defined after running `require('project_nvim').setup()`
+-- ---
+-- Default: `{}` (before calling `setup()`)
+-- ---
+---@field options table|Project.Config.Options
+-- The function called when running `require('project_nvim').setup()`
+-- ---
+---@field setup fun(options: table|Project.Config.Options?)
- -- Table of lsp clients to ignore by name
- -- eg: { "efm", ... }
- ignore_lsp = {},
+---@param pattern string
+---@return string
+local function pattern_exclude(pattern)
+ local HOME_EXPAND = vim.fn.expand('~')
- -- Don't calculate root dir on specific directories
- -- Ex: { "~/.cargo/*", ... }
- exclude_dirs = {},
+ if vim.startswith(pattern, '~/') then
+ pattern = string.format('%s/%s', HOME_EXPAND, pattern:sub(3, #pattern))
+ end
- -- Show hidden files in telescope
- show_hidden = false,
+ return glob.globtopattern(pattern)
+end
- -- When set to false, you will get a message when project.nvim changes your
- -- directory.
- silent_chdir = true,
+---@type Project.Config
+local Config = {}
+Config.defaults = {
+ manual_mode = false,
+ detection_methods = { 'lsp', 'pattern' },
- -- What scope to change the directory, valid options are
- -- * global (default)
- -- * tab
- -- * win
- scope_chdir = 'global',
+ patterns = {
+ '.git',
+ '.github',
+ '_darcs',
+ '.hg',
+ '.bzr',
+ '.svn',
+ 'package.json',
+ '.stylua.toml',
+ 'stylua.toml',
+ },
- -- Path where project.nvim will store the project history for use in
- -- telescope
- datapath = vim.fn.stdpath("data"),
+ ignore_lsp = {},
+ exclude_dirs = {},
+ show_hidden = false,
+ silent_chdir = true,
+ scope_chdir = 'global',
+ datapath = vim.fn.stdpath('data'),
}
----@type ProjectOptions
-M.options = {}
+Config.options = {}
-M.setup = function(options)
- M.options = vim.tbl_deep_extend("force", M.defaults, options or {})
+---@param options? table|Project.Config.Options
+function Config.setup(options)
+ options = type(options) == 'table' and options or {}
- local glob = require("project_nvim.utils.globtopattern")
- local home = vim.fn.expand("~")
- M.options.exclude_dirs = vim.tbl_map(function(pattern)
- if vim.startswith(pattern, "~/") then
- pattern = home .. "/" .. pattern:sub(3, #pattern)
- end
- return glob.globtopattern(pattern)
- end, M.options.exclude_dirs)
+ Config.options = vim.tbl_deep_extend('keep', options, Config.defaults)
+ Config.options.exclude_dirs = vim.tbl_map(pattern_exclude, Config.options.exclude_dirs)
- vim.opt.autochdir = false -- implicitly unset autochdir
+ -- Implicitly unset autochdir
+ vim.opt.autochdir = false
- require("project_nvim.utils.path").init()
- require("project_nvim.project").init()
+ require('project_nvim.utils.path').init()
+ require('project_nvim.project').init()
end
-return M
+return Config
diff --git a/lua/project_nvim/init.lua b/lua/project_nvim/init.lua
index ab45fb33..0581fb18 100644
--- a/lua/project_nvim/init.lua
+++ b/lua/project_nvim/init.lua
@@ -1,8 +1,16 @@
-local config = require("project_nvim.config")
-local history = require("project_nvim.utils.history")
-local M = {}
+---@diagnostic disable:missing-fields
-M.setup = config.setup
-M.get_recent_projects = history.get_recent_projects
+local config = require('project_nvim.config')
+local history = require('project_nvim.utils.history')
-return M
+---@class Project
+---@field setup fun(options: table|Project.Config.Options)
+---@field get_recent_projects fun(): table|string[]
+
+---@type Project
+local Project = {}
+
+Project.setup = config.setup
+Project.get_recent_projects = history.get_recent_projects
+
+return Project
diff --git a/lua/project_nvim/project.lua b/lua/project_nvim/project.lua
index 76972eec..e1b2692e 100644
--- a/lua/project_nvim/project.lua
+++ b/lua/project_nvim/project.lua
@@ -1,286 +1,362 @@
-local config = require("project_nvim.config")
-local history = require("project_nvim.utils.history")
-local glob = require("project_nvim.utils.globtopattern")
-local path = require("project_nvim.utils.path")
-local uv = vim.loop
+---@diagnostic disable:missing-fields
+
+local config = require('project_nvim.config')
+local history = require('project_nvim.utils.history')
+local glob = require('project_nvim.utils.globtopattern')
+local path = require('project_nvim.utils.path')
+
+local uv = vim.uv or vim.loop
+
+-- TODO: (DrKJeff16) Figure out a more appropriate name
+---@class Project.LSP
+---@field init fun()
+---@field attached_lsp boolean
+---@field last_project string?
+---@field find_lsp_root fun(): (string?,string?)
+---@field find_pattern_root fun(): ((string|nil),string?)
+---@field on_attach_lsp fun(client: vim.lsp.Client, bufnr: integer)
+---@field attach_to_lsp fun(): (integer?,string?)
+---@field set_pwd fun(dir: string, method: string): boolean?
+---@field get_project_root fun(): (string?,string?)
+---@field is_file fun(): boolean
+---@field on_buf_enter fun()
+---@field add_project_manually fun()
+
+local in_tbl = vim.tbl_contains
+
+---@type Project.LSP
local M = {}
-- Internal states
M.attached_lsp = false
M.last_project = nil
+---@return (string|nil),string?
function M.find_lsp_root()
- -- Get lsp client for current buffer
- -- Returns nil or string
- local buf_ft = vim.api.nvim_buf_get_option(0, "filetype")
- local clients = vim.lsp.buf_get_clients()
- if next(clients) == nil then
- return nil
- end
-
- for _, client in pairs(clients) do
- local filetypes = client.config.filetypes
- if filetypes and vim.tbl_contains(filetypes, buf_ft) then
- if not vim.tbl_contains(config.options.ignore_lsp, client.name) then
- return client.config.root_dir, client.name
- end
+ -- Get lsp client for current buffer
+ -- Returns nil or string
+ local bufnr = vim.api.nvim_get_current_buf()
+
+ local clients = vim.lsp.get_clients({ bufnr = bufnr })
+ if next(clients) == nil then
+ return nil
end
- end
- return nil
-end
+ local ft = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
-function M.find_pattern_root()
- local search_dir = vim.fn.expand("%:p:h", true)
- if vim.fn.has("win32") > 0 then
- search_dir = search_dir:gsub("\\", "/")
- end
-
- local last_dir_cache = ""
- local curr_dir_cache = {}
-
- local function get_parent(path)
- path = path:match("^(.*)/")
- if path == "" then
- path = "/"
- end
- return path
- end
+ for _, client in next, clients do
+ ---@type table|string[]
+ ---@diagnostic disable-next-line:undefined-field
+ local filetypes = client.config.filetypes -- For whatever reason this field is not declared in the type...
- local function get_files(file_dir)
- last_dir_cache = file_dir
- curr_dir_cache = {}
+ if type(filetypes) ~= 'table' or vim.tbl_isempty(filetypes) then
+ goto continue
+ end
- local dir = uv.fs_scandir(file_dir)
- if dir == nil then
- return
+ if in_tbl(filetypes, ft) and not in_tbl(config.options.ignore_lsp, client.name) then
+ return client.config.root_dir, client.name
+ end
+
+ ::continue::
end
- while true do
- local file = uv.fs_scandir_next(dir)
- if file == nil then
- return
- end
+ return nil
+end
- table.insert(curr_dir_cache, file)
+---@return (string|nil),string?
+function M.find_pattern_root()
+ local search_dir = vim.fn.expand('%:p:h', true)
+ if vim.fn.has('win32') > 0 then
+ search_dir = search_dir:gsub('\\', '/')
end
- end
- local function is(dir, identifier)
- dir = dir:match(".*/(.*)")
- return dir == identifier
- end
+ local last_dir_cache = ''
+ ---@type string[]
+ local curr_dir_cache = {}
- local function sub(dir, identifier)
- local path = get_parent(dir)
- while true do
- if is(path, identifier) then
- return true
- end
- local current = path
- path = get_parent(path)
- if current == path then
- return false
- end
+ ---@param path_str string
+ ---@return string path_str
+ local function get_parent(path_str)
+ path_str = path_str:match('^(.*)/')
+
+ return (path_str ~= '') and path_str or '/'
end
- end
- local function child(dir, identifier)
- local path = get_parent(dir)
- return is(path, identifier)
- end
+ ---@param file_dir string
+ local function get_files(file_dir)
+ last_dir_cache = file_dir
+ curr_dir_cache = {}
+
+ ---@type uv_fs_t|nil
+ local dir = uv.fs_scandir(file_dir)
+ if dir == nil then
+ return
+ end
+
+ ---@type string|nil
+ local file
+
+ while true do
+ file = uv.fs_scandir_next(dir)
+ if file == nil then
+ return
+ end
- local function has(dir, identifier)
- if last_dir_cache ~= dir then
- get_files(dir)
+ table.insert(curr_dir_cache, file)
+ end
end
- local pattern = glob.globtopattern(identifier)
- for _, file in ipairs(curr_dir_cache) do
- if file:match(pattern) ~= nil then
- return true
- end
+
+ ---@param dir string
+ ---@param identifier string
+ ---@return boolean
+ local function is(dir, identifier) return dir:match('.*/(.*)') == identifier end
+
+ ---@param dir string
+ ---@param identifier string
+ local function sub(dir, identifier)
+ local path_str = get_parent(dir)
+ local current = ''
+
+ -- FIXME: (DrKJeff16) This loop is dangerous, even if halting cond is supposedly known
+ while true do
+ if is(path_str, identifier) then
+ return true
+ end
+ current = path_str
+ path_str = get_parent(path_str)
+
+ if current == path_str then
+ return false
+ end
+ end
end
- return false
- end
-
- local function match(dir, pattern)
- local first_char = pattern:sub(1, 1)
- if first_char == "=" then
- return is(dir, pattern:sub(2))
- elseif first_char == "^" then
- return sub(dir, pattern:sub(2))
- elseif first_char == ">" then
- return child(dir, pattern:sub(2))
- else
- return has(dir, pattern)
+
+ ---@param dir string
+ ---@param identifier string
+ local function child(dir, identifier) return is(get_parent(dir), identifier) end
+
+ ---@param dir string
+ ---@param identifier string
+ local function has(dir, identifier)
+ if last_dir_cache ~= dir then
+ get_files(dir)
+ end
+ local pattern = glob.globtopattern(identifier)
+ for _, file in next, curr_dir_cache do
+ if file:match(pattern) ~= nil then
+ return true
+ end
+ end
+ return false
end
- end
-
- -- breadth-first search
- while true do
- for _, pattern in ipairs(config.options.patterns) do
- local exclude = false
- if pattern:sub(1, 1) == "!" then
- exclude = true
- pattern = pattern:sub(2)
- end
- if match(search_dir, pattern) then
- if exclude then
- break
+
+ ---@param dir string
+ ---@param pattern string
+ ---@return boolean
+ local function match(dir, pattern)
+ local first_char = pattern:sub(1, 1)
+ if first_char == '=' then
+ return is(dir, pattern:sub(2))
+ elseif first_char == '^' then
+ return sub(dir, pattern:sub(2))
+ elseif first_char == '>' then
+ return child(dir, pattern:sub(2))
else
- return search_dir, "pattern " .. pattern
+ return has(dir, pattern)
end
- end
end
- local parent = get_parent(search_dir)
- if parent == search_dir or parent == nil then
- return nil
- end
+ -- FIXME: (DrKJeff16) This loop is dangerous, even if halting cond is supposedly known
+ -- breadth-first search
+ while true do
+ for _, pattern in next, config.options.patterns do
+ local exclude = false
+ if pattern:sub(1, 1) == '!' then
+ exclude = true
+ pattern = pattern:sub(2)
+ end
+ if match(search_dir, pattern) then
+ if exclude then
+ break
+ else
+ return search_dir, 'pattern ' .. pattern
+ end
+ end
+ end
+
+ local parent = get_parent(search_dir)
+ if parent == search_dir or parent == nil then
+ return nil
+ end
- search_dir = parent
- end
+ search_dir = parent
+ end
end
----@diagnostic disable-next-line: unused-local
-local on_attach_lsp = function(client, bufnr)
- M.on_buf_enter() -- Recalculate root dir after lsp attaches
+---@param client vim.lsp.Client
+---@param bufnr integer
+local function on_attach_lsp(client, bufnr)
+ M.on_buf_enter() -- Recalculate root dir after lsp attaches
end
+-- WARN: (DrKJeff16) Honestly I'm not feeling good about this one, chief
+---@return integer?,string?
function M.attach_to_lsp()
- if M.attached_lsp then
- return
- end
-
- local _start_client = vim.lsp.start_client
- vim.lsp.start_client = function(lsp_config)
- if lsp_config.on_attach == nil then
- lsp_config.on_attach = on_attach_lsp
- else
- local _on_attach = lsp_config.on_attach
- lsp_config.on_attach = function(client, bufnr)
- on_attach_lsp(client, bufnr)
- _on_attach(client, bufnr)
- end
+ if M.attached_lsp then
+ return
end
- return _start_client(lsp_config)
- end
- M.attached_lsp = true
+ -- Backup old `start_client` function
+ local _start_client = vim.lsp.start_client
+
+ ---@param lsp_config vim.lsp.ClientConfig
+ vim.lsp.start_client = function(lsp_config)
+ if lsp_config.on_attach == nil then
+ lsp_config.on_attach = on_attach_lsp
+ else
+ local _on_attach = lsp_config.on_attach
+ lsp_config.on_attach = function(client, bufnr)
+ on_attach_lsp(client, bufnr)
+ _on_attach(client, bufnr)
+ end
+ end
+ return _start_client(lsp_config)
+ end
+
+ M.attached_lsp = true
end
+---@param dir string
+---@param method string
+---@return boolean?
function M.set_pwd(dir, method)
- if dir ~= nil then
+ if dir == nil then
+ return false
+ end
+
M.last_project = dir
table.insert(history.session_projects, dir)
if vim.fn.getcwd() ~= dir then
- local scope_chdir = config.options.scope_chdir
- if scope_chdir == 'global' then
- vim.api.nvim_set_current_dir(dir)
- elseif scope_chdir == 'tab' then
- vim.cmd('tcd ' .. dir)
- elseif scope_chdir == 'win' then
- vim.cmd('lcd ' .. dir)
- else
- return
- end
+ local scope_chdir = config.options.scope_chdir
+ if scope_chdir == 'global' then
+ vim.api.nvim_set_current_dir(dir)
+ elseif scope_chdir == 'tab' then
+ vim.cmd('tcd ' .. dir)
+ elseif scope_chdir == 'win' then
+ vim.cmd('lcd ' .. dir)
+ else
+ return
+ end
- if config.options.silent_chdir == false then
- vim.notify("Set CWD to " .. dir .. " using " .. method)
- end
+ if not config.options.silent_chdir then
+ vim.notify(string.format('Set CWD to %s using %s\n', dir, method), vim.log.levels.INFO)
+ end
end
- return true
- end
- return false
+ return true
end
+---@return (string|nil),string?
function M.get_project_root()
- -- returns project root, as well as method
- for _, detection_method in ipairs(config.options.detection_methods) do
- if detection_method == "lsp" then
- local root, lsp_name = M.find_lsp_root()
- if root ~= nil then
- return root, '"' .. lsp_name .. '"' .. " lsp"
- end
- elseif detection_method == "pattern" then
- local root, method = M.find_pattern_root()
- if root ~= nil then
- return root, method
- end
+ -- returns project root, as well as method
+ for _, detection_method in next, config.options.detection_methods do
+ if detection_method == 'lsp' then
+ local root, lsp_name = M.find_lsp_root()
+ if root ~= nil then
+ return root, string.format('"%s" lsp\n', lsp_name)
+ end
+ elseif detection_method == 'pattern' then
+ local root, method = M.find_pattern_root()
+ if root ~= nil then
+ return root, method
+ end
+ end
end
- end
+
+ return nil
end
+---@return boolean
function M.is_file()
- local buf_type = vim.api.nvim_buf_get_option(0, "buftype")
-
- local whitelisted_buf_type = { "", "acwrite" }
- local is_in_whitelist = false
- for _, wtype in ipairs(whitelisted_buf_type) do
- if buf_type == wtype then
- is_in_whitelist = true
- break
+ local bufnr = vim.api.nvim_get_current_buf()
+
+ local buf_type = vim.api.nvim_get_option_value('buftype', { buf = bufnr })
+
+ local whitelisted_buf_type = { '', 'acwrite' }
+ for _, wtype in next, whitelisted_buf_type do
+ if buf_type == wtype then
+ return true
+ end
end
- end
- if not is_in_whitelist then
- return false
- end
- return true
+ return false
end
function M.on_buf_enter()
- if vim.v.vim_did_enter == 0 then
- return
- end
+ if vim.v.vim_did_enter == 0 then
+ return
+ end
- if not M.is_file() then
- return
- end
+ if not M.is_file() then
+ return
+ end
- local current_dir = vim.fn.expand("%:p:h", true)
- if not path.exists(current_dir) or path.is_excluded(current_dir) then
- return
- end
+ local current_dir = vim.fn.expand('%:p:h', true)
+ if not path.exists(current_dir) or path.is_excluded(current_dir) then
+ return
+ end
- local root, method = M.get_project_root()
- M.set_pwd(root, method)
+ local root, method = M.get_project_root()
+ M.set_pwd(root, method)
end
function M.add_project_manually()
- local current_dir = vim.fn.expand("%:p:h", true)
- M.set_pwd(current_dir, 'manual')
+ local current_dir = vim.fn.expand('%:p:h', true)
+ M.set_pwd(current_dir, 'manual')
end
function M.init()
- local autocmds = {}
- if not config.options.manual_mode then
- autocmds[#autocmds + 1] = 'autocmd VimEnter,BufEnter * ++nested lua require("project_nvim.project").on_buf_enter()'
-
- if vim.tbl_contains(config.options.detection_methods, "lsp") then
- M.attach_to_lsp()
+ ---@type { integer: string|string[], integer: vim.api.keyset.create_autocmd }[]
+ local autocmds = {}
+
+ -- Create the augroup, clear it
+ local augroup = vim.api.nvim_create_augroup('project_nvim', { clear = true })
+
+ if not config.options.manual_mode then
+ table.insert(autocmds, {
+ { 'VimEnter', 'BufEnter', 'WinEnter' },
+ {
+ pattern = '*',
+ group = augroup,
+ nested = true,
+ callback = function() M.on_buf_enter() end,
+ },
+ })
+
+ if in_tbl(config.options.detection_methods, 'lsp') then
+ M.attach_to_lsp()
+ end
end
- end
-
- vim.cmd([[
- command! ProjectRoot lua require("project_nvim.project").on_buf_enter()
- command! AddProject lua require("project_nvim.project").add_project_manually()
- ]])
- autocmds[#autocmds + 1] =
- 'autocmd VimLeavePre * lua require("project_nvim.utils.history").write_projects_to_history()'
-
- vim.cmd([[augroup project_nvim
- au!
- ]])
- for _, value in ipairs(autocmds) do
- vim.cmd(value)
- end
- vim.cmd("augroup END")
+ -- TODO(DrKJeff16): Rewrite this statement using Lua
+ vim.cmd('command! ProjectRoot lua require("project_nvim.project").on_buf_enter()')
+ vim.cmd('command! AddProject lua require("project_nvim.project").add_project_manually()')
+
+ table.insert(autocmds, {
+ 'VimLeavePre',
+ {
+ pattern = '*',
+ group = augroup,
+ callback = function() history.write_projects_to_history() end,
+ },
+ })
+
+ for _, value in next, autocmds do
+ vim.api.nvim_create_autocmd(value[1], value[2])
+ end
- history.read_projects_from_history()
+ history.read_projects_from_history()
end
return M
diff --git a/lua/project_nvim/utils/globtopattern.lua b/lua/project_nvim/utils/globtopattern.lua
index 1d784cae..99d04ad9 100644
--- a/lua/project_nvim/utils/globtopattern.lua
+++ b/lua/project_nvim/utils/globtopattern.lua
@@ -1,139 +1,153 @@
-- Credits for this module goes to: David Manura
-- https://github.com/davidm/lua-glob-pattern
-local M = { _TYPE = "module", _NAME = "globtopattern", _VERSION = "0.2.1.20120406" }
+---@class Project.Utils.GlobPattern
+---@field _TYPE string
+---@field _NAME string
+---@field _VERSION string
+---@field globtopattern fun(g: string): (p: string)
-function M.globtopattern(g)
- -- Some useful references:
- -- - apr_fnmatch in Apache APR. For example,
- -- http://apr.apache.org/docs/apr/1.3/group__apr__fnmatch.html
- -- which cites POSIX 1003.2-1992, section B.6.
+---@type Project.Utils.GlobPattern
+---@diagnostic disable-next-line:missing-fields
+local M = { _TYPE = 'module', _NAME = 'globtopattern', _VERSION = '0.2.1.20120406' }
- local p = "^" -- pattern being built
- local i = 0 -- index in g
- local c -- char at index i in g.
+-- Some useful references:
+-- - apr_fnmatch in Apache APR. For example,
+-- http://apr.apache.org/docs/apr/1.3/group__apr__fnmatch.html
+-- which cites POSIX 1003.2-1992, section B.6.
+---@param g string
+---@return string p
+function M.globtopattern(g)
+ local p = '^' -- pattern being built
+ local i = 0 -- index in g
+ local c = '' -- char at index i in g.
- -- unescape glob char
- local function unescape()
- if c == "\\" then
- i = i + 1
- c = g:sub(i, i)
- if c == "" then
- p = "[^]"
- return false
- end
+ -- unescape glob char
+ ---@return boolean
+ local function unescape()
+ if c == '\\' then
+ i = i + 1
+ c = g:sub(i, i)
+ if c == '' then
+ p = '[^]'
+ return false
+ end
+ end
+ return true
end
- return true
- end
- -- escape pattern char
- local function escape(c)
- return c:match("^%w$") and c or "%" .. c
- end
+ -- escape pattern char
+ ---@param char string
+ ---@return string
+ local function escape(char) return char:match('^%w$') and c or '%' .. c end
- -- Convert tokens at end of charset.
- local function charset_end()
- while 1 do
- if c == "" then
- p = "[^]"
- return false
- elseif c == "]" then
- p = p .. "]"
- break
- else
- if not unescape() then
- break
+ -- TODO(DrKJeff16): Let's simplify this in the future
+ -- Convert tokens at end of charset.
+ ---@return boolean
+ local function charset_end()
+ while true do
+ if c == '' then
+ p = '[^]'
+ return false
+ elseif c == ']' then
+ p = p .. ']'
+ break
+ else
+ if not unescape() then
+ break
+ end
+ local c1 = c
+ i = i + 1
+ c = g:sub(i, i)
+ if c == '' then
+ p = '[^]'
+ return false
+ elseif c == '-' then
+ i = i + 1
+ c = g:sub(i, i)
+ if c == '' then
+ p = '[^]'
+ return false
+ elseif c == ']' then
+ p = p .. escape(c1) .. '%-]'
+ break
+ else
+ if not unescape() then
+ break
+ end
+ p = p .. escape(c1) .. '-' .. escape(c)
+ end
+ elseif c == ']' then
+ p = p .. escape(c1) .. ']'
+ break
+ else
+ p = p .. escape(c1)
+ i = i - 1 -- put back
+ end
+ end
+ i = i + 1
+ c = g:sub(i, i)
end
- local c1 = c
+ return true
+ end
+
+ -- Convert tokens in charset.
+ ---@return boolean
+ local function charset()
i = i + 1
c = g:sub(i, i)
- if c == "" then
- p = "[^]"
- return false
- elseif c == "-" then
- i = i + 1
- c = g:sub(i, i)
- if c == "" then
- p = "[^]"
+ if c == '' or c == ']' then
+ p = '[^]'
return false
- elseif c == "]" then
- p = p .. escape(c1) .. "%-]"
- break
- else
- if not unescape() then
- break
+ elseif c == '^' or c == '!' then
+ i = i + 1
+ c = g:sub(i, i)
+ if c == ']' then
+ -- ignored
+ else
+ p = p .. '[^'
+ if not charset_end() then
+ return false
+ end
end
- p = p .. escape(c1) .. "-" .. escape(c)
- end
- elseif c == "]" then
- p = p .. escape(c1) .. "]"
- break
else
- p = p .. escape(c1)
- i = i - 1 -- put back
+ p = p .. '['
+ if not charset_end() then
+ return false
+ end
end
- end
- i = i + 1
- c = g:sub(i, i)
+ return true
end
- return true
- end
- -- Convert tokens in charset.
- local function charset()
- i = i + 1
- c = g:sub(i, i)
- if c == "" or c == "]" then
- p = "[^]"
- return false
- elseif c == "^" or c == "!" then
- i = i + 1
- c = g:sub(i, i)
- if c == "]" then
- -- ignored
- else
- p = p .. "[^"
- if not charset_end() then
- return false
+ -- Convert tokens.
+ while true do
+ i = i + 1
+ c = g:sub(i, i)
+ if c == '' then
+ p = p .. '$'
+ break
+ elseif c == '?' then
+ p = p .. '.'
+ elseif c == '*' then
+ p = p .. '.*'
+ elseif c == '[' then
+ if not charset() then
+ break
+ end
+ elseif c == '\\' then
+ i = i + 1
+ c = g:sub(i, i)
+ if c == '' then
+ p = p .. '\\$'
+ break
+ end
+ p = p .. escape(c)
+ else
+ p = p .. escape(c)
end
- end
- else
- p = p .. "["
- if not charset_end() then
- return false
- end
end
- return true
- end
- -- Convert tokens.
- while 1 do
- i = i + 1
- c = g:sub(i, i)
- if c == "" then
- p = p .. "$"
- break
- elseif c == "?" then
- p = p .. "."
- elseif c == "*" then
- p = p .. ".*"
- elseif c == "[" then
- if not charset() then
- break
- end
- elseif c == "\\" then
- i = i + 1
- c = g:sub(i, i)
- if c == "" then
- p = p .. "\\$"
- break
- end
- p = p .. escape(c)
- else
- p = p .. escape(c)
- end
- end
- return p
+ return p
end
return M
diff --git a/lua/project_nvim/utils/history.lua b/lua/project_nvim/utils/history.lua
index 486c1ee3..60cb357b 100644
--- a/lua/project_nvim/utils/history.lua
+++ b/lua/project_nvim/utils/history.lua
@@ -1,178 +1,235 @@
-local path = require("project_nvim.utils.path")
-local uv = vim.loop
-local M = {}
-local is_windows = vim.fn.has('win32') or vim.fn.has('wsl')
-
-M.recent_projects = nil -- projects from previous neovim sessions
-M.session_projects = {} -- projects from current neovim session
-M.has_watch_setup = false
-
+---@alias OpenMode
+---|integer
+---|string
+---|"a"
+---|"a+"
+---|"ax"
+---|"ax+"
+---|"r"
+---|"r+"
+---|"rs"
+---|"rs"
+---|"sr"
+---|"sr+"
+---|"w"
+---|"w+"
+---|"wx"
+---|"wx+"
+---|"xa"
+---|"xa+"
+---|"xw"
+---|"xw+"
+
+---@class ProjParam
+---@field value string
+
+---@class Project.Utils.History
+---@field recent_projects (string|nil)[]|nil
+---@field session_projects (string|nil)[]|nil
+---@field has_watch_setup boolean
+---@field read_projects_from_history fun()
+---@field write_projects_to_history fun()
+---@field get_recent_projects fun(): table|string[]
+---@field delete_project fun(project: ProjParam)
+
+local path = require('project_nvim.utils.path')
+local uv = vim.uv or vim.loop
+local is_windows = (uv.os_uname().version:match('Windows') ~= nil or vim.fn.has('wsl')) -- Thanks to `folke` for
+-- that code
+
+---@type Project.Utils.History
+---@diagnostic disable-next-line:missing-fields
+local M = {
+ -- projects from previous neovim sessions
+ recent_projects = nil,
+
+ -- projects from current neovim session
+ session_projects = {},
+ has_watch_setup = false,
+}
+
+---@param mode OpenMode
+---@param callback? fun(err: string|nil, fd: integer|nil)
+---@return integer|nil
local function open_history(mode, callback)
- if callback ~= nil then -- async
- path.create_scaffolding(function(_, _)
- uv.fs_open(path.historyfile, mode, 438, callback)
- end)
- else -- sync
- path.create_scaffolding()
- return uv.fs_open(path.historyfile, mode, 438)
- end
+ if callback ~= nil then -- async
+ path.create_scaffolding(
+ function(_, _) uv.fs_open(path.historyfile, mode, 438, callback) end
+ )
+ else -- sync
+ path.create_scaffolding()
+ return uv.fs_open(path.historyfile, mode, 438)
+ end
end
+---@param dir string
+---@return boolean
local function dir_exists(dir)
- local stat = uv.fs_stat(dir)
- if stat ~= nil and stat.type == "directory" then
- return true
- end
- return false
+ local stat = uv.fs_stat(dir)
+
+ return (stat ~= nil and stat.type == 'directory')
end
+---@param path_to_normalise string
+---@return string normalised_path
local function normalise_path(path_to_normalise)
- local normalised_path = path_to_normalise:gsub("\\", "/"):gsub("//", "/")
+ local normalised_path = path_to_normalise:gsub('\\', '/'):gsub('//', '/')
if is_windows then
- normalised_path = normalised_path:sub(1,1):lower()..normalised_path:sub(2)
+ normalised_path = normalised_path:sub(1, 1):lower() .. normalised_path:sub(2)
end
return normalised_path
end
+---@param tbl string[]
+---@return string[] res
local function delete_duplicates(tbl)
- local cache_dict = {}
- for _, v in ipairs(tbl) do
- local normalised_path = normalise_path(v)
- if cache_dict[normalised_path] == nil then
- cache_dict[normalised_path] = 1
- else
- cache_dict[normalised_path] = cache_dict[normalised_path] + 1
+ ---@type table
+ local cache_dict = {}
+ for _, v in next, tbl do
+ local normalised_path = normalise_path(v)
+ if cache_dict[normalised_path] == nil then
+ cache_dict[normalised_path] = 1
+ else
+ cache_dict[normalised_path] = cache_dict[normalised_path] + 1
+ end
end
- end
- local res = {}
- for _, v in ipairs(tbl) do
- local normalised_path = normalise_path(v)
- if cache_dict[normalised_path] == 1 then
- table.insert(res, normalised_path)
- else
- cache_dict[normalised_path] = cache_dict[normalised_path] - 1
+ ---@type string[]
+ local res = {}
+ for _, v in next, tbl do
+ local normalised_path = normalise_path(v)
+ if cache_dict[normalised_path] == 1 then
+ table.insert(res, normalised_path)
+ else
+ cache_dict[normalised_path] = cache_dict[normalised_path] - 1
+ end
end
- end
- return res
+ return res
end
+---@param project ProjParam
function M.delete_project(project)
- for k, v in ipairs(M.recent_projects) do
- if v == project.value then
- M.recent_projects[k] = nil
+ for k, v in next, M.recent_projects do
+ if v == project.value then
+ M.recent_projects[k] = nil
+ end
end
- end
end
+---@param history_data string
local function deserialize_history(history_data)
- -- split data to table
- local projects = {}
- for s in history_data:gmatch("[^\r\n]+") do
- if not path.is_excluded(s) and dir_exists(s) then
- table.insert(projects, s)
+ -- split data to table
+ ---@type string[]
+ local projects = {}
+ for s in history_data:gmatch('[^\r\n]+') do
+ if not path.is_excluded(s) and dir_exists(s) then
+ table.insert(projects, s)
+ end
end
- end
- projects = delete_duplicates(projects)
+ projects = delete_duplicates(projects)
- M.recent_projects = projects
+ M.recent_projects = projects
end
local function setup_watch()
- -- Only runs once
- if M.has_watch_setup == false then
- M.has_watch_setup = true
- local event = uv.new_fs_event()
- if event == nil then
- return
+ -- Only runs once
+ if not M.has_watch_setup then
+ M.has_watch_setup = true
+ local event = uv.new_fs_event()
+ if event == nil then
+ return
+ end
+ event:start(path.projectpath, {}, function(err, _, events)
+ if err ~= nil then
+ return
+ end
+ if events.change then
+ M.recent_projects = nil
+ M.read_projects_from_history()
+ end
+ end)
end
- event:start(path.projectpath, {}, function(err, _, events)
- if err ~= nil then
- return
- end
- if events["change"] then
- M.recent_projects = nil
- M.read_projects_from_history()
- end
- end)
- end
end
function M.read_projects_from_history()
- open_history("r", function(_, fd)
- setup_watch()
- if fd ~= nil then
- uv.fs_fstat(fd, function(_, stat)
- if stat ~= nil then
- uv.fs_read(fd, stat.size, -1, function(_, data)
- uv.fs_close(fd, function(_, _) end)
- deserialize_history(data)
- end)
+ open_history('r', function(_, fd)
+ setup_watch()
+ if fd ~= nil then
+ uv.fs_fstat(fd, function(_, stat)
+ if stat ~= nil then
+ uv.fs_read(fd, stat.size, -1, function(_, data)
+ uv.fs_close(fd, function(_, _) end)
+ deserialize_history(data)
+ end)
+ end
+ end)
end
- end)
- end
- end)
+ end)
end
+---@return string[] real_tbl
local function sanitize_projects()
- local tbl = {}
- if M.recent_projects ~= nil then
- vim.list_extend(tbl, M.recent_projects)
- vim.list_extend(tbl, M.session_projects)
- else
- tbl = M.session_projects
- end
-
- tbl = delete_duplicates(tbl)
-
- local real_tbl = {}
- for _, dir in ipairs(tbl) do
- if dir_exists(dir) then
- table.insert(real_tbl, dir)
+ ---@type string[]
+ local tbl = {}
+
+ if M.recent_projects ~= nil then
+ vim.list_extend(tbl, M.recent_projects)
+ vim.list_extend(tbl, M.session_projects)
+ else
+ tbl = M.session_projects
end
- end
- return real_tbl
-end
+ tbl = delete_duplicates(tbl)
+
+ ---@type string[]
+ local real_tbl = {}
+ for _, dir in next, tbl do
+ if dir_exists(dir) then
+ table.insert(real_tbl, dir)
+ end
+ end
-function M.get_recent_projects()
- return sanitize_projects()
+ return real_tbl
end
+---@return string[]
+function M.get_recent_projects() return sanitize_projects() end
+
function M.write_projects_to_history()
- -- Unlike read projects, write projects is synchronous
- -- because it runs when vim ends
- local mode = "w"
- if M.recent_projects == nil then
- mode = "a"
- end
- local file = open_history(mode)
-
- if file ~= nil then
- local res = sanitize_projects()
-
- -- Trim table to last 100 entries
- local len_res = #res
- local tbl_out
- if #res > 100 then
- tbl_out = vim.list_slice(res, len_res - 100, len_res)
- else
- tbl_out = res
+ -- Unlike read projects, write projects is synchronous
+ -- because it runs when vim ends
+ local mode = 'w'
+ if M.recent_projects == nil then
+ mode = 'a'
end
+ local file = open_history(mode)
+
+ if file ~= nil then
+ local res = sanitize_projects()
+
+ -- Trim table to last 100 entries
+ local len_res = #res
+ ---@type string[]
+ local tbl_out
+ if #res > 100 then
+ tbl_out = vim.list_slice(res, len_res - 100, len_res)
+ else
+ tbl_out = res
+ end
- -- Transform table to string
- local out = ""
- for _, v in ipairs(tbl_out) do
- out = out .. v .. "\n"
- end
+ -- Transform table to string
+ local out = ''
+ for _, v in next, tbl_out do
+ out = out .. v .. '\n'
+ end
- -- Write string out to file and close
- uv.fs_write(file, out, -1)
- uv.fs_close(file)
- end
+ -- Write string out to file and close
+ uv.fs_write(file, out, -1)
+ uv.fs_close(file)
+ end
end
return M
diff --git a/lua/project_nvim/utils/path.lua b/lua/project_nvim/utils/path.lua
index 69ac7ade..aafbca74 100644
--- a/lua/project_nvim/utils/path.lua
+++ b/lua/project_nvim/utils/path.lua
@@ -1,37 +1,48 @@
-local config = require("project_nvim.config")
-local uv = vim.loop
+local config = require('project_nvim.config')
+local uv = vim.uv or vim.loop
+
+---@class Project.Utils.Path
+---@field datapath string
+---@field projectpath string
+---@field historyfile string
+---@field init fun()
+---@field create_scaffolding fun(callback: (fun(err: string|nil, success: boolean|nil))?)
+---@field is_excluded fun(dir: string): boolean
+---@field exists fun(path: string): boolean
+
+---@type Project.Utils.Path
+---@diagnostic disable-next-line:missing-fields
local M = {}
-M.datapath = vim.fn.stdpath("data") -- directory
-M.projectpath = M.datapath .. "/project_nvim" -- directory
-M.historyfile = M.projectpath .. "/project_history" -- file
+M.datapath = vim.fn.stdpath('data') -- directory
+M.projectpath = M.datapath .. '/project_nvim' -- directory
+M.historyfile = M.projectpath .. '/project_history' -- file
function M.init()
- M.datapath = require("project_nvim.config").options.datapath
- M.projectpath = M.datapath .. "/project_nvim" -- directory
- M.historyfile = M.projectpath .. "/project_history" -- file
+ M.datapath = config.options.datapath
+ M.projectpath = M.datapath .. '/project_nvim' -- directory
+ M.historyfile = M.projectpath .. '/project_history' -- file
end
+---@param callback fun(err: string|nil, success: boolean|nil)
function M.create_scaffolding(callback)
- if callback ~= nil then -- async
- uv.fs_mkdir(M.projectpath, 448, callback)
- else -- sync
- uv.fs_mkdir(M.projectpath, 448)
- end
+ uv.fs_mkdir(M.projectpath, 448, callback ~= nil and callback or nil)
end
+---@param dir string
+---@return boolean
function M.is_excluded(dir)
- for _, dir_pattern in ipairs(config.options.exclude_dirs) do
- if dir:match(dir_pattern) ~= nil then
- return true
+ for _, dir_pattern in ipairs(config.options.exclude_dirs) do
+ if dir:match(dir_pattern) ~= nil then
+ return true
+ end
end
- end
- return false
+ return false
end
-function M.exists(path)
- return vim.fn.empty(vim.fn.glob(path)) == 0
-end
+---@param path string
+---@return boolean
+function M.exists(path) return vim.fn.empty(vim.fn.glob(path)) == 0 end
return M
diff --git a/lua/telescope/_extensions/projects.lua b/lua/telescope/_extensions/projects.lua
index be35f4f3..8bf92137 100644
--- a/lua/telescope/_extensions/projects.lua
+++ b/lua/telescope/_extensions/projects.lua
@@ -1,180 +1,191 @@
-- Inspiration from:
-- https://github.com/nvim-telescope/telescope-project.nvim
-local has_telescope, telescope = pcall(require, "telescope")
+local has_telescope, telescope = pcall(require, 'telescope')
if not has_telescope then
- return
+ return
end
-local finders = require("telescope.finders")
-local pickers = require("telescope.pickers")
-local telescope_config = require("telescope.config").values
-local actions = require("telescope.actions")
-local state = require("telescope.actions.state")
-local builtin = require("telescope.builtin")
-local entry_display = require("telescope.pickers.entry_display")
+local finders = require('telescope.finders')
+local pickers = require('telescope.pickers')
+local telescope_config = require('telescope.config').values
+local actions = require('telescope.actions')
+local state = require('telescope.actions.state')
+local builtin = require('telescope.builtin')
+local entry_display = require('telescope.pickers.entry_display')
-local history = require("project_nvim.utils.history")
-local project = require("project_nvim.project")
-local config = require("project_nvim.config")
+local history = require('project_nvim.utils.history')
+local project = require('project_nvim.project')
+local config = require('project_nvim.config')
----------
-- Actions
----------
+---@return table
local function create_finder()
- local results = history.get_recent_projects()
-
- -- Reverse results
- for i = 1, math.floor(#results / 2) do
- results[i], results[#results - i + 1] = results[#results - i + 1], results[i]
- end
- local displayer = entry_display.create({
- separator = " ",
- items = {
- {
- width = 30,
- },
- {
- remaining = true,
- },
- },
- })
-
- local function make_display(entry)
- return displayer({ entry.name, { entry.value, "Comment" } })
- end
-
- return finders.new_table({
- results = results,
- entry_maker = function(entry)
- local name = vim.fn.fnamemodify(entry, ":t")
- return {
- display = make_display,
- name = name,
- value = entry,
- ordinal = name .. " " .. entry,
- }
- end,
- })
+ local results = history.get_recent_projects()
+
+ -- Reverse results
+ for i = 1, math.floor(#results / 2) do
+ results[i], results[#results - i + 1] = results[#results - i + 1], results[i]
+ end
+ local displayer = entry_display.create({
+ separator = ' ',
+ items = {
+ {
+ width = 30,
+ },
+ {
+ remaining = true,
+ },
+ },
+ })
+
+ ---@param entry table
+ local function make_display(entry) return displayer({ entry.name, { entry.value, 'Comment' } }) end
+
+ return finders.new_table({
+ results = results,
+ entry_maker = function(entry)
+ local name = vim.fn.fnamemodify(entry, ':t')
+ return {
+ display = make_display,
+ name = name,
+ value = entry,
+ ordinal = name .. ' ' .. entry,
+ }
+ end,
+ })
end
+---@param prompt_bufnr integer
+---@param prompt boolean
+---@return unknown|nil,boolean?
local function change_working_directory(prompt_bufnr, prompt)
- local selected_entry = state.get_selected_entry(prompt_bufnr)
- if selected_entry == nil then
+ local selected_entry = state.get_selected_entry(prompt_bufnr)
+ if selected_entry == nil then
+ actions.close(prompt_bufnr)
+ return
+ end
+ local project_path = selected_entry.value
+
+ -- FIXME: `_close()` is deprecated!!! (what did this conditional do beforehand????)
actions.close(prompt_bufnr)
- return
- end
- local project_path = selected_entry.value
- if prompt == true then
- actions._close(prompt_bufnr, true)
- else
- actions.close(prompt_bufnr)
- end
- local cd_successful = project.set_pwd(project_path, "telescope")
- return project_path, cd_successful
+ local cd_successful = project.set_pwd(project_path, 'telescope')
+ return project_path, cd_successful
end
+---@param prompt_bufnr integer
local function find_project_files(prompt_bufnr)
- local project_path, cd_successful = change_working_directory(prompt_bufnr, true)
- local opt = {
- cwd = project_path,
- hidden = config.options.show_hidden,
- mode = "insert",
- }
- if cd_successful then
- builtin.find_files(opt)
- end
+ local project_path, cd_successful = change_working_directory(prompt_bufnr, true)
+ local opt = {
+ cwd = project_path,
+ hidden = config.options.show_hidden,
+ mode = 'insert',
+ }
+ if cd_successful then
+ builtin.find_files(opt)
+ end
end
+---@param prompt_bufnr integer
local function browse_project_files(prompt_bufnr)
- local project_path, cd_successful = change_working_directory(prompt_bufnr, true)
- local opt = {
- cwd = project_path,
- hidden = config.options.show_hidden,
- }
- if cd_successful then
- builtin.file_browser(opt)
- end
+ local project_path, cd_successful = change_working_directory(prompt_bufnr, true)
+ local opt = {
+ cwd = project_path,
+ hidden = config.options.show_hidden,
+ }
+ if cd_successful then
+ builtin.file_browser(opt)
+ end
end
+---@param prompt_bufnr integer
local function search_in_project_files(prompt_bufnr)
- local project_path, cd_successful = change_working_directory(prompt_bufnr, true)
- local opt = {
- cwd = project_path,
- hidden = config.options.show_hidden,
- mode = "insert",
- }
- if cd_successful then
- builtin.live_grep(opt)
- end
+ local project_path, cd_successful = change_working_directory(prompt_bufnr, true)
+ local opt = {
+ cwd = project_path,
+ hidden = config.options.show_hidden,
+ mode = 'insert',
+ }
+ if cd_successful then
+ builtin.live_grep(opt)
+ end
end
+---@param prompt_bufnr integer
local function recent_project_files(prompt_bufnr)
- local _, cd_successful = change_working_directory(prompt_bufnr, true)
- local opt = {
- cwd_only = true,
- hidden = config.options.show_hidden,
- }
- if cd_successful then
- builtin.oldfiles(opt)
- end
+ local _, cd_successful = change_working_directory(prompt_bufnr, true)
+ local opt = {
+ cwd_only = true,
+ hidden = config.options.show_hidden,
+ }
+ if cd_successful then
+ builtin.oldfiles(opt)
+ end
end
+---@param prompt_bufnr integer
local function delete_project(prompt_bufnr)
- local selectedEntry = state.get_selected_entry(prompt_bufnr)
- if selectedEntry == nil then
- actions.close(prompt_bufnr)
- return
- end
- local choice = vim.fn.confirm("Delete '" .. selectedEntry.value .. "' from project list?", "&Yes\n&No", 2)
+ local selectedEntry = state.get_selected_entry(prompt_bufnr)
+ if selectedEntry == nil then
+ actions.close(prompt_bufnr)
+ return
+ end
+
+ local choice = vim.fn.confirm(
+ string.format("Delete '%s' from project list?", selectedEntry.value),
+ '&Yes\n&No',
+ 2
+ )
+
+ if choice ~= 1 then
+ return
+ end
- if choice == 1 then
history.delete_project(selectedEntry)
local finder = create_finder()
state.get_current_picker(prompt_bufnr):refresh(finder, {
- reset_prompt = true,
+ reset_prompt = true,
})
- end
end
---Main entrypoint for Telescope.
---@param opts table
local function projects(opts)
- opts = opts or {}
-
- pickers.new(opts, {
- prompt_title = "Recent Projects",
- finder = create_finder(),
- previewer = false,
- sorter = telescope_config.generic_sorter(opts),
- attach_mappings = function(prompt_bufnr, map)
- map("n", "f", find_project_files)
- map("n", "b", browse_project_files)
- map("n", "d", delete_project)
- map("n", "s", search_in_project_files)
- map("n", "r", recent_project_files)
- map("n", "w", change_working_directory)
-
- map("i", "", find_project_files)
- map("i", "", browse_project_files)
- map("i", "", delete_project)
- map("i", "", search_in_project_files)
- map("i", "", recent_project_files)
- map("i", "", change_working_directory)
-
- local on_project_selected = function()
- find_project_files(prompt_bufnr)
- end
- actions.select_default:replace(on_project_selected)
- return true
- end,
- }):find()
+ opts = opts or {}
+
+ pickers
+ .new(opts, {
+ prompt_title = 'Recent Projects',
+ finder = create_finder(),
+ previewer = false,
+ sorter = telescope_config.generic_sorter(opts),
+ ---@param prompt_bufnr integer
+ ---@param map fun(mode: string, lhs: string, rhs: string|fun())
+ attach_mappings = function(prompt_bufnr, map)
+ map('n', 'f', find_project_files)
+ map('n', 'b', browse_project_files)
+ map('n', 'd', delete_project)
+ map('n', 's', search_in_project_files)
+ map('n', 'r', recent_project_files)
+ map('n', 'w', change_working_directory)
+
+ map('i', '', find_project_files)
+ map('i', '', browse_project_files)
+ map('i', '', delete_project)
+ map('i', '', search_in_project_files)
+ map('i', '', recent_project_files)
+ map('i', '', change_working_directory)
+
+ local on_project_selected = function() find_project_files(prompt_bufnr) end
+ actions.select_default:replace(on_project_selected)
+ return true
+ end,
+ })
+ :find()
end
-return telescope.register_extension({
- exports = {
- projects = projects,
- },
-})
+return telescope.register_extension({ exports = { projects = projects } })