From 7e8738af90bb8ffbdcd080d8f862bc5eca434293 Mon Sep 17 00:00:00 2001 From: coast Date: Thu, 29 May 2025 22:37:16 +0200 Subject: [PATCH] Add .config/nvim/lua/project_nvim/project.lua --- .config/nvim/lua/project_nvim/project.lua | 286 ++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 .config/nvim/lua/project_nvim/project.lua diff --git a/.config/nvim/lua/project_nvim/project.lua b/.config/nvim/lua/project_nvim/project.lua new file mode 100644 index 0000000..404b23c --- /dev/null +++ b/.config/nvim/lua/project_nvim/project.lua @@ -0,0 +1,286 @@ +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 +local M = {} + +-- Internal states +M.attached_lsp = false +M.last_project = nil + +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.get_clients({bufnr = 0}) + 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 + end + end + + return nil +end + +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 + + local function get_files(file_dir) + last_dir_cache = file_dir + curr_dir_cache = {} + + local dir = uv.fs_scandir(file_dir) + if dir == nil then + return + end + + while true do + local file = uv.fs_scandir_next(dir) + if file == nil then + return + end + + table.insert(curr_dir_cache, file) + end + end + + local function is(dir, identifier) + dir = dir:match(".*/(.*)") + return dir == identifier + end + + 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 + end + end + + local function child(dir, identifier) + local path = get_parent(dir) + return is(path, identifier) + end + + local function has(dir, identifier) + if last_dir_cache ~= dir then + get_files(dir) + end + local pattern = glob.globtopattern(identifier) + for _, file in ipairs(curr_dir_cache) do + if file:match(pattern) ~= nil then + return true + 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) + 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 + 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 +end + +---@diagnostic disable-next-line: unused-local +local on_attach_lsp = function(client, bufnr) + M.on_buf_enter() -- Recalculate root dir after lsp attaches +end + +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 + end + return _start_client(lsp_config) + end + + M.attached_lsp = true +end + +function M.set_pwd(dir, method) + if dir ~= nil then + 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 + + if config.options.silent_chdir == false then + vim.notify("Set CWD to " .. dir .. " using " .. method) + end + end + return true + end + + return false +end + +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 + end + end +end + +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 + end + end + if not is_in_whitelist then + return false + end + + return true +end + +function M.on_buf_enter() + if vim.v.vim_did_enter == 0 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 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') +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() + 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") + + history.read_projects_from_history() +end + +return M