diff --git a/dot-config/vim/lsp.vim b/dot-config/vim/lsp.vim index bf07868..8eb3ff0 100644 --- a/dot-config/vim/lsp.vim +++ b/dot-config/vim/lsp.vim @@ -1,216 +1,34 @@ vim9script +import './lsp/servers.vim' +import './lsp/options.vim' import './tools/strings.vim' -import './tools/perl.vim' -const lspServers = [ - { - name: 'dockerfile-langserver', - filetype: 'dockerfile', - path: expand('~/.local/bin/docker-langserver'), - args: ['--stdio'], - install: 'npm install -g dockerfile-language-server-nodejs', - }, - - { - name: 'lua-language-server', - filetype: 'lua', - path: '/usr/local/bin/lua-language-server', - args: [], - install: 'brew install lua-language-server', - }, - - { - name: 'marksman', - filetype: 'markdown', - path: '/usr/local/bin/marksman', - args: ['server'], - install: 'brew install marksman', - }, - - { - name: 'PerlNavigator', - filetype: 'perl', - path: expand('~/.local/bin/perlnavigator'), - args: ['--stdio'], - install: 'npm install -g perlnavigator-server', - }, - - perl.Lsp('Perl::LanguageServer', ['-e', 'Perl::LanguageServer::run']), - - { - name: 'phpactor', - filetype: 'php', - path: expand('~/bin/phpactor'), - args: ['language-server'], - initializationOptions: { - 'language_server_configuration.auto_config': false, - }, - install: 'curl -Lo phpactor https://github.com/phpactor/phpactor/releases/latest/download/phpactor.phar && chmod u+x phpactor && mv phpactor ~/bin', - }, - - { - name: 'pylsp', - filetype: 'python', - path: '/usr/local/bin/pylsp', - args: [], - install: 'brew install python-lsp-server', - }, - - { - name: 'solargraph', - filetype: 'ruby', - path: '/usr/local/bin/solargraph', - args: ['stdio'], - install: 'brew install solargraph', - }, - - { - name: 'taplo', - filetype: 'toml', - path: '/usr/local/bin/taplo', - args: ['lsp', 'stdio'], - install: 'brew install taplo', - }, - - { - name: 'tilt-lsp', - filetype: 'bzl', - path: '/usr/local/bin/tilt', - args: ['lsp', 'start'], - install: 'brew install tilt', - }, - - { - name: 'typescript-language-server', - filetype: ['javascript', 'typescript'], - path: '/usr/local/bin/typescript-language-server', - args: ['--stdio'], - install: 'brew install typescript-language-server', - }, - - { - name: 'vim-language-server', - filetype: 'vim', - path: expand('~/.local/bin/vim-language-server'), - args: ['--stdio'], - install: 'npm install -g vim-language-server', - }, - - { - name: 'vscode-css-language-server', - filetype: 'css', - path: expand('~/.local/bin/vscode-css-language-server'), - args: ['--stdio'], - install: 'npm install -g vscode-langservers-extracted', - }, - - { - name: 'vscode-html-language-server', - filetype: 'html', - path: expand('~/.local/bin/vscode-html-language-server'), - args: ['--stdio'], - install: 'npm install -g vscode-langservers-extracted', - }, - - { - name: 'vscode-json-language-server', - filetype: ['json', 'jsonc'], - path: expand('~/.local/bin/vscode-json-language-server'), - args: ['--stdio'], - workspaceConfig: {json: { - format: {enable: true}, - validate: {enable: true}, - schemas: g:SchemaStore#Schemata(), - }}, - install: 'npm install -g vscode-langservers-extracted', - }, - - { - name: 'yaml-language-server', - filetype: 'yaml', - path: expand('~/.local/bin/yaml-language-server'), - args: ['--stdio'], - workspaceConfig: {yaml: { - format: {enable: true, singleQuote: true}, - schemaStore: {enable: true, url: 'https://www.schemastore.org/api/json/catalog.json'}, - }}, - install: 'npm install -g yaml-language-server', - }, -] - -const lspOptions = { - aleSupport: true, - ignoreMissingServer: true, -} - -command! -nargs=* -complete=customlist,ListMissingServers -bar LspInstall Install() - -def IsInstalled(server: dict): bool - return server->has_key('installed') ? server.installed() : executable(server.path) == 1 -enddef - -def InstalledServers(): list> - return lspServers->deepcopy()->filter((_, server): bool => server->IsInstalled()) -enddef - -def MissingServers(): list> - return lspServers->deepcopy()->filter((_, server): bool => !server->IsInstalled()) -enddef +command! -nargs=* -complete=customlist,ListMissingServers -bar LspInstall servers.Install([], AddExtraServers) def ListMissingServers(argLead: string, cmdLine: string, cursorPos: number): list - return MissingServers()->mapnew((_, server): string => server.name) + return servers.Missing()->mapnew((_, server): string => server.name) enddef -def ServerHas(feature: string): bool - return lsp#buffer#CurbufGetServer(feature) != {} -enddef - -def LspBufferSettings(): void - if ServerHas('documentFormatting') - setlocal formatexpr=lsp#lsp#FormatExpr() - endif - - if ServerHas('hover') - setlocal keywordprg=:LspHover - endif - - if ServerHas('declaration') - nnoremap gD LspGotoDeclaration - endif - - if ServerHas('definition') - nnoremap gd LspGotoDefinition - endif - - if ServerHas('implementation') - nnoremap gi LspGotoImpl - endif - - if ServerHas('references') - nnoremap gr LspShowReferences - endif - - if ServerHas('selectionRange') - xnoremap e LspSelectionExpand - xnoremap s LspSelectionShrink - endif +def AddExtraServers(extraServers: list>): void + g:lsp#lsp#AddServer(extraServers->deepcopy()) enddef export def Configure(): void augroup dot/vim/lsp.vim autocmd! - autocmd User LspAttached LspBufferSettings() + autocmd User LspAttached options.SetBufferOptions() augroup END # We have to use final rather than const because LspAddServer() assumes it can # modify the dicts it gets, rather than making a fresh copy to mess with. - final installedServers = InstalledServers() - if len(lspServers) != len(installedServers) + final installedServers = servers.Installed() + + const missingCount = servers.CountAll() - len(installedServers) + if missingCount > 0 # Since this code runs during Vim initialisation, this message would # normally pause Vim's startup so the user can read it. We don't want # that, so we're gonna delay it using an autocmd. - const missingCount = len(lspServers) - len(installedServers) const warn = $'{missingCount} language server{missingCount > 1 ? "s are" : " is"} configured, but not installed. You may want to run :LspInstall.' augroup dot/vim/lsp.vim exe $'autocmd VimEnter * ++once echo {strings.Quote(warn)}' @@ -218,41 +36,5 @@ export def Configure(): void endif g:lsp#lsp#AddServer(installedServers) - g:lsp#options#OptionsSet(lspOptions) -enddef - -export def Install(...serverNames: list): void - const missingServers = MissingServers() - - if empty(missingServers) - echo $"All {len(lspServers)} configured language servers are already installed." - return - endif - - const serverNamesSet = strings.ToStringSet(serverNames) - const serversToInstall = empty(serverNamesSet) - ? missingServers - : missingServers->copy()->filter((_, server): bool => serverNamesSet->has_key(server.name)) - - # The installScript runs every server's install command, regardless of - # whether any fail in the process. The script's exit status is the number of - # failed installations. - const installScript = "failed=0\n" .. serversToInstall->copy() - ->map((_, server): string => $"\{ {server.install}; \} || failed=$((failed + 1))") - ->join("\n") .. "\nexit $failed\n" - - const term = term_start('sh', {exit_cb: (job: job, status: number): void => { - # If any installations failed, keep the terminal window open so we can - # troubleshoot. If they all worked fine, close the terminal and reload the - # LSP configuration. - if status == 0 - execute 'bdelete' job->ch_getbufnr('out') - Configure() - endif - }}) - - # We prefer term_sendkeys() over sh -c because that will display each - # command in the terminal as it's being executed, making it easier to - # understand exactly what the install script is doing. - term->term_sendkeys(installScript) + g:lsp#options#OptionsSet(options.lspOptions) enddef diff --git a/dot-config/vim/lsp/options.vim b/dot-config/vim/lsp/options.vim new file mode 100644 index 0000000..5827d17 --- /dev/null +++ b/dot-config/vim/lsp/options.vim @@ -0,0 +1,41 @@ +vim9script + +export const lspOptions = { + aleSupport: true, + ignoreMissingServer: true, +} + +def ServerHas(feature: string): bool + return lsp#buffer#CurbufGetServer(feature) != {} +enddef + +export def SetBufferOptions(): void + if ServerHas('documentFormatting') + setlocal formatexpr=lsp#lsp#FormatExpr() + endif + + if ServerHas('hover') + setlocal keywordprg=:LspHover + endif + + if ServerHas('declaration') + nnoremap gD LspGotoDeclaration + endif + + if ServerHas('definition') + nnoremap gd LspGotoDefinition + endif + + if ServerHas('implementation') + nnoremap gi LspGotoImpl + endif + + if ServerHas('references') + nnoremap gr LspShowReferences + endif + + if ServerHas('selectionRange') + xnoremap e LspSelectionExpand + xnoremap s LspSelectionShrink + endif +enddef diff --git a/dot-config/vim/lsp/servers.vim b/dot-config/vim/lsp/servers.vim new file mode 100644 index 0000000..a2fe2bd --- /dev/null +++ b/dot-config/vim/lsp/servers.vim @@ -0,0 +1,192 @@ +vim9script + +import '../tools/perl.vim' +import '../tools/strings.vim' + +const lspServers = [ + { + name: 'dockerfile-langserver', + filetype: 'dockerfile', + path: expand('~/.local/bin/docker-langserver'), + args: ['--stdio'], + install: 'npm install -g dockerfile-language-server-nodejs', + }, + + { + name: 'lua-language-server', + filetype: 'lua', + path: '/usr/local/bin/lua-language-server', + args: [], + install: 'brew install lua-language-server', + }, + + { + name: 'marksman', + filetype: 'markdown', + path: '/usr/local/bin/marksman', + args: ['server'], + install: 'brew install marksman', + }, + + { + name: 'PerlNavigator', + filetype: 'perl', + path: expand('~/.local/bin/perlnavigator'), + args: ['--stdio'], + install: 'npm install -g perlnavigator-server', + }, + + perl.Lsp('Perl::LanguageServer', ['-e', 'Perl::LanguageServer::run']), + + { + name: 'phpactor', + filetype: 'php', + path: expand('~/bin/phpactor'), + args: ['language-server'], + initializationOptions: { + 'language_server_configuration.auto_config': false, + }, + install: 'curl -Lo phpactor https://github.com/phpactor/phpactor/releases/latest/download/phpactor.phar && chmod u+x phpactor && mv phpactor ~/bin', + }, + + { + name: 'pylsp', + filetype: 'python', + path: '/usr/local/bin/pylsp', + args: [], + install: 'brew install python-lsp-server', + }, + + { + name: 'solargraph', + filetype: 'ruby', + path: '/usr/local/bin/solargraph', + args: ['stdio'], + install: 'brew install solargraph', + }, + + { + name: 'taplo', + filetype: 'toml', + path: '/usr/local/bin/taplo', + args: ['lsp', 'stdio'], + install: 'brew install taplo', + }, + + { + name: 'tilt-lsp', + filetype: 'bzl', + path: '/usr/local/bin/tilt', + args: ['lsp', 'start'], + install: 'brew install tilt', + }, + + { + name: 'typescript-language-server', + filetype: ['javascript', 'typescript'], + path: '/usr/local/bin/typescript-language-server', + args: ['--stdio'], + install: 'brew install typescript-language-server', + }, + + { + name: 'vim-language-server', + filetype: 'vim', + path: expand('~/.local/bin/vim-language-server'), + args: ['--stdio'], + install: 'npm install -g vim-language-server', + }, + + { + name: 'vscode-css-language-server', + filetype: 'css', + path: expand('~/.local/bin/vscode-css-language-server'), + args: ['--stdio'], + install: 'npm install -g vscode-langservers-extracted', + }, + + { + name: 'vscode-html-language-server', + filetype: 'html', + path: expand('~/.local/bin/vscode-html-language-server'), + args: ['--stdio'], + install: 'npm install -g vscode-langservers-extracted', + }, + + { + name: 'vscode-json-language-server', + filetype: ['json', 'jsonc'], + path: expand('~/.local/bin/vscode-json-language-server'), + args: ['--stdio'], + workspaceConfig: {json: { + format: {enable: true}, + validate: {enable: true}, + schemas: g:SchemaStore#Schemata(), + }}, + install: 'npm install -g vscode-langservers-extracted', + }, + + { + name: 'yaml-language-server', + filetype: 'yaml', + path: expand('~/.local/bin/yaml-language-server'), + args: ['--stdio'], + workspaceConfig: {yaml: { + format: {enable: true, singleQuote: true}, + schemaStore: {enable: true, url: 'https://www.schemastore.org/api/json/catalog.json'}, + }}, + install: 'npm install -g yaml-language-server', + }, +] + +def IsInstalled(server: dict): bool + return server->has_key('installed') ? server.installed() : executable(server.path) == 1 +enddef + +export def CountAll(): number + return len(lspServers) +enddef + +export def Installed(): list> + return lspServers->deepcopy()->filter((_, server): bool => server->IsInstalled()) +enddef + +export def Missing(): list> + return lspServers->deepcopy()->filter((_, server): bool => !server->IsInstalled()) +enddef + +export def Install(serverNames: list, OnSuccess: func(list>)): void + const missingServers = Missing() + + if empty(missingServers) + echo $"All {len(lspServers)} configured language servers are already installed." + return + endif + + const serverNamesSet = strings.ToStringSet(serverNames) + const serversToInstall = empty(serverNamesSet) + ? missingServers + : missingServers->copy()->filter((_, server): bool => serverNamesSet->has_key(server.name)) + + # The installScript runs every server's install command, regardless of + # whether any fail in the process. The script's exit status is the number of + # failed installations. + const installScript = "failed=0\n" .. serversToInstall->copy() + ->map((_, server): string => $"\{ {server.install}; \} || failed=$((failed + 1))") + ->join("\n") .. "\nexit $failed\n" + + const term = term_start('sh', {exit_cb: (job: job, status: number): void => { + # If any installations failed, keep the terminal window open so we can + # troubleshoot. If they all worked fine, close the terminal and reload the + # LSP configuration. + if status == 0 + execute 'bdelete' job->ch_getbufnr('out') + OnSuccess(serversToInstall) + endif + }}) + + # We prefer term_sendkeys() over sh -c because that will display each + # command in the terminal as it's being executed, making it easier to + # understand exactly what the install script is doing. + term->term_sendkeys(installScript) +enddef