diff --git a/modules/plugins/languages/ts.nix b/modules/plugins/languages/ts.nix index 6064da98..9b99de07 100644 --- a/modules/plugins/languages/ts.nix +++ b/modules/plugins/languages/ts.nix @@ -4,73 +4,177 @@ lib, ... }: let - inherit (builtins) attrNames; + inherit (builtins) attrNames elem; inherit (lib.options) mkEnableOption mkOption; inherit (lib.modules) mkIf mkMerge; - inherit (lib.lists) isList; inherit (lib.meta) getExe; - inherit (lib.types) enum either listOf package str bool; - inherit (lib.nvim.lua) expToLua toLuaObject; + inherit (lib.types) enum listOf package bool; + inherit (lib.generators) mkLuaInline; + inherit (lib.nvim.attrsets) mapListToAttrs; + inherit (lib.nvim.lua) toLuaObject; inherit (lib.nvim.types) mkGrammarOption diagnostics mkPluginSetupOption; - inherit (lib.nvim.dag) entryAnywhere; + inherit (lib.nvim.dag) entryAnywhere entryBefore; cfg = config.vim.languages.ts; - defaultServer = "ts_ls"; - servers = { + defaultServers = ["ts_ls"]; + servers = let ts_ls = { - package = pkgs.typescript-language-server; - lspConfig = '' - lspconfig.ts_ls.setup { - capabilities = capabilities, - on_attach = function(client, bufnr) - attach_keymaps(client, bufnr); - client.server_capabilities.documentFormattingProvider = false; - end, - cmd = ${ - if isList cfg.lsp.package - then expToLua cfg.lsp.package - else ''{"${cfg.lsp.package}/bin/typescript-language-server", "--stdio"}'' - } - } + cmd = [(getExe pkgs.typescript-language-server) "--stdio"]; + init_options = {hostInfo = "neovim";}; + filetypes = [ + "javascript" + "javascriptreact" + "javascript.jsx" + "typescript" + "typescriptreact" + "typescript.tsx" + ]; + root_markers = ["tsconfig.json" "jsconfig.json" "package.json" ".git"]; + handlers = { + # handle rename request for certain code actions like extracting functions / types + "_typescript.rename" = mkLuaInline '' + function(_, result, ctx) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + vim.lsp.util.show_document({ + uri = result.textDocument.uri, + range = { + start = result.position, + ['end'] = result.position, + }, + }, client.offset_encoding) + vim.lsp.buf.rename() + return vim.NIL + end + ''; + }; + on_attach = mkLuaInline '' + function(client, bufnr) + default_on_attach(client, bufnr); + + -- ts_ls provides `source.*` code actions that apply to the whole file. These only appear in + -- `vim.lsp.buf.code_action()` if specified in `context.only`. + vim.api.nvim_buf_create_user_command(0, 'LspTypescriptSourceAction', function() + local source_actions = vim.tbl_filter(function(action) + return vim.startswith(action, 'source.') + end, client.server_capabilities.codeActionProvider.codeActionKinds) + + vim.lsp.buf.code_action({ + context = { + only = source_actions, + }, + }) + end, {}) + end ''; }; - - denols = { - package = pkgs.deno; - lspConfig = '' - vim.g.markdown_fenced_languages = { "ts=typescript" } - lspconfig.denols.setup { - capabilities = capabilities; - on_attach = attach_keymaps, - cmd = ${ - if isList cfg.lsp.package - then expToLua cfg.lsp.package - else ''{"${cfg.lsp.package}/bin/deno", "lsp"}'' - } - } - ''; - }; - + in { + inherit ts_ls; # Here for backwards compatibility. Still consider tsserver a valid # configuration in the enum, but assert if it's set to *properly* # redirect the user to the correct server. - tsserver = { - package = pkgs.typescript-language-server; - lspConfig = '' - lspconfig.ts_ls.setup { - capabilities = capabilities; - on_attach = attach_keymaps, - cmd = ${ - if isList cfg.lsp.package - then expToLua cfg.lsp.package - else ''{"${cfg.lsp.package}/bin/typescript-language-server", "--stdio"}'' - } - } + tsserver = ts_ls; + + denols = { + cmd = [(getExe pkgs.deno) "lsp"]; + cmd_env = {NO_COLOR = true;}; + filetypes = [ + "javascript" + "javascriptreact" + "javascript.jsx" + "typescript" + "typescriptreact" + "typescript.tsx" + ]; + root_markers = ["deno.json" "deno.jsonc" ".git"]; + settings = { + deno = { + enable = true; + suggest = { + imports = { + hosts = { + "https://deno.land" = true; + }; + }; + }; + }; + }; + handlers = { + "textDocument/definition" = mkLuaInline "nvf_denols_handler"; + "textDocument/typeDefinition" = mkLuaInline "nvf_denols_handler"; + "textDocument/references" = mkLuaInline "nvf_denols_handler"; + }; + on_attach = mkLuaInline '' + function(client, bufnr) + default_on_attach(client, bufnr) + vim.api.nvim_buf_create_user_command(0, 'LspDenolsCache', function() + client:exec_cmd({ + command = 'deno.cache', + arguments = { {}, vim.uri_from_bufnr(bufnr) }, + }, { bufnr = bufnr }, function(err, _result, ctx) + if err then + local uri = ctx.params.arguments[2] + vim.api.nvim_err_writeln('cache command failed for ' .. vim.uri_to_fname(uri)) + end + end) + end, { + desc = 'Cache a module and all of its dependencies.', + }) + end ''; }; }; + denols_handlers = '' + local function nvf_denols_virtual_text_document_handler(uri, res, client) + if not res then + return nil + end + + local lines = vim.split(res.result, '\n') + local bufnr = vim.uri_to_bufnr(uri) + + local current_buf = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + if #current_buf ~= 0 then + return nil + end + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + vim.api.nvim_set_option_value('readonly', true, { buf = bufnr }) + vim.api.nvim_set_option_value('modified', false, { buf = bufnr }) + vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr }) + vim.lsp.buf_attach_client(bufnr, client.id) + end + + local function nvf_denols_virtual_text_document(uri, client) + local params = { + textDocument = { + uri = uri, + }, + } + local result = client.request_sync('deno/virtualTextDocument', params) + nvf_denols_virtual_text_document_handler(uri, result, client) + end + + local function nvf_denols_handler(err, result, ctx, config) + if not result or vim.tbl_isempty(result) then + return nil + end + + local client = vim.lsp.get_client_by_id(ctx.client_id) + for _, res in pairs(result) do + local uri = res.uri or res.targetUri + if uri:match '^deno:' then + nvf_denols_virtual_text_document(uri, client) + res['uri'] = uri + res['targetUri'] = uri + end + end + + vim.lsp.handlers[ctx.method](err, result, ctx, config) + end + ''; + # TODO: specify packages defaultFormat = "prettier"; formats = { @@ -122,17 +226,10 @@ in { lsp = { enable = mkEnableOption "Typescript/Javascript LSP support" // {default = config.vim.lsp.enable;}; - server = mkOption { + servers = mkOption { + type = singleOrListOf (enum (attrNames servers)); + default = defaultServers; description = "Typescript/Javascript LSP server to use"; - type = enum (attrNames servers); - default = defaultServer; - }; - - package = mkOption { - description = "Typescript/Javascript LSP server package, or the command to run as a list of strings"; - example = ''[lib.getExe pkgs.jdt-language-server "-data" "~/.cache/jdtls/workspace"]''; - type = either package (listOf str); - default = servers.${cfg.lsp.server}.package; }; }; @@ -190,8 +287,17 @@ in { }) (mkIf cfg.lsp.enable { - vim.lsp.lspconfig.enable = true; - vim.lsp.lspconfig.sources.ts-lsp = servers.${cfg.lsp.server}.lspConfig; + vim.lsp.servers = + mapListToAttrs (name: { + inherit name; + value = servers.${name}; + }) + cfg.lsp.servers; + }) + + (mkIf (cfg.lsp.enable && elem "denols" cfg.lsp.servers) { + vim.globals.markdown_fenced_languages = ["ts=typescript"]; + vim.luaConfigRC.denols_handlers = entryBefore ["lsp-servers"] denols_handlers; }) (mkIf cfg.format.enable { @@ -234,7 +340,7 @@ in { { assertions = [ { - assertion = cfg.lsp.enable -> cfg.lsp.server != "tsserver"; + assertion = cfg.lsp.enable -> !(elem "tsserver" cfg.lsp.servers); message = '' As of a recent lspconfig update, the `tsserver` configuration has been renamed to `ts_ls` to match upstream behaviour of `lspconfig`, and the name `tsserver`