diff --git a/modules/neovim/init/default.nix b/modules/neovim/init/default.nix index 30a481a1..3f195085 100644 --- a/modules/neovim/init/default.nix +++ b/modules/neovim/init/default.nix @@ -8,5 +8,6 @@ ./highlight.nix ./lsp.nix ./spellcheck.nix + ./util.nix ]; } diff --git a/modules/neovim/init/lsp.nix b/modules/neovim/init/lsp.nix index 0bc2dea8..6c18b98e 100644 --- a/modules/neovim/init/lsp.nix +++ b/modules/neovim/init/lsp.nix @@ -16,6 +16,7 @@ cfg = config.vim.lsp; + # TODO: lspConfigurations filter on enabledServers instead of cfg.servers? lspConfigurations = mapAttrsToList ( name: value: '' @@ -81,7 +82,6 @@ in { } (mkIf (cfg.servers != {}) { - # Enable lspconfig in order to merge in the predefined opts vim.luaConfigRC.lsp-servers = entryAnywhere '' -- Individual LSP configurations managed by nvf. ${concatLines lspConfigurations} diff --git a/modules/neovim/init/util.nix b/modules/neovim/init/util.nix new file mode 100644 index 00000000..cc98d10a --- /dev/null +++ b/modules/neovim/init/util.nix @@ -0,0 +1,178 @@ +{ + config, + lib, + ... +}: let + inherit (builtins) filter; + inherit (lib.modules) mkIf mkMerge; + inherit (lib.nvim.dag) entryBefore; + + cfg = config.vim.lsp; +in { + config = mkMerge [ + (mkIf (cfg.servers != {}) { + vim.luaConfigRC.lsp-util = + entryBefore ["lsp-servers"] + /* + lua + */ + '' + -- Port of nvim-lspconfig util + local util = { path = {} } + + util.default_config = { + log_level = vim.lsp.protocol.MessageType.Warning, + message_level = vim.lsp.protocol.MessageType.Warning, + settings = vim.empty_dict(), + init_options = vim.empty_dict(), + handlers = {}, + autostart = true, + capabilities = vim.lsp.protocol.make_client_capabilities(), + } + + -- global on_setup hook + util.on_setup = nil + + do + local validate = vim.validate + local api = vim.api + local lsp = vim.lsp + local nvim_eleven = vim.fn.has 'nvim-0.11' == 1 + + local iswin = vim.uv.os_uname().version:match 'Windows' + + local function escape_wildcards(path) + return path:gsub('([%[%]%?%*])', '\\%1') + end + + local function is_fs_root(path) + if iswin then + return path:match '^%a:$' + else + return path == '/' + end + end + + local function traverse_parents(path, cb) + path = vim.uv.fs_realpath(path) + local dir = path + -- Just in case our algo is buggy, don't infinite loop. + for _ = 1, 100 do + dir = vim.fs.dirname(dir) + if not dir then + return + end + -- If we can't ascend further, then stop looking. + if cb(dir, path) then + return dir, path + end + if is_fs_root(dir) then + break + end + end + end + + util.root_pattern = function(...) + local patterns = util.tbl_flatten { ... } + return function(startpath) + startpath = util.strip_archive_subpath(startpath) + for _, pattern in ipairs(patterns) do + local match = util.search_ancestors(startpath, function(path) + for _, p in ipairs(vim.fn.glob(table.concat({ escape_wildcards(path), pattern }, '/'), true, true)) do + if vim.uv.fs_stat(p) then + return path + end + end + end) + + if match ~= nil then + return match + end + end + end + end + + util.root_markers_with_field = function(root_files, new_names, field, fname) + local path = vim.fn.fnamemodify(fname, ':h') + local found = vim.fs.find(new_names, { path = path, upward = true }) + + for _, f in ipairs(found or {}) do + -- Match the given `field`. + for line in io.lines(f) do + if line:find(field) then + root_files[#root_files + 1] = vim.fs.basename(f) + break + end + end + end + + return root_files + end + + util.insert_package_json = function(root_files, field, fname) + return util.root_markers_with_field(root_files, { 'package.json', 'package.json5' }, field, fname) + end + + util.strip_archive_subpath = function(path) + -- Matches regex from zip.vim / tar.vim + path = vim.fn.substitute(path, 'zipfile://\\(.\\{-}\\)::[^\\\\].*$', '\\1', ''') + path = vim.fn.substitute(path, 'tarfile:\\(.\\{-}\\)::.*$', '\\1', ''') + return path + end + + util.get_typescript_server_path = function(root_dir) + local project_roots = vim.fs.find('node_modules', { path = root_dir, upward = true, limit = math.huge }) + for _, project_root in ipairs(project_roots) do + local typescript_path = project_root .. '/typescript' + local stat = vim.loop.fs_stat(typescript_path) + if stat and stat.type == 'directory' then + return typescript_path .. '/lib' + end + end + return ''' + end + + util.search_ancestors = function(startpath, func) + if nvim_eleven then + validate('func', func, 'function') + end + if func(startpath) then + return startpath + end + local guard = 100 + for path in vim.fs.parents(startpath) do + -- Prevent infinite recursion if our algorithm breaks + guard = guard - 1 + if guard == 0 then + return + end + + if func(path) then + return path + end + end + end + + util.path.is_descendant = function(root, path) + if not path then + return false + end + + local function cb(dir, _) + return dir == root + end + + local dir, _ = traverse_parents(path, cb) + + return dir == root + end + + util.tbl_flatten = function(t) + --- @diagnostic disable-next-line:deprecated + return nvim_eleven and vim.iter(t):flatten(math.huge):totable() or vim.tbl_flatten(t) + end + end + ''; + }) + ]; +}