diff options
49 files changed, 16502 insertions, 0 deletions
diff --git a/vim/downloads/opt/snippets/snippets.vim b/vim/downloads/opt/snippets/snippets.vim new file mode 100644 index 0000000..de15141 --- /dev/null +++ b/vim/downloads/opt/snippets/snippets.vim @@ -0,0 +1,6 @@ +autocmd FileType go + \ :iabbrev <buffer> err@ if err != nil {}<Left><Enter><Left> + +autocmd FileType go + \ :iabbrev <buffer> fat@ log.Fatal(" %v", err)<Esc>F%i<Left> + diff --git a/vim/pack/downloads/opt/lsp/.gitignore b/vim/pack/downloads/opt/lsp/.gitignore new file mode 100644 index 0000000..b2e4d35 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/.gitignore @@ -0,0 +1,8 @@ +tags +*.swp + +test/X*.c +test/X*.cpp +# test/Xtest.c +# test/Xtest.cpp +test/results.txt diff --git a/vim/pack/downloads/opt/lsp/LICENSE b/vim/pack/downloads/opt/lsp/LICENSE new file mode 100644 index 0000000..9e55f93 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2023 Yegappan Lakshmanan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vim/pack/downloads/opt/lsp/README.md b/vim/pack/downloads/opt/lsp/README.md new file mode 100644 index 0000000..a2abb6d --- /dev/null +++ b/vim/pack/downloads/opt/lsp/README.md @@ -0,0 +1,234 @@ +[![unit-tests](https://github.com/yegappan/lsp/workflows/unit-tests/badge.svg?branch=main)](https://github.com/yegappan/lsp/actions/workflows/unitests.yml?query=branch%3Amain) + +Language Server Protocol (LSP) plugin for Vim. You need Vim version 9.0 or above to use this plugin. This plugin is written using only the Vim9 script. + +## Installation + +You can install this plugin directly from github using the following steps: + +```bash +$ mkdir -p $HOME/.vim/pack/downloads/opt +$ cd $HOME/.vim/pack/downloads/opt +$ git clone https://github.com/yegappan/lsp +$ vim -u NONE -c "helptags $HOME/.vim/pack/downloads/opt/lsp/doc" -c q +``` + +After installing the plugin using the above steps, add the following line to +your $HOME/.vimrc file: + +```viml +packadd lsp +``` + +You can also install and manage this plugin using any one of the Vim plugin managers (dein.vim, pathogen, vam, vim-plug, volt, Vundle, etc.). + +You will also need to download and install one or more language servers corresponding to the programming languages that you are using. Refer to the https://langserver.org/ page for the list of available language servers. This plugin doesn't install the language servers. + +## Features + +The following language server protocol (LSP) features are supported: + +* Code completion +* Jump to definition, declaration, implementation, type definition +* Peek definition, declaration, implementation, type definition and references +* Display warning and error diagnostics +* Find all symbol references +* Document and Workspace symbol search +* Display code outline +* Rename symbol +* Display type and documentation on hover +* Signature help +* Code action +* Display Call hierarchy +* Display Type hierarchy +* Highlight current symbol references +* Formatting code +* Folding code +* Inlay hints +* Visually select symbol block/region +* Semantic Highlight + +## Configuration + +To use the plugin features with a particular file type(s), you need to first register a LSP server for that file type(s). + +The LSP servers are registered using the LspAddServer() function. This function accepts a list of LSP servers. + +To register a LSP server, add the following lines to your .vimrc file (use only the LSP servers that you need from the below list). If you used [vim-plug](https://github.com/junegunn/vim-plug) to install the LSP plugin, the steps are described later in this section. +```viml + +" Clangd language server +call LspAddServer([#{ + \ name: 'clangd', + \ filetype: ['c', 'cpp'], + \ path: '/usr/local/bin/clangd', + \ args: ['--background-index'] + \ }]) + +" Javascript/Typescript language server +call LspAddServer([#{ + \ name: 'typescriptlang', + \ filetype: ['javascript', 'typescript'], + \ path: '/usr/local/bin/typescript-language-server', + \ args: ['--stdio'], + \ }]) + +" Go language server +call LspAddServer([#{ + \ name: 'golang', + \ filetype: ['go', 'gomod'], + \ path: '/usr/local/bin/gopls', + \ args: ['serve'], + \ syncInit: v:true + \ }]) + +" Rust language server +call LspAddServer([#{ + \ name: 'rustlang', + \ filetype: ['rust'], + \ path: '/usr/local/bin/rust-analyzer', + \ args: [], + \ syncInit: v:true + \ }]) +``` + +The above lines register the language servers for C/C++, Javascript/Typescript, Go and Rust file types. Refer to the [Wiki](https://github.com/yegappan/lsp/wiki) page for various language server specific configuration. + +To register a LSP server, the following information is needed: + +Field|Description +-----|----------- +filetype|One or more file types supported by the LSP server. This can be a String or a List. To specify multiple multiple file types, use a List. +path|complete path to the LSP server executable (without any arguments). +args|a list of command-line arguments passed to the LSP server. Each argument is a separate List item. +initializationOptions|User provided initialization options. May be of any type. For example the *intelephense* PHP language server accept several options here with the License Key among others. +customNotificationHandlers|A dictionary of notifications and functions that can be specified to add support for custom language server notifications. +customRequestHandlers|A dictionary of request handlers and functions that can be specified to add support for custom language server requests replies. +features|A dictionary of booleans that can be specified to toggle what things a given LSP is providing (folding, goto definition, etc) This is useful when running multiple servers in one buffer. + +The LspAddServer() function accepts a list of LSP servers with the above information. + +Some of the LSP plugin features can be enabled or disabled by using the LspOptionsSet() function, detailed in `:help lsp-options`. +Here is an example of configuration with default values: +```viml +call LspOptionsSet(#{ + \ aleSupport: v:false, + \ autoComplete: v:true, + \ autoHighlight: v:false, + \ autoHighlightDiags: v:true, + \ autoPopulateDiags: v:false, + \ completionMatcher: 'case', + \ completionMatcherValue: 1, + \ diagSignErrorText: 'E>', + \ diagSignHintText: 'H>', + \ diagSignInfoText: 'I>', + \ diagSignWarningText: 'W>', + \ echoSignature: v:false, + \ hideDisabledCodeActions: v:false, + \ highlightDiagInline: v:true, + \ hoverInPreview: v:false, + \ ignoreMissingServer: v:false, + \ keepFocusInDiags: v:true, + \ keepFocusInReferences: v:true, + \ completionTextEdit: v:true, + \ diagVirtualTextAlign: 'above', + \ diagVirtualTextWrap: 'default', + \ noNewlineInCompletion: v:false, + \ omniComplete: v:null, + \ outlineOnRight: v:false, + \ outlineWinSize: 20, + \ semanticHighlight: v:true, + \ showDiagInBalloon: v:true, + \ showDiagInPopup: v:true, + \ showDiagOnStatusLine: v:false, + \ showDiagWithSign: v:true, + \ showDiagWithVirtualText: v:false, + \ showInlayHints: v:false, + \ showSignature: v:true, + \ snippetSupport: v:false, + \ ultisnipsSupport: v:false, + \ useBufferCompletion: v:false, + \ usePopupInCodeAction: v:false, + \ useQuickfixForLocations: v:false, + \ vsnipSupport: v:false, + \ bufferCompletionTimeout: 100, + \ customCompletionKinds: v:false, + \ completionKinds: {}, + \ filterCompletionDuplicates: v:false, + \ }) +``` + +If you used [vim-plug](https://github.com/junegunn/vim-plug) to install the LSP plugin, then you need to use the LspSetup User autocmd to initialize the LSP server and to set the LSP server options. For example: +```viml +let lspOpts = #{autoHighlightDiags: v:true} +autocmd User LspSetup call LspOptionsSet(lspOpts) + +let lspServers = [#{ + \ name: 'clang', + \ filetype: ['c', 'cpp'], + \ path: '/usr/local/bin/clangd', + \ args: ['--background-index'] + \ }] +autocmd User LspSetup call LspAddServer(lspServers) +``` + +## Supported Commands + +The following commands are provided to use the LSP features. + +Command|Description +-------|----------- +:LspCodeAction|Apply the code action supplied by the language server to the diagnostic in the current line. +:LspCodeLens|Display a list of code lens commands and apply a selected code lens command to the current file. +:LspDiag current|Display the diagnostic message for the current line. +:LspDiag first|Jump to the first diagnostic message for the current buffer. +:LspDiag here|Jump to the next diagnostic message in the current line. +:LspDiag highlight disable|Disable diagnostic message highlights. +:LspDiag highlight enable|Enable diagnostic message highlights. +:LspDiag next|Jump to the next diagnostic message after the current position. +:LspDiag nextWrap|Jump to the next diagnostic message after the current position, wrapping to the first message when the last message is reached. +:LspDiag prev|Jump to the previous diagnostic message before the current position. +:LspDiag prevWrap|Jump to the previous diagnostic message before the current position, wrapping to the last message when the first message is reached. +:LspDiag show|Display the diagnostics messages from the language server for the current buffer in a new location list. +:LspDocumentSymbol|Display the symbols in the current file in a popup menu and jump to the selected symbol. +:LspFold|Fold the current file. +:LspFormat|Format a range of lines in the current file using the language server. The **shiftwidth** and **expandtab** values set for the current buffer are used when format is applied. The default range is the entire file. +:LspGotoDeclaration|Go to the declaration of the keyword under cursor. +:LspGotoDefinition|Go to the definition of the keyword under cursor. +:LspGotoImpl|Go to the implementation of the keyword under cursor. +:LspGotoTypeDef|Go to the type definition of the keyword under cursor. +:LspHighlight|Highlight all the matches for the keyword under cursor. +:LspHighlightClear|Clear all the matches highlighted by :LspHighlight. +:LspHover|Show the documentation for the symbol under the cursor in a popup window. +:LspIncomingCalls|Display the list of symbols calling the current symbol. +:LspOutgoingCalls|Display the list of symbols called by the current symbol. +:LspOutline|Show the list of symbols defined in the current file in a separate window. +:LspPeekDeclaration|Open the declaration of the symbol under cursor in the preview window. +:LspPeekDefinition|Open the definition of the symbol under cursor in the preview window. +:LspPeekImpl|Open the implementation of the symbol under cursor in the preview window. +:LspPeekReferences|Display the list of references to the keyword under cursor in a location list associated with the preview window. +:LspPeekTypeDef|Open the type definition of the symbol under cursor in the preview window. +:LspRename|Rename the current symbol. +:LspSelectionExpand|Expand the current symbol range visual selection. +:LspSelectionShrink|Shrink the current symbol range visual selection. +:LspShowAllServers|Display information about all the registered language servers. +:LspServer|Display the capabilities or messages or status of the language server for the current buffer or restart the server. +:LspShowReferences|Display the list of references to the keyword under cursor in a new location list. +:LspShowSignature|Display the signature of the keyword under cursor. +:LspSubTypeHierarchy|Display the sub type hierarchy in a popup window. +:LspSuperTypeHierarchy|Display the super type hierarchy in a popup window. +:LspSwitchSourceHeader|Switch between a source and a header file. +:LspSymbolSearch|Perform a workspace wide search for a symbol. +:LspWorkspaceAddFolder `{folder}`| Add a folder to the workspace. +:LspWorkspaceListFolders|Show the list of folders in the workspace. +:LspWorkspaceRemoveFolder `{folder}`|Remove a folder from the workspace. + +## Similar Vim LSP Plugins + +1. [vim-lsp: Async Language Server Protocol](https://github.com/prabirshrestha/vim-lsp) +1. [Coc: Conquer of Completion](https://github.com/neoclide/coc.nvim) +1. [vim-lsc: Vim Language Server Client](https://github.com/natebosch/vim-lsc) +1. [LanguageClient-neovim](https://github.com/autozimu/LanguageClient-neovim) +1. [ALE: Asynchronous Lint Engine](https://github.com/dense-analysis/ale) +1. [Neovim built-in LSP client](https://neovim.io/doc/user/lsp.html) +2. [Omnisharp LSP client](https://github.com/OmniSharp/omnisharp-vim) diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/buffer.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/buffer.vim new file mode 100644 index 0000000..5a07654 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/buffer.vim @@ -0,0 +1,194 @@ +vim9script + +# Functions for managing the per-buffer LSP server information + +import './util.vim' + +# A buffer can have one or more attached language servers. The +# "bufnrToServers" Dict contains the list of language servers attached to a +# buffer. The buffer number is the key for the "bufnrToServers" Dict. The +# value is the List of attached language servers. +var bufnrToServers: dict<list<dict<any>>> = {} + +# Add "lspserver" to "bufnrToServers" map for buffer "bnr". +export def BufLspServerSet(bnr: number, lspserver: dict<any>) + if !bufnrToServers->has_key(bnr) + bufnrToServers[bnr] = [] + endif + + bufnrToServers[bnr]->add(lspserver) +enddef + +# Remove "lspserver" from "bufnrToServers" map for buffer "bnr". +export def BufLspServerRemove(bnr: number, lspserver: dict<any>) + if !bufnrToServers->has_key(bnr) + return + endif + + var servers: list<dict<any>> = bufnrToServers[bnr] + servers = servers->filter((key, srv) => srv.id != lspserver.id) + + if servers->empty() + bufnrToServers->remove(bnr) + else + bufnrToServers[bnr] = servers + endif +enddef + +var SupportedCheckFns = { + callHierarchy: (lspserver) => lspserver.isCallHierarchyProvider, + codeAction: (lspserver) => lspserver.isCodeActionProvider, + codeLens: (lspserver) => lspserver.isCodeLensProvider, + completion: (lspserver) => lspserver.isCompletionProvider, + declaration: (lspserver) => lspserver.isDeclarationProvider, + definition: (lspserver) => lspserver.isDefinitionProvider, + documentFormatting: (lspserver) => lspserver.isDocumentFormattingProvider, + documentHighlight: (lspserver) => lspserver.isDocumentHighlightProvider, + documentSymbol: (lspserver) => lspserver.isDocumentSymbolProvider, + foldingRange: (lspserver) => lspserver.isFoldingRangeProvider, + hover: (lspserver) => lspserver.isHoverProvider, + implementation: (lspserver) => lspserver.isImplementationProvider, + inlayHint: (lspserver) => lspserver.isInlayHintProvider || + lspserver.isClangdInlayHintsProvider, + references: (lspserver) => lspserver.isReferencesProvider, + rename: (lspserver) => lspserver.isRenameProvider, + selectionRange: (lspserver) => lspserver.isSelectionRangeProvider, + semanticTokens: (lspserver) => lspserver.isSemanticTokensProvider, + signatureHelp: (lspserver) => lspserver.isSignatureHelpProvider, + typeDefinition: (lspserver) => lspserver.isTypeDefinitionProvider, + typeHierarchy: (lspserver) => lspserver.isTypeHierarchyProvider, + workspaceSymbol: (lspserver) => lspserver.isWorkspaceSymbolProvider +} + +# Returns the LSP server for the buffer "bnr". If "feature" is specified, +# then returns the LSP server that provides the "feature". +# Returns an empty dict if the server is not found. +export def BufLspServerGet(bnr: number, feature: string = null_string): dict<any> + if !bufnrToServers->has_key(bnr) + return {} + endif + + if bufnrToServers[bnr]->empty() + return {} + endif + + if feature == null_string + return bufnrToServers[bnr][0] + endif + + if !SupportedCheckFns->has_key(feature) + # If this happns it is a programming error, and should be fixed in the + # source code + :throw $'Error: ''{feature}'' is not a valid feature' + endif + + var SupportedCheckFn = SupportedCheckFns[feature] + + var possibleLSPs: list<dict<any>> = [] + + for lspserver in bufnrToServers[bnr] + if !lspserver.ready || !SupportedCheckFn(lspserver) + continue + endif + + possibleLSPs->add(lspserver) + endfor + + if possibleLSPs->empty() + return {} + endif + + # LSP server is configured to be a provider for "feature" + for lspserver in possibleLSPs + var has_feature: bool = lspserver.features->get(feature, false) + if has_feature + return lspserver + endif + endfor + + # Return the first LSP server that supports "feature" and doesn't have it + # disabled + for lspserver in possibleLSPs + if lspserver.featureEnabled(feature) + return lspserver + endif + endfor + + return {} +enddef + +# Returns the LSP server for the buffer "bnr" and with ID "id". Returns an empty +# dict if the server is not found. +export def BufLspServerGetById(bnr: number, id: number): dict<any> + if !bufnrToServers->has_key(bnr) + return {} + endif + + for lspserver in bufnrToServers[bnr] + if lspserver.id == id + return lspserver + endif + endfor + + return {} +enddef + +# Returns the LSP servers for the buffer "bnr". Returns an empty list if the +# servers are not found. +export def BufLspServersGet(bnr: number): list<dict<any>> + if !bufnrToServers->has_key(bnr) + return [] + endif + + return bufnrToServers[bnr] +enddef + +# Returns the LSP server for the current buffer with the optionally "feature". +# Returns an empty dict if the server is not found. +export def CurbufGetServer(feature: string = null_string): dict<any> + return BufLspServerGet(bufnr(), feature) +enddef + +# Returns the LSP servers for the current buffer. Returns an empty list if the +# servers are not found. +export def CurbufGetServers(): list<dict<any>> + return BufLspServersGet(bufnr()) +enddef + +export def BufHasLspServer(bnr: number): bool + var lspserver = BufLspServerGet(bnr) + + return !lspserver->empty() +enddef + +# Returns the LSP server for the current buffer with the optinally "feature" if +# it is running and is ready. +# Returns an empty dict if the server is not found or is not ready. +export def CurbufGetServerChecked(feature: string = null_string): dict<any> + var fname: string = @% + if fname->empty() || &filetype->empty() + return {} + endif + + var lspserver: dict<any> = CurbufGetServer(feature) + if lspserver->empty() + if feature == null_string + util.ErrMsg($'Language server for "{&filetype}" file type is not found') + else + util.ErrMsg($'Language server for "{&filetype}" file type supporting "{feature}" feature is not found') + endif + return {} + endif + if !lspserver.running + util.ErrMsg($'Language server for "{&filetype}" file type is not running') + return {} + endif + if !lspserver.ready + util.ErrMsg($'Language server for "{&filetype}" file type is not ready') + return {} + endif + + return lspserver +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/callhierarchy.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/callhierarchy.vim new file mode 100644 index 0000000..aca6a55 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/callhierarchy.vim @@ -0,0 +1,194 @@ +vim9script + +# Functions for dealing with call hierarchy (incoming/outgoing calls) + +import './util.vim' +import './buffer.vim' as buf + +# Jump to the location of the symbol under the cursor in the call hierarchy +# tree window. +def CallHierarchyItemJump() + var item: dict<any> = w:LspCallHierItemMap[line('.')].item + util.JumpToLspLocation(item, '') +enddef + +# Refresh the call hierarchy tree for the symbol at index "idx". +def CallHierarchyTreeItemRefresh(idx: number) + var treeItem: dict<any> = w:LspCallHierItemMap[idx] + + if treeItem.open + # Already retrieved the children for this item + return + endif + + if !treeItem->has_key('children') + # First time retrieving the children for the item at index "idx" + var lspserver = buf.BufLspServerGet(w:LspBufnr, 'callHierarchy') + if lspserver->empty() || !lspserver.running + return + endif + + var reply: any + if w:LspCallHierIncoming + reply = lspserver.getIncomingCalls(treeItem.item) + else + reply = lspserver.getOutgoingCalls(treeItem.item) + endif + + treeItem.children = [] + if !reply->empty() + for item in reply + treeItem.children->add({item: w:LspCallHierIncoming ? item.from : + item.to, open: false}) + endfor + endif + endif + + # Clear and redisplay the tree in the window + treeItem.open = true + var save_cursor = getcurpos() + CallHierarchyTreeRefresh() + setpos('.', save_cursor) +enddef + +# Open the call hierarchy tree item under the cursor +def CallHierarchyTreeItemOpen() + CallHierarchyTreeItemRefresh(line('.')) +enddef + +# Refresh the entire call hierarchy tree +def CallHierarchyTreeRefreshCmd() + w:LspCallHierItemMap[2].open = false + w:LspCallHierItemMap[2]->remove('children') + CallHierarchyTreeItemRefresh(2) +enddef + +# Display the incoming call hierarchy tree +def CallHierarchyTreeIncomingCmd() + w:LspCallHierItemMap[2].open = false + w:LspCallHierItemMap[2]->remove('children') + w:LspCallHierIncoming = true + CallHierarchyTreeItemRefresh(2) +enddef + +# Display the outgoing call hierarchy tree +def CallHierarchyTreeOutgoingCmd() + w:LspCallHierItemMap[2].open = false + w:LspCallHierItemMap[2]->remove('children') + w:LspCallHierIncoming = false + CallHierarchyTreeItemRefresh(2) +enddef + +# Close the call hierarchy tree item under the cursor +def CallHierarchyTreeItemClose() + var treeItem: dict<any> = w:LspCallHierItemMap[line('.')] + treeItem.open = false + var save_cursor = getcurpos() + CallHierarchyTreeRefresh() + setpos('.', save_cursor) +enddef + +# Recursively add the call hierarchy items to w:LspCallHierItemMap +def CallHierarchyTreeItemShow(incoming: bool, treeItem: dict<any>, pfx: string) + var item = treeItem.item + var treePfx: string + if treeItem.open && treeItem->has_key('children') + treePfx = has('gui_running') ? '▼' : '-' + else + treePfx = has('gui_running') ? '▶' : '+' + endif + var fname = util.LspUriToFile(item.uri) + var s = $'{pfx}{treePfx} {item.name} ({fname->fnamemodify(":t")} [{fname->fnamemodify(":h")}])' + append('$', s) + w:LspCallHierItemMap->add(treeItem) + if treeItem.open && treeItem->has_key('children') + for child in treeItem.children + CallHierarchyTreeItemShow(incoming, child, $'{pfx} ') + endfor + endif +enddef + +def CallHierarchyTreeRefresh() + :setlocal modifiable + :silent! :%d _ + + setline(1, $'# {w:LspCallHierIncoming ? "Incoming calls to" : "Outgoing calls from"} "{w:LspCallHierarchyTree.item.name}"') + w:LspCallHierItemMap = [{}, {}] + CallHierarchyTreeItemShow(w:LspCallHierIncoming, w:LspCallHierarchyTree, '') + :setlocal nomodifiable +enddef + +def CallHierarchyTreeShow(incoming: bool, prepareItem: dict<any>, + items: list<dict<any>>) + var save_bufnr = bufnr() + var wid = bufwinid('LSP-CallHierarchy') + if wid != -1 + wid->win_gotoid() + else + :new LSP-CallHierarchy + :setlocal buftype=nofile + :setlocal bufhidden=wipe + :setlocal noswapfile + :setlocal nonumber nornu + :setlocal fdc=0 signcolumn=no + + :nnoremap <buffer> <CR> <ScriptCmd>CallHierarchyItemJump()<CR> + :nnoremap <buffer> - <ScriptCmd>CallHierarchyTreeItemOpen()<CR> + :nnoremap <buffer> + <ScriptCmd>CallHierarchyTreeItemClose()<CR> + :command -buffer LspCallHierarchyRefresh CallHierarchyTreeRefreshCmd() + :command -buffer LspCallHierarchyIncoming CallHierarchyTreeIncomingCmd() + :command -buffer LspCallHierarchyOutgoing CallHierarchyTreeOutgoingCmd() + + :syntax match Comment '^#.*$' + :syntax match Directory '(.*)$' + endif + + w:LspBufnr = save_bufnr + w:LspCallHierIncoming = incoming + w:LspCallHierarchyTree = {} + w:LspCallHierarchyTree.item = prepareItem + w:LspCallHierarchyTree.open = true + w:LspCallHierarchyTree.children = [] + for item in items + w:LspCallHierarchyTree.children->add({item: incoming ? item.from : item.to, open: false}) + endfor + + CallHierarchyTreeRefresh() + + :setlocal nomodified + :setlocal nomodifiable +enddef + +export def IncomingCalls(lspserver: dict<any>) + var prepareReply = lspserver.prepareCallHierarchy() + if prepareReply->empty() + util.WarnMsg('No incoming calls') + return + endif + + var reply = lspserver.getIncomingCalls(prepareReply) + if reply->empty() + util.WarnMsg('No incoming calls') + return + endif + + CallHierarchyTreeShow(true, prepareReply, reply) +enddef + +export def OutgoingCalls(lspserver: dict<any>) + var prepareReply = lspserver.prepareCallHierarchy() + if prepareReply->empty() + util.WarnMsg('No outgoing calls') + return + endif + + var reply = lspserver.getOutgoingCalls(prepareReply) + if reply->empty() + util.WarnMsg('No outgoing calls') + return + endif + + CallHierarchyTreeShow(false, prepareReply, reply) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/capabilities.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/capabilities.vim new file mode 100644 index 0000000..46f5973 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/capabilities.vim @@ -0,0 +1,501 @@ +vim9script + +# Functions for managing the LSP server and client capabilities + +import './options.vim' as opt + +# Process the server capabilities +# interface ServerCapabilities +export def ProcessServerCaps(lspserver: dict<any>, caps: dict<any>) + var serverEncoding = 'utf-16' + if lspserver.caps->has_key('positionEncoding') + serverEncoding = lspserver.caps.positionEncoding + elseif lspserver.caps->has_key('~additionalInitResult_offsetEncoding') + serverEncoding = lspserver.caps['~additionalInitResult_offsetEncoding'] + endif + + if lspserver.forceOffsetEncoding != '' + serverEncoding = lspserver.forceOffsetEncoding + endif + + # one of 'utf-8', 'utf-16' or 'utf-32' + if serverEncoding == 'utf-8' + lspserver.posEncoding = 8 + elseif serverEncoding == 'utf-16' + lspserver.posEncoding = 16 + else + lspserver.posEncoding = 32 + endif + + if has('patch-9.0.1629') && lspserver.posEncoding != 32 + lspserver.needOffsetEncoding = true + else + lspserver.needOffsetEncoding = false + endif + + # completionProvider + if lspserver.caps->has_key('completionProvider') + lspserver.isCompletionProvider = true + if lspserver.caps.completionProvider->has_key('resolveProvider') + lspserver.isCompletionResolveProvider = + lspserver.caps.completionProvider.resolveProvider + else + lspserver.isCompletionResolveProvider = false + endif + else + lspserver.isCompletionProvider = false + lspserver.isCompletionResolveProvider = false + endif + + # definitionProvider + if lspserver.caps->has_key('definitionProvider') + if lspserver.caps.definitionProvider->type() == v:t_bool + lspserver.isDefinitionProvider = lspserver.caps.definitionProvider + else + lspserver.isDefinitionProvider = true + endif + else + lspserver.isDefinitionProvider = false + endif + + # declarationProvider + if lspserver.caps->has_key('declarationProvider') + if lspserver.caps.declarationProvider->type() == v:t_bool + lspserver.isDeclarationProvider = lspserver.caps.declarationProvider + else + lspserver.isDeclarationProvider = true + endif + else + lspserver.isDeclarationProvider = false + endif + + # typeDefinitionProvider + if lspserver.caps->has_key('typeDefinitionProvider') + if lspserver.caps.typeDefinitionProvider->type() == v:t_bool + lspserver.isTypeDefinitionProvider = lspserver.caps.typeDefinitionProvider + else + lspserver.isTypeDefinitionProvider = true + endif + else + lspserver.isTypeDefinitionProvider = false + endif + + # implementationProvider + if lspserver.caps->has_key('implementationProvider') + if lspserver.caps.implementationProvider->type() == v:t_bool + lspserver.isImplementationProvider = lspserver.caps.implementationProvider + else + lspserver.isImplementationProvider = true + endif + else + lspserver.isImplementationProvider = false + endif + + # signatureHelpProvider + if lspserver.caps->has_key('signatureHelpProvider') + lspserver.isSignatureHelpProvider = true + else + lspserver.isSignatureHelpProvider = false + endif + + # hoverProvider + if lspserver.caps->has_key('hoverProvider') + if lspserver.caps.hoverProvider->type() == v:t_bool + lspserver.isHoverProvider = lspserver.caps.hoverProvider + else + lspserver.isHoverProvider = true + endif + else + lspserver.isHoverProvider = false + endif + + # referencesProvider + if lspserver.caps->has_key('referencesProvider') + if lspserver.caps.referencesProvider->type() == v:t_bool + lspserver.isReferencesProvider = lspserver.caps.referencesProvider + else + lspserver.isReferencesProvider = true + endif + else + lspserver.isReferencesProvider = false + endif + + # documentHighlightProvider + if lspserver.caps->has_key('documentHighlightProvider') + if lspserver.caps.documentHighlightProvider->type() == v:t_bool + lspserver.isDocumentHighlightProvider = + lspserver.caps.documentHighlightProvider + else + lspserver.isDocumentHighlightProvider = true + endif + else + lspserver.isDocumentHighlightProvider = false + endif + + # documentSymbolProvider + if lspserver.caps->has_key('documentSymbolProvider') + if lspserver.caps.documentSymbolProvider->type() == v:t_bool + lspserver.isDocumentSymbolProvider = + lspserver.caps.documentSymbolProvider + else + lspserver.isDocumentSymbolProvider = true + endif + else + lspserver.isDocumentSymbolProvider = false + endif + + # documentFormattingProvider + if lspserver.caps->has_key('documentFormattingProvider') + if lspserver.caps.documentFormattingProvider->type() == v:t_bool + lspserver.isDocumentFormattingProvider = + lspserver.caps.documentFormattingProvider + else + lspserver.isDocumentFormattingProvider = true + endif + else + lspserver.isDocumentFormattingProvider = false + endif + + # callHierarchyProvider + if lspserver.caps->has_key('callHierarchyProvider') + if lspserver.caps.callHierarchyProvider->type() == v:t_bool + lspserver.isCallHierarchyProvider = + lspserver.caps.callHierarchyProvider + else + lspserver.isCallHierarchyProvider = true + endif + else + lspserver.isCallHierarchyProvider = false + endif + + # semanticTokensProvider + if lspserver.caps->has_key('semanticTokensProvider') + lspserver.isSemanticTokensProvider = true + lspserver.semanticTokensLegend = + lspserver.caps.semanticTokensProvider.legend + lspserver.semanticTokensRange = + lspserver.caps.semanticTokensProvider->get('range', false) + if lspserver.caps.semanticTokensProvider->has_key('full') + if lspserver.caps.semanticTokensProvider.full->type() == v:t_bool + lspserver.semanticTokensFull = + lspserver.caps.semanticTokensProvider.full + lspserver.semanticTokensDelta = false + else + lspserver.semanticTokensFull = true + if lspserver.caps.semanticTokensProvider.full->has_key('delta') + lspserver.semanticTokensDelta = + lspserver.caps.semanticTokensProvider.full.delta + else + lspserver.semanticTokensDelta = false + endif + endif + else + lspserver.semanticTokensFull = false + lspserver.semanticTokensDelta = false + endif + else + lspserver.isSemanticTokensProvider = false + endif + + # typeHierarchyProvider + if lspserver.caps->has_key('typeHierarchyProvider') + lspserver.isTypeHierarchyProvider = true + else + lspserver.isTypeHierarchyProvider = false + endif + + # renameProvider + if lspserver.caps->has_key('renameProvider') + if lspserver.caps.renameProvider->type() == v:t_bool + lspserver.isRenameProvider = lspserver.caps.renameProvider + else + lspserver.isRenameProvider = true + endif + else + lspserver.isRenameProvider = false + endif + + # codeActionProvider + if lspserver.caps->has_key('codeActionProvider') + if lspserver.caps.codeActionProvider->type() == v:t_bool + lspserver.isCodeActionProvider = lspserver.caps.codeActionProvider + else + lspserver.isCodeActionProvider = true + endif + else + lspserver.isCodeActionProvider = false + endif + + # codeLensProvider + if lspserver.caps->has_key('codeLensProvider') + lspserver.isCodeLensProvider = true + if lspserver.caps.codeLensProvider->has_key('resolveProvider') + lspserver.isCodeLensResolveProvider = true + else + lspserver.isCodeLensResolveProvider = false + endif + else + lspserver.isCodeLensProvider = false + endif + + # workspaceSymbolProvider + if lspserver.caps->has_key('workspaceSymbolProvider') + if lspserver.caps.workspaceSymbolProvider->type() == v:t_bool + lspserver.isWorkspaceSymbolProvider = + lspserver.caps.workspaceSymbolProvider + else + lspserver.isWorkspaceSymbolProvider = true + endif + else + lspserver.isWorkspaceSymbolProvider = false + endif + + # selectionRangeProvider + if lspserver.caps->has_key('selectionRangeProvider') + if lspserver.caps.selectionRangeProvider->type() == v:t_bool + lspserver.isSelectionRangeProvider = + lspserver.caps.selectionRangeProvider + else + lspserver.isSelectionRangeProvider = true + endif + else + lspserver.isSelectionRangeProvider = false + endif + + # foldingRangeProvider + if lspserver.caps->has_key('foldingRangeProvider') + if lspserver.caps.foldingRangeProvider->type() == v:t_bool + lspserver.isFoldingRangeProvider = lspserver.caps.foldingRangeProvider + else + lspserver.isFoldingRangeProvider = true + endif + else + lspserver.isFoldingRangeProvider = false + endif + + # inlayHintProvider + if lspserver.caps->has_key('inlayHintProvider') + if lspserver.caps.inlayHintProvider->type() == v:t_bool + lspserver.isInlayHintProvider = lspserver.caps.inlayHintProvider + else + lspserver.isInlayHintProvider = true + endif + else + lspserver.isInlayHintProvider = false + endif + + # clangdInlayHintsProvider + if lspserver.caps->has_key('clangdInlayHintsProvider') + lspserver.isClangdInlayHintsProvider = + lspserver.caps.clangdInlayHintsProvider + else + lspserver.isClangdInlayHintsProvider = false + endif + + # textDocumentSync capabilities + lspserver.supportsDidSave = false + # Default to TextDocumentSyncKind.None + lspserver.textDocumentSync = 0 + if lspserver.caps->has_key('textDocumentSync') + if lspserver.caps.textDocumentSync->type() == v:t_bool + || lspserver.caps.textDocumentSync->type() == v:t_number + lspserver.supportsDidSave = lspserver.caps.textDocumentSync + lspserver.textDocumentSync = lspserver.caps.textDocumentSync + elseif lspserver.caps.textDocumentSync->type() == v:t_dict + # "save" + if lspserver.caps.textDocumentSync->has_key('save') + if lspserver.caps.textDocumentSync.save->type() == v:t_bool + || lspserver.caps.textDocumentSync.save->type() == v:t_number + lspserver.supportsDidSave = lspserver.caps.textDocumentSync.save + elseif lspserver.caps.textDocumentSync.save->type() == v:t_dict + lspserver.supportsDidSave = true + endif + endif + # "change" + if lspserver.caps.textDocumentSync->has_key('change') + lspserver.textDocumentSync = lspserver.caps.textDocumentSync.change + endif + endif + endif +enddef + +# Return all the LSP client capabilities +export def GetClientCaps(): dict<any> + # client capabilities (ClientCapabilities) + var clientCaps: dict<any> = { + general: { + # Currently we always send character count as position offset, + # which meanas only utf-32 is supported. + # Adding utf-16 simply for good mesure, as I'm scared some servers will + # give up if they don't support utf-32 only. + positionEncodings: ['utf-32', 'utf-16'] + }, + textDocument: { + callHierarchy: { + dynamicRegistration: false + }, + codeAction: { + dynamicRegistration: false, + codeActionLiteralSupport: { + codeActionKind: { + valueSet: ['', 'quickfix', 'refactor', 'refactor.extract', + 'refactor.inline', 'refactor.rewrite', 'source', + 'source.organizeImports'] + } + }, + isPreferredSupport: true, + disabledSupport: true + }, + codeLens: { + dynamicRegistration: false + }, + completion: { + dynamicRegistration: false, + completionItem: { + documentationFormat: ['markdown', 'plaintext'], + resolveSupport: { + properties: ['detail', 'documentation'] + }, + snippetSupport: opt.lspOptions.snippetSupport, + insertReplaceSupport: false + }, + completionItemKind: { + valueSet: range(1, 25) + } + }, + declaration: { + dynamicRegistration: false, + linkSupport: true + }, + definition: { + dynamicRegistration: false, + linkSupport: true + }, + documentHighlight: { + dynamicRegistration: false + }, + documentSymbol: { + dynamicRegistration: false, + symbolKind: { + valueSet: range(1, 25) + }, + hierarchicalDocumentSymbolSupport: true, + labelSupport: false + }, + foldingRange: { + dynamicRegistration: false, + rangeLimit: 5000, + lineFoldingOnly: true, + foldingRangeKind: { + valueSet: ['comment', 'imports', 'region'] + }, + foldingRange: { + collapsedText: true + } + }, + formatting: { + dynamicRegistration: false + }, + hover: { + dynamicRegistration: false, + contentFormat: ['markdown', 'plaintext'] + }, + implementation: { + dynamicRegistration: false, + linkSupport: true + }, + inlayHint: { + dynamicRegistration: false + }, + publishDiagnostics: { + relatedInformation: false, + versionSupport: true, + codeDescriptionSupport: true, + dataSupport: true + }, + rangeFormatting: { + dynamicRegistration: false + }, + references: { + dynamicRegistration: false + }, + rename: { + dynamicRegistration: false, + prepareSupport: false, + }, + selectionRange: { + dynamicRegistration: false, + }, + signatureHelp: { + dynamicRegistration: false, + signatureInformation: { + documentationFormat: ['markdown', 'plaintext'], + activeParameterSupport: true + } + }, + semanticTokens: { + dynamicRegistration: false, + requests: { + range: false, + full: { + delta: true + } + }, + tokenTypes: [ + 'type', 'class', 'enum', 'interface', 'struct', 'typeParameter', + 'parameter', 'variable', 'property', 'enumMember', 'event', + 'function', 'method', 'macro', 'keyword', 'modifier', 'comment', + 'string', 'number', 'regexp', 'operator' + ], + tokenModifiers: [ + 'declaration', 'definition', 'readonly', 'static', 'deprecated', + 'abstract', 'async', 'modification', 'documentation', + 'defaultLibrary' + ], + formats: ['relative'], + overlappingTokenSupport: false, + multilineTokenSupport: false, + serverCancelSupport: false, + augmentsSyntaxTokens: true + }, + synchronization: { + dynamicRegistration: false, + didSave: true, + willSave: false, + WillSaveWaitUntil: false + }, + typeDefinition: { + dynamicRegistration: false, + linkSupport: true + }, + typeHierarchy: { + dynamicRegistration: false, + } + }, + window: {}, + workspace: { + workspaceFolders: true, + applyEdit: true, + workspaceEdit: { + resourceOperations: ['rename', 'create', 'delete'] + }, + configuration: true, + symbol: { + dynamicRegistration: false + } + }, + # This is the way clangd expects to be informated about supported encodings: + # https://clangd.llvm.org/extensions#utf-8-offsets + offsetEncoding: ['utf-32', 'utf-16'] + } + + # Vim patch 1629 is needed to properly encode/decode UTF-16 offsets + if has('patch-9.0.1629') + clientCaps.general.positionEncodings = ['utf-32', 'utf-16', 'utf-8'] + clientCaps.offsetEncoding = ['utf-32', 'utf-16', 'utf-8'] + endif + + return clientCaps +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/codeaction.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/codeaction.vim new file mode 100644 index 0000000..fb2a91a --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/codeaction.vim @@ -0,0 +1,135 @@ +vim9script + +# Functions related to handling LSP code actions to fix diagnostics. + +import './util.vim' +import './textedit.vim' +import './options.vim' as opt + +var CommandHandlers: dict<func> + +export def RegisterCmdHandler(cmd: string, Handler: func) + CommandHandlers[cmd] = Handler +enddef + +export def DoCommand(lspserver: dict<any>, cmd: dict<any>) + if cmd->has_key('command') && CommandHandlers->has_key(cmd.command) + var CmdHandler: func = CommandHandlers[cmd.command] + try + call CmdHandler(cmd) + catch + util.ErrMsg($'"{cmd.command}" handler raised exception {v:exception}') + endtry + else + lspserver.executeCommand(cmd) + endif +enddef + +# Apply the code action selected by the user. +export def HandleCodeAction(lspserver: dict<any>, selAction: dict<any>) + # textDocument/codeAction can return either Command[] or CodeAction[]. + # If it is a CodeAction, it can have either an edit, a command or both. + # Edits should be executed first. + # Both Command and CodeAction interfaces has "command" member + # so we should check "command" type - for Command it will be "string" + if selAction->has_key('edit') + || (selAction->has_key('command') && selAction.command->type() == v:t_dict) + # selAction is a CodeAction instance, apply edit and command + if selAction->has_key('edit') + # apply edit first + textedit.ApplyWorkspaceEdit(selAction.edit) + endif + if selAction->has_key('command') + DoCommand(lspserver, selAction.command) + endif + else + # selAction is a Command instance, apply it directly + DoCommand(lspserver, selAction) + endif +enddef + +# Process the list of code actions returned by the LSP server, ask the user to +# choose one action from the list and then apply it. +# If "query" is a number, then apply the corresponding action in the list. +# If "query" is a regular expression starting with "/", then apply the action +# matching the search string in the list. +# If "query" is a regular string, then apply the action matching the string. +# If "query" is an empty string, then if the "usePopupInCodeAction" option is +# configured by the user, then display the list of items in a popup menu. +# Otherwise display the items in an input list and prompt the user to select +# an action. +export def ApplyCodeAction(lspserver: dict<any>, actionlist: list<dict<any>>, query: string): void + var actions = actionlist + + if opt.lspOptions.hideDisabledCodeActions + actions = actions->filter((ix, act) => !act->has_key('disabled')) + endif + + if actions->empty() + # no action can be performed + util.WarnMsg('No code action is available') + return + endif + + var text: list<string> = [] + var act: dict<any> + for i in actions->len()->range() + act = actions[i] + var t: string = act.title->substitute('\r\n', '\\r\\n', 'g') + t = t->substitute('\n', '\\n', 'g') + text->add(printf(" %d. %s ", i + 1, t)) + endfor + + var choice: number + + var query_ = query->trim() + if query_ =~ '^\d\+$' # digit + choice = query_->str2nr() + elseif query_ =~ '^/' # regex + choice = 1 + util.Indexof(actions, (i, a) => a.title =~ query_[1 : ]) + elseif query_ != '' # literal string + choice = 1 + util.Indexof(actions, (i, a) => a.title[0 : query_->len() - 1] == query_) + elseif opt.lspOptions.usePopupInCodeAction + # Use a popup menu to show the code action + popup_create(text, { + pos: 'botleft', + line: 'cursor-1', + col: 'cursor', + zindex: 1000, + cursorline: 1, + mapping: 0, + wrap: 0, + title: 'Code action', + callback: (_, result) => { + # Invalid item selected or closed the popup + if result <= 0 || result > text->len() + return + endif + + # Do the code action + HandleCodeAction(lspserver, actions[result - 1]) + }, + filter: (winid, key) => { + if key == 'h' || key == 'l' + winid->popup_close(-1) + elseif key->str2nr() > 0 + # assume less than 10 entries are present + winid->popup_close(key->str2nr()) + else + return popup_filter_menu(winid, key) + endif + return 1 + }, + }) + else + choice = inputlist(['Code action:'] + text) + endif + + if choice < 1 || choice > text->len() + return + endif + + HandleCodeAction(lspserver, actions[choice - 1]) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/codelens.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/codelens.vim new file mode 100644 index 0000000..082edf7 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/codelens.vim @@ -0,0 +1,32 @@ +vim9script + +import './codeaction.vim' + +# Functions related to handling LSP code lens + +export def ProcessCodeLens(lspserver: dict<any>, bnr: number, codeLensItems: list<dict<any>>) + var text: list<string> = [] + for i in codeLensItems->len()->range() + var item = codeLensItems[i] + if !item->has_key('command') + # resolve the code lens + item = lspserver.resolveCodeLens(bnr, item) + if item->empty() + continue + endif + codeLensItems[i] = item + endif + text->add(printf("%d. %s\t| L%s:%s", i + 1, item.command.title, + item.range.start.line + 1, + getline(item.range.start.line + 1))) + endfor + + var choice = inputlist(['Code Lens:'] + text) + if choice < 1 || choice > codeLensItems->len() + return + endif + + codeaction.DoCommand(lspserver, codeLensItems[choice - 1].command) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/completion.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/completion.vim new file mode 100644 index 0000000..8ce5899 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/completion.vim @@ -0,0 +1,708 @@ +vim9script + +# LSP completion related functions + +import './util.vim' +import './buffer.vim' as buf +import './options.vim' as opt +import './textedit.vim' +import './snippet.vim' +import './codeaction.vim' + +# per-filetype omni-completion enabled/disabled table +var ftypeOmniCtrlMap: dict<bool> = {} + +var defaultKinds: dict<string> = { + 'Text': 't', + 'Method': 'm', + 'Function': 'f', + 'Constructor': 'C', + 'Field': 'F', + 'Variable': 'v', + 'Class': 'c', + 'Interface': 'i', + 'Module': 'M', + 'Property': 'p', + 'Unit': 'u', + 'Value': 'V', + 'Enum': 'e', + 'Keyword': 'k', + 'Snippet': 'S', + 'Color': 'C', + 'File': 'f', + 'Reference': 'r', + 'Folder': 'F', + 'EnumMember': 'E', + 'Constant': 'd', + 'Struct': 's', + 'Event': 'E', + 'Operator': 'o', + 'TypeParameter': 'T', + 'Buffer': 'B', +} + +# Returns true if omni-completion is enabled for filetype "ftype". +# Otherwise, returns false. +def LspOmniComplEnabled(ftype: string): bool + return ftypeOmniCtrlMap->get(ftype, false) +enddef + +# Enables or disables omni-completion for filetype "fype" +export def OmniComplSet(ftype: string, enabled: bool) + ftypeOmniCtrlMap->extend({[ftype]: enabled}) +enddef + +# Map LSP complete item kind to a character +def LspCompleteItemKindChar(kind: number): string + var kindMap: list<string> = [ + '', + 'Text', + 'Method', + 'Function', + 'Constructor', + 'Field', + 'Variable', + 'Class', + 'Interface', + 'Module', + 'Property', + 'Unit', + 'Value', + 'Enum', + 'Keyword', + 'Snippet', + 'Color', + 'File', + 'Reference', + 'Folder', + 'EnumMember', + 'Constant', + 'Struct', + 'Event', + 'Operator', + 'TypeParameter', + 'Buffer' + ] + + if kind > 26 + return '' + endif + + var kindName = kindMap[kind] + var kindValue = defaultKinds[kindName] + + var lspOpts = opt.lspOptions + if lspOpts.customCompletionKinds && + lspOpts.completionKinds->has_key(kindName) + kindValue = lspOpts.completionKinds[kindName] + endif + + return kindValue +enddef + +# Remove all the snippet placeholders from "str" and return the value. +# Based on a similar function in the vim-lsp plugin. +def MakeValidWord(str_arg: string): string + var str = str_arg->substitute('\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g') + str = str->substitute('\\\(.\)', '\1', 'g') + var valid = str->matchstr('^[^"'' (<{\[\t\r\n]\+') + if valid->empty() + return str + endif + if valid =~ ':$' + return valid[: -2] + endif + return valid +enddef + +# add completion from current buf +def CompletionFromBuffer(items: list<dict<any>>) + var words = {} + var start = reltime() + var timeout = opt.lspOptions.bufferCompletionTimeout + var linenr = 1 + for line in getline(1, '$') + for word in line->split('\W\+') + if !words->has_key(word) && word->len() > 1 + words[word] = 1 + items->add({ + label: word, + data: { + entryNames: [word], + }, + kind: 26, + documentation: "", + }) + endif + endfor + # Check every 200 lines if timeout is exceeded + if timeout > 0 && linenr % 200 == 0 && + start->reltime()->reltimefloat() * 1000 > timeout + break + endif + linenr += 1 + endfor +enddef + +# process the 'textDocument/completion' reply from the LSP server +# Result: CompletionItem[] | CompletionList | null +export def CompletionReply(lspserver: dict<any>, cItems: any) + lspserver.completeItemsIsIncomplete = false + if cItems->empty() + if lspserver.omniCompletePending + lspserver.completeItems = [] + lspserver.omniCompletePending = false + endif + return + endif + + var items: list<dict<any>> + if cItems->type() == v:t_list + items = cItems + else + items = cItems.items + lspserver.completeItemsIsIncomplete = cItems->get('isIncomplete', false) + endif + + var lspOpts = opt.lspOptions + + # Get the keyword prefix before the current cursor column. + var chcol = charcol('.') + var starttext = chcol == 1 ? '' : getline('.')[ : chcol - 2] + var [prefix, start_idx, end_idx] = starttext->matchstrpos('\k*$') + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE + prefix = prefix->tolower() + endif + + var start_col = start_idx + 1 + + if lspOpts.ultisnipsSupport + snippet.CompletionUltiSnips(prefix, items) + elseif lspOpts.vsnipSupport + snippet.CompletionVsnip(items) + endif + + if lspOpts.useBufferCompletion + CompletionFromBuffer(items) + endif + + var completeItems: list<dict<any>> = [] + var itemsUsed: list<string> = [] + for item in items + var d: dict<any> = {} + + # TODO: Add proper support for item.textEdit.newText and + # item.textEdit.range. Keep in mind that item.textEdit.range can start + # way before the typed keyword. + if item->has_key('textEdit') && + lspOpts.completionMatcherValue != opt.COMPLETIONMATCHER_FUZZY + var start_charcol: number + if !prefix->empty() + start_charcol = charidx(starttext, start_idx) + 1 + else + start_charcol = chcol + endif + var textEdit = item.textEdit + var textEditRange: dict<any> = {} + if textEdit->has_key('range') + textEditRange = textEdit.range + elseif textEdit->has_key('insert') + textEditRange = textEdit.insert + endif + var textEditStartCol = + util.GetCharIdxWithoutCompChar(bufnr(), textEditRange.start) + if textEditStartCol != start_charcol + var offset = start_charcol - textEditStartCol - 1 + d.word = textEdit.newText[offset : ] + else + d.word = textEdit.newText + endif + elseif item->has_key('insertText') + d.word = item.insertText + else + d.word = item.label + endif + + if item->get('insertTextFormat', 1) == 2 + # snippet completion. Needs a snippet plugin to expand the snippet. + # Remove all the snippet placeholders + d.word = MakeValidWord(d.word) + elseif !lspserver.completeItemsIsIncomplete || lspOpts.useBufferCompletion + # Filter items only when "isIncomplete" is set (otherwise server would + # have done the filtering) or when buffer completion is enabled + + # plain text completion + if !prefix->empty() + # If the completion item text doesn't start with the current (case + # ignored) keyword prefix, skip it. + var filterText: string = item->get('filterText', d.word) + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE + if filterText->tolower()->stridx(prefix) != 0 + continue + endif + # If the completion item text doesn't fuzzy match with the current + # keyword prefix, skip it. + elseif lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_FUZZY + if matchfuzzy([filterText], prefix)->empty() + continue + endif + # If the completion item text doesn't start with the current keyword + # prefix, skip it. + else + if filterText->stridx(prefix) != 0 + continue + endif + endif + endif + endif + + d.abbr = item.label + d.dup = 1 + + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE + d.icase = 1 + endif + + if item->has_key('kind') && item.kind != null + # namespace CompletionItemKind + # map LSP kind to complete-item-kind + d.kind = LspCompleteItemKindChar(item.kind) + endif + + if lspserver.completionLazyDoc + d.info = 'Lazy doc' + else + if item->has_key('detail') && !item.detail->empty() + # Solve a issue where if a server send a detail field + # with a "\n", on the menu will be everything joined with + # a "^@" separating it. (example: clangd) + d.menu = item.detail->split("\n")[0] + endif + if item->has_key('documentation') + var itemDoc = item.documentation + if itemDoc->type() == v:t_string && !itemDoc->empty() + d.info = itemDoc + elseif itemDoc->type() == v:t_dict + && itemDoc.value->type() == v:t_string + d.info = itemDoc.value + endif + endif + endif + + # Score is used for sorting. + d.score = item->get('sortText') + if d.score->empty() + d.score = item->get('label', '') + endif + + # Dont include duplicate items + if lspOpts.filterCompletionDuplicates + var key = d->get('word', '') .. + d->get('info', '') .. + d->get('kind', '') .. + d->get('score', '') .. + d->get('abbr', '') .. + d->get('dup', '') + if index(itemsUsed, key) != -1 + continue + endif + add(itemsUsed, key) + endif + + d.user_data = item + completeItems->add(d) + endfor + + if lspOpts.completionMatcherValue != opt.COMPLETIONMATCHER_FUZZY + # Lexographical sort (case-insensitive). + completeItems->sort((a, b) => + a.score == b.score ? 0 : a.score >? b.score ? 1 : -1) + endif + + if lspOpts.autoComplete && !lspserver.omniCompletePending + if completeItems->empty() + # no matches + return + endif + + var m = mode() + if m != 'i' && m != 'R' && m != 'Rv' + # If not in insert or replace mode, then don't start the completion + return + endif + + if completeItems->len() == 1 + && getline('.')->matchstr($'\C{completeItems[0].word}\>') != '' + # only one complete match. No need to show the completion popup + return + endif + + completeItems->complete(start_col) + else + lspserver.completeItems = completeItems + lspserver.omniCompletePending = false + endif +enddef + +# Check if completion item is selected +def CheckCompletionItemSel(label: string): bool + var cInfo = complete_info() + if cInfo->empty() || !cInfo.pum_visible || cInfo.selected == -1 + return false + endif + var selItem = cInfo.items->get(cInfo.selected, {}) + if selItem->empty() + || selItem->type() != v:t_dict + || selItem.user_data->type() != v:t_dict + || selItem.user_data.label != label + return false + endif + return true +enddef + +# Process the completion documentation +def ShowCompletionDocumentation(cItem: any) + if cItem->empty() || cItem->type() != v:t_dict + return + endif + + # check if completion item is still selected + if !CheckCompletionItemSel(cItem.label) + return + endif + + var infoText: list<string> + var infoKind: string + + if cItem->has_key('detail') && !cItem.detail->empty() + # Solve a issue where if a server send the detail field with "\n", + # on the completion popup, everything will be joined with "^@" + # (example: typescript-language-server) + infoText->extend(cItem.detail->split("\n")) + endif + + if cItem->has_key('documentation') + if !infoText->empty() + infoText->extend(['- - -']) + endif + var cItemDoc = cItem.documentation + if cItemDoc->type() == v:t_dict + # MarkupContent + if cItemDoc.kind == 'plaintext' + infoText->extend(cItemDoc.value->split("\n")) + infoKind = 'text' + elseif cItemDoc.kind == 'markdown' + infoText->extend(cItemDoc.value->split("\n")) + infoKind = 'lspgfm' + else + util.ErrMsg($'Unsupported documentation type ({cItemDoc.kind})') + return + endif + elseif cItemDoc->type() == v:t_string + infoText->extend(cItemDoc->split("\n")) + else + util.ErrMsg($'Unsupported documentation ({cItemDoc->string()})') + return + endif + endif + + if infoText->empty() + return + endif + + # check if completion item is changed in meantime + if !CheckCompletionItemSel(cItem.label) + return + endif + + # autoComplete or &omnifunc with &completeopt =~ 'popup' + var id = popup_findinfo() + if id > 0 + var bufnr = id->winbufnr() + id->popup_settext(infoText) + infoKind->setbufvar(bufnr, '&ft') + id->popup_show() + else + # &omnifunc with &completeopt =~ 'preview' + try + :wincmd P + :setlocal modifiable + bufnr()->deletebufline(1, '$') + infoText->append(0) + [1, 1]->cursor() + exe $'setlocal ft={infoKind}' + :wincmd p + catch /E441/ # No preview window + endtry + endif +enddef + +# process the 'completionItem/resolve' reply from the LSP server +# Result: CompletionItem +export def CompletionResolveReply(lspserver: dict<any>, cItem: any) + ShowCompletionDocumentation(cItem) +enddef + +# Return trigger kind and trigger char. If completion trigger is not a keyword +# and not one of the triggerCharacters, return -1 for triggerKind. +def GetTriggerAttributes(lspserver: dict<any>): list<any> + var triggerKind: number = 1 + var triggerChar: string = '' + + # Trigger kind is 1 for keyword and 2 for trigger char initiated completion. + var line: string = getline('.') + var cur_col = charcol('.') + if line[cur_col - 2] !~ '\k' + var trigChars = lspserver.completionTriggerChars + var trigidx = trigChars->index(line[cur_col - 2]) + if trigidx == -1 + triggerKind = -1 + else + triggerKind = 2 + triggerChar = trigChars[trigidx] + endif + endif + return [triggerKind, triggerChar] +enddef + + +# omni complete handler +def g:LspOmniFunc(findstart: number, base: string): any + var lspserver: dict<any> = buf.CurbufGetServerChecked('completion') + if lspserver->empty() + return -2 + endif + + if findstart + + var [triggerKind, triggerChar] = GetTriggerAttributes(lspserver) + if triggerKind < 0 + # previous character is not a keyword character or a trigger character, + # so cancel omni completion. + return -2 + endif + + # first send all the changes in the current buffer to the LSP server + listener_flush() + + lspserver.omniCompletePending = true + lspserver.completeItems = [] + + # initiate a request to LSP server to get list of completions + lspserver.getCompletion(triggerKind, triggerChar) + + # locate the start of the word + var line = getline('.')->strpart(0, col('.') - 1) + var keyword = line->matchstr('\k\+$') + lspserver.omniCompleteKeyword = keyword + return line->len() - keyword->len() + else + # Wait for the list of matches from the LSP server + var count: number = 0 + while lspserver.omniCompletePending && count < 1000 + if complete_check() + return v:none + endif + sleep 2m + count += 1 + endwhile + + if lspserver.omniCompletePending + return v:none + endif + + var res: list<dict<any>> = lspserver.completeItems + var prefix = lspserver.omniCompleteKeyword + + # Don't attempt to filter on the items, when "isIncomplete" is set + if prefix->empty() || lspserver.completeItemsIsIncomplete + return res + endif + + var lspOpts = opt.lspOptions + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_FUZZY + return res->matchfuzzy(prefix, { key: 'word' }) + endif + + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE + return res->filter((i, v) => + v.word->tolower()->stridx(prefix->tolower()) == 0) + endif + + return res->filter((i, v) => v.word->stridx(prefix) == 0) + endif +enddef + +# For plugins that implement async completion this function indicates if +# omnifunc is waiting for LSP response. +def g:LspOmniCompletePending(): bool + var lspserver: dict<any> = buf.CurbufGetServerChecked('completion') + return !lspserver->empty() && lspserver.omniCompletePending +enddef + +# Insert mode completion handler. Used when 24x7 completion is enabled +# (default). +def LspComplete() + var lspserver: dict<any> = buf.CurbufGetServer('completion') + if lspserver->empty() || !lspserver.running || !lspserver.ready + return + endif + + var [triggerKind, triggerChar] = GetTriggerAttributes(lspserver) + if triggerKind < 0 + return + endif + + # first send all the changes in the current buffer to the LSP server + listener_flush() + + # initiate a request to LSP server to get list of completions + lspserver.getCompletion(triggerKind, triggerChar) +enddef + +# Lazy complete documentation handler +def LspResolve() + var lspserver: dict<any> = buf.CurbufGetServerChecked('completion') + if lspserver->empty() + return + endif + + var item = v:event.completed_item + if item->has_key('user_data') && !item.user_data->empty() + if item.user_data->type() == v:t_dict && !item.user_data->has_key('documentation') + lspserver.resolveCompletion(item.user_data) + else + ShowCompletionDocumentation(item.user_data) + endif + endif +enddef + +# If the completion popup documentation window displays "markdown" content, +# then set the 'filetype' to "lspgfm". +def LspSetPopupFileType() + var item = v:event.completed_item + var cItem = item->get('user_data', {}) + if cItem->empty() + return + endif + + if cItem->type() != v:t_dict || !cItem->has_key('documentation') + \ || cItem.documentation->type() != v:t_dict + \ || cItem.documentation.kind != 'markdown' + return + endif + + var id = popup_findinfo() + if id > 0 + var bnum = id->winbufnr() + setbufvar(bnum, '&ft', 'lspgfm') + endif +enddef + +# complete done handler (LSP server-initiated actions after completion) +def LspCompleteDone(bnr: number) + var lspserver: dict<any> = buf.BufLspServerGet(bnr, 'completion') + if lspserver->empty() + return + endif + + if v:completed_item->type() != v:t_dict + return + endif + + var completionData: any = v:completed_item->get('user_data', '') + if completionData->type() != v:t_dict + || !opt.lspOptions.completionTextEdit + return + endif + + if !completionData->has_key('additionalTextEdits') + # Some language servers (e.g. typescript) delay the computation of the + # additional text edits. So try to resolve the completion item now to get + # the text edits. + completionData = lspserver.resolveCompletion(completionData, true) + endif + if !completionData->get('additionalTextEdits', {})->empty() + textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits) + endif + + if completionData->has_key('command') + # Some language servers (e.g. haskell-language-server) want to apply + # additional commands after completion. + codeaction.DoCommand(lspserver, completionData.command) + endif + +enddef + +# Initialize buffer-local completion options and autocmds +export def BufferInit(lspserver: dict<any>, bnr: number, ftype: string) + if !lspserver.isCompletionProvider + # no support for completion + return + endif + + if !opt.lspOptions.autoComplete && !LspOmniComplEnabled(ftype) && !opt.lspOptions.omniComplete + # LSP auto/omni completion support is not enabled for this buffer + return + endif + + # buffer-local autocmds for completion + var acmds: list<dict<any>> = [] + + # set options for insert mode completion + if opt.lspOptions.autoComplete + if lspserver.completionLazyDoc + setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect') + else + setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect') + endif + setbufvar(bnr, '&completepopup', + 'width:80,highlight:Pmenu,align:item,border:off') + # <Enter> in insert mode stops completion and inserts a <Enter> + if !opt.lspOptions.noNewlineInCompletion + :inoremap <expr> <buffer> <CR> pumvisible() ? "\<C-Y>\<CR>" : "\<CR>" + endif + + # Trigger 24x7 insert mode completion when text is changed + acmds->add({bufnr: bnr, + event: 'TextChangedI', + group: 'LSPBufferAutocmds', + cmd: 'LspComplete()'}) + endif + + if LspOmniComplEnabled(ftype) + setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc') + endif + + if lspserver.completionLazyDoc + # resolve additional documentation for a selected item + acmds->add({bufnr: bnr, + event: 'CompleteChanged', + group: 'LSPBufferAutocmds', + cmd: 'LspResolve()'}) + endif + + acmds->add({bufnr: bnr, + event: 'CompleteChanged', + group: 'LSPBufferAutocmds', + cmd: 'LspSetPopupFileType()'}) + + # Execute LSP server initiated text edits after completion + acmds->add({bufnr: bnr, + event: 'CompleteDone', + group: 'LSPBufferAutocmds', + cmd: $'LspCompleteDone({bnr})'}) + + autocmd_add(acmds) +enddef + +# Buffer "bnr" is loaded in a window. If omni-completion is enabled for this +# buffer, then set the 'omnifunc' option. +export def BufferLoadedInWin(bnr: number) + if !opt.lspOptions.autoComplete + && LspOmniComplEnabled(bnr->getbufvar('&filetype')) + setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc') + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/diag.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/diag.vim new file mode 100644 index 0000000..6975edf --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/diag.vim @@ -0,0 +1,920 @@ +vim9script + +# Functions related to handling LSP diagnostics. + +import './options.vim' as opt +import './buffer.vim' as buf +import './util.vim' + +# [bnr] = { +# serverDiagnostics: { +# lspServer1Id: [diag, diag, diag] +# lspServer2Id: [diag, diag, diag] +# }, +# serverDiagnosticsByLnum: { +# lspServer1Id: { [lnum]: [diag, diag diag] }, +# lspServer2Id: { [lnum]: [diag, diag diag] }, +# }, +# sortedDiagnostics: [lspServer1.diags, ...lspServer2.diags]->sort() +# } +var diagsMap: dict<dict<any>> = {} + +# Initialize the signs and the text property type used for diagnostics. +export def InitOnce() + # Signs and their highlight groups used for LSP diagnostics + hlset([ + {name: 'LspDiagLine', default: true, linksto: 'NONE'}, + {name: 'LspDiagSignErrorText', default: true, linksto: 'ErrorMsg'}, + {name: 'LspDiagSignWarningText', default: true, linksto: 'Search'}, + {name: 'LspDiagSignInfoText', default: true, linksto: 'Pmenu'}, + {name: 'LspDiagSignHintText', default: true, linksto: 'Question'} + ]) + sign_define([ + { + name: 'LspDiagError', + text: opt.lspOptions.diagSignErrorText, + texthl: 'LspDiagSignErrorText', + linehl: 'LspDiagLine' + }, + { + name: 'LspDiagWarning', + text: opt.lspOptions.diagSignWarningText, + texthl: 'LspDiagSignWarningText', + linehl: 'LspDiagLine' + }, + { + name: 'LspDiagInfo', + text: opt.lspOptions.diagSignInfoText, + texthl: 'LspDiagSignInfoText', + linehl: 'LspDiagLine' + }, + { + name: 'LspDiagHint', + text: opt.lspOptions.diagSignHintText, + texthl: 'LspDiagSignHintText', + linehl: 'LspDiagLine' + } + ]) + + # Diag inline highlight groups and text property types + hlset([ + {name: 'LspDiagInlineError', default: true, linksto: 'SpellBad'}, + {name: 'LspDiagInlineWarning', default: true, linksto: 'SpellCap'}, + {name: 'LspDiagInlineInfo', default: true, linksto: 'SpellRare'}, + {name: 'LspDiagInlineHint', default: true, linksto: 'SpellLocal'} + ]) + + var override = &cursorline + && &cursorlineopt =~ '\<line\>\|\<screenline\>\|\<both\>' + + prop_type_add('LspDiagInlineError', + {highlight: 'LspDiagInlineError', + priority: 10, + override: override}) + prop_type_add('LspDiagInlineWarning', + {highlight: 'LspDiagInlineWarning', + priority: 9, + override: override}) + prop_type_add('LspDiagInlineInfo', + {highlight: 'LspDiagInlineInfo', + priority: 8, + override: override}) + prop_type_add('LspDiagInlineHint', + {highlight: 'LspDiagInlineHint', + priority: 7, + override: override}) + + # Diag virtual text highlight groups and text property types + hlset([ + {name: 'LspDiagVirtualTextError', default: true, linksto: 'SpellBad'}, + {name: 'LspDiagVirtualTextWarning', default: true, linksto: 'SpellCap'}, + {name: 'LspDiagVirtualTextInfo', default: true, linksto: 'SpellRare'}, + {name: 'LspDiagVirtualTextHint', default: true, linksto: 'SpellLocal'}, + ]) + prop_type_add('LspDiagVirtualTextError', + {highlight: 'LspDiagVirtualTextError', override: true}) + prop_type_add('LspDiagVirtualTextWarning', + {highlight: 'LspDiagVirtualTextWarning', override: true}) + prop_type_add('LspDiagVirtualTextInfo', + {highlight: 'LspDiagVirtualTextInfo', override: true}) + prop_type_add('LspDiagVirtualTextHint', + {highlight: 'LspDiagVirtualTextHint', override: true}) + + autocmd_add([{group: 'LspCmds', + event: 'User', + pattern: 'LspOptionsChanged', + cmd: 'LspDiagsOptionsChanged()'}]) + + # ALE plugin support + if opt.lspOptions.aleSupport + opt.lspOptions.autoHighlightDiags = false + autocmd_add([ + { + group: 'LspAleCmds', + event: 'User', + pattern: 'ALEWantResults', + cmd: 'AleHook(g:ale_want_results_buffer)' + } + ]) + endif +enddef + +# Initialize the diagnostics features for the buffer 'bnr' +export def BufferInit(lspserver: dict<any>, bnr: number) + if opt.lspOptions.showDiagInBalloon + :set ballooneval balloonevalterm + setbufvar(bnr, '&balloonexpr', 'g:LspDiagExpr()') + endif + + var acmds: list<dict<any>> = [] + # Show diagnostics on the status line + if opt.lspOptions.showDiagOnStatusLine + acmds->add({bufnr: bnr, + event: 'CursorMoved', + group: 'LSPBufferAutocmds', + cmd: 'ShowCurrentDiagInStatusLine()'}) + endif + autocmd_add(acmds) +enddef + +# Function to sort the diagnostics in ascending order based on the line and +# character offset +def DiagsSortFunc(a: dict<any>, b: dict<any>): number + var a_start: dict<number> = a.range.start + var b_start: dict<number> = b.range.start + var linediff: number = a_start.line - b_start.line + if linediff == 0 + return a_start.character - b_start.character + endif + return linediff +enddef + +# Sort diagnostics ascending based on line and character offset +def SortDiags(diags: list<dict<any>>): list<dict<any>> + return diags->sort(DiagsSortFunc) +enddef + +# Remove the diagnostics stored for buffer "bnr" +export def DiagRemoveFile(bnr: number) + if diagsMap->has_key(bnr) + diagsMap->remove(bnr) + endif +enddef + +def DiagSevToSignName(severity: number): string + var typeMap: list<string> = ['LspDiagError', 'LspDiagWarning', + 'LspDiagInfo', 'LspDiagHint'] + if severity > 4 + return 'LspDiagHint' + endif + return typeMap[severity - 1] +enddef + +def DiagSevToInlineHLName(severity: number): string + var typeMap: list<string> = [ + 'LspDiagInlineError', + 'LspDiagInlineWarning', + 'LspDiagInlineInfo', + 'LspDiagInlineHint' + ] + if severity > 4 + return 'LspDiagInlineHint' + endif + return typeMap[severity - 1] +enddef + +def DiagSevToVirtualTextHLName(severity: number): string + var typeMap: list<string> = [ + 'LspDiagVirtualTextError', + 'LspDiagVirtualTextWarning', + 'LspDiagVirtualTextInfo', + 'LspDiagVirtualTextHint' + ] + if severity > 4 + return 'LspDiagVirtualTextHint' + endif + return typeMap[severity - 1] +enddef + +def DiagSevToSymbolText(severity: number): string + var lspOpts = opt.lspOptions + var typeMap: list<string> = [ + lspOpts.diagSignErrorText, + lspOpts.diagSignWarningText, + lspOpts.diagSignInfoText, + lspOpts.diagSignHintText + ] + if severity > 4 + return lspOpts.diagSignHintText + endif + return typeMap[severity - 1] +enddef + +# Remove signs and text properties for diagnostics in buffer +def RemoveDiagVisualsForBuffer(bnr: number, all: bool = false) + var lspOpts = opt.lspOptions + if lspOpts.showDiagWithSign || all + # Remove all the existing diagnostic signs + sign_unplace('LSPDiag', {buffer: bnr}) + endif + + if lspOpts.showDiagWithVirtualText || all + # Remove all the existing virtual text + prop_remove({type: 'LspDiagVirtualTextError', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagVirtualTextWarning', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagVirtualTextInfo', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagVirtualTextHint', bufnr: bnr, all: true}) + endif + + if lspOpts.highlightDiagInline || all + # Remove all the existing virtual text + prop_remove({type: 'LspDiagInlineError', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagInlineWarning', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagInlineInfo', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagInlineHint', bufnr: bnr, all: true}) + endif +enddef + +# Refresh the placed diagnostics in buffer "bnr" +# This inline signs, inline props, and virtual text diagnostics +export def DiagsRefresh(bnr: number, all: bool = false) + var lspOpts = opt.lspOptions + if !lspOpts.autoHighlightDiags + return + endif + + :silent! bnr->bufload() + + RemoveDiagVisualsForBuffer(bnr, all) + + if !diagsMap->has_key(bnr) || + diagsMap[bnr].sortedDiagnostics->empty() + return + endif + + # Initialize default/fallback properties for diagnostic virtual text: + var diag_align: string = 'above' + var diag_wrap: string = 'truncate' + var diag_symbol: string = '┌─' + + if lspOpts.diagVirtualTextAlign == 'below' + diag_align = 'below' + diag_wrap = 'truncate' + diag_symbol = '└─' + elseif lspOpts.diagVirtualTextAlign == 'after' + diag_align = 'after' + diag_wrap = 'wrap' + diag_symbol = 'E>' + endif + + if lspOpts.diagVirtualTextWrap != 'default' + diag_wrap = lspOpts.diagVirtualTextWrap + endif + + var signs: list<dict<any>> = [] + var diags: list<dict<any>> = diagsMap[bnr].sortedDiagnostics + var inlineHLprops: list<list<list<number>>> = [[], [], [], [], []] + for diag in diags + # TODO: prioritize most important severity if there are multiple + # diagnostics from the same line + var d_range = diag.range + var d_start = d_range.start + var d_end = d_range.end + var lnum = d_start.line + 1 + if lspOpts.showDiagWithSign + signs->add({id: 0, buffer: bnr, group: 'LSPDiag', + lnum: lnum, name: DiagSevToSignName(diag.severity), + priority: 10 - diag.severity}) + endif + + try + if lspOpts.highlightDiagInline + var propLocation: list<number> = [ + lnum, util.GetLineByteFromPos(bnr, d_start) + 1, + d_end.line + 1, util.GetLineByteFromPos(bnr, d_end) + 1 + ] + inlineHLprops[diag.severity]->add(propLocation) + endif + + if lspOpts.showDiagWithVirtualText + var padding: number + var symbol: string = diag_symbol + + if diag_align == 'after' + padding = 3 + symbol = DiagSevToSymbolText(diag.severity) + else + var charIdx = util.GetCharIdxWithoutCompChar(bnr, d_start) + padding = charIdx + if padding > 0 + padding = strdisplaywidth(getline(lnum)[ : charIdx - 1]) + endif + endif + + prop_add(lnum, 0, {bufnr: bnr, + type: DiagSevToVirtualTextHLName(diag.severity), + text: $'{symbol} {diag.message}', + text_align: diag_align, + text_wrap: diag_wrap, + text_padding_left: padding}) + endif + catch /E966\|E964/ # Invalid lnum | Invalid col + # Diagnostics arrive asynchronously and the document changed while they + # were in transit. Ignore this as new once will arrive shortly. + endtry + endfor + + if lspOpts.highlightDiagInline + for i in range(1, 4) + if !inlineHLprops[i]->empty() + try + prop_add_list({bufnr: bnr, type: DiagSevToInlineHLName(i)}, + inlineHLprops[i]) + catch /E966\|E964/ # Invalid lnum | Invalid col + endtry + endif + endfor + endif + + if lspOpts.showDiagWithSign + signs->sign_placelist() + endif +enddef + +# Sends diagnostics to Ale +def SendAleDiags(bnr: number, timerid: number) + if !diagsMap->has_key(bnr) + return + endif + + # Convert to Ale's diagnostics format (:h ale-loclist-format) + ale#other_source#ShowResults(bnr, 'lsp', + diagsMap[bnr].sortedDiagnostics->mapnew((_, v) => { + return {text: v.message, + lnum: v.range.start.line + 1, + col: util.GetLineByteFromPos(bnr, v.range.start) + 1, + end_lnum: v.range.end.line + 1, + end_col: util.GetLineByteFromPos(bnr, v.range.end) + 1, + type: "EWIH"[v.severity - 1]} + }) + ) +enddef + +# Hook called when Ale wants to retrieve new diagnostics +def AleHook(bnr: number) + ale#other_source#StartChecking(bnr, 'lsp') + timer_start(0, function('SendAleDiags', [bnr])) +enddef + +# New LSP diagnostic messages received from the server for a file. +# Update the signs placed in the buffer for this file +export def ProcessNewDiags(bnr: number) + DiagsUpdateLocList(bnr) + + var lspOpts = opt.lspOptions + if lspOpts.aleSupport + SendAleDiags(bnr, -1) + endif + + if bnr == -1 || !diagsMap->has_key(bnr) + return + endif + + var curmode: string = mode() + if curmode == 'i' || curmode == 'R' || curmode == 'Rv' + # postpone placing signs in insert mode and replace mode. These will be + # placed after the user returns to Normal mode. + setbufvar(bnr, 'LspDiagsUpdatePending', true) + return + endif + + DiagsRefresh(bnr) +enddef + +# process a diagnostic notification message from the LSP server +# Notification: textDocument/publishDiagnostics +# Param: PublishDiagnosticsParams +export def DiagNotification(lspserver: dict<any>, uri: string, diags_arg: list<dict<any>>): void + # Diagnostics are disabled for this server? + if !lspserver.featureEnabled('diagnostics') + return + endif + + var fname: string = util.LspUriToFile(uri) + var bnr: number = fname->bufnr() + if bnr == -1 + # Is this condition possible? + return + endif + + var newDiags: list<dict<any>> = diags_arg + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the diags + newDiags->map((_, dval) => { + lspserver.decodeRange(bnr, dval.range) + return dval + }) + endif + + if lspserver.processDiagHandler != null_function + newDiags = lspserver.processDiagHandler(diags_arg) + endif + + # TODO: Is the buffer (bnr) always a loaded buffer? Should we load it here? + var lastlnum: number = bnr->getbufinfo()[0].linecount + + # store the diagnostic for each line separately + var diagsByLnum: dict<list<dict<any>>> = {} + + var diagWithinRange: list<dict<any>> = [] + for diag in newDiags + var d_start = diag.range.start + if d_start.line + 1 > lastlnum + # Make sure the line number is a valid buffer line number + d_start.line = lastlnum - 1 + endif + + var lnum = d_start.line + 1 + if !diagsByLnum->has_key(lnum) + diagsByLnum[lnum] = [] + endif + diagsByLnum[lnum]->add(diag) + + diagWithinRange->add(diag) + endfor + + var serverDiags: dict<list<any>> = diagsMap->has_key(bnr) ? + diagsMap[bnr].serverDiagnostics : {} + serverDiags[lspserver.id] = diagWithinRange + + var serverDiagsByLnum: dict<dict<list<any>>> = diagsMap->has_key(bnr) ? + diagsMap[bnr].serverDiagnosticsByLnum : {} + serverDiagsByLnum[lspserver.id] = diagsByLnum + + # store the diagnostic for each line separately + var joinedServerDiags: list<dict<any>> = [] + for diags in serverDiags->values() + joinedServerDiags->extend(diags) + endfor + + var sortedDiags = SortDiags(joinedServerDiags) + + diagsMap[bnr] = { + sortedDiagnostics: sortedDiags, + serverDiagnosticsByLnum: serverDiagsByLnum, + serverDiagnostics: serverDiags + } + + ProcessNewDiags(bnr) + + # Notify user scripts that diags has been updated + if exists('#User#LspDiagsUpdated') + :doautocmd <nomodeline> User LspDiagsUpdated + endif +enddef + +# get the count of error in the current buffer +export def DiagsGetErrorCount(bnr: number): dict<number> + var diagSevCount: list<number> = [0, 0, 0, 0, 0] + if diagsMap->has_key(bnr) + var diags = diagsMap[bnr].sortedDiagnostics + for diag in diags + var severity = diag->get('severity', 0) + diagSevCount[severity] += 1 + endfor + endif + + return { + Error: diagSevCount[1], + Warn: diagSevCount[2], + Info: diagSevCount[3], + Hint: diagSevCount[4] + } +enddef + +# Map the LSP DiagnosticSeverity to a quickfix type character +def DiagSevToQfType(severity: number): string + var typeMap: list<string> = ['E', 'W', 'I', 'N'] + + if severity > 4 + return '' + endif + + return typeMap[severity - 1] +enddef + +# Update the location list window for the current window with the diagnostic +# messages. +# Returns true if diagnostics is not empty and false if it is empty. +def DiagsUpdateLocList(bnr: number, calledByCmd: bool = false): bool + var fname: string = bnr->bufname()->fnamemodify(':p') + if fname->empty() + return false + endif + + var LspQfId: number = bnr->getbufvar('LspQfId', 0) + if LspQfId == 0 && !opt.lspOptions.autoPopulateDiags && !calledByCmd + # Diags location list is not present. Create the location list only if + # the 'autoPopulateDiags' option is set or the ":LspDiag show" command is + # invoked. + return false + endif + + if LspQfId != 0 && getloclist(0, {id: LspQfId}).id != LspQfId + # Previously used location list for the diagnostics is gone + LspQfId = 0 + endif + + if !diagsMap->has_key(bnr) || + diagsMap[bnr].sortedDiagnostics->empty() + if LspQfId != 0 + setloclist(0, [], 'r', {id: LspQfId, items: []}) + endif + return false + endif + + var qflist: list<dict<any>> = [] + var text: string + + var diags = diagsMap[bnr].sortedDiagnostics + for diag in diags + var d_range = diag.range + var d_start = d_range.start + var d_end = d_range.end + text = diag.message->substitute("\n\\+", "\n", 'g') + qflist->add({filename: fname, + lnum: d_start.line + 1, + col: util.GetLineByteFromPos(bnr, d_start) + 1, + end_lnum: d_end.line + 1, + end_col: util.GetLineByteFromPos(bnr, d_end) + 1, + text: text, + type: DiagSevToQfType(diag.severity)}) + endfor + + var op: string = ' ' + var props = {title: 'Language Server Diagnostics', items: qflist} + if LspQfId != 0 + op = 'r' + props.id = LspQfId + endif + setloclist(0, [], op, props) + if LspQfId == 0 + setbufvar(bnr, 'LspQfId', getloclist(0, {id: 0}).id) + endif + + return true +enddef + +# Display the diagnostic messages from the LSP server for the current buffer +# in a location list +export def ShowAllDiags(): void + var bnr: number = bufnr() + if !DiagsUpdateLocList(bnr, true) + util.WarnMsg($'No diagnostic messages found for {@%}') + return + endif + + var save_winid = win_getid() + # make the diagnostics error list the active one and open it + var LspQfId: number = bnr->getbufvar('LspQfId', 0) + var LspQfNr: number = getloclist(0, {id: LspQfId, nr: 0}).nr + exe $':{LspQfNr} lhistory' + :lopen + if !opt.lspOptions.keepFocusInDiags + save_winid->win_gotoid() + endif +enddef + +# Display the message of "diag" in a popup window right below the position in +# the diagnostic message. +def ShowDiagInPopup(diag: dict<any>) + var d_start = diag.range.start + var dlnum = d_start.line + 1 + var ltext = dlnum->getline() + var dlcol = ltext->byteidxcomp(d_start.character) + 1 + + var lastline = line('$') + if dlnum > lastline + # The line number is outside the last line in the file. + dlnum = lastline + endif + if dlcol < 1 + # The column is outside the last character in line. + dlcol = ltext->len() + 1 + endif + var d = screenpos(0, dlnum, dlcol) + if d->empty() + # If the diag position cannot be converted to Vim lnum/col, then use + # the current cursor position + d = {row: line('.'), col: col('.')} + endif + + # Display a popup right below the diagnostics position + var msg = diag.message->split("\n") + var msglen = msg->reduce((acc, val) => max([acc, val->strcharlen()]), 0) + + var ppopts = {} + ppopts.pos = 'topleft' + ppopts.line = d.row + 1 + ppopts.moved = 'any' + + if msglen > &columns + ppopts.wrap = true + ppopts.col = 1 + else + ppopts.wrap = false + ppopts.col = d.col + endif + + popup_create(msg, ppopts) +enddef + +# Display the "diag" message in a popup or in the status message area +def DisplayDiag(diag: dict<any>) + if opt.lspOptions.showDiagInPopup + # Display the diagnostic message in a popup window. + ShowDiagInPopup(diag) + else + # Display the diagnostic message in the status message area + :echo diag.message + endif +enddef + +# Show the diagnostic message for the current line +export def ShowCurrentDiag(atPos: bool) + var bnr: number = bufnr() + var lnum: number = line('.') + var col: number = charcol('.') + var diag: dict<any> = GetDiagByPos(bnr, lnum, col, atPos) + if diag->empty() + util.WarnMsg($'No diagnostic messages found for current {atPos ? "position" : "line"}') + else + DisplayDiag(diag) + endif +enddef + +# Show the diagnostic message for the current line without linebreak +def ShowCurrentDiagInStatusLine() + var bnr: number = bufnr() + var lnum: number = line('.') + var col: number = charcol('.') + var diag: dict<any> = GetDiagByPos(bnr, lnum, col) + if !diag->empty() + # 15 is a enough length not to cause line break + var max_width = &columns - 15 + var code = '' + if diag->has_key('code') + code = $'[{diag.code}] ' + endif + var msgNoLineBreak = code .. + diag.message->substitute("\n", ' ', '')->substitute("\\n", ' ', '') + :echo msgNoLineBreak[ : max_width] + else + # clear the previous message + :echo '' + endif +enddef + +# Get the diagnostic from the LSP server for a particular line and character +# offset in a file +export def GetDiagByPos(bnr: number, lnum: number, col: number, + atPos: bool = false): dict<any> + var diags_in_line = GetDiagsByLine(bnr, lnum) + + for diag in diags_in_line + var r = diag.range + var startCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.start) + var endCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.end) + if atPos + if col >= startCharIdx + 1 && col < endCharIdx + 1 + return diag + endif + elseif col <= startCharIdx + 1 + return diag + endif + endfor + + # No diagnostic to the right of the position, return the last one instead + if !atPos && diags_in_line->len() > 0 + return diags_in_line[-1] + endif + + return {} +enddef + +# Get all diagnostics from the LSP server for a particular line in a file +export def GetDiagsByLine(bnr: number, lnum: number, lspserver: dict<any> = null_dict): list<dict<any>> + if !diagsMap->has_key(bnr) + return [] + endif + + var diags: list<dict<any>> = [] + + var serverDiagsByLnum = diagsMap[bnr].serverDiagnosticsByLnum + + if lspserver == null_dict + for diagsByLnum in serverDiagsByLnum->values() + if diagsByLnum->has_key(lnum) + diags->extend(diagsByLnum[lnum]) + endif + endfor + else + if !serverDiagsByLnum->has_key(lspserver.id) + return [] + endif + if serverDiagsByLnum[lspserver.id]->has_key(lnum) + diags = serverDiagsByLnum[lspserver.id][lnum] + endif + endif + + return diags->sort((a, b) => { + return a.range.start.character - b.range.start.character + }) +enddef + +# Utility function to do the actual jump +def JumpDiag(diag: dict<any>) + var startPos: dict<number> = diag.range.start + setcursorcharpos(startPos.line + 1, + util.GetCharIdxWithoutCompChar(bufnr(), startPos) + 1) + :normal! zv + if !opt.lspOptions.showDiagWithVirtualText + :redraw + DisplayDiag(diag) + endif +enddef + +# jump to the next/previous/first diagnostic message in the current buffer +export def LspDiagsJump(which: string, a_count: number = 0): void + var fname: string = expand('%:p') + if fname->empty() + return + endif + var bnr: number = bufnr() + + if !diagsMap->has_key(bnr) || + diagsMap[bnr].sortedDiagnostics->empty() + util.WarnMsg($'No diagnostic messages found for {fname}') + return + endif + + var diags = diagsMap[bnr].sortedDiagnostics + + if which == 'first' + JumpDiag(diags[0]) + return + endif + + if which == 'last' + JumpDiag(diags[-1]) + return + endif + + # Find the entry just before the current line (binary search) + var count = a_count > 1 ? a_count : 1 + var curlnum: number = line('.') + var curcol: number = charcol('.') + for diag in (which == 'next' || which == 'nextWrap' || which == 'here') ? + diags : diags->copy()->reverse() + var d_start = diag.range.start + var lnum = d_start.line + 1 + var col = util.GetCharIdxWithoutCompChar(bnr, d_start) + 1 + if ((which == 'next' || which == 'nextWrap') && (lnum > curlnum || lnum == curlnum && col > curcol)) + || ((which == 'prev' || which == 'prevWrap') && (lnum < curlnum || lnum == curlnum + && col < curcol)) + || (which == 'here' && (lnum == curlnum && col >= curcol)) + + # Skip over as many diags as "count" dictates + count = count - 1 + if count > 0 + continue + endif + + JumpDiag(diag) + return + endif + endfor + + # If [count] exceeded the remaining diags + if ((which == 'next' || which == 'nextWrap') && a_count > 1 && a_count != count) + JumpDiag(diags[-1]) + return + endif + + # If [count] exceeded the previous diags + if ((which == 'prev' || which == 'prevWrap') && a_count > 1 && a_count != count) + JumpDiag(diags[0]) + return + endif + + if which == 'nextWrap' || which == 'prevWrap' + JumpDiag(diags[which == 'nextWrap' ? 0 : -1]) + return + endif + + if which == 'here' + util.WarnMsg('No more diagnostics found on this line') + else + util.WarnMsg('No more diagnostics found') + endif +enddef + +# Return the sorted diagnostics for buffer "bnr". Default is the current +# buffer. A copy of the diagnostics is returned so that the caller can modify +# the diagnostics. +export def GetDiagsForBuf(bnr: number = bufnr()): list<dict<any>> + if !diagsMap->has_key(bnr) || + diagsMap[bnr].sortedDiagnostics->empty() + return [] + endif + + return diagsMap[bnr].sortedDiagnostics->deepcopy() +enddef + +# Return the diagnostic text from the LSP server for the current mouse line to +# display in a balloon +def g:LspDiagExpr(): any + if !opt.lspOptions.showDiagInBalloon + return '' + endif + + var diagsInfo: list<dict<any>> = + GetDiagsByLine(v:beval_bufnr, v:beval_lnum) + if diagsInfo->empty() + # No diagnostic for the current cursor location + return '' + endif + var diagFound: dict<any> = {} + for diag in diagsInfo + var r = diag.range + var startcol = util.GetLineByteFromPos(v:beval_bufnr, r.start) + 1 + var endcol = util.GetLineByteFromPos(v:beval_bufnr, r.end) + 1 + if v:beval_col >= startcol && v:beval_col < endcol + diagFound = diag + break + endif + endfor + if diagFound->empty() + # mouse is outside of the diagnostics range + return '' + endif + + # return the found diagnostic + return diagFound.message->split("\n") +enddef + +# Track the current diagnostics auto highlight enabled/disabled state. Used +# when the "autoHighlightDiags" option value is changed. +var save_autoHighlightDiags = opt.lspOptions.autoHighlightDiags +var save_highlightDiagInline = opt.lspOptions.highlightDiagInline +var save_showDiagWithSign = opt.lspOptions.showDiagWithSign +var save_showDiagWithVirtualText = opt.lspOptions.showDiagWithVirtualText + +# Enable the LSP diagnostics highlighting +export def DiagsHighlightEnable() + opt.lspOptions.autoHighlightDiags = true + save_autoHighlightDiags = true + for binfo in getbufinfo({bufloaded: true}) + if diagsMap->has_key(binfo.bufnr) + DiagsRefresh(binfo.bufnr) + endif + endfor +enddef + +# Disable the LSP diagnostics highlighting in all the buffers +export def DiagsHighlightDisable() + # turn off all diags highlight + opt.lspOptions.autoHighlightDiags = false + save_autoHighlightDiags = false + for binfo in getbufinfo() + if diagsMap->has_key(binfo.bufnr) + RemoveDiagVisualsForBuffer(binfo.bufnr) + endif + endfor +enddef + +# Some options are changed. If 'autoHighlightDiags' option is changed, then +# either enable or disable diags auto highlight. +export def LspDiagsOptionsChanged() + if save_autoHighlightDiags && !opt.lspOptions.autoHighlightDiags + DiagsHighlightDisable() + elseif !save_autoHighlightDiags && opt.lspOptions.autoHighlightDiags + DiagsHighlightEnable() + endif + + if save_highlightDiagInline != opt.lspOptions.highlightDiagInline + || save_showDiagWithSign != opt.lspOptions.showDiagWithSign + || save_showDiagWithVirtualText != opt.lspOptions.showDiagWithVirtualText + save_highlightDiagInline = opt.lspOptions.highlightDiagInline + save_showDiagWithSign = opt.lspOptions.showDiagWithSign + save_showDiagWithVirtualText = opt.lspOptions.showDiagWithVirtualText + for binfo in getbufinfo({bufloaded: true}) + if diagsMap->has_key(binfo.bufnr) + DiagsRefresh(binfo.bufnr, true) + endif + endfor + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/handlers.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/handlers.vim new file mode 100644 index 0000000..dd597ed --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/handlers.vim @@ -0,0 +1,305 @@ +vim9script + +# Handlers for messages from the LSP server +# Refer to https://microsoft.github.io/language-server-protocol/specification +# for the Language Server Protocol (LSP) specification. + +import './util.vim' +import './diag.vim' +import './textedit.vim' + +# Process various reply messages from the LSP server +export def ProcessReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void + util.ErrMsg($'Unsupported reply received from LSP server: {reply->string()} for request: {req->string()}') +enddef + +# process a diagnostic notification message from the LSP server +# Notification: textDocument/publishDiagnostics +# Param: PublishDiagnosticsParams +def ProcessDiagNotif(lspserver: dict<any>, reply: dict<any>): void + var params = reply.params + diag.DiagNotification(lspserver, params.uri, params.diagnostics) +enddef + +# Convert LSP message type to a string +def LspMsgTypeToString(lspMsgType: number): string + var msgStrMap: list<string> = ['', 'Error', 'Warning', 'Info', 'Log'] + var mtype: string = 'Log' + if lspMsgType > 0 && lspMsgType < 5 + mtype = msgStrMap[lspMsgType] + endif + return mtype +enddef + +# process a show notification message from the LSP server +# Notification: window/showMessage +# Param: ShowMessageParams +def ProcessShowMsgNotif(lspserver: dict<any>, reply: dict<any>) + var msgType = reply.params.type + if msgType >= 4 + # ignore log messages from the LSP server (too chatty) + # TODO: Add a configuration to control the message level that will be + # displayed. Also store these messages and provide a command to display + # them. + return + endif + if msgType == 1 + util.ErrMsg($'Lsp({lspserver.name}) {reply.params.message}') + elseif msgType == 2 + util.WarnMsg($'Lsp({lspserver.name}) {reply.params.message}') + elseif msgType == 3 + util.InfoMsg($'Lsp({lspserver.name}) {reply.params.message}') + endif +enddef + +# process a log notification message from the LSP server +# Notification: window/logMessage +# Param: LogMessageParams +def ProcessLogMsgNotif(lspserver: dict<any>, reply: dict<any>) + var params = reply.params + var mtype = LspMsgTypeToString(params.type) + lspserver.addMessage(mtype, params.message) +enddef + +# process the log trace notification messages +# Notification: $/logTrace +# Param: LogTraceParams +def ProcessLogTraceNotif(lspserver: dict<any>, reply: dict<any>) + lspserver.addMessage('trace', reply.params.message) +enddef + +# process unsupported notification messages +def ProcessUnsupportedNotif(lspserver: dict<any>, reply: dict<any>) + util.WarnMsg($'Unsupported notification message received from the LSP server ({lspserver.name}), message = {reply->string()}') +enddef + +# Dict to process telemetry notification messages only once per filetype +var telemetryProcessed: dict<bool> = {} +# process unsupported notification messages only once +def ProcessUnsupportedNotifOnce(lspserver: dict<any>, reply: dict<any>) + if !telemetryProcessed->get(&ft, false) + ProcessUnsupportedNotif(lspserver, reply) + telemetryProcessed->extend({[&ft]: true}) + endif +enddef + +# process notification messages from the LSP server +export def ProcessNotif(lspserver: dict<any>, reply: dict<any>): void + var lsp_notif_handlers: dict<func> = + { + 'window/showMessage': ProcessShowMsgNotif, + 'window/logMessage': ProcessLogMsgNotif, + 'textDocument/publishDiagnostics': ProcessDiagNotif, + '$/logTrace': ProcessLogTraceNotif, + 'telemetry/event': ProcessUnsupportedNotifOnce, + } + + # Explicitly ignored notification messages (many of them are specific to a + # particular language server) + var lsp_ignored_notif_handlers: list<string> = + [ + '$/progress', + '$/status/report', + '$/status/show', + # PHP intelephense server sends the "indexingStarted" and + # "indexingEnded" notifications which is not in the LSP specification. + 'indexingStarted', + 'indexingEnded', + # Java language server sends the 'language/status' notification which is + # not in the LSP specification. + 'language/status', + # Typescript language server sends the '$/typescriptVersion' + # notification which is not in the LSP specification. + '$/typescriptVersion', + # Dart language server sends the '$/analyzerStatus' notification which + # is not in the LSP specification. + '$/analyzerStatus', + # pyright language server notifications + 'pyright/beginProgress', + 'pyright/reportProgress', + 'pyright/endProgress', + 'eslint/status', + 'taplo/didChangeSchemaAssociation', + 'sqlLanguageServer.finishSetup', + # ccls language server notifications + '$ccls/publishSkippedRanges', + '$ccls/publishSemanticHighlight', + # omnisharp language server notifications + 'o#/backgrounddiagnosticstatus', + 'o#/msbuildprojectdiagnostics', + 'o#/projectadded', + 'o#/projectchanged', + 'o#/projectconfiguration', + 'o#/projectdiagnosticstatus', + 'o#/unresolveddependencies', + '@/tailwindCSS/projectInitialized' + ] + + if lsp_notif_handlers->has_key(reply.method) + lsp_notif_handlers[reply.method](lspserver, reply) + elseif lspserver.customNotificationHandlers->has_key(reply.method) + lspserver.customNotificationHandlers[reply.method](lspserver, reply) + elseif lsp_ignored_notif_handlers->index(reply.method) == -1 + ProcessUnsupportedNotif(lspserver, reply) + endif +enddef + +# process the workspace/applyEdit LSP server request +# Request: "workspace/applyEdit" +# Param: ApplyWorkspaceEditParams +def ProcessApplyEditReq(lspserver: dict<any>, request: dict<any>) + # interface ApplyWorkspaceEditParams + if !request->has_key('params') + return + endif + var workspaceEditParams: dict<any> = request.params + if workspaceEditParams->has_key('label') + util.InfoMsg($'Workspace edit {workspaceEditParams.label}') + endif + textedit.ApplyWorkspaceEdit(workspaceEditParams.edit) + # TODO: Need to return the proper result of the edit operation + lspserver.sendResponse(request, {applied: true}, {}) +enddef + +# process the workspace/workspaceFolders LSP server request +# Request: "workspace/workspaceFolders" +# Param: none +def ProcessWorkspaceFoldersReq(lspserver: dict<any>, request: dict<any>) + if !lspserver->has_key('workspaceFolders') + lspserver.sendResponse(request, null, {}) + return + endif + if lspserver.workspaceFolders->empty() + lspserver.sendResponse(request, [], {}) + else + lspserver.sendResponse(request, + \ lspserver.workspaceFolders->copy()->map('{name: v:val->fnamemodify(":t"), uri: util.LspFileToUri(v:val)}'), + \ {}) + endif +enddef + +# process the workspace/configuration LSP server request +# Request: "workspace/configuration" +# Param: ConfigurationParams +def ProcessWorkspaceConfiguration(lspserver: dict<any>, request: dict<any>) + var items = request.params.items + var response = items->map((_, item) => lspserver.workspaceConfigGet(item)) + + # Server expect null value if no config is given + if response->type() == v:t_list && response->len() == 1 + && response[0]->type() == v:t_dict + && response[0] == null_dict + response[0] = null + endif + + lspserver.sendResponse(request, response, {}) +enddef + +# process the window/workDoneProgress/create LSP server request +# Request: "window/workDoneProgress/create" +# Param: none +def ProcessWorkDoneProgressCreate(lspserver: dict<any>, request: dict<any>) + lspserver.sendResponse(request, null, {}) +enddef + +# process the window/showMessageRequest LSP server request +# Request: "window/showMessageRequest" +# Param: ShowMessageRequestParams +def ProcessShowMessageRequest(lspserver: dict<any>, request: dict<any>) + # TODO: for now 'showMessageRequest' handled same like 'showMessage' + # regardless 'actions' + ProcessShowMsgNotif(lspserver, request) + lspserver.sendResponse(request, null, {}) +enddef + +# process the client/registerCapability LSP server request +# Request: "client/registerCapability" +# Param: RegistrationParams +def ProcessClientRegisterCap(lspserver: dict<any>, request: dict<any>) + lspserver.sendResponse(request, null, {}) +enddef + +# process the client/unregisterCapability LSP server request +# Request: "client/unregisterCapability" +# Param: UnregistrationParams +def ProcessClientUnregisterCap(lspserver: dict<any>, request: dict<any>) + lspserver.sendResponse(request, null, {}) +enddef + +# process a request message from the server +export def ProcessRequest(lspserver: dict<any>, request: dict<any>) + var lspRequestHandlers: dict<func> = + { + 'client/registerCapability': ProcessClientRegisterCap, + 'client/unregisterCapability': ProcessClientUnregisterCap, + 'window/workDoneProgress/create': ProcessWorkDoneProgressCreate, + 'window/showMessageRequest': ProcessShowMessageRequest, + 'workspace/applyEdit': ProcessApplyEditReq, + 'workspace/configuration': ProcessWorkspaceConfiguration, + 'workspace/workspaceFolders': ProcessWorkspaceFoldersReq + # TODO: Handle the following requests from the server: + # workspace/codeLens/refresh + # workspace/diagnostic/refresh + # workspace/inlayHint/refresh + # workspace/inlineValue/refresh + # workspace/semanticTokens/refresh + } + + # Explicitly ignored requests + var lspIgnoredRequestHandlers: list<string> = + [ + # Eclipse java language server sends the + # 'workspace/executeClientCommand' request (to reload bundles) which is + # not in the LSP specification. + 'workspace/executeClientCommand', + ] + + if lspRequestHandlers->has_key(request.method) + lspRequestHandlers[request.method](lspserver, request) + elseif lspserver.customRequestHandlers->has_key(request.method) + lspserver.customRequestHandlers[request.method](lspserver, request) + elseif lspIgnoredRequestHandlers->index(request.method) == -1 + util.ErrMsg($'Unsupported request message received from the LSP server ({lspserver.name}), message = {request->string()}') + endif +enddef + +# process one or more LSP server messages +export def ProcessMessages(lspserver: dict<any>): void + var idx: number + var len: number + var content: string + var msg: dict<any> + var req: dict<any> + + msg = lspserver.data + if msg->has_key('result') || msg->has_key('error') + # response message from the server + req = lspserver.requests->get(msg.id->string(), {}) + if !req->empty() + # Remove the corresponding stored request message + lspserver.requests->remove(msg.id->string()) + + if msg->has_key('result') + lspserver.processReply(req, msg) + else + # request failed + var emsg: string = msg.error.message + emsg ..= $', code = {msg.error.code}' + if msg.error->has_key('data') + emsg ..= $', data = {msg.error.data->string()}' + endif + util.ErrMsg($'request {req.method} failed ({emsg})') + endif + endif + elseif msg->has_key('id') && msg->has_key('method') + # request message from the server + lspserver.processRequest(msg) + elseif msg->has_key('method') + # notification message from the server + lspserver.processNotif(msg) + else + util.ErrMsg($'Unsupported message ({msg->string()})') + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/hover.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/hover.vim new file mode 100644 index 0000000..41e7f11 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/hover.vim @@ -0,0 +1,137 @@ +vim9script + +# Functions related to displaying hover symbol information. + +import './util.vim' +import './options.vim' as opt + +# Util used to compute the hoverText from textDocument/hover reply +def GetHoverText(lspserver: dict<any>, hoverResult: any): list<any> + if hoverResult->empty() + return ['', ''] + endif + + # MarkupContent + if hoverResult.contents->type() == v:t_dict + && hoverResult.contents->has_key('kind') + if hoverResult.contents.kind == 'plaintext' + return [hoverResult.contents.value->split("\n"), 'text'] + endif + + if hoverResult.contents.kind == 'markdown' + return [hoverResult.contents.value->split("\n"), 'lspgfm'] + endif + + lspserver.errorLog( + $'{strftime("%m/%d/%y %T")}: Unsupported hover contents kind ({hoverResult.contents.kind})' + ) + return ['', ''] + endif + + # MarkedString + if hoverResult.contents->type() == v:t_dict + && hoverResult.contents->has_key('value') + return [ + [$'``` {hoverResult.contents.language}'] + + hoverResult.contents.value->split("\n") + + ['```'], + 'lspgfm' + ] + endif + + # MarkedString + if hoverResult.contents->type() == v:t_string + return [hoverResult.contents->split("\n"), 'lspgfm'] + endif + + # interface MarkedString[] + if hoverResult.contents->type() == v:t_list + var hoverText: list<string> = [] + for e in hoverResult.contents + if !hoverText->empty() + hoverText->extend(['- - -']) + endif + + if e->type() == v:t_string + hoverText->extend(e->split("\n")) + else + hoverText->extend([$'``` {e.language}']) + hoverText->extend(e.value->split("\n")) + hoverText->extend(['```']) + endif + endfor + + return [hoverText, 'lspgfm'] + endif + + lspserver.errorLog( + $'{strftime("%m/%d/%y %T")}: Unsupported hover reply ({hoverResult})' + ) + return ['', ''] +enddef + +# Key filter function for the hover popup window. +# Only keys to scroll the popup window are supported. +def HoverWinFilterKey(hoverWin: number, key: string): bool + var keyHandled = false + + if key == "\<C-E>" + || key == "\<C-D>" + || key == "\<C-F>" + || key == "\<PageDown>" + || key == "\<C-Y>" + || key == "\<C-U>" + || key == "\<C-B>" + || key == "\<PageUp>" + || key == "\<C-Home>" + || key == "\<C-End>" + # scroll the hover popup window + win_execute(hoverWin, $'normal! {key}') + keyHandled = true + endif + + if key == "\<Esc>" + hoverWin->popup_close() + keyHandled = true + endif + + return keyHandled +enddef + +# process the 'textDocument/hover' reply from the LSP server +# Result: Hover | null +export def HoverReply(lspserver: dict<any>, hoverResult: any, cmdmods: string): void + var [hoverText, hoverKind] = GetHoverText(lspserver, hoverResult) + + # Nothing to show + if hoverText->empty() + if cmdmods !~ 'silent' + util.WarnMsg($'No documentation found for current keyword') + endif + return + endif + + if opt.lspOptions.hoverInPreview + execute $':silent! {cmdmods} pedit LspHover' + :wincmd P + :setlocal buftype=nofile + :setlocal bufhidden=delete + bufnr()->deletebufline(1, '$') + hoverText->append(0) + [1, 1]->cursor() + exe $'setlocal ft={hoverKind}' + :wincmd p + else + popup_clear() + var winid = hoverText->popup_atcursor({moved: 'any', + close: 'click', + fixed: true, + maxwidth: 80, + border: [0, 1, 0, 1], + borderchars: [' '], + filter: HoverWinFilterKey}) + win_execute(winid, $'setlocal ft={hoverKind}') + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/inlayhints.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/inlayhints.vim new file mode 100644 index 0000000..a793699 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/inlayhints.vim @@ -0,0 +1,235 @@ +vim9script + +# Functions for dealing with inlay hints + +import './util.vim' +import './buffer.vim' as buf +import './options.vim' as opt + +# Initialize the highlight group and the text property type used for +# inlay hints. +export def InitOnce() + hlset([ + {name: 'LspInlayHintsType', default: true, linksto: 'Label'}, + {name: 'LspInlayHintsParam', default: true, linksto: 'Conceal'} + ]) + prop_type_add('LspInlayHintsType', {highlight: 'LspInlayHintsType'}) + prop_type_add('LspInlayHintsParam', {highlight: 'LspInlayHintsParam'}) + + autocmd_add([{group: 'LspCmds', + event: 'User', + pattern: 'LspOptionsChanged', + cmd: 'LspInlayHintsOptionsChanged()'}]) +enddef + +# Clear all the inlay hints text properties in the current buffer +def InlayHintsClear(bnr: number) + prop_remove({type: 'LspInlayHintsType', bufnr: bnr, all: true}) + prop_remove({type: 'LspInlayHintsParam', bufnr: bnr, all: true}) +enddef + +# LSP inlay hints reply message handler +export def InlayHintsReply(lspserver: dict<any>, bnr: number, inlayHints: any) + if inlayHints->empty() + return + endif + + InlayHintsClear(bnr) + + if mode() != 'n' + # Update inlay hints only in normal mode + return + endif + + for hint in inlayHints + var label = '' + if hint.label->type() == v:t_list + label = hint.label->copy()->map((_, v) => v.value)->join('') + else + label = hint.label + endif + + # add a space before or after the label + var padLeft: bool = hint->get('paddingLeft', false) + var padRight: bool = hint->get('paddingRight', false) + if padLeft + label = $' {label}' + endif + if padRight + label = $'{label} ' + endif + + var kind = hint->has_key('kind') ? hint.kind->string() : '1' + try + lspserver.decodePosition(bnr, hint.position) + var byteIdx = util.GetLineByteFromPos(bnr, hint.position) + if kind == "'type'" || kind == '1' + prop_add(hint.position.line + 1, byteIdx + 1, + {type: 'LspInlayHintsType', text: label, bufnr: bnr}) + elseif kind == "'parameter'" || kind == '2' + prop_add(hint.position.line + 1, byteIdx + 1, + {type: 'LspInlayHintsParam', text: label, bufnr: bnr}) + endif + catch /E966\|E964/ # Invalid lnum | Invalid col + # Inlay hints replies arrive asynchronously and the document might have + # been modified in the mean time. As the reply is stale, ignore invalid + # line number and column number errors. + endtry + endfor +enddef + +# Timer callback to display the inlay hints. +def InlayHintsTimerCb(lspserver: dict<any>, bnr: number, timerid: number) + lspserver.inlayHintsShow(bnr) + setbufvar(bnr, 'LspInlayHintsNeedsUpdate', false) +enddef + +# Update all the inlay hints. A timer is used to throttle the updates. +def LspInlayHintsUpdate(bnr: number) + if !bnr->getbufvar('LspInlayHintsNeedsUpdate', true) + return + endif + + var timerid = bnr->getbufvar('LspInlayHintsTimer', -1) + if timerid != -1 + timerid->timer_stop() + setbufvar(bnr, 'LspInlayHintsTimer', -1) + endif + + var lspserver: dict<any> = buf.BufLspServerGet(bnr, 'inlayHint') + if lspserver->empty() + return + endif + + if get(g:, 'LSPTest') + # When running tests, update the inlay hints immediately + InlayHintsTimerCb(lspserver, bnr, -1) + else + timerid = timer_start(300, function('InlayHintsTimerCb', [lspserver, bnr])) + setbufvar(bnr, 'LspInlayHintsTimer', timerid) + endif +enddef + +# Text is modified. Need to update the inlay hints. +def LspInlayHintsChanged(bnr: number) + setbufvar(bnr, 'LspInlayHintsNeedsUpdate', true) +enddef + +# Trigger an update of the inlay hints in the current buffer. +export def LspInlayHintsUpdateNow(bnr: number) + setbufvar(bnr, 'LspInlayHintsNeedsUpdate', true) + LspInlayHintsUpdate(bnr) +enddef + +# Stop updating the inlay hints. +def LspInlayHintsUpdateStop(bnr: number) + var timerid = bnr->getbufvar('LspInlayHintsTimer', -1) + if timerid != -1 + timerid->timer_stop() + setbufvar(bnr, 'LspInlayHintsTimer', -1) + endif +enddef + +# Do buffer-local initialization for displaying inlay hints +export def BufferInit(lspserver: dict<any>, bnr: number) + if !lspserver.isInlayHintProvider && !lspserver.isClangdInlayHintsProvider + # no support for inlay hints + return + endif + + # Inlays hints are disabled + if !opt.lspOptions.showInlayHints + || !lspserver.featureEnabled('inlayHint') + return + endif + + var acmds: list<dict<any>> = [] + + # Update the inlay hints (if needed) when the cursor is not moved for some + # time. + acmds->add({bufnr: bnr, + event: ['CursorHold'], + group: 'LspInlayHints', + cmd: $'LspInlayHintsUpdate({bnr})'}) + # After the text in the current buffer is modified, the inlay hints need to + # be updated. + acmds->add({bufnr: bnr, + event: ['TextChanged'], + group: 'LspInlayHints', + cmd: $'LspInlayHintsChanged({bnr})'}) + # Editing a file should trigger an inlay hint update. + acmds->add({bufnr: bnr, + event: ['BufReadPost'], + group: 'LspInlayHints', + cmd: $'LspInlayHintsUpdateNow({bnr})'}) + # Inlay hints need not be updated if a buffer is no longer active. + acmds->add({bufnr: bnr, + event: ['BufLeave'], + group: 'LspInlayHints', + cmd: $'LspInlayHintsUpdateStop({bnr})'}) + + # Inlay hints maybe a bit delayed if it was a sync init lsp server. + if lspserver.syncInit + acmds->add({bufnr: bnr, + event: ['User'], + group: 'LspAttached', + cmd: $'LspInlayHintsUpdateNow({bnr})'}) + endif + + autocmd_add(acmds) +enddef + +# Track the current inlay hints enabled/disabled state. Used when the +# "showInlayHints" option value is changed. +var save_showInlayHints = opt.lspOptions.showInlayHints + +# Enable inlay hints. For all the buffers with an attached language server +# that supports inlay hints, refresh the inlay hints. +export def InlayHintsEnable() + opt.lspOptions.showInlayHints = true + for binfo in getbufinfo() + var lspservers: list<dict<any>> = buf.BufLspServersGet(binfo.bufnr) + if lspservers->empty() + continue + endif + for lspserver in lspservers + if !lspserver.ready + || !lspserver.featureEnabled('inlayHint') + || (!lspserver.isInlayHintProvider && + !lspserver.isClangdInlayHintsProvider) + continue + endif + BufferInit(lspserver, binfo.bufnr) + LspInlayHintsUpdateNow(binfo.bufnr) + endfor + endfor + save_showInlayHints = true +enddef + +# Disable inlay hints for the current Vim session. Clear the inlay hints in +# all the buffers. +export def InlayHintsDisable() + opt.lspOptions.showInlayHints = false + for binfo in getbufinfo() + var lspserver: dict<any> = buf.BufLspServerGet(binfo.bufnr, 'inlayHint') + if lspserver->empty() + continue + endif + LspInlayHintsUpdateStop(binfo.bufnr) + :silent! autocmd_delete([{bufnr: binfo.bufnr, group: 'LspInlayHints'}]) + InlayHintsClear(binfo.bufnr) + endfor + save_showInlayHints = false +enddef + +# Some options are changed. If 'showInlayHints' option is changed, then +# either enable or disable inlay hints. +export def LspInlayHintsOptionsChanged() + if save_showInlayHints && !opt.lspOptions.showInlayHints + InlayHintsDisable() + elseif !save_showInlayHints && opt.lspOptions.showInlayHints + InlayHintsEnable() + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/lsp.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/lsp.vim new file mode 100644 index 0000000..507eb00 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/lsp.vim @@ -0,0 +1,1304 @@ +vim9script + +# Vim9 LSP client +# +# The functions called by plugin/lsp.vim are in this file. + +# Needs Vim 9.0 and higher +if v:version < 900 + finish +endif + +import './options.vim' as opt +import './lspserver.vim' as lserver +import './util.vim' +import './buffer.vim' as buf +import './completion.vim' +import './textedit.vim' +import './diag.vim' +import './symbol.vim' +import './outline.vim' +import './signature.vim' +import './codeaction.vim' +import './inlayhints.vim' +import './semantichighlight.vim' + +# LSP server information +var LSPServers: list<dict<any>> = [] + +# filetype to LSP server map +var ftypeServerMap: dict<list<dict<any>>> = {} + +var lspInitializedOnce = false + +def LspInitOnce() + hlset([ + {name: 'LspTextRef', default: true, linksto: 'Search'}, + {name: 'LspReadRef', default: true, linksto: 'DiffChange'}, + {name: 'LspWriteRef', default: true, linksto: 'DiffDelete'} + ]) + + var override = &cursorline + && &cursorlineopt =~ '\<line\>\|\<screenline\>\|\<both\>' + + prop_type_add('LspTextRef', {highlight: 'LspTextRef', override: override}) + prop_type_add('LspReadRef', {highlight: 'LspReadRef', override: override}) + prop_type_add('LspWriteRef', {highlight: 'LspWriteRef', override: override}) + + diag.InitOnce() + inlayhints.InitOnce() + signature.InitOnce() + symbol.InitOnce() + semantichighlight.InitOnce() + + lspInitializedOnce = true +enddef + +# Returns the LSP servers for the a specific filetype. Based on how well there +# score, LSP servers with the same score are being returned. +# Returns an empty list if the servers is not found. +def LspGetServers(bnr: number, ftype: string): list<dict<any>> + if !ftypeServerMap->has_key(ftype) + return [] + endif + + var bufDir = bnr->bufname()->fnamemodify(':p:h') + + return ftypeServerMap[ftype]->filter((key, lspserver) => { + # Don't run the server if no path is found + if !lspserver.runIfSearchFiles->empty() + var path = util.FindNearestRootDir(bufDir, lspserver.runIfSearchFiles) + + if path->empty() + return false + endif + endif + + # Don't run the server if a path is found + if !lspserver.runUnlessSearchFiles->empty() + var path = util.FindNearestRootDir(bufDir, lspserver.runUnlessSearchFiles) + + if !path->empty() + return false + endif + endif + + # Run the server + return true + }) +enddef + +# Add a LSP server for a filetype +def LspAddServer(ftype: string, lspsrv: dict<any>) + var lspsrvlst = ftypeServerMap->has_key(ftype) ? ftypeServerMap[ftype] : [] + lspsrvlst->add(lspsrv) + ftypeServerMap[ftype] = lspsrvlst +enddef + +# Enable/disable the logging of the language server protocol messages +def ServerDebug(arg: string) + if ['errors', 'messages', 'off', 'on']->index(arg) == -1 + util.ErrMsg($'Unsupported argument "{arg}"') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + util.WarnMsg($'No Lsp servers found for "{@%}"') + return + endif + + for lspserver in lspservers + if arg == 'on' + util.ClearTraceLogs(lspserver.logfile) + util.ClearTraceLogs(lspserver.errfile) + lspserver.debug = true + elseif arg == 'off' + lspserver.debug = false + elseif arg == 'messages' + util.ServerMessagesShow(lspserver.logfile) + else + util.ServerMessagesShow(lspserver.errfile) + endif + endfor +enddef + +# Show information about all the LSP servers +export def ShowAllServers() + var lines = [] + # Add filetype to server mapping information + lines->add('Filetype Information') + lines->add('====================') + for [ftype, lspservers] in ftypeServerMap->items() + for lspserver in lspservers + lines->add($"Filetype: '{ftype}'") + lines->add($"Server Name: '{lspserver.name}'") + lines->add($"Server Path: '{lspserver.path}'") + lines->add($"Status: {lspserver.running ? 'Running' : 'Not running'}") + lines->add('') + endfor + endfor + + # Add buffer to server mapping information + lines->add('Buffer Information') + lines->add('==================') + for bnr in range(1, bufnr('$')) + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr) + if !lspservers->empty() + lines->add($"Buffer: '{bufname(bnr)}'") + for lspserver in lspservers + lines->add($"Server Path: '{lspserver.path}'") + lines->add($"Status: {lspserver.running ? 'Running' : 'Not running'}") + endfor + lines->add('') + endif + endfor + + var wid = bufwinid('Language-Servers') + if wid != -1 + wid->win_gotoid() + :setlocal modifiable + :silent! :%d _ + else + :new Language-Servers + :setlocal buftype=nofile + :setlocal bufhidden=wipe + :setlocal noswapfile + :setlocal nonumber nornu + :setlocal fdc=0 signcolumn=no + endif + setline(1, lines) + :setlocal nomodified + :setlocal nomodifiable +enddef + +# Create a new window containing the buffer "bname" or if the window is +# already present then jump to it. +def OpenScratchWindow(bname: string) + var wid = bufwinid(bname) + if wid != -1 + wid->win_gotoid() + :setlocal modifiable + :silent! :%d _ + else + exe $':new {bname}' + :setlocal buftype=nofile + :setlocal bufhidden=wipe + :setlocal noswapfile + :setlocal nonumber nornu + :setlocal fdc=0 signcolumn=no + endif +enddef + +# Show the status of the LSP server for the current buffer +def ShowServer(arg: string) + if ['status', 'capabilities', 'initializeRequest', 'messages']->index(arg) == -1 + util.ErrMsg($'Unsupported argument "{arg}"') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + util.WarnMsg($'No Lsp servers found for "{@%}"') + return + endif + + var windowName: string = '' + var lines: list<string> = [] + if arg->empty() || arg == 'status' + windowName = $'LangServer-Status' + for lspserver in lspservers + if !lines->empty() + lines->extend(['', repeat('=', &columns), '']) + endif + var msg = $"LSP server '{lspserver.name}' is " + if lspserver.running + msg ..= 'running' + else + msg ..= 'not running' + endif + lines->add(msg) + endfor + elseif arg == 'capabilities' + windowName = $'LangServer-Capabilities' + for lspserver in lspservers + if !lines->empty() + lines->extend(['', repeat('=', &columns), '']) + endif + lines->extend(lspserver.getCapabilities()) + endfor + elseif arg == 'initializeRequest' + windowName = $'LangServer-InitializeRequest' + for lspserver in lspservers + if !lines->empty() + lines->extend(['', repeat('=', &columns), '']) + endif + lines->extend(lspserver.getInitializeRequest()) + endfor + elseif arg == 'messages' + windowName = $'LangServer-Messages' + for lspserver in lspservers + if !lines->empty() + lines->extend(['', repeat('=', &columns), '']) + endif + lines->extend(lspserver.getMessages()) + endfor + else + util.ErrMsg($'Unsupported argument "{arg}"') + return + endif + + if lines->len() > 1 + OpenScratchWindow(windowName) + setline(1, lines) + :setlocal nomodified + :setlocal nomodifiable + else + util.InfoMsg(lines[0]) + endif +enddef + +# Get LSP server running status for filetype "ftype" +# Return true if running, or false if not found or not running +export def ServerRunning(ftype: string): bool + if ftypeServerMap->has_key(ftype) + var lspservers = ftypeServerMap[ftype] + for lspserver in lspservers + if lspserver.running + return true + endif + endfor + endif + + return false +enddef + +# Go to a definition using "textDocument/definition" LSP request +export def GotoDefinition(peek: bool, cmdmods: string, count: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('definition') + if lspserver->empty() + return + endif + + lspserver.gotoDefinition(peek, cmdmods, count) +enddef + +# Go to a declaration using "textDocument/declaration" LSP request +export def GotoDeclaration(peek: bool, cmdmods: string, count: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('declaration') + if lspserver->empty() + return + endif + + lspserver.gotoDeclaration(peek, cmdmods, count) +enddef + +# Go to a type definition using "textDocument/typeDefinition" LSP request +export def GotoTypedef(peek: bool, cmdmods: string, count: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('typeDefinition') + if lspserver->empty() + return + endif + + lspserver.gotoTypeDef(peek, cmdmods, count) +enddef + +# Go to a implementation using "textDocument/implementation" LSP request +export def GotoImplementation(peek: bool, cmdmods: string, count: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('implementation') + if lspserver->empty() + return + endif + + lspserver.gotoImplementation(peek, cmdmods, count) +enddef + +# Switch source header using "textDocument/switchSourceHeader" LSP request +# (Clangd specifc extension) +export def SwitchSourceHeader() + var lspserver: dict<any> = buf.CurbufGetServerChecked() + if lspserver->empty() + return + endif + + lspserver.switchSourceHeader() +enddef + +# A buffer is saved. Send the "textDocument/didSave" LSP notification +def LspSavedFile(bnr: number) + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr)->filter( + (key, lspsrv) => !lspsrv->empty() && lspsrv.running + ) + + if lspservers->empty() + return + endif + + for lspserver in lspservers + lspserver.didSaveFile(bnr) + endfor +enddef + +# Called after leaving insert mode. Used to process diag messages (if any) +def LspLeftInsertMode(bnr: number) + var updatePending: bool = bnr->getbufvar('LspDiagsUpdatePending', false) + if !updatePending + return + endif + setbufvar(bnr, 'LspDiagsUpdatePending', false) + + diag.ProcessNewDiags(bnr) +enddef + +# Add buffer-local autocmds when attaching a LSP server to a buffer +def AddBufLocalAutocmds(lspserver: dict<any>, bnr: number): void + var acmds: list<dict<any>> = [] + + # file saved notification handler + acmds->add({bufnr: bnr, + event: 'BufWritePost', + group: 'LSPBufferAutocmds', + cmd: $'LspSavedFile({bnr})'}) + + # Update the diagnostics when insert mode is stopped + acmds->add({bufnr: bnr, + event: 'InsertLeave', + group: 'LSPBufferAutocmds', + cmd: $'LspLeftInsertMode({bnr})'}) + + # Auto highlight all the occurrences of the current keyword + if opt.lspOptions.autoHighlight && + lspserver.isDocumentHighlightProvider + acmds->add({bufnr: bnr, + event: 'CursorMoved', + group: 'LSPBufferAutocmds', + cmd: $'call LspDocHighlightClear({bnr}) | call LspDocHighlight({bnr}, "silent")'}) + endif + + autocmd_add(acmds) +enddef + +# The LSP server with ID "lspserverId" is ready, initialize the LSP features +# for buffer "bnr". +def BufferInit(lspserverId: number, bnr: number): void + var lspserver = buf.BufLspServerGetById(bnr, lspserverId) + if lspserver->empty() || !lspserver.running + return + endif + + var ftype: string = bnr->getbufvar('&filetype') + lspserver.textdocDidOpen(bnr, ftype) + + # add a listener to track changes to this buffer + listener_add((_bnr: number, start: number, end: number, added: number, changes: list<dict<number>>) => { + lspserver.textdocDidChange(bnr, start, end, added, changes) + }, bnr) + + AddBufLocalAutocmds(lspserver, bnr) + + diag.BufferInit(lspserver, bnr) + + var allServersReady = true + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr) + for lspsrv in lspservers + if !lspsrv.ready + allServersReady = false + break + endif + endfor + + if allServersReady + for lspsrv in lspservers + # It's only possible to initialize the features when the server + # capabilities of all the registered language servers for this file type + # are known. + var completionServer = buf.BufLspServerGet(bnr, 'completion') + if !completionServer->empty() && lspsrv.id == completionServer.id + completion.BufferInit(lspsrv, bnr, ftype) + endif + + var signatureServer = buf.BufLspServerGet(bnr, 'signatureHelp') + if !signatureServer->empty() && lspsrv.id == signatureServer.id + signature.BufferInit(lspsrv) + endif + + var inlayHintServer = buf.BufLspServerGet(bnr, 'inlayHint') + if !inlayHintServer->empty() && lspsrv.id == inlayHintServer.id + inlayhints.BufferInit(lspsrv, bnr) + endif + + var semanticServer = buf.BufLspServerGet(bnr, 'semanticTokens') + if !semanticServer->empty() && lspsrv.id == semanticServer.id + semantichighlight.BufferInit(lspserver, bnr) + endif + endfor + + if exists('#User#LspAttached') + doautocmd <nomodeline> User LspAttached + endif + endif +enddef + +# A new buffer is opened. If LSP is supported for this buffer, then add it +export def AddFile(bnr: number): void + if buf.BufHasLspServer(bnr) + # LSP server for this buffer is already initialized and running + return + endif + + # Skip remote files + if util.LspUriRemote(bnr->bufname()->fnamemodify(':p')) + return + endif + + var ftype: string = bnr->getbufvar('&filetype') + if ftype->empty() + return + endif + var lspservers: list<dict<any>> = LspGetServers(bnr, ftype) + if lspservers->empty() + return + endif + for lspserver in lspservers + if !lspserver.running + if !lspInitializedOnce + LspInitOnce() + endif + lspserver.startServer(bnr) + endif + buf.BufLspServerSet(bnr, lspserver) + + if lspserver.ready + BufferInit(lspserver.id, bnr) + else + augroup LSPBufferAutocmds + exe $'autocmd User LspServerReady_{lspserver.id} ++once BufferInit({lspserver.id}, {bnr})' + augroup END + endif + endfor +enddef + +# Notify LSP server to remove a file +export def RemoveFile(bnr: number): void + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr) + for lspserver in lspservers->copy() + if lspserver->empty() + continue + endif + if lspserver.running + lspserver.textdocDidClose(bnr) + endif + diag.DiagRemoveFile(bnr) + buf.BufLspServerRemove(bnr, lspserver) + endfor +enddef + +# Buffer 'bnr' is loaded in a window, send the latest buffer contents to the +# language servers. +export def BufferLoadedInWin(bnr: number) + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr) + if lspservers->empty() + # No language servers for this buffer + return + endif + for lspserver in lspservers + if !lspserver->empty() && lspserver.ready + lspserver.textdocDidChange(bnr, 0, 0, 0, []) + endif + endfor + # Refresh the displayed diags visuals + if opt.lspOptions.autoHighlightDiags + diag.DiagsRefresh(bnr) + endif + + completion.BufferLoadedInWin(bnr) + + # Refresh the semantic highlights + if opt.lspOptions.semanticHighlight + var semanticServer = buf.BufLspServerGet(bnr, 'semanticTokens') + if !semanticServer->empty() + semanticServer.semanticHighlightUpdate(bnr) + endif + endif +enddef + +# Stop all the LSP servers +export def StopAllServers() + for lspserver in LSPServers + if lspserver.running + lspserver.stopServer() + endif + endfor +enddef + +# Add all the buffers with 'filetype' set to "ftype" to the language server. +def AddBuffersToLsp(ftype: string) + # Add all the buffers with the same file type as the current buffer + for binfo in getbufinfo({bufloaded: 1}) + if binfo.bufnr->getbufvar('&filetype') == ftype + AddFile(binfo.bufnr) + endif + endfor +enddef + +# Restart the LSP server for the current buffer +def RestartServer() + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + util.WarnMsg($'No Lsp servers found for "{@%}"') + return + endif + + # Remove all the buffers with the same file type as the current buffer + var ftype: string = &filetype + for binfo in getbufinfo() + if binfo.bufnr->getbufvar('&filetype') == ftype + RemoveFile(binfo.bufnr) + endif + endfor + + for lspserver in lspservers + # Stop the server (if running) + if lspserver.running + lspserver.stopServer() + endif + + # Start the server again + lspserver.startServer(bufnr()) + endfor + + AddBuffersToLsp(ftype) +enddef + +# Add the LSP server for files with 'filetype' as "ftype". +def AddServerForFiltype(lspserver: dict<any>, ftype: string, omnicompl: bool) + LspAddServer(ftype, lspserver) + completion.OmniComplSet(ftype, omnicompl) + + # If a buffer of this file type is already present, then send it to the LSP + # server now. + AddBuffersToLsp(ftype) +enddef + +# Register a LSP server for one or more file types +export def AddServer(serverList: list<dict<any>>) + for server in serverList + if !server->has_key('filetype') || !server->has_key('path') + util.ErrMsg('LSP server information is missing filetype or path') + continue + endif + # Enable omni-completion by default + var omnicompl_def: bool = false + if opt.lspOptions.omniComplete == true + || (opt.lspOptions.omniComplete == null + && !opt.lspOptions.autoComplete) + omnicompl_def = true + endif + server.omnicompl = server->get('omnicompl', omnicompl_def) + + if !server.path->executable() + if !opt.lspOptions.ignoreMissingServer + util.ErrMsg($'LSP server {server.path} is not found') + endif + continue + endif + if server->has_key('args') + if server.args->type() != v:t_list + util.ErrMsg($'Arguments for LSP server {server.args} is not a List') + continue + endif + else + server.args = [] + endif + + if !server->has_key('initializationOptions') + || server.initializationOptions->type() != v:t_dict + server.initializationOptions = {} + endif + + if !server->has_key('forceOffsetEncoding') + || server.forceOffsetEncoding->type() != v:t_string + || (server.forceOffsetEncoding != 'utf-8' + && server.forceOffsetEncoding != 'utf-16' + && server.forceOffsetEncoding != 'utf-32') + server.forceOffsetEncoding = '' + endif + + if !server->has_key('customNotificationHandlers') + || server.customNotificationHandlers->type() != v:t_dict + server.customNotificationHandlers = {} + endif + + if server->has_key('processDiagHandler') + if server.processDiagHandler->type() != v:t_func + util.ErrMsg($'Setting of processDiagHandler {server.processDiagHandler} is not a Funcref nor lambda') + continue + endif + else + server.processDiagHandler = null_function + endif + + if !server->has_key('customRequestHandlers') + || server.customRequestHandlers->type() != v:t_dict + server.customRequestHandlers = {} + endif + + if !server->has_key('features') || server.features->type() != v:t_dict + server.features = {} + endif + + if server.omnicompl->type() != v:t_bool + util.ErrMsg($'Setting of omnicompl {server.omnicompl} is not a Boolean') + continue + endif + + if !server->has_key('syncInit') + server.syncInit = false + endif + + if !server->has_key('name') || server.name->type() != v:t_string + || server.name->empty() + # Use the executable name (without the extension) as the language server + # name. + server.name = server.path->fnamemodify(':t:r') + endif + + if !server->has_key('debug') || server.debug->type() != v:t_bool + server.debug = false + endif + + if !server->has_key('traceLevel') + || server->type() != v:t_string + || (server.traceLevel != 'off' && server.traceLevel != 'debug' + && server.traceLevel != 'verbose') + server.traceLevel = 'off' + endif + + if !server->has_key('workspaceConfig') + || server.workspaceConfig->type() != v:t_dict + server.workspaceConfig = {} + endif + + if !server->has_key('rootSearch') || server.rootSearch->type() != v:t_list + server.rootSearch = [] + endif + + if !server->has_key('runIfSearch') || + server.runIfSearch->type() != v:t_list + server.runIfSearch = [] + endif + + if !server->has_key('runUnlessSearch') || + server.runUnlessSearch->type() != v:t_list + server.runUnlessSearch = [] + endif + + var lspserver: dict<any> = lserver.NewLspServer(server) + + var ftypes = server.filetype + if ftypes->type() == v:t_string + AddServerForFiltype(lspserver, ftypes, server.omnicompl) + elseif ftypes->type() == v:t_list + for ftype in ftypes + AddServerForFiltype(lspserver, ftype, server.omnicompl) + endfor + else + util.ErrMsg($'Unsupported file type information "{ftypes->string()}" in LSP server registration') + continue + endif + endfor +enddef + +# The LSP server is considered ready when the server capabilities are +# received ("initialize" LSP reply message) +export def ServerReady(): bool + var fname: string = @% + if fname->empty() + return false + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + return false + endif + + for lspserver in lspservers + if !lspserver.ready + return false + endif + endfor + + return true +enddef + +# set the LSP server trace level for the current buffer +# Params: SetTraceParams +def ServerTraceSet(traceVal: string) + if ['off', 'messages', 'verbose']->index(traceVal) == -1 + util.ErrMsg($'Unsupported argument "{traceVal}"') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + util.WarnMsg($'No Lsp servers found for "{@%}"') + return + endif + + for lspserver in lspservers + lspserver.setTrace(traceVal) + endfor +enddef + +# Display the diagnostic messages from the LSP server for the current buffer +# in a quickfix list +export def ShowDiagnostics(): void + diag.ShowAllDiags() +enddef + +# Show the diagnostic message for the current line +export def LspShowCurrentDiag(atPos: bool) + diag.ShowCurrentDiag(atPos) +enddef + +# get the count of diagnostics in the current buffer +export def ErrorCount(): dict<number> + var res = {Error: 0, Warn: 0, Info: 0, Hint: 0} + var fname: string = @% + if fname->empty() + return res + endif + + return diag.DiagsGetErrorCount(bufnr()) +enddef + +# jump to the next/previous/first diagnostic message in the current buffer +export def JumpToDiag(which: string, count: number = 0): void + diag.LspDiagsJump(which, count) +enddef + +# Display the hover message from the LSP server for the current cursor +# location +export def Hover(cmdmods: string) + var lspserver: dict<any> = buf.CurbufGetServerChecked('hover') + if lspserver->empty() + return + endif + + lspserver.hover(cmdmods) +enddef + +# Enable or disable inlay hints +export def InlayHints(ctl: string) + if ctl == 'enable' + inlayhints.InlayHintsEnable() + elseif ctl == 'disable' + inlayhints.InlayHintsDisable() + else + util.ErrMsg($'LspInlayHints - Unsupported argument "{ctl}"') + endif +enddef + +# Command-line completion for the ":LspInlayHints" command +export def LspInlayHintsComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + var l = ['enable', 'disable'] + return filter(l, (_, val) => val =~ $'^{arglead}') +enddef + +# show symbol references +export def ShowReferences(peek: bool) + var lspserver: dict<any> = buf.CurbufGetServerChecked('references') + if lspserver->empty() + return + endif + + lspserver.showReferences(peek) +enddef + +# highlight all the places where a symbol is referenced +def g:LspDocHighlight(bnr: number = bufnr(), cmdmods: string = '') + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentHighlight') + if lspserver->empty() + return + endif + + lspserver.docHighlight(bnr, cmdmods) +enddef + +# clear the symbol reference highlight +def g:LspDocHighlightClear(bnr: number = bufnr()) + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentHighlight') + if lspserver->empty() + return + endif + + var propNames = ['LspTextRef', 'LspReadRef', 'LspWriteRef'] + if has('patch-9.0.0233') + prop_remove({types: propNames, bufnr: bnr, all: true}) + else + for propName in propNames + prop_remove({type: propName, bufnr: bnr, all: true}) + endfor + endif +enddef + +def g:LspRequestDocSymbols() + if outline.SkipOutlineRefresh() + return + endif + + var fname: string = @% + if fname->empty() + return + endif + + var lspserver: dict<any> = buf.CurbufGetServer('documentSymbol') + if lspserver->empty() || !lspserver.running || !lspserver.ready + return + endif + + lspserver.getDocSymbols(fname, true) +enddef + +# open a window and display all the symbols in a file (outline) +export def Outline(cmdmods: string, winsize: number) + var fname: string = @% + if fname->empty() + return + endif + + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentSymbol') + if lspserver->empty() || !lspserver.running || !lspserver.ready + return + endif + + outline.OpenOutlineWindow(cmdmods, winsize) + g:LspRequestDocSymbols() +enddef + +# show all the symbols in a file in a popup menu +export def ShowDocSymbols() + var fname: string = @% + if fname->empty() + return + endif + + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentSymbol') + if lspserver->empty() || !lspserver.running || !lspserver.ready + return + endif + + lspserver.getDocSymbols(fname, false) +enddef + +# Format the entire file +export def TextDocFormat(range_args: number, line1: number, line2: number) + if !&modifiable + util.ErrMsg('Current file is not a modifiable file') + return + endif + + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentFormatting') + if lspserver->empty() + return + endif + + var fname: string = @% + if range_args > 0 + lspserver.textDocFormat(fname, true, line1, line2) + else + lspserver.textDocFormat(fname, false, 0, 0) + endif +enddef + +# TODO: Add support for textDocument.onTypeFormatting? +# Will this slow down Vim? + +# Display all the locations where the current symbol is called from. +# Uses LSP "callHierarchy/incomingCalls" request +export def IncomingCalls() + var lspserver: dict<any> = buf.CurbufGetServerChecked('callHierarchy') + if lspserver->empty() + return + endif + + lspserver.incomingCalls(@%) +enddef + +# Display all the symbols used by the current symbol. +# Uses LSP "callHierarchy/outgoingCalls" request +export def OutgoingCalls() + var lspserver: dict<any> = buf.CurbufGetServerChecked('callHierarchy') + if lspserver->empty() + return + endif + + lspserver.outgoingCalls(@%) +enddef + +# Display the type hierarchy for the current symbol. Direction is 0 for +# sub types and 1 for super types. +export def TypeHierarchy(direction: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('typeHierarchy') + if lspserver->empty() + return + endif + + lspserver.typeHierarchy(direction) +enddef + +# Rename a symbol +# Uses LSP "textDocument/rename" request +export def Rename(a_newName: string) + var lspserver: dict<any> = buf.CurbufGetServerChecked('rename') + if lspserver->empty() + return + endif + + var newName: string = a_newName + if newName->empty() + var sym: string = expand('<cword>') + newName = input($"Rename symbol '{sym}' to: ", sym) + if newName->empty() + return + endif + + # clear the input prompt + :echo "\r" + endif + + lspserver.renameSymbol(newName) +enddef + +# Perform a code action +# Uses LSP "textDocument/codeAction" request +export def CodeAction(line1: number, line2: number, query: string) + var lspserver: dict<any> = buf.CurbufGetServerChecked('codeAction') + if lspserver->empty() + return + endif + + var fname: string = @% + lspserver.codeAction(fname, line1, line2, query) +enddef + +# Code lens +# Uses LSP "textDocument/codeLens" request +export def CodeLens() + var lspserver: dict<any> = buf.CurbufGetServerChecked('codeLens') + if lspserver->empty() + return + endif + + lspserver.codeLens(@%) +enddef + +# Perform a workspace wide symbol lookup +# Uses LSP "workspace/symbol" request +export def SymbolSearch(queryArg: string, cmdmods: string) + var lspserver: dict<any> = buf.CurbufGetServerChecked('workspaceSymbol') + if lspserver->empty() + return + endif + + var query: string = queryArg + if query->empty() + query = input('Lookup symbol: ', expand('<cword>')) + if query->empty() + return + endif + endif + :redraw! + + lspserver.workspaceQuery(query, true, cmdmods) +enddef + +# Display the list of workspace folders +export def ListWorkspaceFolders() + var lspservers: list<dict<any>> = buf.CurbufGetServers() + for lspserver in lspservers + util.InfoMsg($'Workspace Folders: "{lspserver.name}" {lspserver.workspaceFolders->string()}') + endfor +enddef + +# Add a workspace folder. Default is to use the current folder. +export def AddWorkspaceFolder(dirArg: string) + var dirName: string = dirArg + if dirName->empty() + dirName = input('Add Workspace Folder: ', getcwd(), 'dir') + if dirName->empty() + return + endif + endif + :redraw! + if !dirName->isdirectory() + util.ErrMsg($'{dirName} is not a directory') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + + for lspserver in lspservers + lspserver.addWorkspaceFolder(dirName) + endfor +enddef + +# Remove a workspace folder. Default is to use the current folder. +export def RemoveWorkspaceFolder(dirArg: string) + var dirName: string = dirArg + if dirName->empty() + dirName = input('Remove Workspace Folder: ', getcwd(), 'dir') + if dirName->empty() + return + endif + endif + :redraw! + if !dirName->isdirectory() + util.ErrMsg($'{dirName} is not a directory') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + for lspserver in lspservers + lspserver.removeWorkspaceFolder(dirName) + endfor +enddef + +# expand the previous selection or start a new selection +export def SelectionExpand() + var lspserver: dict<any> = buf.CurbufGetServerChecked('selectionRange') + if lspserver->empty() + return + endif + + lspserver.selectionExpand() +enddef + +# shrink the previous selection or start a new selection +export def SelectionShrink() + var lspserver: dict<any> = buf.CurbufGetServerChecked('selectionRange') + if lspserver->empty() + return + endif + + lspserver.selectionShrink() +enddef + +# fold the entire document +export def FoldDocument() + var lspserver: dict<any> = buf.CurbufGetServerChecked('foldingRange') + if lspserver->empty() + return + endif + + if &foldmethod != 'manual' + util.ErrMsg("Only works when 'foldmethod' is 'manual'") + return + endif + + var fname: string = @% + lspserver.foldRange(fname) +enddef + +# Enable diagnostic highlighting for all the buffers +export def DiagHighlightEnable() + diag.DiagsHighlightEnable() +enddef + +# Disable diagnostic highlighting for all the buffers +export def DiagHighlightDisable() + diag.DiagsHighlightDisable() +enddef + +# Function to use with the 'tagfunc' option. +export def TagFunc(pat: string, flags: string, info: dict<any>): any + var lspserver: dict<any> = buf.CurbufGetServerChecked('definition') + if lspserver->empty() + return v:null + endif + + return lspserver.tagFunc(pat, flags, info) +enddef + +# Function to use with the 'formatexpr' option. +export def FormatExpr(): number + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentFormatting') + if lspserver->empty() + return 1 + endif + + lspserver.textDocFormat(@%, true, v:lnum, v:lnum + v:count - 1) + return 0 +enddef + +export def RegisterCmdHandler(cmd: string, Handler: func) + codeaction.RegisterCmdHandler(cmd, Handler) +enddef + +# Command-line completion for the ":LspServer <cmd>" and ":LspDiag <cmd>" sub +# commands +def LspSubCmdComplete(cmds: list<string>, arglead: string, cmdline: string, cursorPos: number): list<string> + var wordBegin = cmdline->match('\s\+\zs\S', cursorPos) + if wordBegin == -1 + return cmds + endif + + # Make sure there are no additional sub-commands + var wordEnd = cmdline->stridx(' ', wordBegin) + if wordEnd == -1 + return cmds->filter((_, val) => val =~ $'^{arglead}') + endif + + return [] +enddef + +# Command-line completion for the ":LspDiag highlight" command +def LspDiagHighlightComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + return LspSubCmdComplete(['enable', 'disable'], arglead, cmdline, cursorPos) +enddef + +# Command-line completion for the ":LspDiag" command +export def LspDiagComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + var wordBegin = -1 + var wordEnd = -1 + var l = ['first', 'current', 'here', 'highlight', 'last', 'next', 'nextWrap', + 'prev', 'prevWrap', 'show'] + + # Skip the command name + var i = cmdline->stridx(' ', 0) + wordBegin = cmdline->match('\s\+\zs\S', i) + if wordBegin == -1 + return l + endif + + wordEnd = cmdline->stridx(' ', wordBegin) + if wordEnd == -1 + return filter(l, (_, val) => val =~ $'^{arglead}') + endif + + var cmd = cmdline->strpart(wordBegin, wordEnd - wordBegin) + if cmd == 'highlight' + return LspDiagHighlightComplete(arglead, cmdline, wordEnd) + endif + + return [] +enddef + +# ":LspDiag" command handler +export def LspDiagCmd(args: string, cmdCount: number, force: bool) + if args->stridx('highlight') == 0 + if args[9] == ' ' + var subcmd = args[10 : ]->trim() + if subcmd == 'enable' + diag.DiagsHighlightEnable() + elseif subcmd == 'disable' + diag.DiagsHighlightDisable() + else + util.ErrMsg($':LspDiag highlight - Unsupported argument "{subcmd}"') + endif + else + util.ErrMsg('Argument required for ":LspDiag highlight"') + endif + elseif args == 'first' + diag.LspDiagsJump('first', 0) + elseif args == 'current' + LspShowCurrentDiag(force) + elseif args == 'here' + diag.LspDiagsJump('here', 0) + elseif args == 'last' + diag.LspDiagsJump('last', 0) + elseif args == 'next' + diag.LspDiagsJump('next', cmdCount) + elseif args == 'nextWrap' + diag.LspDiagsJump('nextWrap', cmdCount) + elseif args == 'prev' + diag.LspDiagsJump('prev', cmdCount) + elseif args == 'prevWrap' + diag.LspDiagsJump('prevWrap', cmdCount) + elseif args == 'show' + ShowDiagnostics() + else + util.ErrMsg($':LspDiag - Unsupported argument "{args}"') + endif +enddef + +# Command-line completion for the ":LspServer debug" command +def LspServerDebugComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + return LspSubCmdComplete(['errors', 'messages', 'off', 'on'], arglead, + cmdline, cursorPos) +enddef + +# Command-line completion for the ":LspServer show" command +def LspServerShowComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + return LspSubCmdComplete(['capabilities', 'initializeRequest', 'messages', + 'status'], arglead, cmdline, cursorPos) +enddef + +# Command-line completion for the ":LspServer trace" command +def LspServerTraceComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + return LspSubCmdComplete(['messages', 'off', 'verbose'], arglead, cmdline, + cursorPos) +enddef + +# Command-line completion for the ":LspServer" command +export def LspServerComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + var wordBegin = -1 + var wordEnd = -1 + var l = ['debug', 'restart', 'show', 'trace'] + + # Skip the command name + var i = cmdline->stridx(' ', 0) + wordBegin = cmdline->match('\s\+\zs\S', i) + if wordBegin == -1 + return l + endif + + wordEnd = cmdline->stridx(' ', wordBegin) + if wordEnd == -1 + return filter(l, (_, val) => val =~ $'^{arglead}') + endif + + var cmd = cmdline->strpart(wordBegin, wordEnd - wordBegin) + if cmd == 'debug' + return LspServerDebugComplete(arglead, cmdline, wordEnd) + elseif cmd == 'restart' + elseif cmd == 'show' + return LspServerShowComplete(arglead, cmdline, wordEnd) + elseif cmd == 'trace' + return LspServerTraceComplete(arglead, cmdline, wordEnd) + endif + + return [] +enddef + +# ":LspServer" command handler +export def LspServerCmd(args: string) + if args->stridx('debug') == 0 + if args[5] == ' ' + var subcmd = args[6 : ]->trim() + ServerDebug(subcmd) + else + util.ErrMsg('Argument required for ":LspServer debug"') + endif + elseif args == 'restart' + RestartServer() + elseif args->stridx('show') == 0 + if args[4] == ' ' + var subcmd = args[5 : ]->trim() + ShowServer(subcmd) + else + util.ErrMsg('Argument required for ":LspServer show"') + endif + elseif args->stridx('trace') == 0 + if args[5] == ' ' + var subcmd = args[6 : ]->trim() + ServerTraceSet(subcmd) + else + util.ErrMsg('Argument required for ":LspServer trace"') + endif + else + util.ErrMsg($'LspServer - Unsupported argument "{args}"') + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/lspserver.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/lspserver.vim new file mode 100644 index 0000000..bfbdd77 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/lspserver.vim @@ -0,0 +1,1982 @@ +vim9script + +# LSP server functions +# +# The functions to send request messages to the language server are in this +# file. +# +# Refer to https://microsoft.github.io/language-server-protocol/specification +# for the Language Server Protocol (LSP) specificaiton. + +import './options.vim' as opt +import './handlers.vim' +import './util.vim' +import './capabilities.vim' +import './offset.vim' +import './diag.vim' +import './selection.vim' +import './symbol.vim' +import './textedit.vim' +import './completion.vim' +import './hover.vim' +import './signature.vim' +import './codeaction.vim' +import './codelens.vim' +import './callhierarchy.vim' as callhier +import './typehierarchy.vim' as typehier +import './inlayhints.vim' +import './semantichighlight.vim' + +# LSP server standard output handler +def Output_cb(lspserver: dict<any>, chan: channel, msg: any): void + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Received {msg->string()}') + endif + lspserver.data = msg + lspserver.processMessages() +enddef + +# LSP server error output handler +def Error_cb(lspserver: dict<any>, chan: channel, emsg: string): void + lspserver.errorLog(emsg) +enddef + +# LSP server exit callback +def Exit_cb(lspserver: dict<any>, job: job, status: number): void + util.WarnMsg($'{strftime("%m/%d/%y %T")}: LSP server ({lspserver.name}) exited with status {status}') + lspserver.running = false + lspserver.ready = false + lspserver.requests = {} +enddef + +# Start a LSP server +# +def StartServer(lspserver: dict<any>, bnr: number): number + if lspserver.running + util.WarnMsg($'LSP server "{lspserver.name}" is already running') + return 0 + endif + + var cmd = [lspserver.path] + cmd->extend(lspserver.args) + + var opts = {in_mode: 'lsp', + out_mode: 'lsp', + err_mode: 'raw', + noblock: 1, + out_cb: function(Output_cb, [lspserver]), + err_cb: function(Error_cb, [lspserver]), + exit_cb: function(Exit_cb, [lspserver])} + + lspserver.data = '' + lspserver.caps = {} + lspserver.nextID = 1 + lspserver.requests = {} + lspserver.omniCompletePending = false + lspserver.completionLazyDoc = false + lspserver.completionTriggerChars = [] + lspserver.signaturePopup = -1 + + var job = cmd->job_start(opts) + if job->job_status() == 'fail' + util.ErrMsg($'Failed to start LSP server {lspserver.path}') + return 1 + endif + + # wait a little for the LSP server to start + sleep 10m + + lspserver.job = job + lspserver.running = true + + lspserver.initServer(bnr) + + return 0 +enddef + +# process the "initialize" method reply from the LSP server +# Result: InitializeResult +def ServerInitReply(lspserver: dict<any>, initResult: dict<any>): void + if initResult->empty() + return + endif + + var caps: dict<any> = initResult.capabilities + lspserver.caps = caps + + for [key, val] in initResult->items() + if key == 'capabilities' + continue + endif + + lspserver.caps[$'~additionalInitResult_{key}'] = val + endfor + + capabilities.ProcessServerCaps(lspserver, caps) + + if caps->has_key('completionProvider') + lspserver.completionTriggerChars = + caps.completionProvider->get('triggerCharacters', []) + lspserver.completionLazyDoc = + caps.completionProvider->get('resolveProvider', false) + endif + + # send a "initialized" notification to server + lspserver.sendInitializedNotif() + # send any workspace configuration (optional) + if !lspserver.workspaceConfig->empty() + lspserver.sendWorkspaceConfig() + endif + lspserver.ready = true + if exists($'#User#LspServerReady{lspserver.name}') + exe $'doautocmd <nomodeline> User LspServerReady{lspserver.name}' + endif + # Used internally, and shouldn't be used by users + if exists($'#User#LspServerReady_{lspserver.id}') + exe $'doautocmd <nomodeline> User LspServerReady_{lspserver.id}' + endif + + # set the server debug trace level + if lspserver.traceLevel != 'off' + lspserver.setTrace(lspserver.traceLevel) + endif + + # if the outline window is opened, then request the symbols for the current + # buffer + if bufwinid('LSP-Outline') != -1 + lspserver.getDocSymbols(@%, true) + endif + + # Update the inlay hints (if enabled) + if opt.lspOptions.showInlayHints && (lspserver.isInlayHintProvider + || lspserver.isClangdInlayHintsProvider) + inlayhints.LspInlayHintsUpdateNow(bufnr()) + endif +enddef + +# Request: "initialize" +# Param: InitializeParams +def InitServer(lspserver: dict<any>, bnr: number) + # interface 'InitializeParams' + var initparams: dict<any> = {} + initparams.processId = getpid() + initparams.clientInfo = { + name: 'Vim', + version: v:versionlong->string(), + } + + # Compute the rootpath (based on the directory of the buffer) + var rootPath = '' + var rootSearchFiles = lspserver.rootSearchFiles + var bufDir = bnr->bufname()->fnamemodify(':p:h') + if !rootSearchFiles->empty() + rootPath = util.FindNearestRootDir(bufDir, rootSearchFiles) + endif + if rootPath->empty() + var cwd = getcwd() + + # bufDir is within cwd + var bufDirPrefix = bufDir[0 : cwd->strcharlen() - 1] + if &fileignorecase + ? bufDirPrefix ==? cwd + : bufDirPrefix == cwd + rootPath = cwd + else + rootPath = bufDir + endif + endif + + lspserver.workspaceFolders = [rootPath] + + var rootUri = util.LspFileToUri(rootPath) + initparams.rootPath = rootPath + initparams.rootUri = rootUri + initparams.workspaceFolders = [{ + name: rootPath->fnamemodify(':t'), + uri: rootUri + }] + + initparams.trace = 'off' + initparams.capabilities = capabilities.GetClientCaps() + if !lspserver.initializationOptions->empty() + initparams.initializationOptions = lspserver.initializationOptions + else + initparams.initializationOptions = {} + endif + + lspserver.rpcInitializeRequest = initparams + + lspserver.rpc_a('initialize', initparams, ServerInitReply) +enddef + +# Send a "initialized" notification to the language server +def SendInitializedNotif(lspserver: dict<any>) + # Notification: 'initialized' + # Params: InitializedParams + lspserver.sendNotification('initialized') +enddef + +# Request: shutdown +# Param: void +def ShutdownServer(lspserver: dict<any>): void + lspserver.rpc('shutdown', {}) +enddef + +# Send a 'exit' notification to the language server +def ExitServer(lspserver: dict<any>): void + # Notification: 'exit' + # Params: void + lspserver.sendNotification('exit') +enddef + +# Stop a LSP server +def StopServer(lspserver: dict<any>): number + if !lspserver.running + util.WarnMsg($'LSP server {lspserver.name} is not running') + return 0 + endif + + # Send the shutdown request to the server + lspserver.shutdownServer() + + # Notify the server to exit + lspserver.exitServer() + + # Wait for the server to process the exit notification and exit for a + # maximum of 2 seconds. + var maxCount: number = 1000 + while lspserver.job->job_status() == 'run' && maxCount > 0 + sleep 2m + maxCount -= 1 + endwhile + + if lspserver.job->job_status() == 'run' + lspserver.job->job_stop() + endif + lspserver.running = false + lspserver.ready = false + lspserver.requests = {} + return 0 +enddef + +# Set the language server trace level using the '$/setTrace' notification. +# Supported values for "traceVal" are "off", "messages" and "verbose". +def SetTrace(lspserver: dict<any>, traceVal: string) + # Notification: '$/setTrace' + # Params: SetTraceParams + var params = {value: traceVal} + lspserver.sendNotification('$/setTrace', params) +enddef + +# Log a debug message to the LSP server debug file +def TraceLog(lspserver: dict<any>, msg: string) + if lspserver.debug + util.TraceLog(lspserver.logfile, false, msg) + endif +enddef + +# Log an error message to the LSP server error file +def ErrorLog(lspserver: dict<any>, errmsg: string) + if lspserver.debug + util.TraceLog(lspserver.errfile, true, errmsg) + endif +enddef + +# Return the next id for a LSP server request message +def NextReqID(lspserver: dict<any>): number + var id = lspserver.nextID + lspserver.nextID = id + 1 + return id +enddef + +# create a LSP server request message +def CreateRequest(lspserver: dict<any>, method: string): dict<any> + var req = { + jsonrpc: '2.0', + id: lspserver.nextReqID(), + method: method, + params: {} + } + + # Save the request, so that the corresponding response can be processed + lspserver.requests->extend({[req.id->string()]: req}) + + return req +enddef + +# create a LSP server response message +def CreateResponse(lspserver: dict<any>, req_id: number): dict<any> + var resp = { + jsonrpc: '2.0', + id: req_id + } + return resp +enddef + +# create a LSP server notification message +def CreateNotification(lspserver: dict<any>, notif: string): dict<any> + var req = { + jsonrpc: '2.0', + method: notif, + params: {} + } + + return req +enddef + +# send a response message to the server +def SendResponse(lspserver: dict<any>, request: dict<any>, result: any, error: dict<any>) + if (request.id->type() == v:t_string + && (request.id->trim() =~ '[^[:digit:]]\+' + || request.id->trim()->empty())) + || (request.id->type() != v:t_string && request.id->type() != v:t_number) + util.ErrMsg('request.id of response to LSP server is not a correct number') + return + endif + var resp: dict<any> = lspserver.createResponse( + request.id->type() == v:t_string ? request.id->str2nr() : request.id) + if error->empty() + resp->extend({result: result}) + else + resp->extend({error: error}) + endif + lspserver.sendMessage(resp) +enddef + +# Send a request message to LSP server +def SendMessage(lspserver: dict<any>, content: dict<any>): void + var job = lspserver.job + if job->job_status() != 'run' + # LSP server has exited + return + endif + job->ch_sendexpr(content) + if content->has_key('id') + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Sent {content->string()}') + endif + endif +enddef + +# Send a notification message to the language server +def SendNotification(lspserver: dict<any>, method: string, params: any = {}) + var notif: dict<any> = CreateNotification(lspserver, method) + notif.params->extend(params) + lspserver.sendMessage(notif) +enddef + +# Translate an LSP error code into a readable string +def LspGetErrorMessage(errcode: number): string + var errmap = { + -32001: 'UnknownErrorCode', + -32002: 'ServerNotInitialized', + -32600: 'InvalidRequest', + -32601: 'MethodNotFound', + -32602: 'InvalidParams', + -32603: 'InternalError', + -32700: 'ParseError', + -32800: 'RequestCancelled', + -32801: 'ContentModified', + -32802: 'ServerCancelled', + -32803: 'RequestFailed' + } + + return errmap->get(errcode, errcode->string()) +enddef + +# Process a LSP server response error and display an error message. +def ProcessLspServerError(method: string, responseError: dict<any>) + # request failed + var emsg: string = responseError.message + emsg ..= $', error = {LspGetErrorMessage(responseError.code)}' + if responseError->has_key('data') + emsg ..= $', data = {responseError.data->string()}' + endif + util.ErrMsg($'request {method} failed ({emsg})') +enddef + +# Send a sync RPC request message to the LSP server and return the received +# reply. In case of an error, an empty Dict is returned. +def Rpc(lspserver: dict<any>, method: string, params: any, handleError: bool = true): dict<any> + var req = { + method: method, + params: params + } + + var job = lspserver.job + if job->job_status() != 'run' + # LSP server has exited + return {} + endif + + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Sent {req->string()}') + endif + + # Do the synchronous RPC call + var reply = job->ch_evalexpr(req) + + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Received {reply->string()}') + endif + + if reply->has_key('result') + # successful reply + return reply + endif + + if reply->has_key('error') && handleError + # request failed + ProcessLspServerError(method, reply.error) + endif + + return {} +enddef + +# LSP server asynchronous RPC callback +def AsyncRpcCb(lspserver: dict<any>, method: string, RpcCb: func, chan: channel, reply: dict<any>) + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Received {reply->string()}') + endif + + if reply->empty() + return + endif + + if reply->has_key('error') + # request failed + ProcessLspServerError(method, reply.error) + return + endif + + if !reply->has_key('result') + util.ErrMsg($'request {method} failed (no result)') + return + endif + + RpcCb(lspserver, reply.result) +enddef + +# Send a async RPC request message to the LSP server with a callback function. +# Returns the LSP message id. This id can be used to cancel the RPC request +# (if needed). Returns -1 on error. +def AsyncRpc(lspserver: dict<any>, method: string, params: any, Cbfunc: func): number + var req = { + method: method, + params: params + } + + var job = lspserver.job + if job->job_status() != 'run' + # LSP server has exited + return -1 + endif + + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Sent {req->string()}') + endif + + # Do the asynchronous RPC call + var Fn = function('AsyncRpcCb', [lspserver, method, Cbfunc]) + + var reply: dict<any> + if get(g:, 'LSPTest') + # When running LSP tests, make this a synchronous RPC call + reply = Rpc(lspserver, method, params) + Fn(test_null_channel(), reply) + else + # Otherwise, make an asynchronous RPC call + reply = job->ch_sendexpr(req, {callback: Fn}) + endif + if reply->empty() + return -1 + endif + + return reply.id +enddef + +# Wait for a response message from the LSP server for the request "req" +# Waits for a maximum of 5 seconds +def WaitForResponse(lspserver: dict<any>, req: dict<any>) + var maxCount: number = 2500 + var key: string = req.id->string() + + while lspserver.requests->has_key(key) && maxCount > 0 + sleep 2m + maxCount -= 1 + endwhile +enddef + +# Returns true when the "lspserver" has "feature" enabled. +# By default, all the features of a lsp server are enabled. +def FeatureEnabled(lspserver: dict<any>, feature: string): bool + return lspserver.features->get(feature, true) +enddef + +# Retrieve the Workspace configuration asked by the server. +# Request: workspace/configuration +def WorkspaceConfigGet(lspserver: dict<any>, configItem: dict<any>): dict<any> + if lspserver.workspaceConfig->empty() + return {} + endif + if !configItem->has_key('section') || configItem.section->empty() + return lspserver.workspaceConfig + endif + var config: dict<any> = lspserver.workspaceConfig + for part in configItem.section->split('\.') + if !config->has_key(part) + return {} + endif + config = config[part] + endfor + return config +enddef + +# Update semantic highlighting for buffer "bnr" +# Request: textDocument/semanticTokens/full or +# textDocument/semanticTokens/full/delta +def SemanticHighlightUpdate(lspserver: dict<any>, bnr: number) + if !lspserver.isSemanticTokensProvider + return + endif + + # Send the pending buffer changes to the language server + bnr->listener_flush() + + var method = 'textDocument/semanticTokens/full' + var params: dict<any> = { + textDocument: { + uri: util.LspBufnrToUri(bnr) + } + } + + # Should we send a semantic tokens delta request instead of a full request? + if lspserver.semanticTokensDelta + var prevResultId: string = '' + prevResultId = bnr->getbufvar('LspSemanticResultId', '') + if prevResultId != '' + # semantic tokens delta request + params.previousResultId = prevResultId + method ..= '/delta' + endif + endif + + var reply = lspserver.rpc(method, params) + + if reply->empty() || reply.result->empty() + return + endif + + semantichighlight.UpdateTokens(lspserver, bnr, reply.result) +enddef + +# Send a "workspace/didChangeConfiguration" notification to the language +# server. +def SendWorkspaceConfig(lspserver: dict<any>) + # Params: DidChangeConfigurationParams + var params = {settings: lspserver.workspaceConfig} + lspserver.sendNotification('workspace/didChangeConfiguration', params) +enddef + +# Send a file/document opened notification to the language server. +def TextdocDidOpen(lspserver: dict<any>, bnr: number, ftype: string): void + # Notification: 'textDocument/didOpen' + # Params: DidOpenTextDocumentParams + var params = { + textDocument: { + uri: util.LspBufnrToUri(bnr), + languageId: ftype, + # Use Vim 'changedtick' as the LSP document version number + version: bnr->getbufvar('changedtick'), + text: bnr->getbufline(1, '$')->join("\n") .. "\n" + } + } + lspserver.sendNotification('textDocument/didOpen', params) +enddef + +# Send a file/document closed notification to the language server. +def TextdocDidClose(lspserver: dict<any>, bnr: number): void + # Notification: 'textDocument/didClose' + # Params: DidCloseTextDocumentParams + var params = { + textDocument: { + uri: util.LspBufnrToUri(bnr) + } + } + lspserver.sendNotification('textDocument/didClose', params) +enddef + +# Send a file/document change notification to the language server. +# Params: DidChangeTextDocumentParams +def TextdocDidChange(lspserver: dict<any>, bnr: number, start: number, + end: number, added: number, + changes: list<dict<number>>): void + # Notification: 'textDocument/didChange' + # Params: DidChangeTextDocumentParams + + # var changeset: list<dict<any>> + + ##### FIXME: Sending specific buffer changes to the LSP server doesn't + ##### work properly as the computed line range numbers is not correct. + ##### For now, send the entire buffer content to LSP server. + # # Range + # for change in changes + # var lines: string + # var start_lnum: number + # var end_lnum: number + # var start_col: number + # var end_col: number + # if change.added == 0 + # # lines changed + # start_lnum = change.lnum - 1 + # end_lnum = change.end - 1 + # lines = getbufline(bnr, change.lnum, change.end - 1)->join("\n") .. "\n" + # start_col = 0 + # end_col = 0 + # elseif change.added > 0 + # # lines added + # start_lnum = change.lnum - 1 + # end_lnum = change.lnum - 1 + # start_col = 0 + # end_col = 0 + # lines = getbufline(bnr, change.lnum, change.lnum + change.added - 1)->join("\n") .. "\n" + # else + # # lines removed + # start_lnum = change.lnum - 1 + # end_lnum = change.lnum + (-change.added) - 1 + # start_col = 0 + # end_col = 0 + # lines = '' + # endif + # var range: dict<dict<number>> = {'start': {'line': start_lnum, 'character': start_col}, 'end': {'line': end_lnum, 'character': end_col}} + # changeset->add({'range': range, 'text': lines}) + # endfor + + var params = { + textDocument: { + uri: util.LspBufnrToUri(bnr), + # Use Vim 'changedtick' as the LSP document version number + version: bnr->getbufvar('changedtick') + }, + contentChanges: [ + {text: bnr->getbufline(1, '$')->join("\n") .. "\n"} + ] + } + lspserver.sendNotification('textDocument/didChange', params) +enddef + +# Return the current cursor position as a LSP position. +# find_ident will search for a identifier in front of the cursor, just like +# CTRL-] and c_CTRL-R_CTRL-W does. +# +# LSP line and column numbers start from zero, whereas Vim line and column +# numbers start from one. The LSP column number is the character index in the +# line and not the byte index in the line. +def GetPosition(lspserver: dict<any>, find_ident: bool): dict<number> + var lnum: number = line('.') - 1 + var col: number = charcol('.') - 1 + var line = getline('.') + + if find_ident + # 1. skip to start of identifier + while line[col] != '' && line[col] !~ '\k' + col = col + 1 + endwhile + + # 2. back up to start of identifier + while col > 0 && line[col - 1] =~ '\k' + col = col - 1 + endwhile + endif + + # Compute character index counting composing characters as separate + # characters + var pos = {line: lnum, character: util.GetCharIdxWithCompChar(line, col)} + lspserver.encodePosition(bufnr(), pos) + + return pos +enddef + +# Return the current file name and current cursor position as a LSP +# TextDocumentPositionParams structure +def GetTextDocPosition(lspserver: dict<any>, find_ident: bool): dict<dict<any>> + # interface TextDocumentIdentifier + # interface Position + return {textDocument: {uri: util.LspFileToUri(@%)}, + position: lspserver.getPosition(find_ident)} +enddef + +# Get a list of completion items. +# Request: "textDocument/completion" +# Param: CompletionParams +def GetCompletion(lspserver: dict<any>, triggerKind_arg: number, triggerChar: string): void + # Check whether LSP server supports completion + if !lspserver.isCompletionProvider + util.ErrMsg('LSP server does not support completion') + return + endif + + var fname = @% + if fname->empty() + return + endif + + # interface CompletionParams + # interface TextDocumentPositionParams + var params = lspserver.getTextDocPosition(false) + # interface CompletionContext + params.context = {triggerKind: triggerKind_arg, triggerCharacter: triggerChar} + + lspserver.rpc_a('textDocument/completion', params, + completion.CompletionReply) +enddef + +# Get lazy properties for a completion item. +# Request: "completionItem/resolve" +# Param: CompletionItem +def ResolveCompletion(lspserver: dict<any>, item: dict<any>, sync: bool = false): dict<any> + # Check whether LSP server supports completion item resolve + if !lspserver.isCompletionResolveProvider + return {} + endif + + # interface CompletionItem + if sync + var reply = lspserver.rpc('completionItem/resolve', item) + if !reply->empty() && !reply.result->empty() + return reply.result + endif + else + lspserver.rpc_a('completionItem/resolve', item, + completion.CompletionResolveReply) + endif + return {} +enddef + +# Jump to or peek a symbol location. +# +# Send 'msg' to a LSP server and process the reply. 'msg' is one of the +# following: +# textDocument/definition +# textDocument/declaration +# textDocument/typeDefinition +# textDocument/implementation +# +# Process the LSP server reply and jump to the symbol location. Before +# jumping to the symbol location, save the current cursor position in the tag +# stack. +# +# If 'peekSymbol' is true, then display the symbol location in the preview +# window but don't jump to the symbol location. +# +# Result: Location | Location[] | LocationLink[] | null +def GotoSymbolLoc(lspserver: dict<any>, msg: string, peekSymbol: bool, + cmdmods: string, count: number) + var reply = lspserver.rpc(msg, lspserver.getTextDocPosition(true), false) + if reply->empty() || reply.result->empty() + var emsg: string + if msg == 'textDocument/declaration' + emsg = 'symbol declaration is not found' + elseif msg == 'textDocument/typeDefinition' + emsg = 'symbol type definition is not found' + elseif msg == 'textDocument/implementation' + emsg = 'symbol implementation is not found' + else + emsg = 'symbol definition is not found' + endif + + util.WarnMsg(emsg) + return + endif + + var result = reply.result + var location: dict<any> + if result->type() == v:t_list + if count == 0 + # When there are multiple symbol locations, and a specific one isn't + # requested with 'count', display the locations in a location list. + if result->len() > 1 + var title: string = '' + if msg == 'textDocument/declaration' + title = 'Declarations' + elseif msg == 'textDocument/typeDefinition' + title = 'Type Definitions' + elseif msg == 'textDocument/implementation' + title = 'Implementations' + else + title = 'Definitions' + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the symbol locations + result->map((_, loc) => { + lspserver.decodeLocation(loc) + return loc + }) + endif + + symbol.ShowLocations(lspserver, result, peekSymbol, title) + return + endif + endif + + # Select the location requested in 'count' + var idx = count - 1 + if idx >= result->len() + idx = result->len() - 1 + endif + location = result[idx] + else + location = result + endif + lspserver.decodeLocation(location) + + symbol.GotoSymbol(lspserver, location, peekSymbol, cmdmods) +enddef + +# Request: "textDocument/definition" +# Param: DefinitionParams +def GotoDefinition(lspserver: dict<any>, peek: bool, cmdmods: string, count: number) + # Check whether LSP server supports jumping to a definition + if !lspserver.isDefinitionProvider + util.ErrMsg('Jumping to a symbol definition is not supported') + return + endif + + # interface DefinitionParams + # interface TextDocumentPositionParams + GotoSymbolLoc(lspserver, 'textDocument/definition', peek, cmdmods, count) +enddef + +# Request: "textDocument/declaration" +# Param: DeclarationParams +def GotoDeclaration(lspserver: dict<any>, peek: bool, cmdmods: string, count: number) + # Check whether LSP server supports jumping to a declaration + if !lspserver.isDeclarationProvider + util.ErrMsg('Jumping to a symbol declaration is not supported') + return + endif + + # interface DeclarationParams + # interface TextDocumentPositionParams + GotoSymbolLoc(lspserver, 'textDocument/declaration', peek, cmdmods, count) +enddef + +# Request: "textDocument/typeDefinition" +# Param: TypeDefinitionParams +def GotoTypeDef(lspserver: dict<any>, peek: bool, cmdmods: string, count: number) + # Check whether LSP server supports jumping to a type definition + if !lspserver.isTypeDefinitionProvider + util.ErrMsg('Jumping to a symbol type definition is not supported') + return + endif + + # interface TypeDefinitionParams + # interface TextDocumentPositionParams + GotoSymbolLoc(lspserver, 'textDocument/typeDefinition', peek, cmdmods, count) +enddef + +# Request: "textDocument/implementation" +# Param: ImplementationParams +def GotoImplementation(lspserver: dict<any>, peek: bool, cmdmods: string, count: number) + # Check whether LSP server supports jumping to a implementation + if !lspserver.isImplementationProvider + util.ErrMsg('Jumping to a symbol implementation is not supported') + return + endif + + # interface ImplementationParams + # interface TextDocumentPositionParams + GotoSymbolLoc(lspserver, 'textDocument/implementation', peek, cmdmods, count) +enddef + +# Request: "textDocument/switchSourceHeader" +# Param: TextDocumentIdentifier +# Clangd specific extension +def SwitchSourceHeader(lspserver: dict<any>) + var param = { + uri: util.LspFileToUri(@%) + } + var reply = lspserver.rpc('textDocument/switchSourceHeader', param) + if reply->empty() || reply.result->empty() + util.WarnMsg('Source/Header file is not found') + return + endif + + # process the 'textDocument/switchSourceHeader' reply from the LSP server + # Result: URI | null + var fname = util.LspUriToFile(reply.result) + # TODO: Add support for cmd modifiers + if (&modified && !&hidden) || &buftype != '' + # if the current buffer has unsaved changes and 'hidden' is not set, + # or if the current buffer is a special buffer, then ask to save changes + exe $'confirm edit {fname}' + else + exe $'edit {fname}' + endif +enddef + +# get symbol signature help. +# Request: "textDocument/signatureHelp" +# Param: SignatureHelpParams +def ShowSignature(lspserver: dict<any>): void + # Check whether LSP server supports signature help + if !lspserver.isSignatureHelpProvider + util.ErrMsg('LSP server does not support signature help') + return + endif + + # interface SignatureHelpParams + # interface TextDocumentPositionParams + var params = lspserver.getTextDocPosition(false) + lspserver.rpc_a('textDocument/signatureHelp', params, + signature.SignatureHelp) +enddef + +# Send a file/document saved notification to the language server +def DidSaveFile(lspserver: dict<any>, bnr: number): void + # Check whether the LSP server supports the didSave notification + if !lspserver.supportsDidSave + # LSP server doesn't support text document synchronization + return + endif + + # Notification: 'textDocument/didSave' + # Params: DidSaveTextDocumentParams + var params: dict<any> = {textDocument: {uri: util.LspBufnrToUri(bnr)}} + + if lspserver.caps.textDocumentSync->type() == v:t_dict + && lspserver.caps.textDocumentSync->has_key('save') + if lspserver.caps.textDocumentSync.save->type() == v:t_dict + && lspserver.caps.textDocumentSync.save->has_key('includeText') + && lspserver.caps.textDocumentSync.save.includeText + params.text = bnr->getbufline(1, '$')->join("\n") .. "\n" + endif + endif + + lspserver.sendNotification('textDocument/didSave', params) +enddef + +# get the hover information +# Request: "textDocument/hover" +# Param: HoverParams +def ShowHoverInfo(lspserver: dict<any>, cmdmods: string): void + # Check whether LSP server supports getting hover information. + # caps->hoverProvider can be a "boolean" or "HoverOptions" + if !lspserver.isHoverProvider + return + endif + + # interface HoverParams + # interface TextDocumentPositionParams + var params = lspserver.getTextDocPosition(false) + lspserver.rpc_a('textDocument/hover', params, (_, reply) => { + hover.HoverReply(lspserver, reply, cmdmods) + }) +enddef + +# Request: "textDocument/references" +# Param: ReferenceParams +def ShowReferences(lspserver: dict<any>, peek: bool): void + # Check whether LSP server supports getting reference information + if !lspserver.isReferencesProvider + util.ErrMsg('LSP server does not support showing references') + return + endif + + # interface ReferenceParams + # interface TextDocumentPositionParams + var param: dict<any> + param = lspserver.getTextDocPosition(true) + param.context = {includeDeclaration: true} + var reply = lspserver.rpc('textDocument/references', param) + + # Result: Location[] | null + if reply->empty() || reply.result->empty() + util.WarnMsg('No references found') + return + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the reference locations + reply.result->map((_, loc) => { + lspserver.decodeLocation(loc) + return loc + }) + endif + + symbol.ShowLocations(lspserver, reply.result, peek, 'Symbol References') +enddef + +# process the 'textDocument/documentHighlight' reply from the LSP server +# Result: DocumentHighlight[] | null +def DocHighlightReply(lspserver: dict<any>, docHighlightReply: any, + bnr: number, cmdmods: string): void + if docHighlightReply->empty() + if cmdmods !~ 'silent' + util.WarnMsg($'No highlight for the current position') + endif + return + endif + + for docHL in docHighlightReply + lspserver.decodeRange(bnr, docHL.range) + var kind: number = docHL->get('kind', 1) + var propName: string + if kind == 2 + # Read-access + propName = 'LspReadRef' + elseif kind == 3 + # Write-access + propName = 'LspWriteRef' + else + # textual reference + propName = 'LspTextRef' + endif + try + var docHL_range = docHL.range + var docHL_start = docHL_range.start + var docHL_end = docHL_range.end + prop_add(docHL_start.line + 1, + util.GetLineByteFromPos(bnr, docHL_start) + 1, + {end_lnum: docHL_end.line + 1, + end_col: util.GetLineByteFromPos(bnr, docHL_end) + 1, + bufnr: bnr, + type: propName}) + catch /E966\|E964/ # Invalid lnum | Invalid col + # Highlight replies arrive asynchronously and the document might have + # been modified in the mean time. As the reply is stale, ignore invalid + # line number and column number errors. + endtry + endfor +enddef + +# Request: "textDocument/documentHighlight" +# Param: DocumentHighlightParams +def DocHighlight(lspserver: dict<any>, bnr: number, cmdmods: string): void + # Check whether LSP server supports getting highlight information + if !lspserver.isDocumentHighlightProvider + util.ErrMsg('LSP server does not support document highlight') + return + endif + + # Send the pending buffer changes to the language server + bnr->listener_flush() + + # interface DocumentHighlightParams + # interface TextDocumentPositionParams + var params = lspserver.getTextDocPosition(false) + lspserver.rpc_a('textDocument/documentHighlight', params, (_, reply) => { + DocHighlightReply(lspserver, reply, bufnr(), cmdmods) + }) +enddef + +# Request: "textDocument/documentSymbol" +# Param: DocumentSymbolParams +def GetDocSymbols(lspserver: dict<any>, fname: string, showOutline: bool): void + # Check whether LSP server supports getting document symbol information + if !lspserver.isDocumentSymbolProvider + util.ErrMsg('LSP server does not support getting list of symbols') + return + endif + + # interface DocumentSymbolParams + # interface TextDocumentIdentifier + var params = {textDocument: {uri: util.LspFileToUri(fname)}} + lspserver.rpc_a('textDocument/documentSymbol', params, (_, reply) => { + if showOutline + symbol.DocSymbolOutline(lspserver, reply, fname) + else + symbol.DocSymbolPopup(lspserver, reply, fname) + endif + }) +enddef + +# Request: "textDocument/formatting" +# Param: DocumentFormattingParams +# or +# Request: "textDocument/rangeFormatting" +# Param: DocumentRangeFormattingParams +def TextDocFormat(lspserver: dict<any>, fname: string, rangeFormat: bool, + start_lnum: number, end_lnum: number) + # Check whether LSP server supports formatting documents + if !lspserver.isDocumentFormattingProvider + util.ErrMsg('LSP server does not support formatting documents') + return + endif + + var cmd: string + if rangeFormat + cmd = 'textDocument/rangeFormatting' + else + cmd = 'textDocument/formatting' + endif + + # interface DocumentFormattingParams + # interface TextDocumentIdentifier + # interface FormattingOptions + var fmtopts: dict<any> = { + tabSize: shiftwidth(), + insertSpaces: &expandtab ? true : false, + } + var param = { + textDocument: { + uri: util.LspFileToUri(fname) + }, + options: fmtopts + } + + if rangeFormat + var r: dict<dict<number>> = { + start: {line: start_lnum - 1, character: 0}, + end: {line: end_lnum - 1, character: charcol([end_lnum, '$']) - 1}} + param.range = r + endif + + var reply = lspserver.rpc(cmd, param) + + # result: TextEdit[] | null + + if reply->empty() || reply.result->empty() + # nothing to format + return + endif + + var bnr: number = fname->bufnr() + if bnr == -1 + # file is already removed + return + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the reference locations + reply.result->map((_, textEdit) => { + lspserver.decodeRange(bnr, textEdit.range) + return textEdit + }) + endif + + # interface TextEdit + # Apply each of the text edit operations + var save_cursor: list<number> = getcurpos() + textedit.ApplyTextEdits(bnr, reply.result) + save_cursor->setpos('.') +enddef + +# Request: "textDocument/prepareCallHierarchy" +def PrepareCallHierarchy(lspserver: dict<any>): dict<any> + # interface CallHierarchyPrepareParams + # interface TextDocumentPositionParams + var param: dict<any> + param = lspserver.getTextDocPosition(false) + var reply = lspserver.rpc('textDocument/prepareCallHierarchy', param) + if reply->empty() || reply.result->empty() + return {} + endif + + # Result: CallHierarchyItem[] | null + var choice: number = 1 + if reply.result->len() > 1 + var items: list<string> = ['Select a Call Hierarchy Item:'] + for i in reply.result->len()->range() + items->add(printf("%d. %s", i + 1, reply.result[i].name)) + endfor + choice = items->inputlist() + if choice < 1 || choice > items->len() + return {} + endif + endif + + return reply.result[choice - 1] +enddef + +# Request: "callHierarchy/incomingCalls" +def IncomingCalls(lspserver: dict<any>, fname: string) + # Check whether LSP server supports call hierarchy + if !lspserver.isCallHierarchyProvider + util.ErrMsg('LSP server does not support call hierarchy') + return + endif + + callhier.IncomingCalls(lspserver) +enddef + +def GetIncomingCalls(lspserver: dict<any>, item_arg: dict<any>): any + # Request: "callHierarchy/incomingCalls" + # Param: CallHierarchyIncomingCallsParams + var param = { + item: item_arg + } + var reply = lspserver.rpc('callHierarchy/incomingCalls', param) + if reply->empty() + return null + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the incoming call locations + var bnr = util.LspUriToBufnr(item_arg.uri) + reply.result->map((_, hierItem) => { + lspserver.decodeRange(bnr, hierItem.from.range) + return hierItem + }) + endif + + return reply.result +enddef + +# Request: "callHierarchy/outgoingCalls" +def OutgoingCalls(lspserver: dict<any>, fname: string) + # Check whether LSP server supports call hierarchy + if !lspserver.isCallHierarchyProvider + util.ErrMsg('LSP server does not support call hierarchy') + return + endif + + callhier.OutgoingCalls(lspserver) +enddef + +def GetOutgoingCalls(lspserver: dict<any>, item_arg: dict<any>): any + # Request: "callHierarchy/outgoingCalls" + # Param: CallHierarchyOutgoingCallsParams + var param = { + item: item_arg + } + var reply = lspserver.rpc('callHierarchy/outgoingCalls', param) + if reply->empty() + return null + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the outgoing call locations + var bnr = util.LspUriToBufnr(item_arg.uri) + reply.result->map((_, hierItem) => { + lspserver.decodeRange(bnr, hierItem.to.range) + return hierItem + }) + endif + + return reply.result +enddef + +# Request: "textDocument/inlayHint" +# Inlay hints. +def InlayHintsShow(lspserver: dict<any>, bnr: number) + # Check whether LSP server supports type hierarchy + if !lspserver.isInlayHintProvider && !lspserver.isClangdInlayHintsProvider + util.ErrMsg('LSP server does not support inlay hint') + return + endif + + # Send the pending buffer changes to the language server + bnr->listener_flush() + + var binfo = bnr->getbufinfo() + if binfo->empty() + return + endif + var lastlnum = binfo[0].linecount + var lastline = bnr->getbufline('$') + var lastcol = 1 + if !lastline->empty() && !lastline[0]->empty() + lastcol = lastline[0]->strchars() + endif + var param = { + textDocument: {uri: util.LspBufnrToUri(bnr)}, + range: + { + start: {line: 0, character: 0}, + end: {line: lastlnum - 1, character: lastcol - 1} + } + } + + lspserver.encodeRange(bnr, param.range) + + var msg: string + if lspserver.isClangdInlayHintsProvider + # clangd-style inlay hints + msg = 'clangd/inlayHints' + else + msg = 'textDocument/inlayHint' + endif + var reply = lspserver.rpc_a(msg, param, (_, reply) => { + inlayhints.InlayHintsReply(lspserver, bnr, reply) + }) +enddef + +def DecodeTypeHierarchy(lspserver: dict<any>, isSuper: bool, typeHier: dict<any>) + if !lspserver.needOffsetEncoding + return + endif + var bnr = util.LspUriToBufnr(typeHier.uri) + lspserver.decodeRange(bnr, typeHier.range) + lspserver.decodeRange(bnr, typeHier.selectionRange) + var subType: list<dict<any>> + if isSuper + subType = typeHier->get('parents', []) + else + subType = typeHier->get('children', []) + endif + if !subType->empty() + # Decode the position encoding in all the type hierarchy items + subType->map((_, typeHierItem) => { + DecodeTypeHierarchy(lspserver, isSuper, typeHierItem) + return typeHierItem + }) + endif +enddef + +# Request: "textDocument/typehierarchy" +# Support the clangd version of type hierarchy retrieval method. +# The method described in the LSP 3.17.0 standard is not supported as clangd +# doesn't support that method. +def TypeHierarchy(lspserver: dict<any>, direction: number) + # Check whether LSP server supports type hierarchy + if !lspserver.isTypeHierarchyProvider + util.ErrMsg('LSP server does not support type hierarchy') + return + endif + + # interface TypeHierarchy + # interface TextDocumentPositionParams + var param: dict<any> + param = lspserver.getTextDocPosition(false) + # 0: children, 1: parent, 2: both + param.direction = direction + param.resolve = 5 + var reply = lspserver.rpc('textDocument/typeHierarchy', param) + if reply->empty() || reply.result->empty() + util.WarnMsg('No type hierarchy available') + return + endif + + var isSuper = (direction == 1) + + DecodeTypeHierarchy(lspserver, isSuper, reply.result) + + typehier.ShowTypeHierarchy(lspserver, isSuper, reply.result) +enddef + +# Decode the ranges in "WorkspaceEdit" +def DecodeWorkspaceEdit(lspserver: dict<any>, workspaceEdit: dict<any>) + if !lspserver.needOffsetEncoding + return + endif + if workspaceEdit->has_key('changes') + for [uri, changes] in workspaceEdit.changes->items() + var bnr: number = util.LspUriToBufnr(uri) + if bnr <= 0 + continue + endif + # Decode the position encoding in all the text edit locations + changes->map((_, textEdit) => { + lspserver.decodeRange(bnr, textEdit.range) + return textEdit + }) + endfor + endif + + if workspaceEdit->has_key('documentChanges') + for change in workspaceEdit.documentChanges + if !change->has_key('kind') + var bnr: number = util.LspUriToBufnr(change.textDocument.uri) + if bnr <= 0 + continue + endif + # Decode the position encoding in all the text edit locations + change.edits->map((_, textEdit) => { + lspserver.decodeRange(bnr, textEdit.range) + return textEdit + }) + endif + endfor + endif +enddef + +# Request: "textDocument/rename" +# Param: RenameParams +def RenameSymbol(lspserver: dict<any>, newName: string) + # Check whether LSP server supports rename operation + if !lspserver.isRenameProvider + util.ErrMsg('LSP server does not support rename operation') + return + endif + + # interface RenameParams + # interface TextDocumentPositionParams + var param: dict<any> = {} + param = lspserver.getTextDocPosition(true) + param.newName = newName + + var reply = lspserver.rpc('textDocument/rename', param) + + # Result: WorkspaceEdit | null + if reply->empty() || reply.result->empty() + # nothing to rename + return + endif + + # result: WorkspaceEdit + DecodeWorkspaceEdit(lspserver, reply.result) + textedit.ApplyWorkspaceEdit(reply.result) +enddef + +# Decode the range in "CodeAction" +def DecodeCodeAction(lspserver: dict<any>, actionList: list<dict<any>>) + if !lspserver.needOffsetEncoding + return + endif + actionList->map((_, act) => { + if !act->has_key('disabled') && act->has_key('edit') + DecodeWorkspaceEdit(lspserver, act.edit) + endif + return act + }) +enddef + +# Request: "textDocument/codeAction" +# Param: CodeActionParams +def CodeAction(lspserver: dict<any>, fname_arg: string, line1: number, + line2: number, query: string) + # Check whether LSP server supports code action operation + if !lspserver.isCodeActionProvider + util.ErrMsg('LSP server does not support code action operation') + return + endif + + # interface CodeActionParams + var params: dict<any> = {} + var fname: string = fname_arg->fnamemodify(':p') + var bnr: number = fname_arg->bufnr() + var r: dict<dict<number>> = { + start: { + line: line1 - 1, + character: line1 == line2 ? util.GetCharIdxWithCompChar(getline('.'), charcol('.') - 1) : 0 + }, + end: { + line: line2 - 1, + character: util.GetCharIdxWithCompChar(getline(line2), charcol([line2, '$']) - 1) + } + } + lspserver.encodeRange(bnr, r) + params->extend({textDocument: {uri: util.LspFileToUri(fname)}, range: r}) + var d: list<dict<any>> = [] + for lnum in range(line1, line2) + var diagsInfo: list<dict<any>> = diag.GetDiagsByLine(bnr, lnum, lspserver)->deepcopy() + if lspserver.needOffsetEncoding + diagsInfo->map((_, di) => { + lspserver.encodeRange(bnr, di.range) + return di + }) + endif + d->extend(diagsInfo) + endfor + params->extend({context: {diagnostics: d, triggerKind: 1}}) + + var reply = lspserver.rpc('textDocument/codeAction', params) + + # Result: (Command | CodeAction)[] | null + if reply->empty() || reply.result->empty() + # no action can be performed + util.WarnMsg('No code action is available') + return + endif + + DecodeCodeAction(lspserver, reply.result) + + codeaction.ApplyCodeAction(lspserver, reply.result, query) +enddef + +# Request: "textDocument/codeLens" +# Param: CodeLensParams +def CodeLens(lspserver: dict<any>, fname: string) + # Check whether LSP server supports code lens operation + if !lspserver.isCodeLensProvider + util.ErrMsg('LSP server does not support code lens operation') + return + endif + + var params = {textDocument: {uri: util.LspFileToUri(fname)}} + var reply = lspserver.rpc('textDocument/codeLens', params) + if reply->empty() || reply.result->empty() + util.WarnMsg($'No code lens actions found for the current file') + return + endif + + var bnr = fname->bufnr() + + # Decode the position encoding in all the code lens items + if lspserver.needOffsetEncoding + reply.result->map((_, codeLensItem) => { + lspserver.decodeRange(bnr, codeLensItem.range) + return codeLensItem + }) + endif + + codelens.ProcessCodeLens(lspserver, bnr, reply.result) +enddef + +# Request: "codeLens/resolve" +# Param: CodeLens +def ResolveCodeLens(lspserver: dict<any>, bnr: number, + codeLens: dict<any>): dict<any> + if !lspserver.isCodeLensResolveProvider + return {} + endif + + if lspserver.needOffsetEncoding + lspserver.encodeRange(bnr, codeLens.range) + endif + + var reply = lspserver.rpc('codeLens/resolve', codeLens) + if reply->empty() + return {} + endif + + var codeLensItem: dict<any> = reply.result + + # Decode the position encoding in the code lens item + if lspserver.needOffsetEncoding + lspserver.decodeRange(bnr, codeLensItem.range) + endif + + return codeLensItem +enddef + +# List project-wide symbols matching query string +# Request: "workspace/symbol" +# Param: WorkspaceSymbolParams +def WorkspaceQuerySymbols(lspserver: dict<any>, query: string, firstCall: bool, cmdmods: string = '') + # Check whether the LSP server supports listing workspace symbols + if !lspserver.isWorkspaceSymbolProvider + util.ErrMsg('LSP server does not support listing workspace symbols') + return + endif + + # Param: WorkspaceSymbolParams + var param = { + query: query + } + var reply = lspserver.rpc('workspace/symbol', param) + if reply->empty() || reply.result->empty() + util.WarnMsg($'Symbol "{query}" is not found') + return + endif + + var symInfo: list<dict<any>> = reply.result + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the symbol locations + symInfo->map((_, sym) => { + if sym->has_key('location') + lspserver.decodeLocation(sym.location) + endif + return sym + }) + endif + + if firstCall && symInfo->len() == 1 + # If there is only one symbol, then jump to the symbol location + var symLoc: dict<any> = symInfo[0]->get('location', {}) + if !symLoc->empty() + symbol.GotoSymbol(lspserver, symLoc, false, cmdmods) + endif + else + symbol.WorkspaceSymbolPopup(lspserver, query, symInfo, cmdmods) + endif +enddef + +# Add a workspace folder to the language server. +def AddWorkspaceFolder(lspserver: dict<any>, dirName: string): void + if !lspserver.caps->has_key('workspace') + || !lspserver.caps.workspace->has_key('workspaceFolders') + || !lspserver.caps.workspace.workspaceFolders->has_key('supported') + || !lspserver.caps.workspace.workspaceFolders.supported + util.ErrMsg('LSP server does not support workspace folders') + return + endif + + if lspserver.workspaceFolders->index(dirName) != -1 + util.ErrMsg($'{dirName} is already part of this workspace') + return + endif + + # Notification: 'workspace/didChangeWorkspaceFolders' + # Params: DidChangeWorkspaceFoldersParams + var params = {event: {added: [dirName], removed: []}} + lspserver.sendNotification('workspace/didChangeWorkspaceFolders', params) + + lspserver.workspaceFolders->add(dirName) +enddef + +# Remove a workspace folder from the language server. +def RemoveWorkspaceFolder(lspserver: dict<any>, dirName: string): void + if !lspserver.caps->has_key('workspace') + || !lspserver.caps.workspace->has_key('workspaceFolders') + || !lspserver.caps.workspace.workspaceFolders->has_key('supported') + || !lspserver.caps.workspace.workspaceFolders.supported + util.ErrMsg('LSP server does not support workspace folders') + return + endif + + var idx: number = lspserver.workspaceFolders->index(dirName) + if idx == -1 + util.ErrMsg($'{dirName} is not currently part of this workspace') + return + endif + + # Notification: "workspace/didChangeWorkspaceFolders" + # Param: DidChangeWorkspaceFoldersParams + var params = {event: {added: [], removed: [dirName]}} + lspserver.sendNotification('workspace/didChangeWorkspaceFolders', params) + + lspserver.workspaceFolders->remove(idx) +enddef + +def DecodeSelectionRange(lspserver: dict<any>, bnr: number, selRange: dict<any>) + lspserver.decodeRange(bnr, selRange.range) + if selRange->has_key('parent') + DecodeSelectionRange(lspserver, bnr, selRange.parent) + endif +enddef + +# select the text around the current cursor location +# Request: "textDocument/selectionRange" +# Param: SelectionRangeParams +def SelectionRange(lspserver: dict<any>, fname: string) + # Check whether LSP server supports selection ranges + if !lspserver.isSelectionRangeProvider + util.ErrMsg('LSP server does not support selection ranges') + return + endif + + # clear the previous selection reply + lspserver.selection = {} + + # interface SelectionRangeParams + # interface TextDocumentIdentifier + var param = { + textDocument: { + uri: util.LspFileToUri(fname) + }, + positions: [lspserver.getPosition(false)] + } + var reply = lspserver.rpc('textDocument/selectionRange', param) + + if reply->empty() || reply.result->empty() + return + endif + + # Decode the position encoding in all the selection range items + if lspserver.needOffsetEncoding + var bnr = fname->bufnr() + reply.result->map((_, selItem) => { + DecodeSelectionRange(lspserver, bnr, selItem) + return selItem + }) + endif + + selection.SelectionStart(lspserver, reply.result) +enddef + +# Expand the previous selection or start a new one +def SelectionExpand(lspserver: dict<any>) + # Check whether LSP server supports selection ranges + if !lspserver.isSelectionRangeProvider + util.ErrMsg('LSP server does not support selection ranges') + return + endif + + selection.SelectionModify(lspserver, true) +enddef + +# Shrink the previous selection or start a new one +def SelectionShrink(lspserver: dict<any>) + # Check whether LSP server supports selection ranges + if !lspserver.isSelectionRangeProvider + util.ErrMsg('LSP server does not support selection ranges') + return + endif + + selection.SelectionModify(lspserver, false) +enddef + +# fold the entire document +# Request: "textDocument/foldingRange" +# Param: FoldingRangeParams +def FoldRange(lspserver: dict<any>, fname: string) + # Check whether LSP server supports fold ranges + if !lspserver.isFoldingRangeProvider + util.ErrMsg('LSP server does not support folding') + return + endif + + # interface FoldingRangeParams + # interface TextDocumentIdentifier + var params = {textDocument: {uri: util.LspFileToUri(fname)}} + var reply = lspserver.rpc('textDocument/foldingRange', params) + if reply->empty() || reply.result->empty() + return + endif + + # result: FoldingRange[] + var end_lnum: number + var last_lnum: number = line('$') + for foldRange in reply.result + end_lnum = foldRange.endLine + 1 + if end_lnum < foldRange.startLine + 2 + end_lnum = foldRange.startLine + 2 + endif + exe $':{foldRange.startLine + 2}, {end_lnum}fold' + # Open all the folds, otherwise the subsequently created folds are not + # correct. + :silent! foldopen! + endfor + + if &foldcolumn == 0 + :setlocal foldcolumn=2 + endif +enddef + +# process the 'workspace/executeCommand' reply from the LSP server +# Result: any | null +def WorkspaceExecuteReply(lspserver: dict<any>, execReply: any) + # Nothing to do for the reply +enddef + +# Request the LSP server to execute a command +# Request: workspace/executeCommand +# Params: ExecuteCommandParams +def ExecuteCommand(lspserver: dict<any>, cmd: dict<any>) + # Need to check for lspserver.caps.executeCommandProvider? + var params: dict<any> = {} + params.command = cmd.command + if cmd->has_key('arguments') + params.arguments = cmd.arguments + endif + + lspserver.rpc_a('workspace/executeCommand', params, WorkspaceExecuteReply) +enddef + +# Display the LSP server capabilities (received during the initialization +# stage). +def GetCapabilities(lspserver: dict<any>): list<string> + var l = [] + var heading = $"'{lspserver.path}' Language Server Capabilities" + var underlines = repeat('=', heading->len()) + l->extend([heading, underlines]) + for k in lspserver.caps->keys()->sort() + l->add($'{k}: {lspserver.caps[k]->string()}') + endfor + return l +enddef + +# Display the LSP server initialize request and result +def GetInitializeRequest(lspserver: dict<any>): list<string> + var l = [] + var heading = $"'{lspserver.path}' Language Server Initialize Request" + var underlines = repeat('=', heading->len()) + l->extend([heading, underlines]) + if lspserver->has_key('rpcInitializeRequest') + for k in lspserver.rpcInitializeRequest->keys()->sort() + l->add($'{k}: {lspserver.rpcInitializeRequest[k]->string()}') + endfor + endif + return l +enddef + +# Store a log or trace message received from the language server. +def AddMessage(lspserver: dict<any>, msgType: string, newMsg: string) + # A single message may contain multiple lines separate by newline + var msgs = newMsg->split("\n") + lspserver.messages->add($'{strftime("%m/%d/%y %T")}: [{msgType}]: {msgs[0]}') + lspserver.messages->extend(msgs[1 : ]) + # Keep only the last 500 messages to reduce the memory usage + if lspserver.messages->len() >= 600 + lspserver.messages = lspserver.messages[-500 : ] + endif +enddef + +# Display the log messages received from the LSP server (window/logMessage) +def GetMessages(lspserver: dict<any>): list<string> + if lspserver.messages->empty() + return [$'No messages received from "{lspserver.name}" server'] + endif + + var l = [] + var heading = $"'{lspserver.path}' Language Server Messages" + var underlines = repeat('=', heading->len()) + l->extend([heading, underlines]) + l->extend(lspserver.messages) + return l +enddef + +# Send a 'textDocument/definition' request to the LSP server to get the +# location where the symbol under the cursor is defined and return a list of +# Dicts in a format accepted by the 'tagfunc' option. +# Returns null if the LSP server doesn't support getting the location of a +# symbol definition or the symbol is not defined. +def TagFunc(lspserver: dict<any>, pat: string, flags: string, info: dict<any>): any + # Check whether LSP server supports getting the location of a definition + if !lspserver.isDefinitionProvider + return null + endif + + # interface DefinitionParams + # interface TextDocumentPositionParams + var reply = lspserver.rpc('textDocument/definition', + lspserver.getTextDocPosition(false)) + if reply->empty() || reply.result->empty() + return null + endif + + var taglocations: list<dict<any>> + if reply.result->type() == v:t_list + taglocations = reply.result + else + taglocations = [reply.result] + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the reference locations + taglocations->map((_, loc) => { + lspserver.decodeLocation(loc) + return loc + }) + endif + + return symbol.TagFunc(lspserver, taglocations, pat) +enddef + +# Returns unique ID used for identifying the various servers +var UniqueServerIdCounter = 0 +def GetUniqueServerId(): number + UniqueServerIdCounter = UniqueServerIdCounter + 1 + return UniqueServerIdCounter +enddef + +export def NewLspServer(serverParams: dict<any>): dict<any> + var lspserver: dict<any> = { + id: GetUniqueServerId(), + name: serverParams.name, + path: serverParams.path, + args: serverParams.args->deepcopy(), + running: false, + ready: false, + job: v:none, + data: '', + nextID: 1, + caps: {}, + requests: {}, + callHierarchyType: '', + completionTriggerChars: [], + customNotificationHandlers: serverParams.customNotificationHandlers->deepcopy(), + customRequestHandlers: serverParams.customRequestHandlers->deepcopy(), + debug: serverParams.debug, + features: serverParams.features->deepcopy(), + forceOffsetEncoding: serverParams.forceOffsetEncoding, + initializationOptions: serverParams.initializationOptions->deepcopy(), + messages: [], + needOffsetEncoding: false, + omniCompletePending: false, + peekSymbolFilePopup: -1, + peekSymbolPopup: -1, + processDiagHandler: serverParams.processDiagHandler, + rootSearchFiles: serverParams.rootSearch->deepcopy(), + runIfSearchFiles: serverParams.runIfSearch->deepcopy(), + runUnlessSearchFiles: serverParams.runUnlessSearch->deepcopy(), + selection: {}, + signaturePopup: -1, + syncInit: serverParams.syncInit, + traceLevel: serverParams.traceLevel, + typeHierFilePopup: -1, + typeHierPopup: -1, + workspaceConfig: serverParams.workspaceConfig->deepcopy(), + workspaceSymbolPopup: -1, + workspaceSymbolQuery: '' + } + lspserver.logfile = $'lsp-{lspserver.name}.log' + lspserver.errfile = $'lsp-{lspserver.name}.err' + + # Add the LSP server functions + lspserver->extend({ + startServer: function(StartServer, [lspserver]), + initServer: function(InitServer, [lspserver]), + stopServer: function(StopServer, [lspserver]), + shutdownServer: function(ShutdownServer, [lspserver]), + exitServer: function(ExitServer, [lspserver]), + setTrace: function(SetTrace, [lspserver]), + traceLog: function(TraceLog, [lspserver]), + errorLog: function(ErrorLog, [lspserver]), + nextReqID: function(NextReqID, [lspserver]), + createRequest: function(CreateRequest, [lspserver]), + createResponse: function(CreateResponse, [lspserver]), + sendResponse: function(SendResponse, [lspserver]), + sendMessage: function(SendMessage, [lspserver]), + sendNotification: function(SendNotification, [lspserver]), + rpc: function(Rpc, [lspserver]), + rpc_a: function(AsyncRpc, [lspserver]), + waitForResponse: function(WaitForResponse, [lspserver]), + processReply: function(handlers.ProcessReply, [lspserver]), + processNotif: function(handlers.ProcessNotif, [lspserver]), + processRequest: function(handlers.ProcessRequest, [lspserver]), + processMessages: function(handlers.ProcessMessages, [lspserver]), + encodePosition: function(offset.EncodePosition, [lspserver]), + decodePosition: function(offset.DecodePosition, [lspserver]), + encodeRange: function(offset.EncodeRange, [lspserver]), + decodeRange: function(offset.DecodeRange, [lspserver]), + encodeLocation: function(offset.EncodeLocation, [lspserver]), + decodeLocation: function(offset.DecodeLocation, [lspserver]), + getPosition: function(GetPosition, [lspserver]), + getTextDocPosition: function(GetTextDocPosition, [lspserver]), + featureEnabled: function(FeatureEnabled, [lspserver]), + textdocDidOpen: function(TextdocDidOpen, [lspserver]), + textdocDidClose: function(TextdocDidClose, [lspserver]), + textdocDidChange: function(TextdocDidChange, [lspserver]), + sendInitializedNotif: function(SendInitializedNotif, [lspserver]), + sendWorkspaceConfig: function(SendWorkspaceConfig, [lspserver]), + getCompletion: function(GetCompletion, [lspserver]), + resolveCompletion: function(ResolveCompletion, [lspserver]), + gotoDefinition: function(GotoDefinition, [lspserver]), + gotoDeclaration: function(GotoDeclaration, [lspserver]), + gotoTypeDef: function(GotoTypeDef, [lspserver]), + gotoImplementation: function(GotoImplementation, [lspserver]), + tagFunc: function(TagFunc, [lspserver]), + switchSourceHeader: function(SwitchSourceHeader, [lspserver]), + showSignature: function(ShowSignature, [lspserver]), + didSaveFile: function(DidSaveFile, [lspserver]), + hover: function(ShowHoverInfo, [lspserver]), + showReferences: function(ShowReferences, [lspserver]), + docHighlight: function(DocHighlight, [lspserver]), + getDocSymbols: function(GetDocSymbols, [lspserver]), + textDocFormat: function(TextDocFormat, [lspserver]), + prepareCallHierarchy: function(PrepareCallHierarchy, [lspserver]), + incomingCalls: function(IncomingCalls, [lspserver]), + getIncomingCalls: function(GetIncomingCalls, [lspserver]), + outgoingCalls: function(OutgoingCalls, [lspserver]), + getOutgoingCalls: function(GetOutgoingCalls, [lspserver]), + inlayHintsShow: function(InlayHintsShow, [lspserver]), + typeHierarchy: function(TypeHierarchy, [lspserver]), + renameSymbol: function(RenameSymbol, [lspserver]), + codeAction: function(CodeAction, [lspserver]), + codeLens: function(CodeLens, [lspserver]), + resolveCodeLens: function(ResolveCodeLens, [lspserver]), + workspaceQuery: function(WorkspaceQuerySymbols, [lspserver]), + addWorkspaceFolder: function(AddWorkspaceFolder, [lspserver]), + removeWorkspaceFolder: function(RemoveWorkspaceFolder, [lspserver]), + selectionRange: function(SelectionRange, [lspserver]), + selectionExpand: function(SelectionExpand, [lspserver]), + selectionShrink: function(SelectionShrink, [lspserver]), + foldRange: function(FoldRange, [lspserver]), + executeCommand: function(ExecuteCommand, [lspserver]), + workspaceConfigGet: function(WorkspaceConfigGet, [lspserver]), + semanticHighlightUpdate: function(SemanticHighlightUpdate, [lspserver]), + getCapabilities: function(GetCapabilities, [lspserver]), + getInitializeRequest: function(GetInitializeRequest, [lspserver]), + addMessage: function(AddMessage, [lspserver]), + getMessages: function(GetMessages, [lspserver]) + }) + + return lspserver +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/markdown.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/markdown.vim new file mode 100644 index 0000000..fd73cee --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/markdown.vim @@ -0,0 +1,757 @@ +vim9script + +# Markdown parser +# Refer to https://github.github.com/gfm/ +# for the GitHub Flavored Markdown specification. + +# TODO: different highlight for different heading level +# TODO: links +# TODO: pretty table + + +# Container blocks +var block_quote = '^ \{,3\}\zs> \=' +var list_marker = '[-+*]\|[0-9]\{1,9}[.)]' +var list_item = $'^\%({list_marker}\)\ze\s*$\|^ \{{,3}}\zs\%({list_marker}\) \{{1,4}}\ze\S\|^ \{{,3}}\zs\%({list_marker}\) \{{5}}\ze\s*\S' +# pattern to match list items +export var list_pattern = $'^ *\%({list_marker}\) *' + + +# Leaf blocks +var blank_line = '^\s*$' +var thematic_break = '^ \{,3\}\([-_*]\)\%(\s*\1\)\{2,\}\s*$' +var code_fence = '^ \{,3\}\(`\{3,\}\|\~\{3,\}\)\s*\(\S*\)' +var code_indent = '^ \{4\}\zs\s*\S.*' +var paragraph = '^\s*\zs\S.\{-}\s*\ze$' + +var atx_heading = '^ \{,3}\zs\(#\{1,6}\) \s*\(.\{-}\)\s*\ze\%( #\{1,}\s*\)\=$' +var setext_heading = '^ \{,3}\zs\%(=\{1,}\|-\{1,}\)\ze *$' +var setext_heading_level = {"=": 1, "-": 2} + +var table_delimiter = '^|\=\zs *:\=-\{1,}:\= *\%(| *:\=-\{1,}:\= *\)*\ze|\=$' + +var punctuation = "[!\"#$%&'()*+,-./:;<=>?@[\\\\\\\]^_`{|}~]" + +# Setting text properties +highlight LspBold term=bold cterm=bold gui=bold +highlight LspItalic term=italic cterm=italic gui=italic +highlight LspStrikeThrough term=strikethrough cterm=strikethrough gui=strikethrough +prop_type_add('LspMarkdownBold', {highlight: 'LspBold'}) +prop_type_add('LspMarkdownItalic', {highlight: 'LspItalic'}) +prop_type_add('LspMarkdownStrikeThrough', {highlight: 'LspStrikeThrough'}) +prop_type_add('LspMarkdownHeading', {highlight: 'Function'}) +prop_type_add('LspMarkdownCode', {highlight: 'PreProc'}) +prop_type_add('LspMarkdownCodeBlock', {highlight: 'PreProc'}) +prop_type_add('LspMarkdownListMarker', {highlight: 'Special'}) +prop_type_add('LspMarkdownTableHeader', {highlight: 'Label'}) +prop_type_add('LspMarkdownTableMarker', {highlight: 'Special'}) + + +def GetMarkerProp(marker: string, col: number, ...opt: list<any>): dict<any> + if marker == 'list_item' + return { + type: 'LspMarkdownListMarker', + col: col, + length: opt[0] + } + elseif marker == 'code_block' + return { + type: 'LspMarkdownCodeBlock', + col: col, + end_lnum: opt[0], + end_col: opt[1] + } + elseif marker == 'heading' + return { + type: 'LspMarkdownHeading', + col: col, + length: opt[0] + } + elseif marker == 'table_header' + return { + type: 'LspMarkdownTableHeader', + col: col, + length: opt[0] + } + elseif marker == 'table_sep' + return { + type: 'LspMarkdownTableMarker', + col: col, + length: opt[0] + } + elseif marker == 'code_span' + return { + type: 'LspMarkdownCode', + col: col, + length: opt[0] + } + elseif marker == 'emphasis' + return { + type: 'LspMarkdownItalic', + col: col, + length: opt[0] + } + elseif marker == 'strong' + return { + type: 'LspMarkdownBold', + col: col, + length: opt[0] + } + elseif marker == 'strikethrough' + return { + type: 'LspMarkdownStrikeThrough', + col: col, + length: opt[0] + } + endif + return {} +enddef + +def GetCodeSpans(text: string): list<dict<any>> + var code_spans = [] + var pos = 0 + while pos < text->len() + var backtick = text->matchstrpos('\\*`', pos) + if backtick[1] < 0 + break + endif + if backtick[0]->len() % 2 == 0 + # escaped backtick + pos = backtick[2] + continue + endif + pos = backtick[2] - 1 + var code_span = text->matchstrpos('^\(`\+\)`\@!.\{-}`\@1<!\1`\@!', pos) + if code_span[1] < 0 + break + endif + var code_text = text->matchstrpos('^\(`\+\)\%(\zs \+\ze\|\([ \n]\=\)\zs.\{-}\S.\{-}\ze\2\)`\@1<!\1`\@!', pos) + code_spans->add({ + marker: '`', + start: [code_span[1], code_text[1]], + end: [code_text[2], code_span[2]] + }) + pos = code_span[2] + endwhile + return code_spans +enddef + +def Unescape(text: string, block_marker: string = ""): string + if block_marker == '`' + # line breaks do not occur inside code spans + return text->substitute('\n', ' ', 'g') + endif + # use 2 spaces instead of \ for hard line break + var result = text->substitute('\\\@<!\(\(\\\\\)*\)\\\n', '\1 \n', 'g') + # change soft line breaks + result = result->substitute(' \@<! \=\n', ' ', 'g') + # change hard line breaks + result = result->substitute(' \{2,}\n', '\n', 'g') + # replace non-breaking spaces with spaces + result = result->substitute(' ', ' ', 'g') + return result->substitute($'\\\({punctuation}\)', '\1', 'g') +enddef + +def GetNextInlineDelimiter(text: string, start_pos: number, end_pos: number): dict<any> + var pos = start_pos + while pos < text->len() + # search the first delimiter char + var delimiter = text->matchstrpos('\\*[_*~]', pos) + if delimiter[1] < 0 || delimiter[1] > end_pos + return {} + endif + if delimiter[0]->len() % 2 == 0 + # escaped delimiter char + pos = delimiter[2] + continue + endif + pos = delimiter[2] - 1 + var delimiter_run = text->matchstrpos( + $'{delimiter[0][-1]->substitute("\\([*~]\\)", "\\\\\\1", "g")}\+', + pos) + if delimiter_run[0][0] == '~' && delimiter_run[0]->len() > 2 + pos = delimiter_run[2] + continue + endif + var add_char = '' + if pos > 0 + pos -= 1 + add_char = '.' + endif + var delim_regex = delimiter_run[0]->substitute('\([*~]\)', '\\\1', 'g') + var is_left = text->match($'^{add_char}{delim_regex}\%(\s\|$\|{punctuation}\)\@!\|^{add_char}\%(\s\|^\|{punctuation}\)\@1<={delim_regex}{punctuation}', pos) >= 0 + var is_right = text->match($'^{add_char}\%(\s\|^\|{punctuation}\)\@1<!{delim_regex}\|^{add_char}{punctuation}\@1<={delim_regex}\%(\s\|$\|{punctuation}\)', pos) >= 0 + if !is_left && ! is_right + pos = delimiter_run[2] + continue + endif + if delimiter_run[0][0] == '_' + && text->match($'^\w{delimiter_run[0]}\w', pos) >= 0 + # intraword emphasis is disallowed + pos = delimiter_run[2] + continue + endif + return { + marker: delimiter_run[0], + start: [delimiter_run[1], delimiter_run[2]], + left: is_left, + right: is_right + } + endwhile + return {} +enddef + +def GetNextInlineBlock(text: string, blocks: list<any>, rel_pos: number): dict<any> + var result = { + text: '', + props: [] + } + var cur = blocks->remove(0) + var pos = cur.start[1] + while blocks->len() > 0 && cur.end[0] >= blocks[0].start[0] + result.text ..= Unescape(text->strpart(pos, blocks[0].start[0] - pos), cur.marker[0]) + # get nested block + var part = GetNextInlineBlock(text, blocks, rel_pos + result.text->len()) + result.text ..= part.text + result.props += part.props + pos = part.end_pos + endwhile + result.text ..= Unescape(text->strpart(pos, cur.end[0] - pos), cur.marker[0]) + # add props for current inline block + var prop_type = { + '`': 'code_span', + '_': 'emphasis', + '__': 'strong', + '*': 'emphasis', + '**': 'strong', + '~': 'strikethrough', + '~~': 'strikethrough' + } + result.props->insert(GetMarkerProp(prop_type[cur.marker], + rel_pos + 1, + result.text->len())) + result->extend({'end_pos': cur.end[1]}) + return result +enddef + +def ParseInlines(text: string, rel_pos: number = 0): dict<any> + var formatted = { + text: '', + props: [] + } + var code_spans = GetCodeSpans(text) + + var pos = 0 + var seq = [] + # search all emphasis + while pos < text->len() + var code_pos: list<number> + if code_spans->len() > 0 + code_pos = [code_spans[0].start[0], code_spans[0].end[1]] + if pos >= code_pos[0] + pos = code_pos[1] + seq->add(code_spans->remove(0)) + continue + endif + else + code_pos = [text->len(), text->len()] + endif + var delimiter = GetNextInlineDelimiter(text, pos, code_pos[0]) + if delimiter->empty() + pos = code_pos[1] + continue + endif + if delimiter.right + var idx = seq->len() - 1 + while idx >= 0 + if delimiter.marker[0] != seq[idx].marker[0] + || seq[idx]->has_key('end') + idx -= 1 + continue + endif + if delimiter.left || seq[idx].right + # check the sum rule + if (delimiter.marker->len() + seq[idx].marker->len()) % 3 == 0 + && (delimiter.marker->len() % 3 > 0 + || seq[idx].marker->len() % 3 > 0) + # not valid condition + idx -= 1 + continue + endif + endif + var marker_len = min([delimiter.marker->len(), + seq[idx].marker->len(), 2]) + if seq[idx].marker->len() > marker_len + var new_delim = { + marker: delimiter.marker[0]->repeat(marker_len), + start: [seq[idx].start[1] - marker_len, seq[idx].start[1]], + left: true, + right: false + } + seq[idx].marker = seq[idx].marker[: -1 - marker_len] + seq[idx].start[1] -= marker_len + seq[idx].right = false + idx += 1 + seq->insert(new_delim, idx) + endif + seq[idx]->extend({ + end: [delimiter.start[0], + delimiter.start[0] + marker_len]}) + # close all overlapped emphasis spans not closed + for i in range(seq->len() - 1, idx + 1, -1) + if !seq[i]->has_key('end') + seq->remove(i) + endif + endfor + if delimiter.marker->len() > marker_len + delimiter.start[0] += marker_len + else + delimiter.left = false + break + endif + idx -= 1 + endwhile + endif + if delimiter.left + seq->add(delimiter) + endif + pos = delimiter.start[1] + endwhile + while code_spans->len() > 0 + seq->add(code_spans->remove(0)) + endwhile + # remove all not closed delimiters + for i in range(seq->len() - 1, 0, -1) + if !seq[i]->has_key('end') + seq->remove(i) + endif + endfor + + # compose final text + pos = 0 + while seq->len() > 0 + if pos < seq[0].start[0] + formatted.text ..= Unescape(text->strpart(pos, seq[0].start[0] - pos)) + pos = seq[0].start[0] + endif + var inline = GetNextInlineBlock(text, seq, + rel_pos + formatted.text->len()) + formatted.text ..= inline.text + formatted.props += inline.props + pos = inline.end_pos + endwhile + if pos < text->len() + formatted.text ..= Unescape(text->strpart(pos)) + endif + return formatted +enddef + +# new open container block +def CreateContainerBlock(match: list<any>, start_lnum: number): dict<any> + if match[0][0] == '>' + return { + type: 'quote_block', + lnum: start_lnum, + indent: 0 + } + else + return { + type: 'list_item', + lnum: start_lnum, + marker: $' {match[0]->matchstr("\\S\\+")} ', + indent: match[2] + } + endif +enddef + +# new open leaf block +def CreateLeafBlock(block_type: string, line: string, ...opt: list<any>): dict<any> + if block_type == 'fenced_code' + var token = line->matchlist(code_fence) + return { + type: block_type, + fence: token[1], + language: token[2], + text: [] + } + elseif block_type == 'indented_code' + return { + type: block_type, + text: [line->matchstr(code_indent)] + } + elseif block_type == 'paragraph' + return { + type: block_type, + text: [line->matchstr(paragraph)] + } + elseif block_type == 'heading' + return { + type: block_type, + level: opt[0], + text: line + } + elseif block_type == 'table' + return { + type: block_type, + header: line, + delimiter: opt[0], + text: [] + } + endif + return {} +enddef + +def NeedBlankLine(prev: string, cur: string): bool + if prev == 'hr' || cur == 'hr' + return false + elseif prev == 'heading' || cur == 'heading' + return true + elseif prev == 'paragraph' && cur == 'paragraph' + return true + elseif prev != cur + return true + endif + return false +enddef + +def SplitLine(line: dict<any>, indent: number = 0): list<dict<any>> + var lines: list<dict<any>> = [] + var tokens: list<string> = line.text->split("\n", true) + if tokens->len() == 1 + lines->add(line) + return lines + endif + var props: list<dict<any>> = line.props + for cur_text in tokens + var cur_props: list<dict<any>> = [] + var next_props: list<dict<any>> = [] + var length: number = cur_text->len() + for prop in props + if prop.col + prop.length - 1 <= length + cur_props->add(prop) + elseif prop.col > length + prop.col -= length + 1 + next_props->add(prop) + else + var cur_length: number = length - prop.col + 1 + cur_props->add({ + type: prop.type, + col: prop.col, + length: cur_length + }) + prop.col = 1 + prop.length -= cur_length + 1 + next_props->add(prop) + endif + endfor + lines->add({ + text: cur_text, + props: cur_props + }) + props = next_props + endfor + return lines +enddef + +var last_block: string = '' + +def CloseBlocks(document: dict<list<any>>, blocks: list<dict<any>>, start: number = 0): void + if start >= blocks->len() + return + endif + var line: dict<any> = { + text: '', + props: [] + } + if !document.content->empty() && NeedBlankLine(last_block, blocks[0].type) + document.content->add({text: '', props: []}) + endif + last_block = blocks[0].type + + for i in start->range() + if blocks[i]->has_key('marker') + if blocks[i].marker =~ '\S' + line.props->add(GetMarkerProp('list_item', + line.text->len() + 1, + blocks[i].marker->len())) + line.text ..= blocks[i].marker + blocks[i].marker = ' '->repeat(blocks[i].marker->len()) + else + line.text ..= blocks[i].marker + endif + endif + endfor + for block in blocks->remove(start, -1) + if block.type =~ 'quote_block\|list_item' + if block->has_key('marker') + if block.marker =~ '\S' + line.props->add(GetMarkerProp('list_item', + line.text->len() + 1, + block.marker->len())) + line.text ..= block.marker + block.marker = ' '->repeat(block.marker->len()) + else + line.text ..= block.marker + endif + endif + else + # leaf block + if block.type =~ '_code' + if block.type == 'indented_code' + while !block.text->empty() && block.text[0] !~ '\S' + block.text->remove(0) + endwhile + while !block.text->empty() && block.text[-1] !~ '\S' + block.text->remove(-1) + endwhile + endif + if !block.text->empty() + var indent = ' '->repeat(line.text->len()) + var max_len = mapnew(block.text, (_, l) => l->len())->max() + var text = block.text->remove(0) + line.text ..= text + document.content->add(line) + var startline = document.content->len() + for l in block.text + document.content->add({text: indent .. l}) + endfor + if block->has_key('language') + && !globpath(&rtp, $'syntax/{block.language}.vim')->empty() + document.syntax->add({lang: block.language, + start: $'\%{startline}l\%{indent->len() + 1}c', + end: $'\%{document.content->len()}l$'}) + else + line.props->add(GetMarkerProp('code_block', + indent->len() + 1, + document.content->len(), + indent->len() + max_len + 1)) + endif + endif + elseif block.type == 'heading' + line.props->add(GetMarkerProp('heading', + line.text->len() + 1, + block.text->len(), + block.level)) + var format = ParseInlines(block.text, line.text->len()) + line.text ..= format.text + line.props += format.props + document.content += SplitLine(line) + elseif block.type == 'table' + var indent = line.text + var head = block.header->split('\\\@1<!|') + var col1 = head->remove(0) + var format = ParseInlines(col1, line.text->len()) + line.props->add(GetMarkerProp('table_header', + line.text->len() + 1, + format.text->len())) + line.text ..= format.text + line.props += format.props + for colx in head + format = ParseInlines(colx, line.text->len() + 1) + line.props->add(GetMarkerProp('table_sep', line.text->len() + 1, 1)) + line.props->add(GetMarkerProp('table_header', + line.text->len() + 2, + format.text->len())) + line.text ..= $'|{format.text}' + line.props += format.props + endfor + document.content->add(line) + var data = { + text: indent .. block.delimiter, + props: [GetMarkerProp('table_sep', + indent->len() + 1, + block.delimiter->len())] + } + document.content->add(data) + for row in block.text + data = { + text: indent, + props: [] + } + var cell = row->split('\\\@1<!|') + col1 = cell->remove(0) + format = ParseInlines(col1, data.text->len()) + data.text ..= format.text + data.props += format.props + for colx in cell + format = ParseInlines(colx, data.text->len() + 1) + data.props->add(GetMarkerProp('table_sep', + data.text->len() + 1, + 1)) + data.text ..= $'|{format.text}' + data.props += format.props + endfor + document.content->add(data) + endfor + elseif block.type == 'paragraph' + var indent = line.text->len() + var format = ParseInlines(block.text->join("\n")->substitute('\s\+$', '', ''), indent) + line.text ..= format.text + line.props += format.props + document.content += SplitLine(line, indent) + endif + endif + endfor +enddef + +def ExpandTabs(line: string): string + var block_marker = line->matchstrpos($'^ \{{,3}}>[ \t]\+\|^[ \t]*\%({list_marker}\)\=[ \t]*') + if block_marker[0]->match('\t') < 0 + return line + endif + var begin: string = "" + for char in block_marker[0] + if char == ' ' + begin ..= ' '->repeat(4 - (begin->len() % 4)) + else + begin ..= char + endif + endfor + return begin .. line[block_marker[2] :] +enddef + +export def ParseMarkdown(data: list<string>, width: number = 80): dict<list<any>> + var document: dict<list<any>> = {content: [], syntax: []} + var open_blocks: list<dict<any>> = [] + + for l in data + var line: string = ExpandTabs(l) + var cur = 0 + + # for each open block check if current line continue it + while cur < open_blocks->len() + if open_blocks[cur].type == 'quote_block' + var marker = line->matchstrpos(block_quote) + if marker[1] == -1 + break + endif + line = line->strpart(marker[2]) + elseif open_blocks[cur].type == 'list_item' + var marker = line->matchstrpos($'^ \{{{open_blocks[cur].indent}}}') + if marker[1] == -1 + break + endif + line = line->strpart(marker[2]) + elseif open_blocks[cur].type == 'fenced_code' + if line =~ $'^ \{{,3}}{open_blocks[cur].fence}{open_blocks[cur].fence[0]}* *$' + CloseBlocks(document, open_blocks, cur) + else + open_blocks[cur].text->add(line) + endif + cur = -1 + break + elseif open_blocks[cur].type == 'indented_code' + var marker = line->matchstrpos(code_indent) + if marker[1] >= 0 + open_blocks[cur].text->add(marker[0]) + cur = -1 + endif + break + elseif open_blocks[cur].type == 'paragraph' + if line =~ setext_heading + var marker = line->matchstrpos(setext_heading) + open_blocks->add(CreateLeafBlock( + 'heading', + open_blocks->remove(cur).text->join("\n")->substitute('\s\+$', '', ''), + setext_heading_level[marker[0][0]])) + CloseBlocks(document, open_blocks, cur) + cur = -1 + elseif open_blocks[cur].text->len() == 1 + # may be a table + var marker = line->matchstr(table_delimiter) + if !marker->empty() + if open_blocks[cur].text[0]->split('\\\@1<!|')->len() == marker->split('|')->len() + open_blocks->add(CreateLeafBlock( + 'table', + open_blocks->remove(cur).text[0], + marker)) + cur = -1 + endif + endif + endif + break + endif + cur += 1 + endwhile + + if cur < 0 + # the whole line is already consumed + continue + endif + + # a thematic break close all previous blocks + if line =~ thematic_break + CloseBlocks(document, open_blocks) + if &g:encoding == 'utf-8' + document.content->add({text: "\u2500"->repeat(width)}) + else + document.content->add({text: '-'->repeat(width)}) + endif + last_block = 'hr' + continue + endif + + # check for new container blocks + while true + var block = line->matchstrpos($'{block_quote}\|{list_item}') + if block[1] < 0 + break + endif + # close unmatched blocks + CloseBlocks(document, open_blocks, cur) + # start a new block + open_blocks->add(CreateContainerBlock(block, document->len())) + cur = open_blocks->len() + line = line->strpart(block[2]) + endwhile + + # check for leaf block + if line =~ code_fence + CloseBlocks(document, open_blocks, cur) + open_blocks->add(CreateLeafBlock('fenced_code', line)) + elseif line =~ blank_line + if open_blocks->empty() + continue + endif + if open_blocks[-1].type == 'paragraph' + CloseBlocks(document, open_blocks, min([cur, open_blocks->len() - 1])) + elseif open_blocks[-1].type == 'table' + CloseBlocks(document, open_blocks, open_blocks->len() - 1) + elseif open_blocks[-1].type =~ '_code' + open_blocks[-1].text->add(line) + endif + elseif line =~ code_indent + if open_blocks->empty() + open_blocks->add(CreateLeafBlock('indented_code', line)) + elseif open_blocks[-1].type =~ '_code' + open_blocks[-1].text->add(line->matchstr(code_indent)) + elseif open_blocks[-1].type == 'paragraph' + open_blocks[-1].text->add(line->matchstr(paragraph)) + else + CloseBlocks(document, open_blocks, cur) + open_blocks->add(CreateLeafBlock('indented_code', line)) + endif + elseif line =~ atx_heading + CloseBlocks(document, open_blocks, cur) + var token = line->matchlist(atx_heading) + open_blocks->add(CreateLeafBlock('heading', token[2], token[1]->len())) + CloseBlocks(document, open_blocks, cur) + elseif !open_blocks->empty() + if open_blocks[-1].type == 'table' + open_blocks[-1].text->add(line) + elseif open_blocks[-1].type == 'paragraph' + open_blocks[-1].text->add(line->matchstr(paragraph)) + else + CloseBlocks(document, open_blocks, cur) + open_blocks->add(CreateLeafBlock('paragraph', line)) + endif + else + open_blocks->add(CreateLeafBlock('paragraph', line)) + endif + endfor + + CloseBlocks(document, open_blocks) + return document +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/offset.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/offset.vim new file mode 100644 index 0000000..699964e --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/offset.vim @@ -0,0 +1,168 @@ +vim9script + +import './util.vim' + +# Functions for encoding and decoding the LSP position offsets. Language +# servers support either UTF-8 or UTF-16 or UTF-32 position offsets. The +# character related Vim functions use the UTF-32 position offset. The +# encoding used is negotiated during the language server initialization. + +# Encode the UTF-32 character offset in the LSP position "pos" to the encoding +# negotiated with the language server. +# +# Modifies in-place the UTF-32 offset in pos.character to a UTF-8 or UTF-16 or +# UTF-32 offset. +export def EncodePosition(lspserver: dict<any>, bnr: number, pos: dict<number>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 || bnr <= 0 + # LSP client plugin also uses utf-32 encoding + return + endif + + :silent! bnr->bufload() + var text = bnr->getbufline(pos.line + 1)->get(0, '') + if text->empty() + return + endif + + if lspserver.posEncoding == 16 + pos.character = text->utf16idx(pos.character, true, true) + else + pos.character = text->byteidxcomp(pos.character) + endif + endif +enddef + +# Decode the character offset in the LSP position "pos" using the encoding +# negotiated with the language server to a UTF-32 offset. +# +# Modifies in-place the UTF-8 or UTF-16 or UTF-32 offset in pos.character to a +# UTF-32 offset. +export def DecodePosition(lspserver: dict<any>, bnr: number, pos: dict<number>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 || bnr <= 0 + # LSP client plugin also uses utf-32 encoding + return + endif + + :silent! bnr->bufload() + var text = bnr->getbufline(pos.line + 1)->get(0, '') + # If the line is empty then don't decode the character position. + if text->empty() + return + endif + + # If the character position is out-of-bounds, then don't decode the + # character position. + var textLen = 0 + if lspserver.posEncoding == 16 + textLen = text->strutf16len(true) + else + textLen = text->strlen() + endif + + if pos.character > textLen + return + endif + + if pos.character == textLen + pos.character = text->strchars() + else + if lspserver.posEncoding == 16 + pos.character = text->charidx(pos.character, true, true) + else + pos.character = text->charidx(pos.character, true) + endif + endif + endif +enddef + +# Encode the start and end UTF-32 character offsets in the LSP range "range" +# to the encoding negotiated with the language server. +# +# Modifies in-place the UTF-32 offset in range.start.character and +# range.end.character to a UTF-8 or UTF-16 or UTF-32 offset. +export def EncodeRange(lspserver: dict<any>, bnr: number, + range: dict<dict<number>>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 + return + endif + + EncodePosition(lspserver, bnr, range.start) + EncodePosition(lspserver, bnr, range.end) + endif +enddef + +# Decode the start and end character offsets in the LSP range "range" to +# UTF-32 offsets. +# +# Modifies in-place the offset value in range.start.character and +# range.end.character to a UTF-32 offset. +export def DecodeRange(lspserver: dict<any>, bnr: number, + range: dict<dict<number>>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 + return + endif + + DecodePosition(lspserver, bnr, range.start) + DecodePosition(lspserver, bnr, range.end) + endif +enddef + +# Encode the range in the LSP position "location" to the encoding negotiated +# with the language server. +# +# Modifies in-place the UTF-32 offset in location.range to a UTF-8 or UTF-16 +# or UTF-32 offset. +export def EncodeLocation(lspserver: dict<any>, location: dict<any>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 + return + endif + + var bnr = 0 + if location->has_key('targetUri') + # LocationLink + bnr = util.LspUriToBufnr(location.targetUri) + if bnr > 0 + # We use only the "targetSelectionRange" item. The + # "originSelectionRange" and the "targetRange" items are not used. + lspserver.encodeRange(bnr, location.targetSelectionRange) + endif + else + # Location + bnr = util.LspUriToBufnr(location.uri) + if bnr > 0 + lspserver.encodeRange(bnr, location.range) + endif + endif + endif +enddef + +# Decode the range in the LSP location "location" to UTF-32. +# +# Modifies in-place the offset value in location.range to a UTF-32 offset. +export def DecodeLocation(lspserver: dict<any>, location: dict<any>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 + return + endif + + var bnr = 0 + if location->has_key('targetUri') + # LocationLink + bnr = util.LspUriToBufnr(location.targetUri) + # We use only the "targetSelectionRange" item. The + # "originSelectionRange" and the "targetRange" items are not used. + lspserver.decodeRange(bnr, location.targetSelectionRange) + else + # Location + bnr = util.LspUriToBufnr(location.uri) + lspserver.decodeRange(bnr, location.range) + endif + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/options.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/options.vim new file mode 100644 index 0000000..50697a3 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/options.vim @@ -0,0 +1,179 @@ +vim9script + +export const COMPLETIONMATCHER_CASE = 1 +export const COMPLETIONMATCHER_ICASE = 2 +export const COMPLETIONMATCHER_FUZZY = 3 + +# LSP plugin options +# User can override these by calling the OptionsSet() function. +export var lspOptions: dict<any> = { + # Enable ale diagnostics support. + # If true, diagnostics will be sent to ale, which will be responsible for + # showing them. + aleSupport: false, + + # In insert mode, complete the current symbol automatically + # Otherwise, use omni-completion + autoComplete: true, + + # In normal mode, highlight the current symbol automatically + autoHighlight: false, + + # Automatically highlight diagnostics messages from LSP server + autoHighlightDiags: true, + + # Automatically populate the location list with new diagnostics + autoPopulateDiags: false, + + # icase | fuzzy | case match for language servers that replies with a full + # list of completion items + completionMatcher: 'case', + + # Due to a bug in the earlier versions of Vim, cannot use the + # COMPLETIONMATCHER_CASE constant here for initialization. + completionMatcherValue: 1, + + # diagnostics signs options + diagSignErrorText: 'E>', + diagSignHintText: 'H>', + diagSignInfoText: 'I>', + diagSignWarningText: 'W>', + + # In insert mode, echo the current symbol signature in the status line + # instead of showing it in a popup + echoSignature: false, + + # hide disabled code actions + hideDisabledCodeActions: false, + + # Highlight diagnostics inline + highlightDiagInline: true, + + # Show the symbol documentation in the preview window instead of in a popup + hoverInPreview: false, + + # Don't print message when a configured language server is missing. + ignoreMissingServer: false, + + # Focus on the location list window after ":LspDiag show" + keepFocusInDiags: true, + + # Focus on the location list window after LspShowReferences + keepFocusInReferences: true, + + # If true, apply the LSP server supplied text edits after a completion. + # If a snippet plugin is going to apply the text edits, then set this to + # false to avoid applying the text edits twice. + completionTextEdit: true, + + # Alignment of virtual diagnostic text, when showDiagWithVirtualText is true + # Allowed values: 'above' | 'below' | 'after' (default is 'above') + diagVirtualTextAlign: 'above', + + # Wrapping of virtual diagnostic text, when showDiagWithVirtualText is true. + # Allowed valuse: 'default' | 'truncate' | 'wrap' (default is 'default') + diagVirtualTextWrap: 'default', + + # Suppress adding a new line on completion selection with <CR> + noNewlineInCompletion: false, + + # Omni-completion support. To keep backward compatibility, this option is + # set to null by default instead of false. + omniComplete: null, + + # Open outline window on right side + outlineOnRight: false, + + # Outline window size + outlineWinSize: 20, + + # Enable semantic highlighting + semanticHighlight: false, + + # Show diagnostic text in a balloon when the mouse is over the diagnostic + showDiagInBalloon: true, + + # Make diagnostics show in a popup instead of echoing + showDiagInPopup: true, + + # Suppress diagnostic hover from appearing when the mouse is over the line + # Show a diagnostic message on a status line + showDiagOnStatusLine: false, + + # Show a diagnostic messages using signs + showDiagWithSign: true, + + # Show a diagnostic messages with virtual text + showDiagWithVirtualText: false, + + # enable inlay hints + showInlayHints: false, + + # In insert mode, show the current symbol signature automatically + showSignature: true, + + # enable snippet completion support + snippetSupport: false, + + # enable SirVer/ultisnips completion support + ultisnipsSupport: false, + + # add to autocomplition list current buffer words + useBufferCompletion: false, + + # Use a floating menu to show the code action menu instead of asking for + # input + usePopupInCodeAction: false, + + # ShowReferences in a quickfix list instead of a location list` + useQuickfixForLocations: false, + + # enable hrsh7th/vim-vsnip completion support + vsnipSupport: false, + + # Limit the time autocompletion searches for words in current buffer (in + # milliseconds) + bufferCompletionTimeout: 100, + + # Enable support for custom completion kinds + customCompletionKinds: false, + + # A dictionary with all completion kinds that you want to customize + completionKinds: {}, + + # Filter duplicate completion items + filterCompletionDuplicates: false, +} + +# set the LSP plugin options from the user provided option values +export def OptionsSet(opts: dict<any>) + lspOptions->extend(opts) + if !has('patch-9.0.0178') + lspOptions.showInlayHints = false + endif + if !has('patch-9.0.1157') + lspOptions.showDiagWithVirtualText = false + endif + + # For faster comparison, convert the 'completionMatcher' option value from a + # string to a number. + if lspOptions.completionMatcher == 'icase' + lspOptions.completionMatcherValue = COMPLETIONMATCHER_ICASE + elseif lspOptions.completionMatcher == 'fuzzy' + lspOptions.completionMatcherValue = COMPLETIONMATCHER_FUZZY + else + lspOptions.completionMatcherValue = COMPLETIONMATCHER_CASE + endif + + # Apply the changed options + if exists('#LspCmds#User#LspOptionsChanged') + :doautocmd <nomodeline> LspCmds User LspOptionsChanged + endif +enddef + +# return a copy of the LSP plugin options +export def OptionsGet(): dict<any> + return lspOptions->deepcopy() +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/outline.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/outline.vim new file mode 100644 index 0000000..b414884 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/outline.vim @@ -0,0 +1,304 @@ +vim9script + +import './util.vim' +import './options.vim' as opt + +# jump to a symbol selected in the outline window +def OutlineJumpToSymbol() + var lnum: number = line('.') - 1 + if w:lspSymbols.lnumTable[lnum]->empty() + return + endif + + var slnum: number = w:lspSymbols.lnumTable[lnum].lnum + var scol: number = w:lspSymbols.lnumTable[lnum].col + var fname: string = w:lspSymbols.filename + + # Highlight the selected symbol + prop_remove({type: 'LspOutlineHighlight'}) + var col: number = getline('.')->match('\S') + 1 + prop_add(line('.'), col, {type: 'LspOutlineHighlight', + length: w:lspSymbols.lnumTable[lnum].name->len()}) + + # disable the outline window refresh + skipRefresh = true + + # If the file is already opened in a window, jump to it. Otherwise open it + # in another window + var wid: number = fname->bufwinid() + if wid == -1 + # Find a window showing a normal buffer and use it + for w in getwininfo() + if w.winid->getwinvar('&buftype')->empty() + wid = w.winid + wid->win_gotoid() + break + endif + endfor + if wid == -1 + var symWinid: number = win_getid() + :rightbelow vnew + # retain the fixed symbol window width + win_execute(symWinid, 'vertical resize 20') + endif + + exe $'edit {fname}' + else + wid->win_gotoid() + endif + [slnum, scol]->cursor() + skipRefresh = false +enddef + +# Skip refreshing the outline window. Used to prevent recursive updates to the +# outline window +var skipRefresh: bool = false + +export def SkipOutlineRefresh(): bool + return skipRefresh +enddef + +def AddSymbolText(bnr: number, + symbolTypeTable: dict<list<dict<any>>>, + pfx: string, + text: list<string>, + lnumMap: list<dict<any>>, + children: bool) + var prefix: string = pfx .. ' ' + for [symType, symbols] in symbolTypeTable->items() + if !children + # Add an empty line for the top level symbol types. For types in the + # children symbols, don't add the empty line. + text->extend(['']) + lnumMap->extend([{}]) + endif + if children + text->extend([prefix .. symType]) + prefix ..= ' ' + else + text->extend([symType]) + endif + lnumMap->extend([{}]) + for s in symbols + text->add(prefix .. s.name) + # remember the line number for the symbol + var s_start = s.range.start + var start_col: number = util.GetLineByteFromPos(bnr, s_start) + 1 + lnumMap->add({name: s.name, lnum: s_start.line + 1, + col: start_col}) + s.outlineLine = lnumMap->len() + if s->has_key('children') && !s.children->empty() + AddSymbolText(bnr, s.children, prefix, text, lnumMap, true) + endif + endfor + endfor +enddef + +# update the symbols displayed in the outline window +export def UpdateOutlineWindow(fname: string, + symbolTypeTable: dict<list<dict<any>>>, + symbolLineTable: list<dict<any>>) + var wid: number = bufwinid('LSP-Outline') + if wid == -1 + return + endif + + # stop refreshing the outline window recursively + skipRefresh = true + + var prevWinID: number = win_getid() + wid->win_gotoid() + + # if the file displayed in the outline window is same as the new file, then + # save and restore the cursor position + var symbols = wid->getwinvar('lspSymbols', {}) + var saveCursor: list<number> = [] + if !symbols->empty() && symbols.filename == fname + saveCursor = getcurpos() + endif + + :setlocal modifiable + :silent! :%d _ + setline(1, ['# LSP Outline View', + $'# {fname->fnamemodify(":t")} ({fname->fnamemodify(":h")})']) + + # First two lines in the buffer display comment information + var lnumMap: list<dict<any>> = [{}, {}] + var text: list<string> = [] + AddSymbolText(fname->bufnr(), symbolTypeTable, '', text, lnumMap, false) + text->append('$') + w:lspSymbols = {filename: fname, lnumTable: lnumMap, + symbolsByLine: symbolLineTable} + :setlocal nomodifiable + + if !saveCursor->empty() + saveCursor->setpos('.') + endif + + prevWinID->win_gotoid() + + # Highlight the current symbol + OutlineHighlightCurrentSymbol() + + # re-enable refreshing the outline window + skipRefresh = false +enddef + +def OutlineHighlightCurrentSymbol() + var fname: string = expand('%')->fnamemodify(':p') + if fname->empty() || &filetype->empty() + return + endif + + var wid: number = bufwinid('LSP-Outline') + if wid == -1 + return + endif + + # Check whether the symbols for this file are displayed in the outline + # window + var lspSymbols = wid->getwinvar('lspSymbols', {}) + if lspSymbols->empty() || lspSymbols.filename != fname + return + endif + + var symbolTable: list<dict<any>> = lspSymbols.symbolsByLine + + # line number to locate the symbol + var lnum: number = line('.') + + # Find the symbol for the current line number (binary search) + var left: number = 0 + var right: number = symbolTable->len() - 1 + var mid: number + while left <= right + mid = (left + right) / 2 + var r = symbolTable[mid].range + if lnum >= (r.start.line + 1) && lnum <= (r.end.line + 1) + break + endif + if lnum > (r.start.line + 1) + left = mid + 1 + else + right = mid - 1 + endif + endwhile + + # clear the highlighting in the outline window + var bnr: number = wid->winbufnr() + prop_remove({bufnr: bnr, type: 'LspOutlineHighlight'}) + + if left > right + # symbol not found + return + endif + + # Highlight the selected symbol + var col: number = + bnr->getbufline(symbolTable[mid].outlineLine)->get(0, '')->match('\S') + 1 + prop_add(symbolTable[mid].outlineLine, col, + {bufnr: bnr, type: 'LspOutlineHighlight', + length: symbolTable[mid].name->len()}) + + # if the line is not visible, then scroll the outline window to make the + # line visible + var wininfo = wid->getwininfo() + if symbolTable[mid].outlineLine < wininfo[0].topline + || symbolTable[mid].outlineLine > wininfo[0].botline + var cmd: string = $'call cursor({symbolTable[mid].outlineLine}, 1) | normal z.' + win_execute(wid, cmd) + endif +enddef + +# when the outline window is closed, do the cleanup +def OutlineCleanup() + # Remove the outline autocommands + :silent! autocmd_delete([{group: 'LSPOutline'}]) + + :silent! syntax clear LSPTitle +enddef + +# open the symbol outline window +export def OpenOutlineWindow(cmdmods: string, winsize: number) + var wid: number = bufwinid('LSP-Outline') + if wid != -1 + return + endif + + var prevWinID: number = win_getid() + + var mods = cmdmods + if mods->empty() + if opt.lspOptions.outlineOnRight + mods = ':vert :botright' + else + mods = ':vert :topleft' + endif + endif + + var size = winsize + if size == 0 + size = opt.lspOptions.outlineWinSize + endif + + execute $'{mods} :{size}new LSP-Outline' + :setlocal modifiable + :setlocal noreadonly + :silent! :%d _ + :setlocal buftype=nofile + :setlocal bufhidden=delete + :setlocal noswapfile nobuflisted + :setlocal nonumber norelativenumber fdc=0 nowrap winfixheight winfixwidth + :setlocal shiftwidth=2 + :setlocal foldenable + :setlocal foldcolumn=4 + :setlocal foldlevel=4 + :setlocal foldmethod=indent + setline(1, ['# File Outline']) + :nnoremap <silent> <buffer> q :quit<CR> + :nnoremap <silent> <buffer> <CR> :call <SID>OutlineJumpToSymbol()<CR> + :setlocal nomodifiable + + # highlight all the symbol types + :syntax keyword LSPTitle File Module Namespace Package Class Method Property + :syntax keyword LSPTitle Field Constructor Enum Interface Function Variable + :syntax keyword LSPTitle Constant String Number Boolean Array Object Key Null + :syntax keyword LSPTitle EnumMember Struct Event Operator TypeParameter + + if str2nr(&t_Co) > 2 + :highlight clear LSPTitle + :highlight default link LSPTitle Title + endif + + prop_type_add('LspOutlineHighlight', {bufnr: bufnr(), highlight: 'Search', override: true}) + + try + autocmd_delete([{group: 'LSPOutline', event: '*'}]) + catch /E367:/ + endtry + var acmds: list<dict<any>> + + # Refresh or add the symbols in a buffer to the outline window + acmds->add({event: 'BufEnter', + group: 'LSPOutline', + pattern: '*', + cmd: 'call g:LspRequestDocSymbols()'}) + + # when the outline window is closed, do the cleanup + acmds->add({event: 'BufUnload', + group: 'LSPOutline', + pattern: 'LSP-Outline', + cmd: 'OutlineCleanup()'}) + + # Highlight the current symbol when the cursor is not moved for sometime + acmds->add({event: 'CursorHold', + group: 'LSPOutline', + pattern: '*', + cmd: 'OutlineHighlightCurrentSymbol()'}) + + autocmd_add(acmds) + + prevWinID->win_gotoid() +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/selection.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/selection.vim new file mode 100644 index 0000000..1ef289b --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/selection.vim @@ -0,0 +1,107 @@ +vim9script + +# Functions related to handling LSP range selection. + +import './util.vim' + +# Visually (character-wise) select the text in a range +def SelectText(bnr: number, range: dict<dict<number>>) + var rstart = range.start + var rend = range.end + var start_col: number = util.GetLineByteFromPos(bnr, rstart) + 1 + var end_col: number = util.GetLineByteFromPos(bnr, rend) + + :normal! v"_y + setcharpos("'<", [0, rstart.line + 1, start_col, 0]) + setcharpos("'>", [0, rend.line + 1, end_col, 0]) + :normal! gv +enddef + +# Process the range selection reply from LSP server and start a new selection +export def SelectionStart(lspserver: dict<any>, sel: list<dict<any>>) + if sel->empty() + return + endif + + var bnr: number = bufnr() + + # save the reply for expanding or shrinking the selected text. + lspserver.selection = {bnr: bnr, selRange: sel[0], index: 0} + + SelectText(bnr, sel[0].range) +enddef + +# Locate the range in the LSP reply at a specified level +def GetSelRangeAtLevel(selRange: dict<any>, level: number): dict<any> + var r: dict<any> = selRange + var idx: number = 0 + + while idx != level + if !r->has_key('parent') + break + endif + r = r.parent + idx += 1 + endwhile + + return r +enddef + +# Returns true if the current visual selection matches a range in the +# selection reply from LSP. +def SelectionFromLSP(range: dict<any>, startpos: list<number>, endpos: list<number>): bool + var rstart = range.start + var rend = range.end + return startpos[1] == rstart.line + 1 + && endpos[1] == rend.line + 1 + && startpos[2] == rstart.character + 1 + && endpos[2] == rend.character +enddef + +# Expand or Shrink the current selection or start a new one. +export def SelectionModify(lspserver: dict<any>, expand: bool) + var fname: string = @% + var bnr: number = bufnr() + + if mode() == 'v' && !lspserver.selection->empty() + && lspserver.selection.bnr == bnr + && !lspserver.selection->empty() + # Already in characterwise visual mode and the previous LSP selection + # reply for this buffer is available. Modify the current selection. + + var selRange: dict<any> = lspserver.selection.selRange + var startpos: list<number> = getcharpos('v') + var endpos: list<number> = getcharpos('.') + var idx: number = lspserver.selection.index + + # Locate the range in the LSP reply for the current selection + selRange = GetSelRangeAtLevel(selRange, lspserver.selection.index) + + # If the current selection is present in the LSP reply, then modify the + # selection + if SelectionFromLSP(selRange.range, startpos, endpos) + if expand + # expand the selection + if selRange->has_key('parent') + selRange = selRange.parent + lspserver.selection.index = idx + 1 + endif + else + # shrink the selection + if idx > 0 + idx -= 1 + selRange = GetSelRangeAtLevel(lspserver.selection.selRange, idx) + lspserver.selection.index = idx + endif + endif + + SelectText(bnr, selRange.range) + return + endif + endif + + # Start a new selection + lspserver.selectionRange(fname) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/semantichighlight.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/semantichighlight.vim new file mode 100644 index 0000000..243aea9 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/semantichighlight.vim @@ -0,0 +1,264 @@ +vim9script + +# LSP semantic highlighting functions + +import './offset.vim' +import './options.vim' as opt +import './buffer.vim' as buf + +# Map token type names to higlight group/text property type names +var TokenTypeMap: dict<string> = { + 'namespace': 'LspSemanticNamespace', + 'type': 'LspSemanticType', + 'class': 'LspSemanticClass', + 'enum': 'LspSemanticEnum', + 'interface': 'LspSemanticInterface', + 'struct': 'LspSemanticStruct', + 'typeParameter': 'LspSemanticTypeParameter', + 'parameter': 'LspSemanticParameter', + 'variable': 'LspSemanticVariable', + 'property': 'LspSemanticProperty', + 'enumMember': 'LspSemanticEnumMember', + 'event': 'LspSemanticEvent', + 'function': 'LspSemanticFunction', + 'method': 'LspSemanticMethod', + 'macro': 'LspSemanticMacro', + 'keyword': 'LspSemanticKeyword', + 'modifier': 'LspSemanticModifier', + 'comment': 'LspSemanticComment', + 'string': 'LspSemanticString', + 'number': 'LspSemanticNumber', + 'regexp': 'LspSemanticRegexp', + 'operator': 'LspSemanticOperator', + 'decorator': 'LspSemanticDecorator' +} + +export def InitOnce() + # Define the default semantic token type highlight groups + hlset([ + {name: 'LspSemanticNamespace', default: true, linksto: 'Type'}, + {name: 'LspSemanticType', default: true, linksto: 'Type'}, + {name: 'LspSemanticClass', default: true, linksto: 'Type'}, + {name: 'LspSemanticEnum', default: true, linksto: 'Type'}, + {name: 'LspSemanticInterface', default: true, linksto: 'TypeDef'}, + {name: 'LspSemanticStruct', default: true, linksto: 'Type'}, + {name: 'LspSemanticTypeParameter', default: true, linksto: 'Type'}, + {name: 'LspSemanticParameter', default: true, linksto: 'Identifier'}, + {name: 'LspSemanticVariable', default: true, linksto: 'Identifier'}, + {name: 'LspSemanticProperty', default: true, linksto: 'Identifier'}, + {name: 'LspSemanticEnumMember', default: true, linksto: 'Constant'}, + {name: 'LspSemanticEvent', default: true, linksto: 'Identifier'}, + {name: 'LspSemanticFunction', default: true, linksto: 'Function'}, + {name: 'LspSemanticMethod', default: true, linksto: 'Function'}, + {name: 'LspSemanticMacro', default: true, linksto: 'Macro'}, + {name: 'LspSemanticKeyword', default: true, linksto: 'Keyword'}, + {name: 'LspSemanticModifier', default: true, linksto: 'Type'}, + {name: 'LspSemanticComment', default: true, linksto: 'Comment'}, + {name: 'LspSemanticString', default: true, linksto: 'String'}, + {name: 'LspSemanticNumber', default: true, linksto: 'Number'}, + {name: 'LspSemanticRegexp', default: true, linksto: 'String'}, + {name: 'LspSemanticOperator', default: true, linksto: 'Operator'}, + {name: 'LspSemanticDecorator', default: true, linksto: 'Macro'} + ]) + + for hlName in TokenTypeMap->values() + prop_type_add(hlName, {highlight: hlName, combine: true}) + endfor +enddef + +def ParseSemanticTokenMods(lspserverTokenMods: list<string>, tokenMods: number): string + var n = tokenMods + var tokenMod: number + var str = '' + + while n > 0 + tokenMod = float2nr(log10(and(n, invert(n - 1))) / log10(2)) + str = $'{str}{lspserverTokenMods[tokenMod]},' + n = and(n, n - 1) + endwhile + + return str +enddef + +# Apply the edit operations in a semantic tokens delta update message +# (SemanticTokensDelta) from the language server. +# +# The previous list of tokens are stored in the buffer-local +# LspSemanticTokensData variable. After applying the edits in +# semTokens.edits, the new set of tokens are returned in semTokens.data. +def ApplySemanticTokenEdits(bnr: number, semTokens: dict<any>) + if semTokens.edits->empty() + return + endif + + # Need to sort the edits and apply the last edit first. + semTokens.edits->sort((a: dict<any>, b: dict<any>) => a.start - b.start) + + # TODO: Remove this code + # var d = bnr->getbufvar('LspSemanticTokensData', []) + # for e in semTokens.edits + # var insertData = e->get('data', []) + # d = (e.start > 0 ? d[: e.start - 1] : []) + insertData + + # d[e.start + e.deleteCount :] + # endfor + # semTokens.data = d + + var oldTokens = bnr->getbufvar('LspSemanticTokensData', []) + var newTokens = [] + var idx = 0 + for e in semTokens.edits + if e.start > 0 + newTokens->extend(oldTokens[idx : e.start - 1]) + endif + newTokens->extend(e->get('data', [])) + idx = e.start + e.deleteCount + endfor + newTokens->extend(oldTokens[idx : ]) + semTokens.data = newTokens +enddef + +# Process a list of semantic tokens and return the corresponding text +# properties for highlighting. +def ProcessSemanticTokens(lspserver: dict<any>, bnr: number, tokens: list<number>): dict<list<list<number>>> + var props: dict<list<list<number>>> = {} + var tokenLine: number = 0 + var startChar: number = 0 + var length: number = 0 + var tokenType: number = 0 + var tokenMods: number = 0 + var prevTokenLine = 0 + var lnum = 1 + var charIdx = 0 + + var lspserverTokenTypes: list<string> = + lspserver.semanticTokensLegend.tokenTypes + var lspserverTokenMods: list<string> = + lspserver.semanticTokensLegend.tokenModifiers + + # Each semantic token uses 5 items in the tokens List + var i = 0 + while i < tokens->len() + tokenLine = tokens[i] + # tokenLine is relative to the previous token line number + lnum += tokenLine + if prevTokenLine != lnum + # this token is on a different line from the previous token + charIdx = 0 + prevTokenLine = lnum + endif + startChar = tokens[i + 1] + charIdx += startChar + length = tokens[i + 2] + tokenType = tokens[i + 3] + tokenMods = tokens[i + 4] + + var typeStr = lspserverTokenTypes[tokenType] + var modStr = ParseSemanticTokenMods(lspserverTokenMods, tokenMods) + + # Decode the semantic token line number, column number and length to + # UTF-32 encoding. + var r = { + start: { + line: lnum - 1, + character: charIdx + }, + end: { + line: lnum - 1, + character: charIdx + length + } + } + offset.DecodeRange(lspserver, bnr, r) + + if !props->has_key(typeStr) + props[typeStr] = [] + endif + props[typeStr]->add([ + lnum, r.start.character + 1, + lnum, r.end.character + 1 + ]) + + i += 5 + endwhile + + return props +enddef + +# Parse the semantic highlight reply from the language server and update the +# text properties +export def UpdateTokens(lspserver: dict<any>, bnr: number, semTokens: dict<any>) + + if semTokens->has_key('edits') + # Delta semantic update. Need to sort the edits and apply the last edit + # first. + ApplySemanticTokenEdits(bnr, semTokens) + endif + + # Cache the semantic tokens in a buffer-local variable, it will be used + # later for a delta update. + setbufvar(bnr, 'LspSemanticResultId', semTokens->get('resultId', '')) + if !semTokens->has_key('data') + return + endif + setbufvar(bnr, 'LspSemanticTokensData', semTokens.data) + + var props: dict<list<list<number>>> + props = ProcessSemanticTokens(lspserver, bnr, semTokens.data) + + # First clear all the previous text properties + if has('patch-9.0.0233') + prop_remove({types: TokenTypeMap->values(), bufnr: bnr, all: true}) + else + for propName in TokenTypeMap->values() + prop_remove({type: propName, bufnr: bnr, all: true}) + endfor + endif + + if props->empty() + return + endif + + # Apply the new text properties + for tokenType in TokenTypeMap->keys() + if props->has_key(tokenType) + prop_add_list({bufnr: bnr, type: TokenTypeMap[tokenType]}, + props[tokenType]) + endif + endfor +enddef + +# Update the semantic highlighting for buffer "bnr" +def LspUpdateSemanticHighlight(bnr: number) + var lspserver: dict<any> = buf.BufLspServerGet(bnr, 'semanticTokens') + if lspserver->empty() + return + endif + + lspserver.semanticHighlightUpdate(bnr) +enddef + +# Initialize the semantic highlighting for the buffer 'bnr' +export def BufferInit(lspserver: dict<any>, bnr: number) + if !opt.lspOptions.semanticHighlight || !lspserver.isSemanticTokensProvider + # no support for semantic highlighting + return + endif + + # Highlight all the semantic tokens + LspUpdateSemanticHighlight(bnr) + + # buffer-local autocmds for semantic highlighting + var acmds: list<dict<any>> = [] + + acmds->add({bufnr: bnr, + event: 'TextChanged', + group: 'LSPBufferAutocmds', + cmd: $'LspUpdateSemanticHighlight({bnr})'}) + acmds->add({bufnr: bnr, + event: 'BufUnload', + group: 'LSPBufferAutocmds', + cmd: $"b:LspSemanticTokensData = [] | b:LspSemanticResultId = ''"}) + + autocmd_add(acmds) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/signature.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/signature.vim new file mode 100644 index 0000000..ca2bed4 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/signature.vim @@ -0,0 +1,145 @@ +vim9script + +# Functions related to handling LSP symbol signature help. + +import './options.vim' as opt +import './util.vim' +import './buffer.vim' as buf + +# close the signature popup window +def CloseSignaturePopup(lspserver: dict<any>) + if lspserver.signaturePopup != -1 + lspserver.signaturePopup->popup_close() + endif + lspserver.signaturePopup = -1 +enddef + +def CloseCurBufSignaturePopup() + var lspserver: dict<any> = buf.CurbufGetServer('signatureHelp') + if lspserver->empty() + return + endif + + CloseSignaturePopup(lspserver) +enddef + +# Show the signature using "textDocument/signatureHelp" LSP method +# Invoked from an insert-mode mapping, so return an empty string. +def g:LspShowSignature(): string + var lspserver: dict<any> = buf.CurbufGetServerChecked('signatureHelp') + if lspserver->empty() + return '' + endif + + # first send all the changes in the current buffer to the LSP server + listener_flush() + lspserver.showSignature() + return '' +enddef + +export def InitOnce() + hlset([{name: 'LspSigActiveParameter', default: true, linksto: 'LineNr'}]) +enddef + +# Initialize the signature triggers for the current buffer +export def BufferInit(lspserver: dict<any>) + if !lspserver.isSignatureHelpProvider + || !lspserver.caps.signatureHelpProvider->has_key('triggerCharacters') + # no support for signature help + return + endif + + if !opt.lspOptions.showSignature + || !lspserver.featureEnabled('signatureHelp') + # Show signature support is disabled + return + endif + + # map characters that trigger signature help + for ch in lspserver.caps.signatureHelpProvider.triggerCharacters + exe $"inoremap <buffer> <silent> {ch} {ch}<C-R>=g:LspShowSignature()<CR>" + endfor + + # close the signature popup when leaving insert mode + autocmd_add([{bufnr: bufnr(), + event: 'InsertLeave', + cmd: 'CloseCurBufSignaturePopup()'}]) +enddef + +# process the 'textDocument/signatureHelp' reply from the LSP server and +# display the symbol signature help. +# Result: SignatureHelp | null +export def SignatureHelp(lspserver: dict<any>, sighelp: any): void + if sighelp->empty() + CloseSignaturePopup(lspserver) + return + endif + + if sighelp.signatures->len() <= 0 + CloseSignaturePopup(lspserver) + return + endif + + var sigidx: number = 0 + if sighelp->has_key('activeSignature') + sigidx = sighelp.activeSignature + endif + + var sig: dict<any> = sighelp.signatures[sigidx] + var text: string = sig.label + var hllen: number = 0 + var startcol: number = 0 + if sig->has_key('parameters') && sighelp->has_key('activeParameter') + var params: list<dict<any>> = sig.parameters + var params_len: number = params->len() + var activeParam: number = sighelp.activeParameter + if params_len > 0 && activeParam < params_len + var paramInfo: dict<any> = params[activeParam] + var label: any = paramInfo.label + if label->type() == v:t_string + # label string + var label_str: string = label + hllen = label_str->len() + startcol = text->stridx(label_str) + else + # [inclusive start offset, exclusive end offset] + var label_offset: list<number> = params[activeParam].label + var start_offset: number = label_offset[0] + var end_offset: number = label_offset[1] + + if has('patch-9.0.1629') + # Convert UTF-16 offsets + startcol = text->byteidx(start_offset, true) + var endcol: number = text->byteidx(end_offset, true) + hllen = endcol - startcol + else + startcol = start_offset + hllen = end_offset - start_offset + endif + endif + endif + endif + + if opt.lspOptions.echoSignature + :echon "\r\r" + :echon '' + :echon text->strpart(0, startcol) + :echoh LspSigActiveParameter + :echon text->strpart(startcol, hllen) + :echoh None + :echon text->strpart(startcol + hllen) + else + # Close the previous signature popup and open a new one + lspserver.signaturePopup->popup_close() + + var popupID = text->popup_atcursor({padding: [0, 1, 0, 1], moved: [col('.') - 1, 9999999]}) + var bnr: number = popupID->winbufnr() + prop_type_add('signature', {bufnr: bnr, highlight: 'LspSigActiveParameter'}) + if hllen > 0 + prop_add(1, startcol + 1, {bufnr: bnr, length: hllen, type: 'signature'}) + endif + lspserver.signaturePopup = popupID + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/snippet.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/snippet.vim new file mode 100644 index 0000000..c03b49d --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/snippet.vim @@ -0,0 +1,82 @@ +vim9script + +# Snippet support + +# Integration with the UltiSnips plugin +export def CompletionUltiSnips(prefix: string, items: list<dict<any>>) + call UltiSnips#SnippetsInCurrentScope(1) + for key in matchfuzzy(g:current_ulti_dict_info->keys(), prefix) + var item = g:current_ulti_dict_info[key] + var parts = split(item.location, ':') + var txt = parts[0]->readfile()[parts[1]->str2nr() : parts[1]->str2nr() + 20] + var restxt = item.description .. "\n\n" + for line in txt + if line->empty() || line[0 : 6] == "snippet" + break + else + restxt = restxt .. line .. "\n" + endif + endfor + items->add({ + label: key, + data: { + entryNames: [key], + }, + kind: 15, + documentation: restxt, + }) + endfor +enddef + +# Integration with the vim-vsnip plugin +export def CompletionVsnip(items: list<dict<any>>) + def Pattern(abbr: string): string + var chars = escape(abbr, '\/?')->split('\zs') + var chars_pattern = '\%(\V' .. chars->join('\m\|\V') .. '\m\)' + var separator = chars[0] =~ '\a' ? '\<' : '' + return $'{separator}\V{chars[0]}\m{chars_pattern}*$' + enddef + + if charcol('.') == 1 + return + endif + var starttext = getline('.')->slice(0, charcol('.') - 1) + for item in vsnip#get_complete_items(bufnr()) + var match = starttext->matchstrpos(Pattern(item.abbr)) + if match[0] != '' + var user_data = item.user_data->json_decode() + var documentation = [] + for line in vsnip#to_string(user_data.vsnip.snippet)->split("\n") + documentation->add(line) + endfor + items->add({ + label: item.abbr, + filterText: item.word, + insertTextFormat: 2, + textEdit: { + newText: user_data.vsnip.snippet->join("\n"), + range: { + start: { + line: line('.'), + character: match[1], + }, + ['end']: { + line: line('.'), + character: match[2], + }, + }, + }, + data: { + entryNames: [item.word], + }, + kind: 15, + documentation: { + kind: 'markdown', + value: documentation->join("\n"), + }, + }) + endif + endfor +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/symbol.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/symbol.vim new file mode 100644 index 0000000..bcd547a --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/symbol.vim @@ -0,0 +1,1014 @@ +vim9script + +# Functions for dealing with symbols. +# - LSP symbol menu and for searching symbols across the workspace. +# - show locations +# - jump to a symbol definition, declaration, type definition or +# implementation + +import './options.vim' as opt +import './util.vim' +import './outline.vim' + +# Initialize the highlight group and the text property type used for +# document symbol search +export def InitOnce() + # Use a high priority value to override other highlights in the line + hlset([ + {name: 'LspSymbolName', default: true, linksto: 'Search'}, + {name: 'LspSymbolRange', default: true, linksto: 'Visual'} + ]) + prop_type_add('LspSymbolNameProp', {highlight: 'LspSymbolName', + combine: false, + override: true, + priority: 201}) + prop_type_add('LspSymbolRangeProp', {highlight: 'LspSymbolRange', + combine: false, + override: true, + priority: 200}) +enddef + +# Handle keys pressed when the workspace symbol popup menu is displayed +def FilterSymbols(lspserver: dict<any>, popupID: number, key: string): bool + var key_handled: bool = false + var update_popup: bool = false + var query: string = lspserver.workspaceSymbolQuery + + if key == "\<BS>" || key == "\<C-H>" + # Erase one character from the filter text + if query->len() >= 1 + query = query[: -2] + update_popup = true + endif + key_handled = true + elseif key == "\<C-U>" + # clear the filter text + query = '' + update_popup = true + key_handled = true + elseif key == "\<C-F>" + || key == "\<C-B>" + || key == "\<PageUp>" + || key == "\<PageDown>" + || key == "\<C-Home>" + || key == "\<C-End>" + || key == "\<C-N>" + || key == "\<C-P>" + # scroll the popup window + var cmd: string = 'normal! ' .. (key == "\<C-N>" ? 'j' : key == "\<C-P>" ? 'k' : key) + win_execute(popupID, cmd) + key_handled = true + elseif key == "\<Up>" || key == "\<Down>" + # Use native Vim handling for these keys + key_handled = false + elseif key =~ '^\f$' || key == "\<Space>" + # Filter the names based on the typed key and keys typed before + query ..= key + update_popup = true + key_handled = true + endif + + if update_popup + # Update the popup with the new list of symbol names + popupID->popup_settext('') + if query != '' + lspserver.workspaceQuery(query, false) + else + []->setwinvar(popupID, 'LspSymbolTable') + endif + :echo $'Symbol: {query}' + endif + + # Update the workspace symbol query string + lspserver.workspaceSymbolQuery = query + + if key_handled + return true + endif + + return popupID->popup_filter_menu(key) +enddef + +# Jump to the location of a symbol selected in the popup menu +def JumpToWorkspaceSymbol(cmdmods: string, popupID: number, result: number): void + # clear the message displayed at the command-line + :echo '' + + if result <= 0 + # popup is canceled + return + endif + + var symTbl: list<dict<any>> = popupID->getwinvar('LspSymbolTable', []) + if symTbl->empty() + return + endif + try + # Save the current location in the tag stack + util.PushCursorToTagStack() + + # if the selected file is already present in a window, then jump to it + var fname: string = symTbl[result - 1].file + var bnr = fname->bufnr() + if cmdmods->empty() + var winList: list<number> = bnr->win_findbuf() + if winList->empty() + # Not present in any window + if &modified || &buftype != '' + # the current buffer is modified or is not a normal buffer, then + # open the file in a new window + exe $'split {symTbl[result - 1].file}' + else + exe $'confirm edit {symTbl[result - 1].file}' + endif + else + # If the target buffer is opened in the current window, then don't + # change the window. + if bufnr() != bnr + # If the target buffer is opened in a window in the current tab + # page, then use it. + var winID = fname->bufwinid() + if winID == -1 + # not present in the current tab page. Use the first window. + winID = winList[0] + endif + winID->win_gotoid() + endif + endif + else + exe $'{cmdmods} split {symTbl[result - 1].file}' + endif + # Set the previous cursor location mark. Instead of using setpos(), m' is + # used so that the current location is added to the jump list. + :normal m' + setcursorcharpos(symTbl[result - 1].pos.line + 1, + util.GetCharIdxWithoutCompChar(bufnr(), + symTbl[result - 1].pos) + 1) + :normal! zv + catch + # ignore exceptions + endtry +enddef + +# display a list of symbols from the workspace +def ShowSymbolMenu(lspserver: dict<any>, query: string, cmdmods: string) + # Create the popup menu + var lnum = &lines - &cmdheight - 2 - 10 + var popupAttr = { + title: 'Workspace Symbol Search', + wrap: false, + pos: 'topleft', + line: lnum, + col: 2, + minwidth: 60, + minheight: 10, + maxheight: 10, + maxwidth: 60, + mapping: false, + fixed: 1, + close: 'button', + filter: function(FilterSymbols, [lspserver]), + callback: function('JumpToWorkspaceSymbol', [cmdmods]) + } + lspserver.workspaceSymbolPopup = popup_menu([], popupAttr) + lspserver.workspaceSymbolQuery = query + prop_type_add('lspworkspacesymbol', + {bufnr: lspserver.workspaceSymbolPopup->winbufnr(), + highlight: 'Title'}) + :echo $'Symbol: {query}' +enddef + +# Convert a file name to <filename> (<dirname>) format. +# Make sure the popup doesn't occupy the entire screen by reducing the width. +def MakeMenuName(popupWidth: number, fname: string): string + var filename: string = fname->fnamemodify(':t') + var flen: number = filename->len() + var dirname: string = fname->fnamemodify(':h') + + if fname->len() > popupWidth && flen < popupWidth + # keep the full file name and reduce directory name length + # keep some characters at the beginning and end (equally). + # 6 spaces are used for "..." and " ()" + var dirsz = (popupWidth - flen - 6) / 2 + dirname = dirname[: dirsz] .. '...' .. dirname[-dirsz : ] + endif + var str: string = filename + if dirname != '.' + str ..= $' ({dirname}/)' + endif + return str +enddef + +# process the 'workspace/symbol' reply from the LSP server +# Result: SymbolInformation[] | null +export def WorkspaceSymbolPopup(lspserver: dict<any>, query: string, + symInfo: list<dict<any>>, cmdmods: string) + var symbols: list<dict<any>> = [] + var symbolType: string + var fileName: string + var symName: string + + # Create a symbol popup menu if it is not present + if lspserver.workspaceSymbolPopup->winbufnr() == -1 + ShowSymbolMenu(lspserver, query, cmdmods) + endif + + for symbol in symInfo + if !symbol->has_key('location') + # ignore entries without location information + continue + endif + + # interface SymbolInformation + fileName = util.LspUriToFile(symbol.location.uri) + + symName = symbol.name + if symbol->has_key('containerName') && symbol.containerName != '' + symName = $'{symbol.containerName}::{symName}' + endif + symName ..= $' [{SymbolKindToName(symbol.kind)}]' + symName ..= ' ' .. MakeMenuName( + lspserver.workspaceSymbolPopup->popup_getpos().core_width, + fileName) + + symbols->add({name: symName, + file: fileName, + pos: symbol.location.range.start}) + endfor + symbols->setwinvar(lspserver.workspaceSymbolPopup, 'LspSymbolTable') + lspserver.workspaceSymbolPopup->popup_settext( + symbols->copy()->mapnew('v:val.name')) +enddef + +# map the LSP symbol kind number to string +export def SymbolKindToName(symkind: number): string + var symbolMap: list<string> = [ + '', + 'File', + 'Module', + 'Namespace', + 'Package', + 'Class', + 'Method', + 'Property', + 'Field', + 'Constructor', + 'Enum', + 'Interface', + 'Function', + 'Variable', + 'Constant', + 'String', + 'Number', + 'Boolean', + 'Array', + 'Object', + 'Key', + 'Null', + 'EnumMember', + 'Struct', + 'Event', + 'Operator', + 'TypeParameter' + ] + if symkind > 26 + return '' + endif + return symbolMap[symkind] +enddef + +def UpdatePeekFilePopup(lspserver: dict<any>, locations: list<dict<any>>) + if lspserver.peekSymbolPopup->winbufnr() == -1 + return + endif + + lspserver.peekSymbolFilePopup->popup_close() + + var n = line('.', lspserver.peekSymbolPopup) - 1 + var [uri, range] = util.LspLocationParse(locations[n]) + var fname: string = util.LspUriToFile(uri) + + var bnr: number = fname->bufnr() + if bnr == -1 + bnr = fname->bufadd() + endif + + var popupAttrs = { + title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})", + wrap: false, + fixed: true, + minheight: 10, + maxheight: 10, + minwidth: winwidth(0) - 38, + maxwidth: winwidth(0) - 38, + cursorline: true, + border: [], + mapping: false, + line: 'cursor+1', + col: 1 + } + + lspserver.peekSymbolFilePopup = popup_create(bnr, popupAttrs) + var rstart = range.start + var cmds =<< trim eval END + :setlocal number + [{rstart.line + 1}, 1]->cursor() + :normal! z. + END + win_execute(lspserver.peekSymbolFilePopup, cmds) + + lspserver.peekSymbolFilePopup->clearmatches() + var start_col = util.GetLineByteFromPos(bnr, rstart) + 1 + var end_col = util.GetLineByteFromPos(bnr, range.end) + var pos = [[rstart.line + 1, + start_col, end_col - start_col + 1]] + matchaddpos('Search', pos, 10, -1, {window: lspserver.peekSymbolFilePopup}) +enddef + +def LocPopupFilter(lspserver: dict<any>, locations: list<dict<any>>, + popup_id: number, key: string): bool + popup_filter_menu(popup_id, key) + if lspserver.peekSymbolPopup->winbufnr() == -1 + if lspserver.peekSymbolFilePopup->winbufnr() != -1 + lspserver.peekSymbolFilePopup->popup_close() + endif + lspserver.peekSymbolPopup = -1 + lspserver.peekSymbolFilePopup = -1 + else + UpdatePeekFilePopup(lspserver, locations) + endif + return true +enddef + +def LocPopupCallback(lspserver: dict<any>, locations: list<dict<any>>, + popup_id: number, selIdx: number) + if lspserver.peekSymbolFilePopup->winbufnr() != -1 + lspserver.peekSymbolFilePopup->popup_close() + endif + lspserver.peekSymbolPopup = -1 + if selIdx != -1 + util.PushCursorToTagStack() + util.JumpToLspLocation(locations[selIdx - 1], '') + endif +enddef + +# Display the locations in a popup menu. Display the corresponding file in +# an another popup window. +def PeekLocations(lspserver: dict<any>, locations: list<dict<any>>, + title: string) + if lspserver.peekSymbolPopup->winbufnr() != -1 + # If the symbol popup window is already present, close it. + lspserver.peekSymbolPopup->popup_close() + endif + + var w: number = &columns + var fnamelen = float2nr(w * 0.4) + + var curlnum = line('.') + var symIdx = 1 + var curSymIdx = 1 + var menuItems: list<string> = [] + for loc in locations + var [uri, range] = util.LspLocationParse(loc) + var fname: string = util.LspUriToFile(uri) + var bnr: number = fname->bufnr() + if bnr == -1 + bnr = fname->bufadd() + endif + :silent! bnr->bufload() + + var lnum = range.start.line + 1 + var text: string = bnr->getbufline(lnum)->get(0, '') + menuItems->add($'{lnum}: {text}') + + if lnum == curlnum + curSymIdx = symIdx + endif + symIdx += 1 + endfor + + var popupAttrs = { + title: title, + wrap: false, + pos: 'topleft', + line: 'cursor+1', + col: winwidth(0) - 34, + minheight: 10, + maxheight: 10, + minwidth: 30, + maxwidth: 30, + mapping: false, + fixed: true, + filter: function(LocPopupFilter, [lspserver, locations]), + callback: function(LocPopupCallback, [lspserver, locations]) + } + lspserver.peekSymbolPopup = popup_menu(menuItems, popupAttrs) + # Select the current symbol in the menu + var cmds =<< trim eval END + [{curSymIdx}, 1]->cursor() + END + win_execute(lspserver.peekSymbolPopup, cmds, 'silent!') + UpdatePeekFilePopup(lspserver, locations) +enddef + +export def ShowLocations(lspserver: dict<any>, locations: list<dict<any>>, + peekSymbol: bool, title: string) + if peekSymbol + PeekLocations(lspserver, locations, title) + return + endif + + # create a loclist the location of the locations + var qflist: list<dict<any>> = [] + for loc in locations + var [uri, range] = util.LspLocationParse(loc) + var fname: string = util.LspUriToFile(uri) + var bnr: number = fname->bufnr() + if bnr == -1 + bnr = fname->bufadd() + endif + :silent! bnr->bufload() + var rstart = range.start + var text: string = bnr->getbufline(rstart.line + 1)->get(0, '')->trim("\t ", 1) + qflist->add({filename: fname, + lnum: rstart.line + 1, + col: util.GetLineByteFromPos(bnr, rstart) + 1, + text: text}) + endfor + + var save_winid = win_getid() + + if opt.lspOptions.useQuickfixForLocations + setqflist([], ' ', {title: title, items: qflist}) + var mods: string = '' + exe $'{mods} copen' + else + setloclist(0, [], ' ', {title: title, items: qflist}) + var mods: string = '' + exe $'{mods} lopen' + endif + + if !opt.lspOptions.keepFocusInReferences + save_winid->win_gotoid() + endif +enddef + +# Key filter callback function used for the symbol popup window. +# Vim doesn't close the popup window when the escape key is pressed. +# This is function supports that. +def SymbolFilterCB(lspserver: dict<any>, id: number, key: string): bool + if key == "\<Esc>" + lspserver.peekSymbolPopup->popup_close() + return true + endif + + return false +enddef + +# Display the file specified by LSP "LocationLink" in a popup window and +# highlight the range in "location". +def PeekSymbolLocation(lspserver: dict<any>, location: dict<any>) + var [uri, range] = util.LspLocationParse(location) + var fname = util.LspUriToFile(uri) + var bnum = fname->bufadd() + if bnum == 0 + # Failed to create or find a buffer + return + endif + :silent! bnum->bufload() + + if lspserver.peekSymbolPopup->winbufnr() != -1 + # If the symbol popup window is already present, close it. + lspserver.peekSymbolPopup->popup_close() + endif + var CbFunc = function(SymbolFilterCB, [lspserver]) + var popupAttrs = { + title: $"{fnamemodify(fname, ':t')} ({fnamemodify(fname, ':h')})", + wrap: false, + moved: 'any', + minheight: 10, + maxheight: 10, + minwidth: 10, + maxwidth: 60, + cursorline: true, + border: [], + mapping: false, + filter: CbFunc + } + lspserver.peekSymbolPopup = popup_atcursor(bnum, popupAttrs) + + # Highlight the symbol name and center the line in the popup + var pwid = lspserver.peekSymbolPopup + var pwbuf = pwid->winbufnr() + var pos: list<number> = [] + var start_col: number + var end_col: number + var rstart = range.start + start_col = util.GetLineByteFromPos(pwbuf, rstart) + 1 + end_col = util.GetLineByteFromPos(pwbuf, range.end) + 1 + pos->add(rstart.line + 1) + pos->extend([start_col, end_col - start_col]) + matchaddpos('Search', [pos], 10, 101, {window: pwid}) + var cmds =<< trim eval END + :setlocal number + [{rstart.line + 1}, 1]->cursor() + :normal! z. + END + win_execute(pwid, cmds, 'silent!') +enddef + +# Jump to the definition, declaration or implementation of a symbol. +# Also, used to peek at the definition, declaration or implementation of a +# symbol. +export def GotoSymbol(lspserver: dict<any>, location: dict<any>, + peekSymbol: bool, cmdmods: string) + if peekSymbol + PeekSymbolLocation(lspserver, location) + else + # Save the current cursor location in the tag stack. + util.PushCursorToTagStack() + util.JumpToLspLocation(location, cmdmods) + endif +enddef + +# Process the LSP server reply message for a 'textDocument/definition' request +# and return a list of Dicts in a format accepted by the 'tagfunc' option. +export def TagFunc(lspserver: dict<any>, + taglocations: list<dict<any>>, + pat: string): list<dict<any>> + var retval: list<dict<any>> + + for tagloc in taglocations + var tagitem = {} + tagitem.name = pat + + var [uri, range] = util.LspLocationParse(tagloc) + tagitem.filename = util.LspUriToFile(uri) + var bnr = util.LspUriToBufnr(uri) + var rstart = range.start + var startByteIdx = util.GetLineByteFromPos(bnr, rstart) + tagitem.cmd = $"/\\%{rstart.line + 1}l\\%{startByteIdx + 1}c" + + retval->add(tagitem) + endfor + + return retval +enddef + +# process SymbolInformation[] +def ProcessSymbolInfoTable(lspserver: dict<any>, + bnr: number, + symbolInfoTable: list<dict<any>>, + symbolTypeTable: dict<list<dict<any>>>, + symbolLineTable: list<dict<any>>) + var fname: string + var symbolType: string + var name: string + var r: dict<dict<number>> + var symInfo: dict<any> + + for syminfo in symbolInfoTable + fname = util.LspUriToFile(syminfo.location.uri) + symbolType = SymbolKindToName(syminfo.kind) + name = syminfo.name + if syminfo->has_key('containerName') + if syminfo.containerName != '' + name ..= $' [{syminfo.containerName}]' + endif + endif + r = syminfo.location.range + lspserver.decodeRange(bnr, r) + + if !symbolTypeTable->has_key(symbolType) + symbolTypeTable[symbolType] = [] + endif + symInfo = {name: name, range: r} + symbolTypeTable[symbolType]->add(symInfo) + symbolLineTable->add(symInfo) + endfor +enddef + +# process DocumentSymbol[] +def ProcessDocSymbolTable(lspserver: dict<any>, + bnr: number, + docSymbolTable: list<dict<any>>, + symbolTypeTable: dict<list<dict<any>>>, + symbolLineTable: list<dict<any>>) + var symbolType: string + var name: string + var r: dict<dict<number>> + var symInfo: dict<any> + var symbolDetail: string + var childSymbols: dict<list<dict<any>>> + + for syminfo in docSymbolTable + name = syminfo.name + symbolType = SymbolKindToName(syminfo.kind) + r = syminfo.selectionRange + lspserver.decodeRange(bnr, r) + if syminfo->has_key('detail') + symbolDetail = syminfo.detail + endif + if !symbolTypeTable->has_key(symbolType) + symbolTypeTable[symbolType] = [] + endif + childSymbols = {} + if syminfo->has_key('children') + ProcessDocSymbolTable(lspserver, bnr, syminfo.children, childSymbols, + symbolLineTable) + endif + symInfo = {name: name, range: r, detail: symbolDetail, + children: childSymbols} + symbolTypeTable[symbolType]->add(symInfo) + symbolLineTable->add(symInfo) + endfor +enddef + +# process the 'textDocument/documentSymbol' reply from the LSP server +# Open a symbols window and display the symbols as a tree +# Result: DocumentSymbol[] | SymbolInformation[] | null +export def DocSymbolOutline(lspserver: dict<any>, docSymbol: any, fname: string) + var bnr = fname->bufnr() + var symbolTypeTable: dict<list<dict<any>>> = {} + var symbolLineTable: list<dict<any>> = [] + + if docSymbol->empty() + # No symbols defined for this file. Clear the outline window. + outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable) + return + endif + + if docSymbol[0]->has_key('location') + # SymbolInformation[] + ProcessSymbolInfoTable(lspserver, bnr, docSymbol, symbolTypeTable, + symbolLineTable) + else + # DocumentSymbol[] + ProcessDocSymbolTable(lspserver, bnr, docSymbol, symbolTypeTable, + symbolLineTable) + endif + + # sort the symbols by line number + symbolLineTable->sort((a, b) => a.range.start.line - b.range.start.line) + outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable) +enddef + +# Process the list of symbols (LSP interface "SymbolInformation") in +# "symbolInfoTable". For each symbol, create the name to display in the popup +# menu along with the symbol range and return the List. +def GetSymbolsInfoTable(lspserver: dict<any>, + bnr: number, + symbolInfoTable: list<dict<any>>): list<dict<any>> + var symbolTable: list<dict<any>> = [] + var symbolType: string + var name: string + var containerName: string + var r: dict<dict<number>> + + for syminfo in symbolInfoTable + symbolType = SymbolKindToName(syminfo.kind) + name = $'{symbolType} : {syminfo.name}' + if syminfo->has_key('containerName') && !syminfo.containerName->empty() + name ..= $' [{syminfo.containerName}]' + endif + r = syminfo.location.range + lspserver.decodeRange(bnr, r) + + symbolTable->add({name: name, range: r, selectionRange: {}}) + endfor + + return symbolTable +enddef + +# Process the list of symbols (LSP interface "DocumentSymbol") in +# "docSymbolTable". For each symbol, create the name to display in the popup +# menu along with the symbol range and return the List in "symbolTable" +def GetSymbolsDocSymbol(lspserver: dict<any>, + bnr: number, + docSymbolTable: list<dict<any>>, + symbolTable: list<dict<any>>, + parentName: string = '') + var symbolType: string + var name: string + var r: dict<dict<number>> + var sr: dict<dict<number>> + var symInfo: dict<any> + + for syminfo in docSymbolTable + var symName = syminfo.name + symbolType = SymbolKindToName(syminfo.kind)->tolower() + sr = syminfo.selectionRange + lspserver.decodeRange(bnr, sr) + r = syminfo.range + lspserver.decodeRange(bnr, r) + name = $'{symbolType} : {symName}' + if parentName != '' + name ..= $' [{parentName}]' + endif + # TODO: Should include syminfo.detail? Will it clutter the menu? + symInfo = {name: name, range: r, selectionRange: sr} + symbolTable->add(symInfo) + + if syminfo->has_key('children') + # Process all the child symbols + GetSymbolsDocSymbol(lspserver, bnr, syminfo.children, symbolTable, + symName) + endif + endfor +enddef + +# Highlight the name and the range of lines for the symbol at symTbl[symIdx] +def SymbolHighlight(symTbl: list<dict<any>>, symIdx: number) + prop_remove({type: 'LspSymbolNameProp', all: true}) + prop_remove({type: 'LspSymbolRangeProp', all: true}) + if symTbl->empty() + return + endif + + var r = symTbl[symIdx].range + if r->empty() + return + endif + var rangeStart = r.start + var rangeEnd = r.end + var start_lnum = rangeStart.line + 1 + var start_col = rangeStart.character + 1 + var end_lnum = rangeEnd.line + 1 + var end_col: number + var last_lnum = line('$') + if end_lnum > line('$') + end_lnum = last_lnum + end_col = col([last_lnum, '$']) + else + end_col = rangeEnd.character + 1 + endif + prop_add(start_lnum, start_col, + {type: 'LspSymbolRangeProp', + end_lnum: end_lnum, + end_col: end_col}) + cursor(start_lnum, 1) + :normal! z. + + var sr = symTbl[symIdx].selectionRange + if sr->empty() + return + endif + rangeStart = sr.start + rangeEnd = sr.end + prop_add(rangeStart.line + 1, 1, + {type: 'LspSymbolNameProp', + start_col: rangeStart.character + 1, + end_lnum: rangeEnd.line + 1, + end_col: rangeEnd.character + 1}) +enddef + +# Callback invoked when an item is selected in the symbol popup menu +# "symTbl" - list of symbols +# "symInputPopup" - Symbol search input popup window ID +# "save_curpos" - Cursor position before invoking the symbol search. If the +# symbol search is canceled, restore the cursor to this +# position. +def SymbolMenuItemSelected(symPopupMenu: number, + result: number) + var symTblFiltered = symPopupMenu->getwinvar('symbolTableFiltered', []) + var symInputPopup = symPopupMenu->getwinvar('symbolInputPopup', 0) + var save_curpos = symPopupMenu->getwinvar('saveCurPos', []) + + # Restore the cursor to the location where the command was invoked + setpos('.', save_curpos) + + if result > 0 + # A symbol is selected in the popup menu + + # Set the previous cursor location mark. Instead of using setpos(), m' is + # used so that the current location is added to the jump list. + :normal m' + + # Jump to the selected symbol location + var r = symTblFiltered[result - 1].selectionRange + if r->empty() + # SymbolInformation doesn't have the selectionRange field + r = symTblFiltered[result - 1].range + endif + setcursorcharpos(r.start.line + 1, + util.GetCharIdxWithoutCompChar(bufnr(), r.start) + 1) + :normal! zv + endif + symInputPopup->popup_close() + prop_remove({type: 'LspSymbolNameProp', all: true}) + prop_remove({type: 'LspSymbolRangeProp', all: true}) +enddef + +# Key filter function for the symbol popup menu. +def SymbolMenuFilterKey(symPopupMenu: number, + key: string): bool + var keyHandled = true + var updateInputPopup = false + var inputText = symPopupMenu->getwinvar('inputText', '') + var symInputPopup = symPopupMenu->getwinvar('symbolInputPopup', 0) + + if key == "\<BS>" || key == "\<C-H>" + # Erase a character in the input popup + if inputText->len() >= 1 + inputText = inputText[: -2] + updateInputPopup = true + else + keyHandled = false + endif + elseif key == "\<C-U>" + # Erase all the characters in the input popup + inputText = '' + updateInputPopup = true + elseif key == "\<tab>" + || key == "\<C-n>" + || key == "\<Down>" + || key == "\<ScrollWheelDown>" + var ln = getcurpos(symPopupMenu)[1] + win_execute(symPopupMenu, "normal! j") + if ln == getcurpos(symPopupMenu)[1] + win_execute(symPopupMenu, "normal! gg") + endif + elseif key == "\<S-tab>" + || key == "\<C-p>" + || key == "\<Up>" + || key == "\<ScrollWheelUp>" + var ln = getcurpos(symPopupMenu)[1] + win_execute(symPopupMenu, "normal! k") + if ln == getcurpos(symPopupMenu)[1] + win_execute(symPopupMenu, "normal! G") + endif + elseif key == "\<PageDown>" + win_execute(symPopupMenu, "normal! \<C-d>") + elseif key == "\<PageUp>" + win_execute(symPopupMenu, "normal! \<C-u>") + elseif key == "\<C-F>" + || key == "\<C-B>" + || key == "\<C-Home>" + || key == "\<C-End>" + win_execute(symPopupMenu, $"normal! {key}") + elseif key =~ '^\k$' + # A keyword character is typed. Add to the input text and update the + # popup + inputText ..= key + updateInputPopup = true + else + keyHandled = false + endif + + var symTblFiltered: list<dict<any>> = [] + symTblFiltered = symPopupMenu->getwinvar('symbolTableFiltered', []) + + if updateInputPopup + # Update the input popup with the new text and update the symbol popup + # window with the matching symbol names. + symInputPopup->popup_settext(inputText) + + var symbolTable = symPopupMenu->getwinvar('symbolTable') + symTblFiltered = symbolTable->deepcopy() + var symbolMatchPos: list<list<number>> = [] + + # Get the list of symbols fuzzy matching the entered text + if inputText != '' + var t = symTblFiltered->matchfuzzypos(inputText, {key: 'name'}) + symTblFiltered = t[0] + symbolMatchPos = t[1] + endif + + var popupText: list<dict<any>> + var text: list<dict<any>> + if !symbolMatchPos->empty() + # Generate a list of symbol names and the corresponding text properties + # to highlight the matching characters. + popupText = symTblFiltered->mapnew((idx, val): dict<any> => ({ + text: val.name, + props: symbolMatchPos[idx]->mapnew((_, w: number): dict<any> => ({ + col: w + 1, + length: 1, + type: 'LspSymbolMatch'} + ))} + )) + else + popupText = symTblFiltered->mapnew((idx, val): dict<string> => { + return {text: val.name} + }) + endif + symPopupMenu->popup_settext(popupText) + + # Select the first symbol and highlight the corresponding text range + win_execute(symPopupMenu, 'cursor(1, 1)') + SymbolHighlight(symTblFiltered, 0) + endif + + # Save the filtered symbol table and the search text in popup window + # variables + setwinvar(symPopupMenu, 'inputText', inputText) + setwinvar(symPopupMenu, 'symbolTableFiltered', symTblFiltered) + + if !keyHandled + # Use the default handler for the key + symPopupMenu->popup_filter_menu(key) + endif + + # Highlight the name and range of the selected symbol + var lnum = line('.', symPopupMenu) - 1 + if lnum >= 0 + SymbolHighlight(symTblFiltered, lnum) + endif + + return true +enddef + +# Display the symbols popup menu +def SymbolPopupMenu(symbolTable: list<dict<any>>) + var curLine = line('.') + var curSymIdx = 0 + + # Get the names of all the symbols. Also get the index of the symbol under + # the cursor. + var symNames = symbolTable->mapnew((idx, val): string => { + var r = val.range + if !r->empty() && curSymIdx == 0 + if curLine >= r.start.line + 1 && curLine <= r.end.line + 1 + curSymIdx = idx + endif + endif + return val.name + }) + + var symInputPopupAttr = { + title: 'Select Symbol', + wrap: false, + pos: 'topleft', + line: &lines - 14, + col: 10, + minwidth: 60, + minheight: 1, + maxheight: 1, + maxwidth: 60, + fixed: 1, + close: 'button', + border: [] + } + var symInputPopup = popup_create('', symInputPopupAttr) + + var symNamesPopupattr = { + wrap: false, + pos: 'topleft', + line: &lines - 11, + col: 10, + minwidth: 60, + minheight: 10, + maxheight: 10, + maxwidth: 60, + fixed: 1, + border: [0, 0, 0, 0], + callback: SymbolMenuItemSelected, + filter: SymbolMenuFilterKey, + } + var symPopupMenu = popup_menu(symNames, symNamesPopupattr) + + # Save the state in the popup menu window variables + setwinvar(symPopupMenu, 'symbolTable', symbolTable) + setwinvar(symPopupMenu, 'symbolTableFiltered', symbolTable->deepcopy()) + setwinvar(symPopupMenu, 'symbolInputPopup', symInputPopup) + setwinvar(symPopupMenu, 'saveCurPos', getcurpos()) + prop_type_add('LspSymbolMatch', {bufnr: symPopupMenu->winbufnr(), + highlight: 'Title', + override: true}) + + # Start with the symbol under the cursor + var cmds =<< trim eval END + [{curSymIdx + 1}, 1]->cursor() + :normal! z. + END + win_execute(symPopupMenu, cmds, 'silent!') + + # Highlight the name and range of the first symbol + SymbolHighlight(symbolTable, curSymIdx) +enddef + +# process the 'textDocument/documentSymbol' reply from the LSP server +# Result: DocumentSymbol[] | SymbolInformation[] | null +# Display the symbols in a popup window and jump to the selected symbol +export def DocSymbolPopup(lspserver: dict<any>, docSymbol: any, fname: string) + var symList: list<dict<any>> = [] + + if docSymbol->empty() + return + endif + + var bnr = fname->bufnr() + + if docSymbol[0]->has_key('location') + # SymbolInformation[] + symList = GetSymbolsInfoTable(lspserver, bnr, docSymbol) + else + # DocumentSymbol[] + GetSymbolsDocSymbol(lspserver, bnr, docSymbol, symList) + endif + + :redraw! + SymbolPopupMenu(symList) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/textedit.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/textedit.vim new file mode 100644 index 0000000..ee4cc4a --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/textedit.vim @@ -0,0 +1,321 @@ +vim9script + +import './util.vim' + +# sort the list of edit operations in the descending order of line and column +# numbers. +# 'a': {'A': [lnum, col], 'B': [lnum, col]} +# 'b': {'A': [lnum, col], 'B': [lnum, col]} +def Edit_sort_func(a: dict<any>, b: dict<any>): number + # line number + if a.A[0] != b.A[0] + return b.A[0] - a.A[0] + endif + # column number + if a.A[1] != b.A[1] + return b.A[1] - a.A[1] + endif + + # Assume that the LSP sorted the lines correctly to begin with + return b.idx - a.idx +enddef + +# Replaces text in a range with new text. +# +# CAUTION: Changes in-place! +# +# 'lines': Original list of strings +# 'A': Start position; [line, col] +# 'B': End position [line, col] +# 'new_lines' A list of strings to replace the original +# +# returns the modified 'lines' +def Set_lines(lines: list<string>, A: list<number>, B: list<number>, + new_lines: list<string>): list<string> + var i_0: number = A[0] + + # If it extends past the end, truncate it to the end. This is because the + # way the LSP describes the range including the last newline is by + # specifying a line number after what we would call the last line. + var numlines: number = lines->len() + var i_n = [B[0], numlines - 1]->min() + + if i_0 < 0 || i_0 >= numlines || i_n < 0 || i_n >= numlines + #util.WarnMsg("set_lines: Invalid range, A = " .. A->string() + # .. ", B = " .. B->string() .. ", numlines = " .. numlines + # .. ", new lines = " .. new_lines->string()) + var msg = $"set_lines: Invalid range, A = {A->string()}" + msg ..= $", B = {B->string()}, numlines = {numlines}" + msg ..= $", new lines = {new_lines->string()}" + util.WarnMsg(msg) + return lines + endif + + # save the prefix and suffix text before doing the replacements + var prefix: string = '' + var suffix: string = lines[i_n][B[1] :] + if A[1] > 0 + prefix = lines[i_0][0 : A[1] - 1] + endif + + var new_lines_len: number = new_lines->len() + + #echomsg $"i_0 = {i_0}, i_n = {i_n}, new_lines = {string(new_lines)}" + var n: number = i_n - i_0 + 1 + if n != new_lines_len + if n > new_lines_len + # remove the deleted lines + lines->remove(i_0, i_0 + n - new_lines_len - 1) + else + # add empty lines for newly the added lines (will be replaced with the + # actual lines below) + lines->extend(repeat([''], new_lines_len - n), i_0) + endif + endif + #echomsg $"lines(1) = {string(lines)}" + + # replace the previous lines with the new lines + for i in new_lines_len->range() + lines[i_0 + i] = new_lines[i] + endfor + #echomsg $"lines(2) = {string(lines)}" + + # append the suffix (if any) to the last line + if suffix != '' + var i = i_0 + new_lines_len - 1 + lines[i] = lines[i] .. suffix + endif + #echomsg $"lines(3) = {string(lines)}" + + # prepend the prefix (if any) to the first line + if prefix != '' + lines[i_0] = prefix .. lines[i_0] + endif + #echomsg $"lines(4) = {string(lines)}" + + return lines +enddef + +# Apply set of text edits to the specified buffer +# The text edit logic is ported from the Neovim lua implementation +export def ApplyTextEdits(bnr: number, text_edits: list<dict<any>>): void + if text_edits->empty() + return + endif + + # if the buffer is not loaded, load it and make it a listed buffer + :silent! bnr->bufload() + setbufvar(bnr, '&buflisted', true) + + var start_line: number = 4294967295 # 2 ^ 32 + var finish_line: number = -1 + var updated_edits: list<dict<any>> = [] + var start_row: number + var start_col: number + var end_row: number + var end_col: number + + # create a list of buffer positions where the edits have to be applied. + var idx = 0 + for e in text_edits + # Adjust the start and end columns for multibyte characters + var r = e.range + var rstart: dict<any> = r.start + var rend: dict<any> = r.end + start_row = rstart.line + start_col = util.GetCharIdxWithoutCompChar(bnr, rstart) + end_row = rend.line + end_col = util.GetCharIdxWithoutCompChar(bnr, rend) + start_line = [rstart.line, start_line]->min() + finish_line = [rend.line, finish_line]->max() + + updated_edits->add({A: [start_row, start_col], + B: [end_row, end_col], + idx: idx, + lines: e.newText->split("\n", true)}) + idx += 1 + endfor + + # Reverse sort the edit operations by descending line and column numbers so + # that they can be applied without interfering with each other. + updated_edits->sort('Edit_sort_func') + + var lines: list<string> = bnr->getbufline(start_line + 1, finish_line + 1) + var fix_eol: bool = bnr->getbufvar('&fixeol') + var set_eol = fix_eol && bnr->getbufinfo()[0].linecount <= finish_line + 1 + if !lines->empty() && set_eol && lines[-1]->len() != 0 + lines->add('') + endif + + #echomsg $'lines(1) = {string(lines)}' + #echomsg updated_edits + + for e in updated_edits + var A: list<number> = [e.A[0] - start_line, e.A[1]] + var B: list<number> = [e.B[0] - start_line, e.B[1]] + lines = Set_lines(lines, A, B, e.lines) + endfor + + #echomsg $'lines(2) = {string(lines)}' + + # If the last line is empty and we need to set EOL, then remove it. + if !lines->empty() && set_eol && lines[-1]->len() == 0 + lines->remove(-1) + endif + + #echomsg $'ApplyTextEdits: start_line = {start_line}, finish_line = {finish_line}' + #echomsg $'lines = {string(lines)}' + + # if the buffer is empty, appending lines before the first line adds an + # extra empty line at the end. Delete the empty line after appending the + # lines. + var dellastline: bool = false + if start_line == 0 && bnr->getbufinfo()[0].linecount == 1 && + bnr->getbufline(1)->get(0, '')->empty() + dellastline = true + endif + + # Now we apply the textedits to the actual buffer. + # In theory we could just delete all old lines and append the new lines. + # This would however cause the cursor to change position: It will always be + # on the last line added. + # + # Luckily there is an even simpler solution, that has no cursor sideeffects. + # + # Logically this method is split into the following three cases: + # + # 1. The number of new lines is equal to the number of old lines: + # Just replace the lines inline with setbufline() + # + # 2. The number of new lines is greater than the old ones: + # First append the missing lines at the **end** of the range, then use + # setbufline() again. This does not cause the cursor to change position. + # + # 3. The number of new lines is less than before: + # First use setbufline() to replace the lines that we can replace. + # Then remove superfluous lines. + # + # Luckily, the three different cases exist only logically, we can reduce + # them to a single case practically, because appendbufline() does not append + # anything if an empty list is passed just like deletebufline() does not + # delete anything, if the last line of the range is before the first line. + # We just need to be careful with all indices. + appendbufline(bnr, finish_line + 1, lines[finish_line - start_line + 1 : -1]) + setbufline(bnr, start_line + 1, lines) + deletebufline(bnr, start_line + 1 + lines->len(), finish_line + 1) + + if dellastline + bnr->deletebufline(bnr->getbufinfo()[0].linecount) + endif +enddef + +# interface TextDocumentEdit +def ApplyTextDocumentEdit(textDocEdit: dict<any>) + var bnr: number = util.LspUriToBufnr(textDocEdit.textDocument.uri) + if bnr == -1 + util.ErrMsg($'Text Document edit, buffer {textDocEdit.textDocument.uri} is not found') + return + endif + ApplyTextEdits(bnr, textDocEdit.edits) +enddef + +# interface CreateFile +# Create the "createFile.uri" file +def FileCreate(createFile: dict<any>) + var fname: string = util.LspUriToFile(createFile.uri) + var opts: dict<bool> = createFile->get('options', {}) + var ignoreIfExists: bool = opts->get('ignoreIfExists', true) + var overwrite: bool = opts->get('overwrite', false) + + # LSP Spec: Overwrite wins over `ignoreIfExists` + if fname->filereadable() && ignoreIfExists && !overwrite + return + endif + + fname->fnamemodify(':p:h')->mkdir('p') + []->writefile(fname) + fname->bufadd() +enddef + +# interface DeleteFile +# Delete the "deleteFile.uri" file +def FileDelete(deleteFile: dict<any>) + var fname: string = util.LspUriToFile(deleteFile.uri) + var opts: dict<bool> = deleteFile->get('options', {}) + var recursive: bool = opts->get('recursive', false) + var ignoreIfNotExists: bool = opts->get('ignoreIfNotExists', true) + + if !fname->filereadable() && ignoreIfNotExists + return + endif + + var flags: string = '' + if recursive + # # NOTE: is this a dangerous operation? The LSP server can send a + # # DeleteFile message to recursively delete all the files in the disk. + # flags = 'rf' + util.ErrMsg($'Recursively deleting files is not supported') + return + elseif fname->isdirectory() + flags = 'd' + endif + var bnr: number = fname->bufadd() + fname->delete(flags) + exe $'{bnr}bwipe!' +enddef + +# interface RenameFile +# Rename file "renameFile.oldUri" to "renameFile.newUri" +def FileRename(renameFile: dict<any>) + var old_fname: string = util.LspUriToFile(renameFile.oldUri) + var new_fname: string = util.LspUriToFile(renameFile.newUri) + + var opts: dict<bool> = renameFile->get('options', {}) + var overwrite: bool = opts->get('overwrite', false) + var ignoreIfExists: bool = opts->get('ignoreIfExists', true) + + if new_fname->filereadable() && (!overwrite || ignoreIfExists) + return + endif + + old_fname->rename(new_fname) +enddef + +# interface WorkspaceEdit +export def ApplyWorkspaceEdit(workspaceEdit: dict<any>) + if workspaceEdit->has_key('documentChanges') + for change in workspaceEdit.documentChanges + if change->has_key('kind') + if change.kind == 'create' + FileCreate(change) + elseif change.kind == 'delete' + FileDelete(change) + elseif change.kind == 'rename' + FileRename(change) + else + util.ErrMsg($'Unsupported change in workspace edit [{change.kind}]') + endif + else + ApplyTextDocumentEdit(change) + endif + endfor + return + endif + + if !workspaceEdit->has_key('changes') + return + endif + + for [uri, changes] in workspaceEdit.changes->items() + var bnr: number = util.LspUriToBufnr(uri) + if bnr == 0 + # file is not present + continue + endif + + # interface TextEdit + ApplyTextEdits(bnr, changes) + endfor +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/typehierarchy.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/typehierarchy.vim new file mode 100644 index 0000000..8786b1b --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/typehierarchy.vim @@ -0,0 +1,174 @@ +vim9script + +# Functions for dealing with type hierarchy (super types/sub types) + +import './util.vim' +import './symbol.vim' + +# Parse the type hierarchy in "typeHier" and displays a tree of type names +# in the current buffer. This function is called recursively to display the +# super/sub type hierarchy. +# +# Returns the line number where the next type name should be added. +def TypeTreeGenerate(isSuper: bool, typeHier: dict<any>, pfx_arg: string, + typeTree: list<string>, typeUriMap: list<dict<any>>) + + var itemHasChildren = false + if isSuper + if typeHier->has_key('parents') && !typeHier.parents->empty() + itemHasChildren = true + endif + else + if typeHier->has_key('children') && !typeHier.children->empty() + itemHasChildren = true + endif + endif + + var itemBranchPfx: string + if itemHasChildren + itemBranchPfx = '▾ ' + else + itemBranchPfx = pfx_arg->empty() ? '' : ' ' + endif + + var typestr: string + var kindstr = symbol.SymbolKindToName(typeHier.kind) + if kindstr != '' + typestr = $'{pfx_arg}{itemBranchPfx}{typeHier.name} ({kindstr[0]})' + else + typestr = $'{pfx_arg}{itemBranchPfx}{typeHier.name}' + endif + typeTree->add(typestr) + typeUriMap->add(typeHier) + + # last item to process + if !itemHasChildren + return + endif + + var items: list<dict<any>> + items = isSuper ? typeHier.parents : typeHier.children + + for item in items + TypeTreeGenerate(isSuper, item, $'{pfx_arg}| ', typeTree, typeUriMap) + endfor +enddef + +# Display a popup with the file containing a type and highlight the line and +# the type name. +def UpdateTypeHierFileInPopup(lspserver: dict<any>, typeUriMap: list<dict<any>>) + if lspserver.typeHierPopup->winbufnr() == -1 + return + endif + + lspserver.typeHierFilePopup->popup_close() + + var n = line('.', lspserver.typeHierPopup) - 1 + var fname: string = util.LspUriToFile(typeUriMap[n].uri) + + var bnr = fname->bufadd() + if bnr == 0 + return + endif + + var popupAttrs = { + title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})", + wrap: false, + fixed: true, + minheight: 10, + maxheight: 10, + minwidth: winwidth(0) - 38, + maxwidth: winwidth(0) - 38, + cursorline: true, + border: [], + line: 'cursor+1', + col: 1 + } + lspserver.typeHierFilePopup = popup_create(bnr, popupAttrs) + var cmds =<< trim eval END + [{typeUriMap[n].range.start.line + 1}, 1]->cursor() + :normal! z. + END + win_execute(lspserver.typeHierFilePopup, cmds) + + lspserver.typeHierFilePopup->clearmatches() + var start_col = util.GetLineByteFromPos(bnr, + typeUriMap[n].selectionRange.start) + 1 + var end_col = util.GetLineByteFromPos(bnr, typeUriMap[n].selectionRange.end) + var pos = [[typeUriMap[n].selectionRange.start.line + 1, + start_col, end_col - start_col + 1]] + matchaddpos('Search', pos, 10, -1, {window: lspserver.typeHierFilePopup}) +enddef + +def TypeHierPopupFilter(lspserver: dict<any>, typeUriMap: list<dict<any>>, + popupID: number, key: string): bool + popupID->popup_filter_menu(key) + if lspserver.typeHierPopup->winbufnr() == -1 + # popup is closed + if lspserver.typeHierFilePopup->winbufnr() != -1 + lspserver.typeHierFilePopup->popup_close() + endif + lspserver.typeHierFilePopup = -1 + lspserver.typeHierPopup = -1 + else + UpdateTypeHierFileInPopup(lspserver, typeUriMap) + endif + + return true +enddef + +def TypeHierPopupCallback(lspserver: dict<any>, typeUriMap: list<dict<any>>, + popupID: number, selIdx: number) + if lspserver.typeHierFilePopup->winbufnr() != -1 + lspserver.typeHierFilePopup->popup_close() + endif + lspserver.typeHierFilePopup = -1 + lspserver.typeHierPopup = -1 + + if selIdx <= 0 + # popup is canceled + return + endif + + # Save the current cursor location in the tag stack. + util.PushCursorToTagStack() + util.JumpToLspLocation(typeUriMap[selIdx - 1], '') +enddef + +# Show the super or sub type hierarchy items "types" as a tree in a popup +# window +export def ShowTypeHierarchy(lspserver: dict<any>, isSuper: bool, types: dict<any>) + + if lspserver.typeHierPopup->winbufnr() != -1 + # If the type hierarchy popup window is already present, close it. + lspserver.typeHierPopup->popup_close() + endif + + var typeTree: list<string> + var typeUriMap: list<dict<any>> + + # Generate a tree of the type hierarchy items + TypeTreeGenerate(isSuper, types, '', typeTree, typeUriMap) + + # Display a popup window with the type hierarchy tree and a popup window for + # the file. + var popupAttrs = { + title: $'{isSuper ? "Super" : "Sub"}Type Hierarchy', + wrap: 0, + pos: 'topleft', + line: 'cursor+1', + col: winwidth(0) - 34, + minheight: 10, + maxheight: 10, + minwidth: 30, + maxwidth: 30, + mapping: false, + fixed: true, + filter: function(TypeHierPopupFilter, [lspserver, typeUriMap]), + callback: function(TypeHierPopupCallback, [lspserver, typeUriMap]) + } + lspserver.typeHierPopup = popup_menu(typeTree, popupAttrs) + UpdateTypeHierFileInPopup(lspserver, typeUriMap) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/util.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/util.vim new file mode 100644 index 0000000..eb0a64f --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/util.vim @@ -0,0 +1,364 @@ +vim9script + +# Display an info message +export def InfoMsg(msg: string) + :echohl Question + :echomsg $'Info: {msg}' + :echohl None +enddef + +# Display a warning message +export def WarnMsg(msg: string) + :echohl WarningMsg + :echomsg $'Warn: {msg}' + :echohl None +enddef + +# Display an error message +export def ErrMsg(msg: string) + :echohl Error + :echomsg $'Error: {msg}' + :echohl None +enddef + +# Lsp server trace log directory +var lsp_log_dir: string +if has('unix') + lsp_log_dir = '/tmp/' +else + lsp_log_dir = $TEMP .. '\\' +endif + +# Log a message from the LSP server. stderr is true for logging messages +# from the standard error and false for stdout. +export def TraceLog(fname: string, stderr: bool, msg: string) + if stderr + writefile(msg->split("\n"), $'{lsp_log_dir}{fname}', 'a') + else + writefile([msg], $'{lsp_log_dir}{fname}', 'a') + endif +enddef + +# Empty out the LSP server trace logs +export def ClearTraceLogs(fname: string) + writefile([], $'{lsp_log_dir}{fname}') +enddef + +# Open the LSP server debug messages file. +export def ServerMessagesShow(fname: string) + var fullname = $'{lsp_log_dir}{fname}' + if !filereadable(fullname) + WarnMsg($'File {fullname} is not found') + return + endif + var wid = fullname->bufwinid() + if wid == -1 + exe $'split {fullname}' + else + win_gotoid(wid) + endif + setlocal autoread + setlocal bufhidden=wipe + setlocal nomodified + setlocal nomodifiable +enddef + +# Parse a LSP Location or LocationLink type and return a List with two items. +# The first item is the DocumentURI and the second item is the Range. +export def LspLocationParse(lsploc: dict<any>): list<any> + if lsploc->has_key('targetUri') + # LocationLink + return [lsploc.targetUri, lsploc.targetSelectionRange] + else + # Location + return [lsploc.uri, lsploc.range] + endif +enddef + +# Convert a LSP file URI (file://<absolute_path>) to a Vim file name +export def LspUriToFile(uri: string): string + # Replace all the %xx numbers (e.g. %20 for space) in the URI to character + var uri_decoded: string = substitute(uri, '%\(\x\x\)', + '\=nr2char(str2nr(submatch(1), 16))', 'g') + + # File URIs on MS-Windows start with file:///[a-zA-Z]:' + if uri_decoded =~? '^file:///\a:' + # MS-Windows URI + uri_decoded = uri_decoded[8 : ] + uri_decoded = uri_decoded->substitute('/', '\\', 'g') + # On GNU/Linux (pattern not end with `:`) + elseif uri_decoded =~? '^file:///\a' + uri_decoded = uri_decoded[7 : ] + endif + + return uri_decoded +enddef + +# Convert a LSP file URI (file://<absolute_path>) to a Vim buffer number. +# If the file is not in a Vim buffer, then adds the buffer. +# Returns 0 on error. +export def LspUriToBufnr(uri: string): number + return LspUriToFile(uri)->bufadd() +enddef + +# Returns if the URI refers to a remote file (e.g. ssh://) +# Credit: vim-lsp plugin +export def LspUriRemote(uri: string): bool + return uri =~ '^\w\+::' || uri =~ '^[a-z][a-z0-9+.-]*://' +enddef + +var resolvedUris = {} + +# Convert a Vim filename to an LSP URI (file://<absolute_path>) +export def LspFileToUri(fname: string): string + var fname_full: string = fname->fnamemodify(':p') + + if resolvedUris->has_key(fname_full) + return resolvedUris[fname_full] + endif + + var uri: string = fname_full + + if has("win32unix") + # We're in Cygwin, convert POSIX style paths to Windows style. + # The substitution is to remove the '^@' escape character from the end of + # line. + uri = system($'cygpath -m {uri}')->substitute('^\(\p*\).*$', '\=submatch(1)', "") + endif + + var on_windows: bool = false + if uri =~? '^\a:' + on_windows = true + endif + + if on_windows + # MS-Windows + uri = uri->substitute('\\', '/', 'g') + endif + + uri = uri->substitute('\([^A-Za-z0-9-._~:/]\)', + '\=printf("%%%02x", char2nr(submatch(1)))', 'g') + + if on_windows + uri = $'file:///{uri}' + else + uri = $'file://{uri}' + endif + + resolvedUris[fname_full] = uri + return uri +enddef + +# Convert a Vim buffer number to an LSP URI (file://<absolute_path>) +export def LspBufnrToUri(bnr: number): string + return LspFileToUri(bnr->bufname()) +enddef + +# Returns the byte number of the specified LSP position in buffer "bnr". +# LSP's line and characters are 0-indexed. +# Vim's line and columns are 1-indexed. +# Returns a zero-indexed column. +export def GetLineByteFromPos(bnr: number, pos: dict<number>): number + var col: number = pos.character + # When on the first character, we can ignore the difference between byte and + # character + if col <= 0 + return col + endif + + # Need a loaded buffer to read the line and compute the offset + :silent! bnr->bufload() + + var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '') + if ltext->empty() + return col + endif + + var byteIdx = ltext->byteidxcomp(col) + if byteIdx != -1 + return byteIdx + endif + + return col +enddef + +# Get the index of the character at [pos.line, pos.character] in buffer "bnr" +# without counting the composing characters. The LSP server counts composing +# characters as separate characters whereas Vim string indexing ignores the +# composing characters. +export def GetCharIdxWithoutCompChar(bnr: number, pos: dict<number>): number + var col: number = pos.character + # When on the first character, nothing to do. + if col <= 0 + return col + endif + + # Need a loaded buffer to read the line and compute the offset + :silent! bnr->bufload() + + var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '') + if ltext->empty() + return col + endif + + # Convert the character index that includes composing characters as separate + # characters to a byte index and then back to a character index ignoring the + # composing characters. + var byteIdx = ltext->byteidxcomp(col) + if byteIdx != -1 + if byteIdx == ltext->strlen() + # Byte index points to the byte after the last byte. + return ltext->strcharlen() + else + return ltext->charidx(byteIdx, false) + endif + endif + + return col +enddef + +# Get the index of the character at [pos.line, pos.character] in buffer "bnr" +# counting the composing characters as separate characters. The LSP server +# counts composing characters as separate characters whereas Vim string +# indexing ignores the composing characters. +export def GetCharIdxWithCompChar(ltext: string, charIdx: number): number + # When on the first character, nothing to do. + if charIdx <= 0 || ltext->empty() + return charIdx + endif + + # Convert the character index that doesn't include composing characters as + # separate characters to a byte index and then back to a character index + # that includes the composing characters as separate characters + var byteIdx = ltext->byteidx(charIdx) + if byteIdx != -1 + if byteIdx == ltext->strlen() + return ltext->strchars() + else + return ltext->charidx(byteIdx, true) + endif + endif + + return charIdx +enddef + +# push the current location on to the tag stack +export def PushCursorToTagStack() + settagstack(winnr(), {items: [ + { + bufnr: bufnr(), + from: getpos('.'), + matchnr: 1, + tagname: expand('<cword>') + }]}, 't') +enddef + +# Jump to the LSP "location". The "location" contains the file name, line +# number and character number. The user specified window command modifiers +# (e.g. topleft) are in "cmdmods". +export def JumpToLspLocation(location: dict<any>, cmdmods: string) + var [uri, range] = LspLocationParse(location) + var fname = LspUriToFile(uri) + + # jump to the file and line containing the symbol + var bnr: number = fname->bufnr() + if cmdmods->empty() + if bnr == bufnr() + # Set the previous cursor location mark. Instead of using setpos(), m' is + # used so that the current location is added to the jump list. + :normal m' + else + var wid = fname->bufwinid() + if wid != -1 + wid->win_gotoid() + else + if bnr != -1 + # Reuse an existing buffer. If the current buffer has unsaved changes + # and 'hidden' is not set or if the current buffer is a special + # buffer, then open the buffer in a new window. + if (&modified && !&hidden) || &buftype != '' + exe $'belowright sbuffer {bnr}' + else + exe $'buf {bnr}' + endif + else + if (&modified && !&hidden) || &buftype != '' + # if the current buffer has unsaved changes and 'hidden' is not set, + # or if the current buffer is a special buffer, then open the file + # in a new window + exe $'belowright split {fname}' + else + exe $'edit {fname}' + endif + endif + endif + endif + else + if bnr == -1 + exe $'{cmdmods} split {fname}' + else + # Use "sbuffer" so that the 'switchbuf' option settings are used. + exe $'{cmdmods} sbuffer {bnr}' + endif + endif + var rstart = range.start + setcursorcharpos(rstart.line + 1, + GetCharIdxWithoutCompChar(bufnr(), rstart) + 1) + :normal! zv +enddef + +# indexof() function is not present in older Vim 9 versions. So use this +# function. +export def Indexof(list: list<any>, CallbackFn: func(number, any): bool): number + var ix = 0 + for val in list + if CallbackFn(ix, val) + return ix + endif + ix += 1 + endfor + return -1 +enddef + +# Find the nearest root directory containing a file or directory name from the +# list of names in "files" starting with the directory "startDir". +# Based on a similar implementation in the vim-lsp plugin. +# Searches upwards starting with the directory "startDir". +# If a file name ends with '/' or '\', then it is a directory name, otherwise +# it is a file name. +# Returns '' if none of the file and directory names in "files" can be found +# in one of the parent directories. +export def FindNearestRootDir(startDir: string, files: list<any>): string + var foundDirs: dict<bool> = {} + + for file in files + if file->type() != v:t_string || file->empty() + continue + endif + var isDir = file[-1 : ] == '/' || file[-1 : ] == '\' + var relPath: string + if isDir + relPath = finddir(file, $'{startDir};') + else + relPath = findfile(file, $'{startDir};') + endif + if relPath->empty() + continue + endif + var rootDir = relPath->fnamemodify(isDir ? ':p:h:h' : ':p:h') + foundDirs[rootDir] = true + endfor + if foundDirs->empty() + return '' + endif + + # Sort the directory names by length + var sortedList: list<string> = foundDirs->keys()->sort((a, b) => { + return b->len() - a->len() + }) + + # choose the longest matching path (the nearest directory from "startDir") + return sortedList[0] +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/doc/lsp.txt b/vim/pack/downloads/opt/lsp/doc/lsp.txt new file mode 100644 index 0000000..0d39a35 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/doc/lsp.txt @@ -0,0 +1,2001 @@ +*lsp.txt* Language Server Protocol (LSP) Plugin for Vim9 + + +Author: Yegappan Lakshmanan (yegappan AT yahoo DOT com) +For Vim version 9.0 and above +Last change: Feb 13, 2024 + +============================================================================== +CONTENTS *lsp-contents* + + 1. Overview ................................. |lsp-overview| + 2. Requirements ............................. |lsp-installation| + 3. Usage .................................... |lsp-usage| + 4. Configuration............................. |lsp-configuration| + 5. Commands ................................. |lsp-commands| + 6. Insert Mode Completion ................... |lsp-ins-mode-completion| + 7. Diagnostics .............................. |lsp-diagnostics| + 8. Tag Function ............................. |lsp-tagfunc| + 9. LSP Formatting ........................... |lsp-format| + 10. Call Hierarchy ........................... |lsp-call-hierarchy| + 11. Autocommands ............................. |lsp-autocmds| + 12. Highlight Groups ......................... |lsp-highlight-groups| + 13. Debugging ................................ |lsp-debug| + 14. Custom Command Handlers .................. |lsp-custom-commands| + 15. Custom LSP Completion Kinds .............. |lsp-custom-kinds| + 16. Multiple Language Servers for a buffer ... |lsp-multiple-servers| + 17. Language Servers Features ................ |lsp-features| + 18. License .................................. |lsp-license| + +============================================================================== +1. Overview *lsp-overview* + +The Language Server Protocol (LSP) plugin implements a LSP client for Vim9. +Refer to the following pages for more information about LSP: + + https://microsoft.github.io/language-server-protocol/ + https://langserver.org/ + +This plugin needs Vim version 9.0 and after. You will need a programming +language specific server in your system to use this plugin. Refer to the above +pages for a list of available language servers for the various programming +languages. + +The Github repository for this plugin is available at: + + http://github.com/yegappan/lsp + +============================================================================== +2. Installation *lsp-installation* + +You can install this plugin directly from github using the following steps: + + $ mkdir -p $HOME/.vim/pack/downloads/opt + $ cd $HOME/.vim/pack/downloads/opt + $ git clone https://github.com/yegappan/lsp + $ vim -u NONE -c "helptags $HOME/.vim/pack/downloads/opt/lsp/doc" -c q + +or you can use any one of the Vim plugin managers (dein.vim, pathogen, vam, +vim-plug, volt, Vundle, etc.) to install and manage this plugin. + +To uninstall the LSP plugin, either use the uninstall command provided by the +plugin manager or manually remove the $HOME/.vim/pack/downloads/lsp directory. + +To use this plugin, add the following line to your .vimrc file: + + packadd lsp + +============================================================================== +3. Usage *lsp-usage* + +The following commands are provided: + +:LspCodeAction Apply the code action supplied by the language server + to the diagnostic in the current line. +:LspCodeLens Display all the code lens commands available for the + current file and apply the selected command. +:LspDiag current Display the diagnostic message for the current line. +:LspDiag first Jump to the first diagnostic message for the current + buffer. +:LspDiag here Jump to the next diagnostic message in the current + line. +:LspDiag highlight disable + Disable highlighting lines with a diagnostic message + for the current Vim session. +:LspDiag highlight enable + Enable highlighting lines with a diagnostic message + for the current Vim session. +:LspDiag last Jump to the last diagnostic message for the current + buffer. +:LspDiag next Jump to the next diagnostic message for the current + buffer after the current cursor position. +:LspDiag nextWrap Jump to the next diagnostic message for the current + buffer after the current cursor position. + Wrap back to the first message when no more messages + are found. +:LspDiag prev Jump to the previous diagnostic message for the + current buffer before the current current position. +:LspDiag prevWrap Jump to the previous diagnostic message for the + current buffer before the current current position. + Wrap back to the last message when no previous + messages are found. +:LspDiag show Display the diagnostics messages from the language + server for the current buffer in a location list. +:LspDocumentSymbol Display the symbols in the current file in a popup + menu and jump to the location of a selected symbol. +:LspFold Fold the current file +:LspFormat Format a range of lines in the current file using the + language server. The default range is the entire + file. See |lsp-format| for more information. +:LspGotoDeclaration Go to the declaration of the symbol under cursor +:LspGotoDefinition Go to the definition of the symbol under cursor +:LspGotoImpl Go to the implementation of the symbol under cursor +:LspGotoTypeDef Go to the type definition of the symbol under cursor +:LspHighlight Highlight all the matches for the keyword under cursor +:LspHighlightClear Clear all the matches highlighted by :LspHighlight +:LspHover Show the documentation for the symbol under the cursor + in a popup window. +:LspIncomingCalls Display the list of symbols calling the current symbol + in a window. +:LspInlayHints Enable or disable inlay hints. +:LspOutgoingCalls Display the list of symbols called by the current + symbol in a window. +:LspOutline Show the list of symbols defined in the current file + in a separate window. +:LspPeekDeclaration Open the declaration of the symbol under cursor in a + popup window. +:LspPeekDefinition Open the definition of the symbol under cursor in a + popup window. +:LspPeekImpl Open the implementation of the symbol under cursor in + a popup window. +:LspPeekReferences Display the list of references to the symbol under + cursor in a popup window. +:LspPeekTypeDef Open the type definition of the symbol under cursor in + a popup window. +:LspRename Rename the current symbol +:LspSelectionExpand Expand the current symbol range visual selection +:LspSelectionShrink Shrink the current symbol range visual selection +:LspServer Command to display the status and messages from a + language server and to restart the language server. +:LspShowAllServers Display the status of all the registered language + servers. +:LspShowReferences Display the list of references to the keyword under + cursor in a new location list. +:LspShowSignature Display the signature of the symbol under cursor. +:LspSubTypeHierarchy Display the sub type hierarchy in a popup window. +:LspSuperTypeHierarchy Display the super type hierarchy in a popup window. +:LspSwitchSourceHeader Switch between a source and a header file. +:LspSymbolSearch Perform a workspace wide search for a symbol +:LspWorkspaceAddFolder {folder} + Add a folder to the workspace +:LspWorkspaceListFolders + Show the list of folders in the workspace +:LspWorkspaceRemoveFolder {folder} + Remove a folder from the workspace + +============================================================================== +4. Configuration *lsp-configuration* + *LspAddServer()* *g:LspAddServer()* + +To use the plugin features with a particular file type(s), you need to first +register a language server for that file type(s). + +To register one or more language servers, use the LspAddServer() function with +a list of lanaguge server details in the .vimrc file. + +To register a language server, add the following lines to your .vimrc file +(use only the language servers that you need from the below list). +If you used [vim-plug](https://github.com/junegunn/vim-plug) to install the +LSP plugin, the steps are described later in this section: > + + vim9script + var lspServers = [ + { + name: 'typescriptls', + filetype: ['javascript', 'typescript'], + path: '/usr/local/bin/typescript-language-server', + args: ['--stdio'] + }, + { + name: 'pythonls', + filetype: 'python', + path: '/usr/local/bin/pyls', + args: ['--check-parent-process', '-v'] + } + ] + LspAddServer(lspServers) +< +Depending on the location of the typescript and python pyls language servers +installed in your system, update the "path" in the above snippet +appropriately. + +Another example, for adding the language servers for the C, C++, Golang, Rust, +Shell script, Vim script and PHP file types: > + + vim9script + var lspServers = [ + { + name: 'clangd', + filetype: ['c', 'cpp'], + path: '/usr/local/bin/clangd', + args: ['--background-index'] + }, + { + name: 'golang', + filetype: ['go', 'gomod', 'gohtmltmpl', 'gotexttmpl'], + path: '/path/to/.go/bin/gopls', + args: [], + syncInit: true, + }, + { + name: 'rustls', + filetype: ['rust'], + path: '/path/to/.cargo/bin/rust-analyzer', + args: [], + syncInit: true, + }, + { + name: 'bashls', + filetype: 'sh', + path: '/usr/local/bin/bash-language-server', + args: ['start'] + }, + { + name: 'vimls', + filetype: ['vim'], + path: '/usr/local/bin/vim-language-server', + args: ['--stdio'] + }, + { + name: 'phpls', + filetype: ['php'], + path': '/usr/local/bin/intelephense', + args: ['--stdio'], + syncInit: true, + initializationOptions: { + licenceKey: 'xxxxxxxxxxxxxxx' + } + } + ] + LspAddServer(lspServers) +< +To add a language server, the following information is needed: + + *lsp-cfg-name* + name (Optional) name of the language server. Can by any + string. Used in LSP messages and log files. + *lsp-cfg-path* + path complete path to the language server executable + (without any arguments). + *lsp-cfg-args* + args a |List| of command-line arguments passed to the + language server. Each space separated language server + command-line argument is a separate List item. + *lsp-cfg-filetype* + filetype One or more file types supported by the language + server. This can be a |String| or a |List|. To + specify multiple file types, use a List. + *lsp-cfg-initializationOptions* + initializationOptions + (Optional) for lsp servers (e.g. intelephense) some + additional initialization options may be required + or useful for initialization. Those can be provided in + this dictionary and if present will be transmitted to + the lsp server. + *lsp-cfg-workspaceConfig* + workspaceConfig (Optional) a json encodable value that will be sent to + the language server after initialization as the + "settings" in a "workspace/didChangeConfiguration" + notification. Refer to the language server + documentation for the values that will be accepted in + this notification. This configuration is also used to + respond to the "workspace/configuration" request + message from the language server. + *lsp-cfg-rootSearch* + rootSearch (Optional) a List of file and directory names used to + locate the root path or uri of the workspace. The + directory names in "rootSearch" must end in "/" or + "\". Each file and directory name in "rootSearch" is + searched upwards in all the parent directories. If + multiple directories are found, then the directory + closest to the directory of the current buffer is used + as the workspace root. + + If this parameter is not specified or the files are + not found, then the current working directory is used + as the workspace root for decendent files, for any + other files the parent directory of the file is used. + + *lsp-cfg-runIfSearch* + runIfSearch (Optional) a List of file and directory names used to + determinate if a server should run or not. The + directory names in "runIfSearch" must end in "/" or + "\". Each file and directory name in "runIfSearch" is + searched upwards in all the parent directories. + Exactly like |lsp-cfg-rootSearch|. + + If a file or directory is found then the server will + be started, otherwise it will not. + + If this parameter is not specified or is an empty + list, then the server will be started unless + |lsp-cfg-runUnlessSearch| prevents it. + + *lsp-cfg-runUnlessSearch* + runUnlessSearch (Optional) Opposite of |lsp-cfg-runIfSearch|. + +Additionally the following configurations can be made: + + *lsp-cfg-customNotificationHandlers* + customNotificationHandlers + (Optional) some lsp servers (e.g. + typescript-language-server) will send additional + notifications which you might want to silence or + handle. The provided notification handlers will be + called with a reference to the "lspserver" and the + "reply". > + + vim9script + g:LspAddServer([{ + filetype: ['javascript', 'typescript'], + path: '/usr/local/bin/typescript-language-server', + args: ['--stdio'], + customNotificationHandlers: { + '$/typescriptVersion': (lspserver, reply) => { + echom printf("TypeScript Version = %s", + reply.params.version) + } + } + }]) +< + *lsp-cfg-customRequestHandlers* + customRequestHandlers + (Optional) some lsp servers will send additional + request replies which you might want to silence or + handle. The provided request handlers will be called + with a reference to the "lspserver" and the "request". + + features *lsp-cfg-features* + (Optional) toggle which features should be enabled for + a given language server. See |lsp-multiple-servers| + and |lsp-features| for more information. + + forceOffsetEncoding *lsp-cfg-forceOffsetEncoding* + (Optional) a |String| value that forces the use of a + specific offset encoding in LSP messages. If this + option is not specified, then the UTF offset encoding + is negotiated with the server during initialization. + Supported values are 'utf-8' or 'utf-16' or 'utf-32'. + The Vim native offset encoding is 'utf-32'. For the + 'utf-8' and 'utf-16' encodings, the offsets need to be + encoded and decoded in every LSP message and will + incur some overhead. + + *lsp-cfg-omnicompl* + omnicompl (Optional) a boolean value that enables (true) + or disables (false) omni-completion for these file + types. By default this is set to "v:true". This value + is applicable only if auto completion is disabled + (|lsp-opt-autoComplete|). + + *lsp-cfg-processDiagHandler* + processDiagHandler + (Optional) A |Funcref| or |lambda| that takes a list + of language server diagnostics and returns a new list + of filtered, or otherwise changed diagnostics. Can be + used to remove unwanted diagnostics, prefix the + diagnostics text, etc. The following example will + remove all but errors and warnings: > + + vim9script + g:LspAddServer([{ + filetype: ['javascript', 'typescript'], + path: '/usr/local/bin/typescript-language-server', + args: ['--stdio'], + processDiagHandler: (diags: list<dict<any>>) => { + # Only include errors and warnings + return diags->filter((diag, ix) => { + return diag.severity <= 2 + }) + }, + }]) +< + And this example will prefix the diagnostic message + with the string "TypeScript: ": > + + vim9script + g:LspAddServer([{ + filetype: ['javascript', 'typescript'], + path: '/usr/local/bin/typescript-language-server', + args: ['--stdio'], + processDiagHandler: (diags: list<dict<any>>) => { + return diags->map((diag, ix) => { + diag.message = $'TypeScript: {diag.message}' + return diag + }) + }, + }]) +< + *lsp-cfg-syncInit* + syncInit (Optional) for language servers (e.g. rust analyzer, + gopls, etc.) that take time to initialize and reply to + a "initialize" request message this should be set to + "true". If this is set to true, then a synchronous + call is used to initialize the language server, + otherwise the server is initialized asynchronously. + By default this is set to "false". + + *lsp-cfg-debug* + debug (Optional) log the messages printed by this language + server in stdout and stderr to a file. Useful for + debugging a language server. By default the + messages are not logged. See |lsp-debug| for more + information. + + *lsp-cfg-traceLevel* + traceLevel (Optional) set the debug trace level for this language + server. Supported values are: "off", "debug" and + "verbose". By default this is seto "off". + +The language servers are added using the LspAddServer() function. This +function accepts a list of language servers with the above information. + +If you used [vim-plug](https://github.com/junegunn/vim-plug) to install the +LSP plugin, then you need to use the LspSetup User autocmd to initialize the +language server and to set the language server options. For example: > + + vim9script + + var lspOpts = {autoHighlightDiags: true} + autocmd User LspSetup LspOptionsSet(lspOpts) + + var lspServers = [ + { + name: 'clangd', + filetype: ['c', 'cpp'], + path: '/usr/local/bin/clangd', + args: ['--background-index'] + } + ] + autocmd User LspSetup LspAddServer(lspServers) +< + *lsp-options* *LspOptionsSet()* + *g:LspOptionsSet()* + +Some of the LSP plugin features can be enabled or disabled by using the +LspOptionsSet() function. This function accepts a dictionary argument with the +following optional items: + + *lsp-opt-aleSupport* +aleSupport |Boolean| option. If true, diagnostics will be sent to + Ale, instead of being displayed by this plugin. + This is useful to combine all LSP and linter + diagnostics. By default this is set to false. + + *lsp-opt-autoComplete* +autoComplete |Boolean| option. In insert mode, automatically + complete the current symbol. Otherwise use + omni-completion. By default this is set to true. + + *lsp-opt-autoHighlight* +autoHighlight |Boolean| option. In normal mode, automatically + highlight all the occurrences of the symbol under the + cursor. By default this is set to false. + + *lsp-opt-autoHighlightDiags* +autoHighlightDiags |Boolean| option. Automatically place signs on the + lines with a diagnostic message from the language + server. By default this is set to true. + + *lsp-opt-autoPopulateDiags* +autoPopulateDiags |Boolean| option. Automatically populate the location + list with diagnostics from the language server. + By default this is set to false. + + *lsp-opt-completionMatcher* +completionMatcher |String| option. Enable fuzzy or case insensitive + completion for language servers that replies with a + full list of completion items. Some language servers + does completion filtering in the server, while other + relies on the client to do the filtering. + + This option only works for language servers that + expect the client to filter the completion items. + + This option accepts one of the following values: + case - case sensitive matching (default). + fuzzy - fuzzy match completion items. + icase - ignore case when matching items. + + *lsp-opt-completionTextEdit* +completionTextEdit |Boolean| option. If true, apply the LSP server + supplied text edits after a completion. If a snippet + plugin is going to apply the text edits, then set + this to false to avoid applying the text edits twice. + By default this is set to true. + + *lsp-opt-completionKinds* +completionKinds |Dictionary| option. See |lsp-custom-kinds| for all + completion kind names. + + *lsp-opt-customCompletionKinds* +customCompletionKinds |Boolean| option. If you set this to true, you can + set custom completion kinds using the option + completionKinds. + + *lsp-opt-diagSignErrorText* +diagSignErrorText |String| option. Change diag sign text for errors + By default 'E>' + + *lsp-opt-diagSignHintText* +diagSignHintText |String| option. Change diag sign text for hints + By default 'H>', + + *lsp-opt-diagSignInfoText* +diagSignInfoText |String| option. Change diag sign text for info + By default 'I>', + + *lsp-opt-diagSignWarningText* +diagSignWarningText |String| option. Change diag sign text for warnings + By default 'W>', + + *lsp-opt-diagVirtualTextAlign* +diagVirtualTextAlign |String| option. Alignment of diagnostics messages + if |lsp-opt-showDiagWithVirtualText| is set to true. + Allowed values are 'above', 'below' or 'after' + By default this is set to 'above', + + *lsp-opt-diagVirtualTextWrap* +diagVirtualTextWrap |String| option. Wrapping of diagnostics messages + if |lsp-opt-showDiagWithVirtualText| is set to true. + Allowed values are 'default', 'wrap' or 'truncate' + By default this is set to 'default', + + *lsp-opt-echoSignature* +echoSignature |Boolean| option. In insert mode, echo the current + symbol signature instead of showing it in a popup. + By default this is set to false. + + *lsp-opt-hideDisabledCodeActions* +hideDisabledCodeActions |Boolean| option. Hide all the disabled code actions. + By default this is set to false. + + *lsp-opt-highlightDiagInline* +highlightDiagInline |Boolean| option. Highlight the diagnostics inline. + By default this is set to true. + + *lsp-opt-hoverInPreview* +hoverInPreview |Boolean| option. Show |:LspHover| in a preview window + instead of a popup. + By default this is set to false. + + *lsp-opt-ignoreMissingServer* +ignoreMissingServer |Boolean| option. Do not print a missing language + server executable. By default this is set to false. + + *lsp-opt-keepFocusInDiags* +keepFocusInDiags |Boolean| option. Focus on the location list window + after ":LspDiag show". + By default this is set to true. + + *lsp-opt-keepFocusInReferences* +keepFocusInReferences |Boolean| option. Focus on the location list window + after LspShowReferences. + By default this is set to true. + + *lsp-opt-noNewlineInCompletion* +noNewlineInCompletion |Boolean| option. Suppress adding a new line on + completion selection with <CR>. + By default this is set to false. + + *lsp-opt-omniComplete* +omniComplete |Boolean| option. Enables or disables omni-completion. + By default this is set to v:false. If "autoComplete" + is set to v:false, then omni-completion is enabled by + default. By setting "omniComplete" option to v:false, + omni-completion can also be disabled. + + *lsp-opt-outlineOnRight* +outlineOnRight |Boolean| option. Open the outline window on the + right side, by default this is false. + + *lsp-opt-outlineWinSize* +outlineWinSize |Number| option. The size of the symbol Outline + window. By default this is set to 20. + + *lsp-opt-semanticHighlight* +semanticHighlight |Boolean| option. Enables or disables semantic + highlighting. + By default this is set to false. + + *lsp-opt-showDiagInBalloon* +showDiagInBalloon |Boolean| option. When the mouse is over a range of + text referenced by a diagnostic, display the + diagnostic text in a balloon. By default this is set + to true. In a GUI Vim, this needs the |+balloon_eval| + feature. In a terminal Vim, this needs the + |+balloon_eval_term| feature. In a terminal Vim, + 'mouse' option should be set to enable mouse. + If this option is set to true, then the 'ballooneval' + and 'balloonevalterm' options are set. + + *lsp-opt-showDiagInPopup* +showDiagInPopup |Boolean| option. When using the ":LspDiag current" + command to display the diagnostic message for the + current line, use a popup window to display the + message instead of echoing in the status area. + By default this is set to true. + + *lsp-opt-showDiagOnStatusLine* +showDiagOnStatusLine |Boolean| option. Show a diagnostic message on a + status line. By default this is set to false. + + *lsp-opt-showDiagWithSign* +showDiagWithSign |Boolean| option. Place a sign on lines with + diagnostics. By default this is set to true. The + "autoHighlightDiags" option should be set to true. + + *lsp-opt-showDiagWithVirtualText* +showDiagWithVirtualText |Boolean| option. Show diagnostic message text from + the language server with virtual text. By default + this is set to false. The "autoHighlightDiags" option + should be set to true. + Needs Vim version 9.0.1157 or later. + + *lsp-opt-showInlayHints* +showInlayHints |Boolean| option. Show inlay hints from the language + server. By default this is set to false. The inlay + hint text is displayed as a virtual text. Needs Vim + version 9.0.0178 or later. + + *lsp-opt-showSignature* +showSignature |Boolean| option. In insert mode, automatically show + the current symbol signature in a popup. + By default this is set to true. + + *lsp-opt-snippetSupport* +snippetSupport |Boolean| option. Enable snippet completion support. + Need a snippet completion plugin like vim-vsnip. + By default this is set to false. + + *lsp-opt-ultisnipsSupport* +ultisnipsSupport |Boolean| option. Enable SirVer/ultisnips support. + Need a snippet completion plugin SirVer/ultisnips. + By default this is set to false. + + *lsp-opt-vssnipSupport* +vsnipSupport |Boolean| option. Enable hrsh7th/vim-vsnip support. + Need snippet completion plugins hrsh7th/vim-vsnip + and hrsh7th/vim-vsnip-integ. Make sure + ultisnipsSupport is set to false before enabling this. + By default this option is set to false. + + *lsp-opt-usePopupInCodeAction* +usePopupInCodeAction |Boolean| option. When using the |:LspCodeAction| + command to display the code action for the current + line, use a popup menu instead of echoing. + By default this is set to false. + + *lsp-opt-useQuickfixForLocations* +useQuickfixForLocations |Boolean| option. Show |:LspShowReferences| in a + quickfix list instead of a location list. + By default this is set to false. + + *lsp-opt-useBufferCompletion* +useBufferCompletion |Boolean| option. If enabled, the words from the + current buffer are added to the auto completion list. + By default this is set to false. + + *lsp-opt-bufferCompletionTimeout* +bufferCompletionTimeout |Number| option. Specifies how long (in milliseconds) + to wait while processing current buffer for + autocompletion words. If set too high Vim performance + may degrade as the current buffer contents are + processed every time the completion menu is displayed. + If set to 0 the entire buffer is processed without + regard to timeout. + By default this is set to 100 ms. + + *lsp-opt-filterCompletionDuplicates* +filterCompletionDuplicates |Boolean| option. If enabled, duplicate completion + items sent from the server will be filtered to only + include one instance of the duplicates. + +For example, to disable the automatic placement of signs for the LSP +diagnostic messages, you can add the following line to your .vimrc file: > + + call LspOptionsSet({'autoHighlightDiags': false}) +< + *LspOptionsGet()* +The LspOptionsGet() function returns a |Dict| of all the LSP plugin options, +To get a particular option value you can use the following: > + + echo LspOptionsGet()['autoHighlightDiags'] +< +============================================================================== +5. Commands *lsp-commands* + +A description of the various commands provided by this plugin is below. You +can map these commands to keys and make it easier to invoke them. + + *:LspCodeAction* +:LspCodeAction [query] Apply the code action supplied by the language server + to the diagnostic in the current line. This works only + if there is a diagnostic message for the current line. + You can use the ":LspDiag current" command to display + the diagnostic for the current line. + + When [query] is given the code action starting with + [query] will be applied. [query] can be a regexp + pattern, or a digit corresponding to the index of the + code actions in the created prompt. + + When [query] is not given you will be prompted to + select one of the actions supplied by the language + server. + + *:LspCodeLens* +:LspCodeLens Display a list of code lens commands available for the + current buffer and apply the selected code lens + command. + + *:LspDiag-current* +:LspDiag current Displays the diagnostic message (if any) for the + current line. If the option 'showDiagInPopup' is set + to true (default), then the message is displayed in + a popup window. Otherwise the message is displayed in + the status message area. + +:LspDiag! current Only display a diagnostic message if it's directly + under the cursor. Otherwise works exactly like + ":LspDiag current" + + To show the current diagnotic under the cursor while + moving around the following autocmd can be used: > + + augroup LspCustom + au! + au CursorMoved * silent! LspDiag! current + augroup END +< + *:LspDiag-first* +:LspDiag first Jumps to the location of the first diagnostic message + for the current file. + + *:LspDiag-here* +:LspDiag here Jumps to the location of the diagnostic message in + the current line (start from current column). + +:LspDiag highlight disable *:LspDiag-highlight-disable* + Disable highlighting lines with a diagnostic message + for the current Vim session. + To always disable the highlighting, set the + autoHighlightDiags option to false. + +:LspDiag highlight enable *:LspDiag-highlight-enable* + Enable highlighting lines with a diagnostic message + for the current Vim session. Note that highlighting + lines with a diagnostic message is enabled by default. + + *:LspDiag-last* +:LspDiag last Jumps to the location of the first diagnostic message + for the current file. + + *:LspDiag-next* +:[count]LspDiag next Go to the [count] diagnostic message after the current + cursor position. If [count] is omitted, then 1 is + used. If [count] exceeds the number of diagnostics + after the current position, then the last diagnostic + is selected. + + *:LspDiag-prev* +:[count]LspDiag prev Go to the [count] diagnostic message before the + current cursor position. If [count] is omitted, then + 1 is used. If [count] exceeds the number of + diagnostics before the current position, then first + last diagnostic is selected. + + *:LspDiag-show* +:LspDiag show Creates a new location list with the diagnostics + messages (if any) from the language server for the + current file and opens the location list window. You + can use the Vim location list commands to browse the + list. + + *:LspDocumentSymbol* +:LspDocumentSymbol Display the symbols in the current file in a popup + menu. When a symbol is selected in the popup menu by + pressing <Enter> or <Space>, jump to the location of + the symbol. + + The <Up>, <Down>, <Tab>, <S-Tab>, <C-N>, <C-P>, + <ScrollWheelUp>, ScrollWheelDown> keys can be used to + scroll popup menu one item at a time. <PageUp> and + <PageDown> can be used to scroll a page of popup + window, while <C-F> and <C-B> can be used to scroll a + page of underlying window. The <Esc> or <Ctrl-C> keys + can be used to cancel the popup menu. + + If one or more keyword characters are typed, then only + the symbols containing the keyword characters are + displayed in the popup menu. Fuzzy searching is used + to get the list of matching symbols. The <BS> key can + be used to erase the last typed character. The <C-U> + key can be used to erase all the characters. + + When scrolling through the symbols in the popup menu, + the corresponding range of lines is highlighted. + + *:LspFold* +:LspFold Create folds for the current buffer. + + *:LspFormat* +:LspFormat Format the current file using the language server. The + 'shiftwidth' and 'expandtab' values set for the + current buffer are used when format is applied. + +:{range}LspFormat Format the specified range of lines in the current + file using the language server. + + *:LspGotoDeclaration* +:[count]LspGotoDeclaration + Jumps to the declaration of the symbol under the + cursor. The behavior of this command is similar to the + |:LspGotoDefinition| command. + + *:LspGotoDefinition* +:[count]LspGotoDefinition + Jumps to the [count] definition of the symbol under + the cursor. If there are multiple matches and [count] + isn't specified, then a location list will be created + with the list of locations. + + If there is only one location, or [count] is provided + then the following will apply: + + If the file is already present in a window, then jumps + to that window. Otherwise, opens the file in a new + window. If the current buffer is modified and + 'hidden' is not set or if the current buffer is a + special buffer, then a new window is opened. If the + jump is successful, then the current cursor location + is pushed onto the tag stack. The |CTRL-T| command + can be used to go back up the tag stack. Also the + |``| mark is set to the position before the jump. + + This command supports |:command-modifiers|. You can + use the modifiers to specify whether a new window or + a new tab page is used and where the window is opened. + Example(s): > + + # Open a horizontally split window + :topleft LspGotoDefinition + # Open a vertically split window + :vert LspGotoDefinition + # Open a new tab page + :tab LspGotoDefinition +< + You may want to map a key to invoke this command: > + + nnoremap <buffer> gd <Cmd>LspGotoDefinition<CR> + nnoremap <buffer> <C-W>gd <Cmd>topleft LspGotoDefinition<CR> +< + Or if you want to support [count]gd > + + nnoremap <buffer> gd <Cmd>execute v:count .. 'LspGotoDefinition'<CR> + nnoremap <buffer> <C-W>gd <Cmd>execute 'topleft ' .. v:count .. 'LspGotoDefinition'<CR> +< + *:LspGotoImpl* +:[count]LspGotoImpl Jumps to the implementation of the symbol under the + cursor. The behavior of this command is similar to the + |:LspGotoDefinition| command. Note that not all the + language servers support this feature. + + You may want to map a key to invoke this command: > + + nnoremap <buffer> gi <Cmd>LspGotoImpl<CR> +< + *:LspGotoTypeDef* +:[count]LspGotoTypeDef Jumps to the type definition of the symbol under the + cursor. The behavior of this command is similar to the + |:LspGotoDefinition| command. Note that not all the + language servers support this feature. + + You may want to map a key to invoke this command: > + + nnoremap <buffer> gt <Cmd>LspGotoTypeDef<CR> +< + *:LspHighlight* +:LspHighlight Highlights all the matches for the symbol under + cursor. The text, read and write references to the + symbol are highlighted using Search, DiffChange and + DiffDelete highlight groups respectively. + + *:LspHighlightClear* +:LspHighlightClear Clears all the symbol matches highlighted by the + |:LspHighlight| command. + + *:LspHover* +:LspHover Show the documentation for the symbol under the cursor + in a popup window. The following keys can be used to + scroll the popup window: + + <CTRL-E> - Scroll window downwards by a line. + <CTRL-D> - Scroll window downwards by 'scroll' + lines. + <CTRL-F> - Scroll window downards by a page. + <PageDown> - ditto. + <CTRL-Y> - Scroll window upwards by a line. + <CTRL-U> - Scroll window upwards by 'scroll' + lines. + <CTRL-B> - Scroll window upwards by a page. + <PageUp> - ditto. + <CTRL-Home> - Goto the first line + <CTRL-End> - Goto the last line + + Pressing any other key will close the popup window. + + If you want to show the symbol documentation in the + |preview-window| instead of in a popup window set > + + LspOptionsSet({'hoverInPreview': true}) +< + You can use the |:pclose| command to close the preview + window. + + You can use the |K| key in normal mode to display the + documentation for the keyword under the cursor by + setting the 'keywordprg' Vim option: > + + :set keywordprg=:LspHover +< + *:LspIncomingCalls* +:LspIncomingCalls Display a hierarchy of symbols calling the symbol + under the cursor in a window. See + |lsp-call-hierarchy| for more information. Note that + not all the language servers support this feature. + + *:LspInlayHints* +:LspInlayHints Enable or disable inlay hints. Supports the "enable" + and "disable" arguments. When "enable" is specified, + enables the inlay hints for all the buffers with a + language server that supports inlay hints. When + "disable" is specified, disables the inlay hints. + + *:LspOutoingCalls* +:LspOutgoingCalls Display a hierarchy of symbols called by the symbol + under the cursor in a window. See + |lsp-call-hierarchy| for more information. Note that + not all the language servers support this feature. + + *:LspOutline* +:[count]LspOutline Opens a vertically split window with the list of + symbols defined in the current file. The current + symbol is highlighted. The symbols are grouped by + their type. You can select a symbol and press <Enter> + to jump to the position of the symbol. As you move the + cursor in a file, the current symbol is automatically + highlighted in the outline window. If you open a new + file, the outline window is automatically updated with + the symbols in the new file. Folds are created in the + outline window for the various group of symbols. + + You can use |lsp-opt-outlineOnRight| and + |lsp-opt-outlineWinSize| to customize the placement + and size of the window. + + This command also supports |:command-modifiers|. You + can use the modifiers specify the position of the + window. Note that the default is ":vert :topleft" or + ":vert :botright" depending on + |lsp-opt-outlineOnRight| + + This command also supports providing a [count] to + specify the size of the window. Note that this + overrides the values defined in + |lsp-opt-outlineWinSize|. + Example: > + + # Open the outline window just above the current + # window + :aboveleft LspOutline + + # Open the outline window just next to the current + # window, this is different from the default, when + # you have multiple splits already + :vert aboveleft LspOutline + + # Same as above, but with a width of 50 + :vert aboveleft 50LspOutline +< + *:LspPeekDeclaration* +:[count]LspPeekDeclaration + Displays the line where the symbol under the + cursor is declared in a popup window. The + behavior of this command is similar to the + |:LspPeekDefinition| command. + + *:LspPeekDefinition* +:[count]LspPeekDefinition + Displays the line where the symbol under the cursor is + defined in a popup window. The symbol is highlighted + in the popup window. Moving the cursor or pressing + <Esc> will close the popup window. + When more than one symbol is found all of them will be + shown. The corresponding file for the symbol is + displayed in another popup window. As the selection + in the symbol popup menu changes, the file in the + popup is updated. + When [count] is provided only the [count] symbol will + be shown. + + *:LspPeekImpl* +:[count]LspPeekImpl Displays the implementation of the symbol under the + cursor in a popup window. The behavior of this + command is similar to the |:LspPeekDefinition| + command. Note that not all the language servers + support this feature. + + *:LspPeekReferences* +:LspPeekReferences Displays the list of references to the symbol under + cursor in a popup menu. The corresponding file for + the reference is displayed in another popup window. + As the selection in the reference popup menu changes, + the file in the popup is updated. + + *:LspPeekTypeDef* +:[count]LspPeekTypeDef Displays the line where the type of the symbol under + the cursor is defined in a popup window. The + behavior of this command is similar to the + |:LspPeekDefinition| command. Note that not all the + language servers support this feature. + + *:LspRename* +:LspRename [newName] Rename the current symbol. + + When [newName] is not given, then you will be prompted + to enter the new name for the symbol. You can press + <Esc> or enter an empty string in the prompt to cancel + the operation. + + *:LspSelectionExpand* +:LspSelectionExpand Visually select the region of the symbol under the + cursor. In visual mode, expands the current symbol + visual region selection to include the next level. + + For example, if the cursor is on a "for" statement, + this command selects the "for" statement and the body + of the "for" statement. + + It is useful to create a visual map to use this + command. Example: > + + xnoremap <silent> <Leader>e <Cmd>LspSelectionExpand<CR> +< + With the above map, you can press "\e" in visual mode + successively to expand the current symbol visual + region. + + *:LspSelectionShrink* +:LspSelectionShrink Shrink the current symbol range visual selection. It + is useful to create a visual map to use this command. + Example: > + + xnoremap <silent> <Leader>s <Cmd>LspSelectionShrink<CR> +< + With the above map, you can press "\s" in visual mode + successively to shrink the current symbol visual + region. + + *:LspServer* +:LspServer { debug | restart | show | trace } + Command to display and control the language server for + the current buffer. Each argument has additional + sub-commands which are described below. + + debug { on | off | messages | errors } + Command to enable or disable the language server + debug messages and to display the debug messages + and error messages received from the language + server. The following sub-commands are supported: + errors Open the log file containing the + language server error messages. + messages + Open the log file containing the + language server debug messages. + off Disable the logging of the language + server messages. + on Enable the logging of the messages + emitted by the language server in the + standard output and standard error. + By default, the language server messages are not + logged. On a Unix-like system, when enabled, + these messages are logged to the + /tmp/lsp-<server-name>.log and + /tmp/lsp-<server-name>.err file respectively. On + MS-Windows, the %TEMP%/lsp-<server-name>.log and + %TEMP%/lsp-<server-name>.err% files are used. See + |lsp-debug| for more information. + + restart + Restart (stop and then start) the language server + for the current buffer. All the loaded buffers + with the same filetype as the current buffer are + added back to the server. + + show {capabilities | initializeRequest | messages + | status} + The following sub-commands are supported: + capabilities + Display the list of language server + capabilities for the current buffer. + The server capabilities are described + in the LSP protocol specification + under the "ServerCapabilities" + interface. + initializeRequest + Display the contents of the language + server initialization request message + (initialize). + messages + Display the log messages received from + the language server. This includes + the messages received using the + "window/logMessage" and "$/logTrace" + LSP notifications. + status + Display the language server status for + the current buffer. The output shows + the path to the language server + executable and the server status. + + trace { off | messages | verbose } + Set the language server debug trace value using + the "$/setTrace" command. + + *:LspShowAllServers* +:LspShowAllServers Displays the list of registered language servers and + their status. The language servers are registered + using the LspAddServer() function. The output is + displayed in a scratch buffer. The output shows the + Vim file type, the corresponding language server + status and the path to the language server executable. + The language server information for each buffer is + also shown. + + *:LspShowReferences* +:LspShowReferences Creates a new location list with the list of locations + where the symbol under the cursor is referenced and + opens the location window. If you want to show the + references in a quickfix list instead of in a location + list set > + + LspOptionsSet({'useQuickfixForLocations': true}) +< + *:LspShowSignature* +:LspShowSignature Displays the signature of the symbol (e.g. a function + or method) before the cursor in a popup. + + The popup is also automatically displayed in insert + mode after entering a symbol name followed by a + separator (e.g. a opening parenthesis). To disable + this, you can set the showSignature option to false in + your .vimrc file: > + + LspOptionsSet({'showSignature': false}) +< + Default is true. + + You can get the function signature echoed in cmdline + rather than displayed in popup if you use > + + LspOptionsSet({'echoSignature': true}) +< + Default is false. + + *:LspSubTypeHierarchy* +:LspSubTypeHierarchy Show the sub type hierarchy for the symbol under the + cursor in a popup window. The file containing the + type is shown in another popup window. You can jump + to the location where a type is defined by browsing + the popup menu and selecting an entry. + + *:LspSuperTypeHierarchy* +:LspSuperTypeHierarchy Show the super type hierarchy for the symbol under the + cursor in a popup window. The file containing the + type is shown in another popup window. As the current + entry in the type hierarchy popup menu changes, the + file popup window is updated to show the location + where the type is defined. You can jump to the + location where a type is defined by selecting the + entry in the popup menu. + + Note that the type hierarchy support is based on the + protocol supported by clangd. This is different from + the one specified in the 3.17 of the LSP standard. + + *:LspSwitchSourceHeader* +:LspSwitchSourceHeader Switch between source and header files. This is a + Clangd specific extension and only works with C/C++ + source files. + + *:LspSymbolSearch* +:LspSymbolSearch <sym> Perform a workspace wide search for the symbol <sym>. + If <sym> is not supplied, then you will be prompted to + enter the symbol name (the keyword under the cursor is + used as the default). If there is only one matching + symbol, then the cursor will be positioned at the + symbol location. Otherwise a popup window is opened + with the list of matching symbols. You can enter a + few characters to narrow down the list of matches. The + displayed symbol name can be erased by pressing + <Backspace> or <C-U> and a new symbol search pattern + can be entered. You can close the popup menu by + pressing the escape key or by pressing CTRL-C. + + In the popup menu, the following keys can be used: + + CTRL-F - Scroll one page forward + <PageDown> - idem + CTRL-B - Scroll one page backward + <PageUp> - idem + CTRL-Home - Jump to the first entry + CTRL-End - Jump to the last entry + <Up> - Go up one entry + <C-P> - idem + <Down> - Go down one entry + <C-N> - idem + <Enter> - Open the selected file + <Esc> - Close the popup menu + <CTRL-C> - idem + <BS> - Erase one character from the + filter text + <C-H> - idem + <C-U> - Erase the filter text + + Any other alphanumeric key will be used to narrow down + the list of names displayed in the popup menu. When + you type a filter string, then only the symbols fuzzy + matching the string are displayed in the popup menu. + You can enter a new search pattern to do a workspace + wide symbol search. + + This command accepts |:command-modifiers| which can be + used to jump to a symbol in a horizontally or + vertically split window or a new tab page: > + + :topleft LspSymbolSearch foo + :vert LspSymbolSearch bar + :tab LspSymbolSearch baz +< + *:LspWorkspaceAddFolder* +:LspWorkspaceAddFolder {folder} + Add a folder to the workspace + +:LspWorkspaceListFolders *:LspWorkspaceListFolders* + Show the list of folders in the workspace. + + *:LspWorkspaceRemoveFolder* +:LspWorkspaceRemoveFolder {folder} + Remove a folder from the workspace + +============================================================================== +6. Insert Mode Completion *lsp-ins-mode-completion* + +By default, when you are in insert mode, the LSP plugin will automatically +display suggestions for the symbol under the cursor in an insert-completion +popup menu. The keys specified in |popupmenu-keys| can be used to interact +with this menu. + +To disable this auto-completion feature for all files, you can set the +"autoComplete" option to false in your .vimrc file using the |LspOptionsSet()| +function: > + + call LspOptionsSet({'autoComplete': false}) +< +By setting the "autoComplete" option to |v:false|, the LSP plugin will no +longer automatically trigger completion suggestions in insert mode. Instead, +it will use omni-completion (|compl-omni|) and set the 'omnifunc' option for +buffers that have a registered language server. To manually trigger symbol +completion in insert mode, you can press CTRL-X CTRL-O. This key combination +will invoke completion using the suggestions provided by the language server. + +To enable omni-completion for all the buffers, set the "omniComplete" option +to v:true. To explicitly disable omni-completion for all the buffers, set the +"omniComplete" option to v:false (default). + +In addition to the general auto-completion behavior discussed above, you +have the option to enable or disable omni-completion for a specific language +server when registering it for a particular filetype. + +To do this, you can set the 'omnicompl' item to |v:false| in the configuration +when registering the language server for the desired filetype. If the +'omnicompl' item is not specified, omni-completion is enabled by default. + +Here's an example of how to disable omni-completion for Python: > + + vim9script + var lspServers = [ + { + filetype: 'python', + omnicompl: false, + path: '/usr/local/bin/pyls', + args: ['--check-parent-process', '-v'] + } + ] +< +In this example, the language server for Python is registered using the +|LspAddServer()| function, and the 'omnicompl' item is explicitly set to +|v:false|. As a result, omni-completion will be disabled for Python files +associated with this language server. + +Please note that if 'omnicompl' is not included in the configuration +when registering the language server, omni-completion will be enabled by +default. + +In insert-mode completion, the plugin sends a completion request message to +the language server and obtains a list of potential completion matches based +on the current cursor position. To achieve this, the plugin retrieves the +keyword immediately preceding the cursor (refer to 'iskeyword' setting) and +then filters the list of completion items received from the language server +based on this keyword. The resulting filtered list is displayed as the +completion menu. + +It's worth noting that different language servers handle completion filtering +in distinct ways. Some servers perform the filtering directly on the +server-side, while others delegate this task to the client-side, which is the +plugin in this context. + +By default, the plugin uses a case-sensitive comparison method to filter the +returned completion items. However, you have the flexibility to customize this +behavior by modifying the "completionMatcher" option. This option allows you +to switch between case-insensitive or fuzzy comparison methods as per your +preference and requirements for completion matching. + +In addition to automatic completion and omni completion, there is a +possibility to utilize external completion engines with the LSP client. This +can be achieved by repurposing the |g:LspOmniFunc| function. The external +completion engine adapter needs to invoke this function twice, following the +approach outlined in the |complete-functions| documentation. + +The process works as follows: + +1. First Invocation: The external completion engine adapter calls + |g:LspOmniFunc| to initiate a request to the LSP server for completion + candidates. +2. After the first invocation, a request is sent to the LSP server to find + completion candidates. +3. Second Invocation: The external completion engine adapter calls + |g:LspOmniFunc| again to retrieve the matches returned by the LSP server. +4. If the LSP server is not ready to reply immediately, |g:LspOmniFunc| waits + for up to 2 seconds. +5. However, this wait could block the caller from performing other tasks, + which might be a concern for asynchronous completion engines. +6. To address this issue, the adapter can use the |g:LspOmniCompletePending| + function, which allows for a non-blocking check. It returns true + immediately if the language server is not ready to respond yet. +7. To proceed with the second invocation of g:LspOmniFunc, it is crucial to + ensure that |g:LspOmniCompletePending| returns false, indicating that the + language server is now ready to provide the completion matches. + +============================================================================== +7. Diagnostics *lsp-diagnostics* + +The LSP plugin offers a feature to highlight syntax errors, warnings, and +static analysis warnings in a source file by placing signs in the sign column. +These signs serve as visual indicators of the diagnostics reported by the +language server. + +To interact with these diagnostics, you can use various commands provided by +the LSP plugin: + +1. ":LspDiag show": This command displays all the diagnostic messages for the + current file in a location-list window. The location-list window allows + you to view a list of all the diagnostic messages, along with their + corresponding line numbers and descriptions. +2. ":LspDiag first": Use this command to jump directly to the line containing + the first diagnostic message. It helps you quickly navigate to the + location of the initial issue detected by the language server. +3. ":LspDiag next": With this command, you can navigate to the next nearest + line with a diagnostic message. It helps you step through the list of + diagnostics one by one. +4. ":LspDiag prev": Conversely, this command allows you to jump to the + previous nearest line with a diagnostic message. It is useful for + reviewing diagnostics in reverse order. +5. ":LspDiag here": If you want to focus solely on the diagnostic message for + the current line, you can use this command to jump directly to it. +6. ":LspDiag current": This command displays the entire diagnostic message + from the language server for the current line. It provides detailed + information about the specific issue and its description. + +By using these commands, you can efficiently navigate and inspect the +diagnostics reported by the language server, making it easier to identify and +address syntax errors, warnings, or static analysis issues in your code. + +By default, the LSP plugin marks lines with diagnostic messages by placing a +sign on them and highlighting the range of text associated with the +diagnostic. However, you have the option to customize this behavior by +adjusting certain configuration settings: + +1. Disabling Automatic Sign Placement: If you wish to prevent the automatic + placement of signs on lines with diagnostic messages, you can achieve this + by setting the "showDiagWithSign" option to |v:false|. By default, this + option is set to |v:true|, meaning that signs are automatically placed on + lines with diagnostics. +2. Disabling Diagnostic Text Highlighting: If you prefer not to have the + diagnostic text highlighted, you can do so by setting the + "highlightDiagInline" option to |v:false|. By default, this option is set + to |v:true|, resulting in the highlighting of the text range associated + with each diagnostic. +3. Highlight Group for Line with Diagnostics: The LSP plugin uses the + "LspDiagLine" highlight group to highlight lines containing diagnostics. + By default, this highlight group is not set, allowing you to define your + own highlighting style for lines with diagnostics if desired. + +In addition to the default display of the diagnostic messages with signs and +text highlighting, the LSP plugin offers the option to present the diagnostic +message as virtual text, located near the relevant location of the +diagnostics. To enable this feature, you can set the +"showDiagWithVirtualText" option to |v:true|. However, please note that this +functionality requires Vim version 9.0.1157 or later. By default, this option +is set to |v:false|, meaning that virtual text display is not activated. + +The position of the virtual text can be controlled using the +"diagVirtualTextAlign" option, which determines its alignment relative to the +affected line. By default, this option is set to 'above', which places the +virtual text above the line with the diagnostic message. The other supported +values for "diagVirtualTextAlign" are 'below', which positions the virtual +text below the affected line, and 'after', which displays the virtual text +immediately after the text on the affected line. + +The wrapping of the virtual text can be controlled using the +"diagVirtualTextWrap" option. By default, this option is set to 'default', +which will 'truncate' virtual text placed 'above' or 'below' the affected +line, and 'wrap' text placed 'after' the affected line. Setting the value to +'wrap' or 'truncate' will force the specified behavior for the current +value of "diagVirtualTextAlign". If 'truncate' is used while +"diagVirtualTextAlign" is set to 'after', and a diagnostic message has already +been truncated for the affected line, then further diagnostics will be placed +below the affected line. + +The LSP plugin offers convenient ways to highlight diagnostic messages, making +it easier to spot errors, warnings, hints, or informational notices within +your code. By default, the plugin automatically highlights the range of text +associated with each diagnostic message when the "highlightDiagInline" option +is set to |v:true.| + +The highlighting is done using different highlight groups based on the type of +diagnostic message: + + "LspDiagInlineError" for error messages. + "LspDiagInlineHint" for hints. + "LspDiagInlineInfo" for informational messages. + "LspDiagInlineWarning" for warning messages. + +If you wish to temporarily disable the automatic diagnostic highlighting for +the current Vim session, you can achieve this using the ":LspDiag highlight +disable" command. When you want to re-enable the highlighting, you can use +the ":LspDiag highlight enable" command. + +To permanently disable the automatic highlighting of diagnostics, you can set +the "autoHighlightDiags" option to |v:false| in your .vimrc file. This +configuration can be achieved using the |LspOptionsSet()| function: > + + call LspOptionsSet({'autoHighlightDiags': v:false}) +< +By default, the "autoHighlightDiags" option is set to |v:true|, ensuring that +diagnostic messages are automatically highlighted during your coding sessions. + +The lsp#lsp#ErrorCount() function returns the count of diagnostic messages in +the current buffer, categorized by their types. When called, this function +returns a Dictionary containing four keys: "Info," "Hint," "Warn," and +"Error." Each key corresponds to a specific diagnostic type, and its +associated value is the number of diagnostic messages of that particular type +found in the buffer. With the information gathered using this function, you +can easily display the number of diagnostics in the current buffer in your +'statusline'. + +For some diagnostic errors/warnings, the language server may provide an +automatic fix. To apply this fix, you can use the |:LspCodeAction| command. +This command applies the action provided by the language server (if any) for +the current line. + +The ":LspDiag show" command creates a new location list with the current list +of diagnostics for the current buffer. To automatically refresh the location +list with the latest diagnostics received from the language server, you can +set the "autoPopulateDiags" option to |v:true|. By default this option is set +to |v:false|. When new diagnostics are received for a buffer, if a location +list with the diagnostics is already present, then it is refreshed with the +new diagnostics. + +In GUI Vim or terminal Vim with the 'balloonevalterm' option enabled, a +helpful feature allows you to view diagnostic messages in a popup balloon when +you hover the mouse over the affected range of text. This provides a +convenient way to quickly access diagnostic information without the need to +execute additional commands or navigate through the location list. + +By default, the LSP plugin is configured to display diagnostic messages in the +popup balloon, enhancing the user experience and providing visual feedback as +you interact with your code. This default behavior is governed by the +"showDiagInBalloon" option, which is set to |v:true| by default. + +However, if you prefer not to see the diagnostic messages in the popup +balloons and prefer to rely solely on other methods, you have the flexibility +to customize this behavior. By setting the "showDiagInBalloon" option to +|v:false|, you can disable the display of diagnostic messages in the popup +balloons. This can be useful if you find the balloons intrusive or if you +prefer to view diagnostics through other means, such as the location list or +the status line. + +To display the diagnostic message for the current line in the status area, you +can set the "showDiagOnStatusLine" option to |v:true|. By default, this +option is set to |v:false|. + +By default, the ":LspDiag current" command displays the diagnostic message for +the current line in a popup window. To display the message in the status +message area instead, you can set the 'showDiagInPopup' option to |v:false|. +By default this is set to |v:true|. + +The lsp#diag#GetDiagsForBuf() function can be used to get all the LSP +diagnostics in a buffer. This function optionally accepts a buffer number. +If the buffer number argument is not specified, then the current buffer is +used. This function returns a |List| of diagnostics sorted by their line and +column number. Each diagnostic is a |Dict| returned by the language server. + +============================================================================== +8. Tag Function *lsp-tagfunc* + +The |:LspGotoDefinition| command can be used jump to the location where a +symbol is defined. To jump to the symbol definition using the Vim +|tag-commands|, you can set the 'tagfunc' option to the 'lsp#lsp#TagFunc' +function: > + + setlocal tagfunc=lsp#lsp#TagFunc +< +After setting the above option, you can use |Ctrl-]| and other tag related +commands to jump to the symbol definition. + +Note that most of the language servers return only one symbol location even if +the symbol is defined in multiple places in the code. + +============================================================================== +9. Code Formatting *lsp-format* + +The |:LspFormat| command can be used to format either the entire file or a +selected range of lines using the language server. The 'shiftwidth' and +'expandtab' values set for the current buffer are used when format is applied. + +To format code using the 'gq' command, you can set the 'formatexpr' option: > + + setlocal formatexpr=lsp#lsp#FormatExpr() +< +============================================================================== +10. Call Hierarchy *lsp-call-hierarchy* + +The |:LspIncomingCalls| and the |:LspOutoingCalls| commands can be used to +display the call hierarchy of a symbol. For example, the functions calling a +function or the functions called by a function. These two commands open a +window containing the call hierarchy tree. You can use the Vim motion +commands to browse the call hierarchy. + +In the call hierarchy tree window, the following keys are supported: + +<Enter> Jump to the location of the symbol under the + cursor. +- Expand and show the symbols calling or called + by the symbol under the cursor. ++ Close the call hierarchy for the symbol under + the cursor. + +You can display either the incoming call hierarchy or the outgoing call +hierarchy in this window. You cannot display both at the same time. + +In the call hierarchy tree window, the following commands are supported: + + *:LspCallHierarchyRefresh* +:LspCallHierarchyRefresh Query the language server again for the top + level symbol and refresh the call hierarchy + tree. + *:LspCallHierarchyIncoming* +:LspCallHierarchyIncoming Display the incoming call hierarchy for the + top level symbol. If the window is currently + displaying the outgoing calls, then it is + refreshed to display the incoming calls. + *:LspCallHierarchyOutgoing* +:LspCallHierarchyOutgoing Display the outgoing call hierarchy for the + top level symbol. If the window is currently + displaying the incoming calls, then it is + refreshed to display the outgoing calls. + +============================================================================== +11. Autocommands *lsp-autocmds* + + *LspSetup* +LspSetup A |User| autocommand fired when the LSP plugin + is loaded. Can be used to add language + servers using the |LspAddServer()| function + and to set plugin options using the + |LspOptionsSet()| function. + + *LspAttached* +LspAttached A |User| autocommand fired when the LSP client + attaches to a buffer. Can be used to configure + buffer-local mappings or options. + + *LspDiagsUpdated* +LspDiagsUpdated A |User| autocommand invoked when new + diagnostics are received from the language + server. This is invoked after the LSP client + has processed the diagnostics. The function + lsp#diag#GetDiagsForBuf() can be used to get + all the diagnostics for a buffer. + +============================================================================== +12. Highlight Groups *lsp-highlight-groups* + +The following highlight groups are used by the LSP plugin. You can define +these highlight groups in your .vimrc file before sourcing this plugin to +override them. + +*LspDiagInlineError* Used to highlight inline error diagnostics. + By default, linked to the "SpellBad" highlight + group. +*LspDiagInlineHint* Used to highlight inline hint diagnostics. + By default, linked to the "SpellLocal" + highlight group. +*LspDiagInlineInfo* Used to highlight inline info diagnostics. + By default, linked to the "SpellRare" + highlight group. +*LspDiagInlineWarning* Used to highlight inline warning diagnostics. + By default, linked to the "SpellCap" highlight + group. +*LspDiagLine* Used to highlight a line with one or more + diagnostics. By default linked to "NONE" + (cleared). You can link this to a highlight + group to highlight the line. +*LspDiagSignErrorText* Used to highlight the sign text for error + diags. By default linked to 'ErrorMsg'. +*LspDiagSignHintText* Used to highlight the sign text for hint + diags. By default linked to 'Question'. +*LspDiagSignInfoText* Used to highlight the sign text for info + diags. By default linked to 'Pmenu'. +*LspDiagSignWarningText* Used to highlight the sign text for warning + diags. By default linked to 'Search'. +*LspDiagVirtualText* Used to highlight diagnostic virtual text. + By default, linked to the "LineNr" highlight + group. +*LspDiagVirtualTextError* Used to highlight virtual text for error diags. + By default, linked to the "SpellBad" highlight + group. +*LspDiagVirtualTextHint* Used to highlight virtual text for hint + diags. By default, linked to the "SpellLocal" + highlight group. +*LspDiagVirtualTextInfo* Used to highlight virtual text for info + diags. By default, linked to the "SpellRare" + highlight group. +*LspDiagVirtualTextWarning* Used to highlight virtual text for warning + diags. By default, linked to the "SpellCap" + highlight group. +*LspInlayHintsParam* Used to highlight inlay hints of kind + "parameter". By default, linked to the + "Label" highlight group. +*LspInlayHintsType* Used to highlight inlay hints of kind "type". + By default, linked to the "Conceal" highlight + group. +*LspSigActiveParameter* Used to highlight the active signature + parameter. By default, linked to the "LineNr" + highlight group. +*LspSymbolName* Used to highlight the symbol name when using + the |:LspDocumentSymbol| command. By default, + linked to the "Search" highlight group. +*LspSymbolRange* Used to highlight the range of lines + containing a symbol when using the + |:LspDocumentSymbol| command. By default, + linked to the "Visual" highlight group. + +For example, to override the highlight used for diagnostics virtual text, you +can use the following: > + + highlight LspDiagVirtualText ctermfg=Cyan guifg=Blue +< +or > + + highlight link LspDiagLine DiffAdd + highlight link LspDiagVirtualText WarningMsg +< +============================================================================== +13. Debugging *lsp-debug* + +To debug this plugin, you can log the language server protocol messages sent +and received by the plugin from the language server. The following command +enables the logging of the messages from the language server for the current +buffer: > + + :LspServer debug on +< +This command also clears the log files. The following command disables the +logging of the messages from the language server for the current buffer: > + + :LspServer debug off +< +By default, the messages are not logged. Another method to enable the debug +is to set the "debug" field to true when adding a language server +using |LspAddServer()|. + +The messages printed by the language server in the stdout are logged to the +lsp-<server-name>.log file and the messages printed in the stderr are logged +to the lsp-<server-name>.err file. On a Unix-like system, these files are +created in the /tmp directory. On MS-Windows, these files are created in the +%TEMP% directory. + +The following command opens the file containing the messages printed by the +language server in the stdout: > + + :LspServer debug messages +< +The following command opens the file containing the messages printed by the +language server in the stderr: > + + :LspServer debug errors +< +To debug language server initialization problems, after enabling the above +server debug, you can restart the server for the file type in the current +buffer using the following command: > + + :LspServer restart +< +The language servers typically support command line options to enable debug +messages and to increase the verbosity of the messages. You can refer to the +language server documentation for information about this. You can include +these options when registering the language server with this plugin. + +If a language server supports the "$/logTrace" LSP notification, then you can +use the :LspServerTrace command to set the server trace value: > + + :LspServer trace { off | messages | verbose } +< +============================================================================== +14. Custom Command Handlers *lsp-custom-commands* + +When applying a code action, the language server may issue a non-standard +command. For example, the Java language server uses non-standard commands +(e.g. java.apply.workspaceEdit). To handle these commands, you can register a +callback function for each command using the LspRegisterCmdHandler() function. +For example: > + + vim9script + import autoload "lsp/textedit.vim" + + def WorkspaceEdit(cmd: dict<any>) + for editAct in cmd.arguments + textedit.ApplyWorkspaceEdit(editAct) + endfor + enddef + g:LspRegisterCmdHandler('java.apply.workspaceEdit', WorkspaceEdit) +< +Place the above code in a file named lsp_java/plugin/lsp_java.vim and load +this plugin. + +The callback function should accept a Dict argument. The Dict argument +contains the LSP Command interface fields. Refer to the LSP specification for +more information about the "Command" interface. + +============================================================================== +15. Custom LSP Completion Kinds *lsp-custom-kinds* + +When a completion popup is triggered, the LSP client will use a default kind +list to show in the completion "kind" section, to customize it, you need to +use the option |lsp-opt-customCompletionKinds| and set all custom kinds in the +option |lsp-opt-completionKinds| . There is a table with all default LSP +kinds: + + Kind Name | Value +------------------------|-------------------- + Text | t + Method | m + Function | f + Constructor | C + Field | F + Variable | v + Class | c + Interface | i + Module | M + Property | p + Unit | u + Value | V + Enum | e + Keyword | k + Snippet | S + Color | C + File | f + Reference | r + Folder | F + EnumMember | E + Constant | d + Struct | s + Event | E + Operator | o + TypeParameter | T + Buffer | B + +For example, if you want to change the "Method" kind to the kind "method()": > + + vim9script + + g:LspOptionsSet({ + customCompletionKinds: true, + completionKinds: { + "Method": "method()" + } + }) +< +In the completion popup, will show something like this: > + + var file = new File() + + file.cre + | create method() | + | createIfNotExists method() | + | ... | +< +============================================================================== +16. Multiple Language Servers for a buffer *lsp-multiple-servers* + +It's possible to run multiple language servers for a given buffer. + +By default the language server defined first will be used for as much as it +supports, then the next and so on. With the exception that diagnostics from +all running language servers will be combined. + +This means that you can define a language server that only supports a subset +of features at first and then define the general purpose language server after +it: +> + vim9script + + g:LspAddServer([ + # This language server reports that it only supports + # textDocument/documentFormatting, so it will be used + # for :LspFormat but nothing else. + { + filetype: ['html'], + path: 'html-pretty-lsp', + args: ['--stdio'] + }, + # This language server also supports + # textDocument/documentFormatting, but since it's been + # defined later, the one above will be used instead. + # However this server also supports + # textDocument/definition, textDocument/declaration, + # etc, so it will be used for :LspGotoDefinition, + # :LspGotoDeclaration, etc + { + filetype: ['html'], + path: 'html-language-server', + args: ['--stdio'] + } + ]) +< +As shown in the example above the order of when the language servers are being +defined is taken into account for a given method. However sometimes the +language server that you want to use for formatting also reports that it +supports other features. In such a case you can do one of two things: + +1. change the order of language servers, and specify that a given language +server should be used for a given method. + +2. set the unwanted features to |false| in the features |Dictionary| > + + features: { 'codeAction': false } +< +For example, if you want to use the efm-langserver for formatting, but the +typescript-language-server for everything else: > + + vim9script + + g:LspAddServer([ + # this language server will be used by default, as it's defined + # as the first LSP for 'javascript' and 'typescript' + { + filetype: ['javascript', 'typescript'], + path: '/usr/local/bin/typescript-language-server', + args: ['--stdio'] + }, + # this language server will be used for documentFormatting + { + filetype: ['javascript', 'typescript'], + path: '/usr/local/bin/efm-langserver', + args: [], + features: { + documentFormatting: true + } + } + ]) +< +Another way is to disable the unwanted features: for example if you don't want +diagnostics from the typescript-language-server, but want to use it for +everything else: > + + vim9script + + g:LspAddServer([ + { + filetype: ['javascript', 'typescript'], + path: '/usr/local/bin/typescript-language-server', + args: ['--stdio'], + features: { + diagnostics: false + } + }, + ]) +< +============================================================================== +17. Language Server Features *lsp-features* + +When using multiple language servers for a given file type, by providing the +configuration |lsp-cfg-features| it is possible to specify which language +server should be used for a given method/functionality. The following feature +flags are supported: See |lsp-multiple-servers| for examples. + + *lsp-features-callHierarchy* +callHierarchy Used by the|:LspIncomingCalls| and the + |:LspOutgoingCalls| commands. + *lsp-features-codeAction* +codeAction Used by the |:LspCodeAction| command. + *lsp-features-codeLens* +codeLens Used by the |:LspCodeLens| command. + *lsp-features-completion* +completion Used by 24/7 Completion and 'omnifunc' + *lsp-features-declaration* +declaration Used by the |:LspGotoDeclaration|, and + the |:LspPeekDeclaration| commands. + *lsp-features-definition* +definition Used by the|:LspGotoDefinition|, and + the |:LspPeekDefinition| commands. + *lsp-features-diagnostics* +diagnostics Used to disable diagnostics for a single + language server, by default diagnostics are + combined from all running servers, by setting + this to |false| you can ignore diagnostics + from a specific server. + *lsp-features-documentFormatting* +documentFormatting Used by the |:LspFormat| command, and + 'formatexpr' + *lsp-features-documentHighlight* +documentHighlight Used by the |:LspHighlight| and the + |:LspHighlightClear| commands. + *lsp-features-documentSymbol* +documentSymbol Used by the |:LspDocumentSymbol| and the + |:LspOutline| commands. + *lsp-features-foldingRange* +foldingRange Used by the|:LspFold| command. + *lsp-features-hover* +hover Used by the |:LspHover| command. + *lsp-features-implementation* +implementation Used by the |:LspGotoImpl| and the + |:LspPeekImpl| commands. + *lsp-features-inlayHint* +inlayHint Used to show the inlay hints for + function/method arguments. + *lsp-features-references* +references Used by the |:LspShowReferences| command. + *lsp-features-rename* +rename Used by the |:LspRename| command. + *lsp-features-selectionRange* +selectionRange Used by the |:LspSelectionExpand| and the + |:LspSelectionShrink| commands. + *lsp-features-signatureHelp* +signatureHelp Used by the |:LspShowSignature| command. + *lsp-features-typeDefinition* +typeDefinition Used by the |:LspGotoTypeDef| and the + |:LspPeekTypeDef| commands. +typeHierarchy Used by the |:LspSubTypeHierarchy| and the + |:LspSuperTypeHiearchy| commands. +workspaceSymbol Used by the |:LspSymbolSearch| command. + +============================================================================== + *lsp-license* +License: MIT License +Copyright (c) 2020-2023 Yegappan Lakshmanan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +============================================================================== + +vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/vim/pack/downloads/opt/lsp/ftplugin/lspgfm.vim b/vim/pack/downloads/opt/lsp/ftplugin/lspgfm.vim new file mode 100644 index 0000000..2e9bf76 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/ftplugin/lspgfm.vim @@ -0,0 +1,78 @@ +vim9script + +import autoload 'lsp/markdown.vim' as md + +# Update the preview window with the github flavored markdown text +def UpdatePreviewWindowContents(bnr: number, contentList: list<dict<any>>) + :silent! bnr->deletebufline(1, '$') + + var lines: list<string> = [] + var props: dict<list<list<number>>> + var lnum = 0 + + # Each item in "contentList" is a Dict with the following items: + # text: text for this line + # props: list of text properties. Each list item is a Dict. See + # |popup-props| for more information. + # + # Need to convert the text properties from the format used by + # popup_settext() to that used by prop_add_list(). + for entry in contentList + lines->add(entry.text) + lnum += 1 + if entry->has_key('props') + for p in entry.props + if !props->has_key(p.type) + props[p.type] = [] + endif + if p->has_key('end_lnum') + props[p.type]->add([lnum, p.col, p.end_lnum, p.end_col]) + else + props[p.type]->add([lnum, p.col, lnum, p.col + p.length]) + endif + endfor + endif + endfor + setbufline(bnr, 1, lines) + for prop_type in props->keys() + prop_add_list({type: prop_type}, props[prop_type]) + endfor +enddef + +# Render the github flavored markdown text. +# Text can be displayed either in a popup window or in a preview window. +def RenderGitHubMarkdownText() + var bnr: number = bufnr() + var winId: number = win_getid() + var document: dict<list<any>> + var inPreviewWindow = false + + if win_gettype() == 'preview' + inPreviewWindow = true + endif + + try + if !inPreviewWindow + winId = bnr->getbufinfo()[0].popups[0] + endif + # parse the github markdown content and convert it into a list of text and + # list of associated text properties. + document = md.ParseMarkdown(bnr->getbufline(1, '$'), winId->winwidth()) + catch /.*/ + b:markdown_fallback = v:true + return + endtry + + b:lsp_syntax = document.syntax + md.list_pattern->setbufvar(bnr, '&formatlistpat') + var settings = 'linebreak breakindent breakindentopt=list:-1' + win_execute(winId, $'setlocal {settings}') + if inPreviewWindow + UpdatePreviewWindowContents(bnr, document.content) + else + winId->popup_settext(document.content) + endif +enddef +RenderGitHubMarkdownText() + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/plugin/lsp.vim b/vim/pack/downloads/opt/lsp/plugin/lsp.vim new file mode 100644 index 0000000..3b3815a --- /dev/null +++ b/vim/pack/downloads/opt/lsp/plugin/lsp.vim @@ -0,0 +1,163 @@ +if !has('vim9script') || v:version < 900 + " Needs Vim version 9.0 and above + finish +endif + +vim9script noclear + +# Language Server Protocol (LSP) plugin for vim + +if get(g:, 'loaded_lsp', false) + finish +endif +g:loaded_lsp = true + +import '../autoload/lsp/options.vim' +import autoload '../autoload/lsp/lsp.vim' + +# Set LSP plugin options from 'opts'. +def g:LspOptionsSet(opts: dict<any>) + options.OptionsSet(opts) +enddef + +# Return a copy of all the LSP plugin options +def g:LspOptionsGet(): dict<any> + return options.OptionsGet() +enddef + +# Add one or more LSP servers in 'serverList' +def g:LspAddServer(serverList: list<dict<any>>) + lsp.AddServer(serverList) +enddef + +# Register 'Handler' callback function for LSP command 'cmd'. +def g:LspRegisterCmdHandler(cmd: string, Handler: func) + lsp.RegisterCmdHandler(cmd, Handler) +enddef + +# Returns true if the language server for the current buffer is initialized +# and ready to accept requests. +def g:LspServerReady(): bool + return lsp.ServerReady() +enddef + +# Returns true if the language server for 'ftype' file type is running +def g:LspServerRunning(ftype: string): bool + return lsp.ServerRunning(ftype) +enddef + +augroup LSPAutoCmds + au! + autocmd BufNewFile,BufReadPost,FileType * lsp.AddFile(expand('<abuf>')->str2nr()) + # Note that when BufWipeOut is invoked, the current buffer may be different + # from the buffer getting wiped out. + autocmd BufWipeOut * lsp.RemoveFile(expand('<abuf>')->str2nr()) + autocmd BufWinEnter * lsp.BufferLoadedInWin(expand('<abuf>')->str2nr()) +augroup END + +# TODO: Is it needed to shutdown all the LSP servers when exiting Vim? +# This takes some time. +# autocmd VimLeavePre * call lsp.StopAllServers() + +# LSP commands +command! -nargs=? -bar -range LspCodeAction lsp.CodeAction(<line1>, <line2>, <q-args>) +command! -nargs=0 -bar LspCodeLens lsp.CodeLens() +command! -nargs=+ -bar -bang -count -complete=customlist,lsp.LspDiagComplete LspDiag lsp.LspDiagCmd(<q-args>, <count>, <bang>false) +command! -nargs=0 -bar -bang LspDiagCurrent lsp.LspShowCurrentDiag(<bang>false) +command! -nargs=0 -bar LspDiagFirst lsp.JumpToDiag('first') +command! -nargs=0 -bar LspDiagLast lsp.JumpToDiag('last') +command! -nargs=0 -bar -count=1 LspDiagNext lsp.JumpToDiag('next', <count>) +command! -nargs=0 -bar -count=1 LspDiagNextWrap lsp.JumpToDiag('nextWrap', <count>) +command! -nargs=0 -bar -count=1 LspDiagPrev lsp.JumpToDiag('prev', <count>) +command! -nargs=0 -bar -count=1 LspDiagPrevWrap lsp.JumpToDiag('prevWrap', <count>) +command! -nargs=0 -bar LspDiagShow lsp.ShowDiagnostics() +command! -nargs=0 -bar LspDiagHere lsp.JumpToDiag('here') +command! -nargs=0 -bar LspDocumentSymbol lsp.ShowDocSymbols() +command! -nargs=0 -bar LspFold lsp.FoldDocument() +command! -nargs=0 -bar -range=% LspFormat lsp.TextDocFormat(<range>, <line1>, <line2>) +command! -nargs=0 -bar -count LspGotoDeclaration lsp.GotoDeclaration(v:false, <q-mods>, <count>) +command! -nargs=0 -bar -count LspGotoDefinition lsp.GotoDefinition(v:false, <q-mods>, <count>) +command! -nargs=0 -bar -count LspGotoImpl lsp.GotoImplementation(v:false, <q-mods>, <count>) +command! -nargs=0 -bar -count LspGotoTypeDef lsp.GotoTypedef(v:false, <q-mods>, <count>) +command! -nargs=0 -bar LspHighlight call LspDocHighlight(bufnr(), <q-mods>) +command! -nargs=0 -bar LspHighlightClear call LspDocHighlightClear() +command! -nargs=? -bar LspHover lsp.Hover(<q-mods>) +command! -nargs=1 -bar -complete=customlist,lsp.LspInlayHintsComplete LspInlayHints lsp.InlayHints(<q-args>) +command! -nargs=0 -bar LspIncomingCalls lsp.IncomingCalls() +command! -nargs=0 -bar LspOutgoingCalls lsp.OutgoingCalls() +command! -nargs=0 -bar -count LspOutline lsp.Outline(<q-mods>, <count>) +command! -nargs=0 -bar -count LspPeekDeclaration lsp.GotoDeclaration(v:true, <q-mods>, <count>) +command! -nargs=0 -bar -count LspPeekDefinition lsp.GotoDefinition(v:true, <q-mods>, <count>) +command! -nargs=0 -bar -count LspPeekImpl lsp.GotoImplementation(v:true, <q-mods>, <count>) +command! -nargs=0 -bar LspPeekReferences lsp.ShowReferences(v:true) +command! -nargs=0 -bar -count LspPeekTypeDef lsp.GotoTypedef(v:true, <q-mods>, <count>) +command! -nargs=? -bar LspRename lsp.Rename(<q-args>) +command! -nargs=0 -bar LspSelectionExpand lsp.SelectionExpand() +command! -nargs=0 -bar LspSelectionShrink lsp.SelectionShrink() +command! -nargs=+ -bar -complete=customlist,lsp.LspServerComplete LspServer lsp.LspServerCmd(<q-args>) +command! -nargs=0 -bar LspShowReferences lsp.ShowReferences(v:false) +command! -nargs=0 -bar LspShowAllServers lsp.ShowAllServers() +command! -nargs=0 -bar LspShowSignature call LspShowSignature() +command! -nargs=0 -bar LspSubTypeHierarchy lsp.TypeHierarchy(0) +command! -nargs=0 -bar LspSuperTypeHierarchy lsp.TypeHierarchy(1) +# Clangd specifc extension to switch from one C/C++ source file to a +# corresponding header file +command! -nargs=0 -bar LspSwitchSourceHeader lsp.SwitchSourceHeader() +command! -nargs=? -bar LspSymbolSearch lsp.SymbolSearch(<q-args>, <q-mods>) +command! -nargs=1 -bar -complete=dir LspWorkspaceAddFolder lsp.AddWorkspaceFolder(<q-args>) +command! -nargs=0 -bar LspWorkspaceListFolders lsp.ListWorkspaceFolders() +command! -nargs=1 -bar -complete=dir LspWorkspaceRemoveFolder lsp.RemoveWorkspaceFolder(<q-args>) + +# Add the GUI menu entries +if has('gui_running') + anoremenu <silent> L&sp.Goto.Definition :LspGotoDefinition<CR> + anoremenu <silent> L&sp.Goto.Declaration :LspGotoDeclaration<CR> + anoremenu <silent> L&sp.Goto.Implementation :LspGotoImpl<CR> + anoremenu <silent> L&sp.Goto.TypeDef :LspGotoTypeDef<CR> + + anoremenu <silent> L&sp.Show\ Signature :LspShowSignature<CR> + anoremenu <silent> L&sp.Show\ References :LspShowReferences<CR> + anoremenu <silent> L&sp.Show\ Detail :LspHover<CR> + anoremenu <silent> L&sp.Outline :LspOutline<CR> + + anoremenu <silent> L&sp.Goto\ Symbol :LspDocumentSymbol<CR> + anoremenu <silent> L&sp.Symbol\ Search :LspSymbolSearch<CR> + anoremenu <silent> L&sp.Outgoing\ Calls :LspOutgoingCalls<CR> + anoremenu <silent> L&sp.Incoming\ Calls :LspIncomingCalls<CR> + anoremenu <silent> L&sp.Rename :LspRename<CR> + anoremenu <silent> L&sp.Code\ Action :LspCodeAction<CR> + + anoremenu <silent> L&sp.Highlight\ Symbol :LspHighlight<CR> + anoremenu <silent> L&sp.Highlight\ Clear :LspHighlightClear<CR> + + # Diagnostics + anoremenu <silent> L&sp.Diagnostics.Current :LspDiag current<CR> + anoremenu <silent> L&sp.Diagnostics.Show\ All :LspDiag show<CR> + anoremenu <silent> L&sp.Diagnostics.First :LspDiag first<CR> + anoremenu <silent> L&sp.Diagnostics.Last :LspDiag last<CR> + anoremenu <silent> L&sp.Diagnostics.Next :LspDiag next<CR> + anoremenu <silent> L&sp.Diagnostics.Previous :LspDiag prev<CR> + anoremenu <silent> L&sp.Diagnostics.This :LspDiag here<CR> + + if &mousemodel =~ 'popup' + anoremenu <silent> PopUp.L&sp.Go\ to\ Definition + \ :LspGotoDefinition<CR> + anoremenu <silent> PopUp.L&sp.Go\ to\ Declaration + \ :LspGotoDeclaration<CR> + anoremenu <silent> PopUp.L&sp.Find\ All\ References + \ :LspShowReferences<CR> + anoremenu <silent> PopUp.L&sp.Show\ Detail + \ :LspHover<CR> + anoremenu <silent> PopUp.L&sp.Highlight\ Symbol + \ :LspHighlight<CR> + anoremenu <silent> PopUp.L&sp.Highlight\ Clear + \ :LspHighlightClear<CR> + endif +endif + +# Invoke autocmd to register LSP servers and to set LSP options +if exists('#User#LspSetup') + :doautocmd <nomodeline> User LspSetup +endif + +# vim: shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/syntax/lspgfm.vim b/vim/pack/downloads/opt/lsp/syntax/lspgfm.vim new file mode 100644 index 0000000..9e71010 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/syntax/lspgfm.vim @@ -0,0 +1,23 @@ +vim9script + +if get(b:, 'markdown_fallback', v:false) + runtime! syntax/markdown.vim + finish +endif + +var group: dict<string> = {} +for region in get(b:, 'lsp_syntax', []) + if !group->has_key(region.lang) + group[region.lang] = region.lang->substitute('\(^.\|_\a\)', '\u&', 'g') + try + exe $'syntax include @{group[region.lang]} syntax/{region.lang}.vim' + catch /.*/ + group[region.lang] = '' + endtry + endif + if !group[region.lang]->empty() + exe $'syntax region lspCodeBlock start="{region.start}" end="{region.end}" contains=@{group[region.lang]}' + endif +endfor + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/test/clangd_offsetencoding.vim b/vim/pack/downloads/opt/lsp/test/clangd_offsetencoding.vim new file mode 100644 index 0000000..397f459 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/clangd_offsetencoding.vim @@ -0,0 +1,478 @@ +vim9script +# Unit tests for language server protocol offset encoding using clangd + +source common.vim + +# Start the C language server. Returns true on success and false on failure. +def g:StartLangServer(): bool + if has('patch-9.0.1629') + return g:StartLangServerWithFile('Xtest.c') + endif + return false +enddef + +if !has('patch-9.0.1629') + # Need patch 9.0.1629 to properly encode/decode the UTF-16 offsets + finish +endif + +var lspOpts = {autoComplete: false} +g:LspOptionsSet(lspOpts) + +var lspServers = [{ + filetype: ['c', 'cpp'], + path: (exepath('clangd-15') ?? exepath('clangd')), + args: ['--background-index', + '--clang-tidy', + $'--offset-encoding={$LSP_OFFSET_ENCODING}'] + }] +call LspAddServer(lspServers) + +# Test for :LspCodeAction with symbols containing multibyte and composing +# characters +def g:Test_LspCodeAction_multibyte() + silent! edit XLspCodeAction_mb.c + sleep 200m + var lines =<< trim END + #include <stdio.h> + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n", aVar): + printf("áb́áb́ = %d\n", aVar): + printf("ą́ą́ą́ą́ = %d\n", aVar): + } + END + setline(1, lines) + g:WaitForServerFileLoad(3) + :redraw! + cursor(5, 5) + redraw! + :LspCodeAction 1 + assert_equal(' printf("😊😊😊😊 = %d\n", aVar);', getline(5)) + cursor(6, 5) + redraw! + :LspCodeAction 1 + assert_equal(' printf("áb́áb́ = %d\n", aVar);', getline(6)) + cursor(7, 5) + redraw! + :LspCodeAction 1 + assert_equal(' printf("ą́ą́ą́ą́ = %d\n", aVar);', getline(7)) + + :%bw! +enddef + +# Test for ":LspDiag show" when using multibyte and composing characters +def g:Test_LspDiagShow_multibyte() + :silent! edit XLspDiagShow_mb.c + sleep 200m + var lines =<< trim END + #include <stdio.h> + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n". aVar); + printf("áb́áb́ = %d\n". aVar); + printf("ą́ą́ą́ą́ = %d\n". aVar); + } + END + setline(1, lines) + g:WaitForServerFileLoad(3) + :redraw! + :LspDiag show + var qfl: list<dict<any>> = getloclist(0) + assert_equal([5, 37], [qfl[0].lnum, qfl[0].col]) + assert_equal([6, 33], [qfl[1].lnum, qfl[1].col]) + assert_equal([7, 41], [qfl[2].lnum, qfl[2].col]) + :lclose + :%bw! +enddef + +# Test for :LspFormat when using multibyte and composing characters +def g:Test_LspFormat_multibyte() + :silent! edit XLspFormat_mb.c + sleep 200m + var lines =<< trim END + void fn(int aVar) + { + int 😊😊😊😊 = aVar + 1; + int áb́áb́ = aVar + 1; + int ą́ą́ą́ą́ = aVar + 1; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + :redraw! + :LspFormat + var expected =<< trim END + void fn(int aVar) { + int 😊😊😊😊 = aVar + 1; + int áb́áb́ = aVar + 1; + int ą́ą́ą́ą́ = aVar + 1; + } + END + assert_equal(expected, getline(1, '$')) + :%bw! +enddef + +# Test for :LspGotoDefinition when using multibyte and composing characters +def g:Test_LspGotoDefinition_multibyte() + :silent! edit XLspGotoDefinition_mb.c + sleep 200m + var lines: list<string> =<< trim END + #include <stdio.h> + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n", aVar); + printf("áb́áb́ = %d\n", aVar); + printf("ą́ą́ą́ą́ = %d\n", aVar); + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + for [lnum, colnr] in [[4, 27], [5, 39], [6, 35], [7, 43]] + cursor(lnum, colnr) + :LspGotoDefinition + assert_equal([2, 13], [line('.'), col('.')]) + endfor + + :%bw! +enddef + +# Test for :LspGotoDefinition when using multibyte and composing characters +def g:Test_LspGotoDefinition_after_multibyte() + :silent! edit XLspGotoDef_after_mb.c + sleep 200m + var lines =<< trim END + void fn(int aVar) + { + /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int αβγδ, bVar; + /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int 😊😊😊😊, cVar; + /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int áb́áb́, dVar; + /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int ą́ą́ą́ą́, eVar; + bVar = 1; + cVar = 2; + dVar = 3; + eVar = 4; + aVar = αβγδ + 😊😊😊😊 + áb́áb́ + ą́ą́ą́ą́ + bVar; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + :redraw! + cursor(7, 5) + :LspGotoDefinition + assert_equal([3, 88], [line('.'), col('.')]) + cursor(8, 5) + :LspGotoDefinition + assert_equal([4, 96], [line('.'), col('.')]) + cursor(9, 5) + :LspGotoDefinition + assert_equal([5, 92], [line('.'), col('.')]) + cursor(10, 5) + :LspGotoDefinition + assert_equal([6, 100], [line('.'), col('.')]) + cursor(11, 12) + :LspGotoDefinition + assert_equal([3, 78], [line('.'), col('.')]) + cursor(11, 23) + :LspGotoDefinition + assert_equal([4, 78], [line('.'), col('.')]) + cursor(11, 42) + :LspGotoDefinition + assert_equal([5, 78], [line('.'), col('.')]) + cursor(11, 57) + :LspGotoDefinition + assert_equal([6, 78], [line('.'), col('.')]) + + :%bw! +enddef + +# Test for doing omni completion for symbols with multibyte and composing +# characters +def g:Test_OmniComplete_multibyte() + :silent! edit XOmniComplete_mb.c + sleep 200m + var lines: list<string> =<< trim END + void Func1(void) + { + int 😊😊😊😊, aVar; + int áb́áb́, bVar; + int ą́ą́ą́ą́, cVar; + + + + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + cursor(6, 4) + feedkeys("aaV\<C-X>\<C-O> = 😊😊\<C-X>\<C-O>;", 'xt') + assert_equal(' aVar = 😊😊😊😊;', getline('.')) + cursor(7, 4) + feedkeys("abV\<C-X>\<C-O> = áb́\<C-X>\<C-O>;", 'xt') + assert_equal(' bVar = áb́áb́;', getline('.')) + cursor(8, 4) + feedkeys("acV\<C-X>\<C-O> = ą́ą́\<C-X>\<C-O>;", 'xt') + assert_equal(' cVar = ą́ą́ą́ą́;', getline('.')) + feedkeys("oáb́\<C-X>\<C-O> = ą́ą́\<C-X>\<C-O>;", 'xt') + assert_equal(' áb́áb́ = ą́ą́ą́ą́;', getline('.')) + feedkeys("oą́ą́\<C-X>\<C-O> = áb́\<C-X>\<C-O>;", 'xt') + assert_equal(' ą́ą́ą́ą́ = áb́áb́;', getline('.')) + :%bw! +enddef + +# Test for :LspOutline with multibyte and composing characters +def g:Test_Outline_multibyte() + silent! edit XLspOutline_mb.c + sleep 200m + var lines: list<string> =<< trim END + typedef void 😊😊😊😊; + typedef void áb́áb́; + typedef void ą́ą́ą́ą́; + + 😊😊😊😊 Func1() + { + } + + áb́áb́ Func2() + { + } + + ą́ą́ą́ą́ Func3() + { + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + cursor(1, 1) + :LspOutline + assert_equal(2, winnr('$')) + + :wincmd w + cursor(5, 1) + feedkeys("\<CR>", 'xt') + assert_equal([2, 5, 18], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(6, 1) + feedkeys("\<CR>", 'xt') + assert_equal([2, 9, 14], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(7, 1) + feedkeys("\<CR>", 'xt') + assert_equal([2, 13, 22], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(10, 1) + feedkeys("\<CR>", 'xt') + assert_equal([2, 1, 14], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(11, 1) + feedkeys("\<CR>", 'xt') + assert_equal([2, 2, 14], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(12, 1) + feedkeys("\<CR>", 'xt') + assert_equal([2, 3, 14], [winnr(), line('.'), col('.')]) + + :%bw! +enddef + +# Test for :LspRename with multibyte and composing characters +def g:Test_LspRename_multibyte() + silent! edit XLspRename_mb.c + sleep 200m + var lines: list<string> =<< trim END + #include <stdio.h> + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n", aVar); + printf("áb́áb́ = %d\n", aVar); + printf("ą́ą́ą́ą́ = %d\n", aVar); + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + cursor(2, 12) + :LspRename bVar + redraw! + var expected: list<string> =<< trim END + #include <stdio.h> + void fn(int bVar) + { + printf("aVar = %d\n", bVar); + printf("😊😊😊😊 = %d\n", bVar); + printf("áb́áb́ = %d\n", bVar); + printf("ą́ą́ą́ą́ = %d\n", bVar); + } + END + assert_equal(expected, getline(1, '$')) + :%bw! +enddef + +# Test for :LspShowReferences when using multibyte and composing characters +def g:Test_LspShowReferences_multibyte() + :silent! edit XLspShowReferences_mb.c + sleep 200m + var lines: list<string> =<< trim END + #include <stdio.h> + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n", aVar); + printf("áb́áb́ = %d\n", aVar); + printf("ą́ą́ą́ą́ = %d\n", aVar); + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + cursor(4, 27) + :LspShowReferences + var qfl: list<dict<any>> = getloclist(0) + assert_equal([2, 13], [qfl[0].lnum, qfl[0].col]) + assert_equal([4, 27], [qfl[1].lnum, qfl[1].col]) + assert_equal([5, 39], [qfl[2].lnum, qfl[2].col]) + assert_equal([6, 35], [qfl[3].lnum, qfl[3].col]) + assert_equal([7, 43], [qfl[4].lnum, qfl[4].col]) + :lclose + + :%bw! +enddef + +# Test for :LspSymbolSearch when using multibyte and composing characters +def g:Test_LspSymbolSearch_multibyte() + silent! edit XLspSymbolSearch_mb.c + sleep 200m + var lines: list<string> =<< trim END + typedef void 😊😊😊😊; + typedef void áb́áb́; + typedef void ą́ą́ą́ą́; + + 😊😊😊😊 Func1() + { + } + + áb́áb́ Func2() + { + } + + ą́ą́ą́ą́ Func3() + { + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + + cursor(1, 1) + feedkeys(":LspSymbolSearch Func1\<CR>", "xt") + assert_equal([5, 18], [line('.'), col('.')]) + cursor(1, 1) + feedkeys(":LspSymbolSearch Func2\<CR>", "xt") + assert_equal([9, 14], [line('.'), col('.')]) + cursor(1, 1) + feedkeys(":LspSymbolSearch Func3\<CR>", "xt") + assert_equal([13, 22], [line('.'), col('.')]) + + :%bw! +enddef + +# Test for setting the 'tagfunc' with multibyte and composing characters in +# symbols +def g:Test_LspTagFunc_multibyte() + var lines =<< trim END + void fn(int aVar) + { + int 😊😊😊😊, bVar; + int áb́áb́, cVar; + int ą́ą́ą́ą́, dVar; + bVar = 10; + cVar = 10; + dVar = 10; + } + END + writefile(lines, 'Xtagfunc_mb.c') + :silent! edit! Xtagfunc_mb.c + g:WaitForServerFileLoad(0) + :setlocal tagfunc=lsp#lsp#TagFunc + cursor(6, 5) + :exe "normal \<C-]>" + assert_equal([3, 27], [line('.'), col('.')]) + cursor(7, 5) + :exe "normal \<C-]>" + assert_equal([4, 23], [line('.'), col('.')]) + cursor(8, 5) + :exe "normal \<C-]>" + assert_equal([5, 31], [line('.'), col('.')]) + :set tagfunc& + + :%bw! + delete('Xtagfunc_mb.c') +enddef + +# Test for the :LspSuperTypeHierarchy and :LspSubTypeHierarchy commands with +# multibyte and composing characters +def g:Test_LspTypeHier_multibyte() + silent! edit XLspTypeHier_mb.cpp + sleep 200m + var lines =<< trim END + /* αβ😊😊ááą́ą́ */ class parent { + }; + + /* αβ😊😊ááą́ą́ */ class child : public parent { + }; + + /* αβ😊😊ááą́ą́ */ class grandchild : public child { + }; + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + cursor(1, 42) + :LspSubTypeHierarchy + call feedkeys("\<CR>", 'xt') + assert_equal([1, 36], [line('.'), col('.')]) + cursor(1, 42) + + :LspSubTypeHierarchy + call feedkeys("\<Down>\<CR>", 'xt') + assert_equal([4, 42], [line('.'), col('.')]) + + cursor(1, 42) + :LspSubTypeHierarchy + call feedkeys("\<Down>\<Down>\<CR>", 'xt') + assert_equal([7, 42], [line('.'), col('.')]) + + cursor(7, 42) + :LspSuperTypeHierarchy + call feedkeys("\<CR>", 'xt') + assert_equal([7, 36], [line('.'), col('.')]) + + cursor(7, 42) + :LspSuperTypeHierarchy + call feedkeys("\<Down>\<CR>", 'xt') + assert_equal([4, 36], [line('.'), col('.')]) + + cursor(7, 42) + :LspSuperTypeHierarchy + call feedkeys("\<Down>\<Down>\<CR>", 'xt') + assert_equal([1, 36], [line('.'), col('.')]) + + :%bw! +enddef + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/test/clangd_tests.vim b/vim/pack/downloads/opt/lsp/test/clangd_tests.vim new file mode 100644 index 0000000..b084e86 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/clangd_tests.vim @@ -0,0 +1,1782 @@ +vim9script +# Unit tests for Vim Language Server Protocol (LSP) clangd client + +source common.vim + +var lspOpts = {autoComplete: false} +g:LspOptionsSet(lspOpts) + +g:LSPTest_modifyDiags = false + +var lspServers = [{ + filetype: ['c', 'cpp'], + path: (exepath('clangd-15') ?? exepath('clangd')), + args: ['--background-index', '--clang-tidy'], + initializationOptions: { clangdFileStatus: true }, + customNotificationHandlers: { + 'textDocument/clangd.fileStatus': (lspserver: dict<any>, reply: dict<any>) => { + g:LSPTest_customNotificationHandlerReplied = true + } + }, + processDiagHandler: (diags: list<dict<any>>) => { + if g:LSPTest_modifyDiags != true + return diags + endif + + return diags->map((ix, diag) => { + diag.message = $'this is overridden' + return diag + }) + } + }] +call LspAddServer(lspServers) + +var clangdVerDetail = systemlist($'{shellescape(lspServers[0].path)} --version') +var clangdVerMajor = clangdVerDetail->matchstr('.*version \d\+\..*')->substitute('.* \(\d\+\)\..*', '\1', 'g')->str2nr() +echomsg clangdVerDetail + + +# Test for formatting a file using LspFormat +def g:Test_LspFormat() + :silent! edit XLspFormat.c + sleep 200m + setline(1, [' int i;', ' int j;']) + :redraw! + :LspFormat + assert_equal(['int i;', 'int j;'], getline(1, '$')) + + deletebufline('', 1, '$') + setline(1, ['int f1(int i)', '{', 'int j = 10; return j;', '}']) + :redraw! + :LspFormat + assert_equal(['int f1(int i) {', ' int j = 10;', ' return j;', '}'], + getline(1, '$')) + + deletebufline('', 1, '$') + setline(1, ['', 'int i;']) + :redraw! + :LspFormat + assert_equal(['', 'int i;'], getline(1, '$')) + + deletebufline('', 1, '$') + setline(1, [' int i;']) + :redraw! + :LspFormat + assert_equal(['int i;'], getline(1, '$')) + + deletebufline('', 1, '$') + setline(1, [' int i; ']) + :redraw! + :LspFormat + assert_equal(['int i;'], getline(1, '$')) + + deletebufline('', 1, '$') + setline(1, ['int i;', '', '', '']) + :redraw! + :LspFormat + assert_equal(['int i;'], getline(1, '$')) + + deletebufline('', 1, '$') + setline(1, ['int f1(){int x;int y;x=1;y=2;return x+y;}']) + :redraw! + :LspFormat + var expected: list<string> =<< trim END + int f1() { + int x; + int y; + x = 1; + y = 2; + return x + y; + } + END + assert_equal(expected, getline(1, '$')) + + deletebufline('', 1, '$') + setline(1, ['', '', '', '']) + :redraw! + :LspFormat + assert_equal([''], getline(1, '$')) + + deletebufline('', 1, '$') + var lines: list<string> =<< trim END + int f1() { + int i, j; + for (i = 1; i < 10; i++) { j++; } + for (j = 1; j < 10; j++) { i++; } + } + END + setline(1, lines) + :redraw! + :4LspFormat + expected =<< trim END + int f1() { + int i, j; + for (i = 1; i < 10; i++) { j++; } + for (j = 1; j < 10; j++) { + i++; + } + } + END + assert_equal(expected, getline(1, '$')) + + deletebufline('', 1, '$') + # shrinking multiple lines into a single one works + setline(1, ['int \', 'i \', '= \', '42;']) + :redraw! + :4LspFormat + assert_equal(['int i = 42;'], getline(1, '$')) + bw! + + # empty file + assert_equal('', execute('LspFormat')) + + # file without an LSP server + edit a.raku + assert_equal('Error: Language server for "raku" file type supporting "documentFormatting" feature is not found', + execute('LspFormat')->split("\n")[0]) + + :%bw! +enddef + +# Test for formatting a file using 'formatexpr' +def g:Test_LspFormatExpr() + :silent! edit XLspFormat.c + sleep 200m + setlocal formatexpr=lsp#lsp#FormatExpr() + setline(1, [' int i;', ' int j;']) + :redraw! + normal! ggVGgq + assert_equal(['int i;', 'int j;'], getline(1, '$')) + + # empty line/file + deletebufline('', 1, '$') + setline(1, ['']) + redraw! + normal! ggVGgq + assert_equal([''], getline(1, '$')) + + setlocal formatexpr& + :%bw! +enddef + +# Test for :LspShowReferences - showing all the references to a symbol in a +# file using LSP +def g:Test_LspShowReferences() + :silent! edit XshowRefs.c + sleep 200m + var lines: list<string> =<< trim END + int count; + void redFunc() + { + int count, i; + count = 10; + i = count; + } + void blueFunc() + { + int count, j; + count = 20; + j = count; + } + END + setline(1, lines) + :redraw! + cursor(5, 2) + var bnr: number = bufnr() + :LspShowReferences + sleep 100m + assert_equal('quickfix', getwinvar(winnr('$'), '&buftype')) + var loclist: list<dict<any>> = getloclist(0) + assert_equal(bnr, loclist[0].bufnr) + assert_equal(3, loclist->len()) + assert_equal([4, 6], [loclist[0].lnum, loclist[0].col]) + assert_equal([5, 2], [loclist[1].lnum, loclist[1].col]) + assert_equal([6, 6], [loclist[2].lnum, loclist[2].col]) + :lclose + cursor(1, 5) + :LspShowReferences + assert_equal(1, getloclist(0)->len()) + loclist = getloclist(0) + assert_equal([1, 5], [loclist[0].lnum, loclist[0].col]) + :lclose + + # Test for opening in qf list + g:LspOptionsSet({useQuickfixForLocations: true}) + cursor(5, 2) + :LspShowReferences + sleep 100m + assert_equal('quickfix', getwinvar(winnr('$'), '&buftype')) + :cclose + var qfl: list<dict<any>> = getqflist() + assert_equal(3, qfl->len()) + assert_equal(bufnr(), qfl[0].bufnr) + assert_equal([4, 6], [qfl[0].lnum, qfl[0].col]) + assert_equal([5, 2], [qfl[1].lnum, qfl[1].col]) + assert_equal([6, 6], [qfl[2].lnum, qfl[2].col]) + cursor(1, 5) + :LspShowReferences + assert_equal(1, getqflist()->len()) + qfl = getqflist() + assert_equal([1, 5], [qfl[0].lnum, qfl[0].col]) + :cclose + g:LspOptionsSet({useQuickfixForLocations: false}) + + # Test for maintaining buffer focus + g:LspOptionsSet({keepFocusInReferences: false}) + :LspShowReferences + assert_equal('', getwinvar(0, '&buftype')) + :lclose + g:LspOptionsSet({keepFocusInReferences: true}) + + # Test for LspPeekReferences + + # Opening the preview window with an unsaved buffer displays the "E37: No + # write since last change" error message. To disable this message, mark the + # buffer as not modified. + setlocal nomodified + cursor(10, 6) + :LspPeekReferences + sleep 50m + var ids = popup_list() + assert_equal(2, ids->len()) + var filePopupAttrs = ids[0]->popup_getoptions() + var refPopupAttrs = ids[1]->popup_getoptions() + assert_match('XshowRefs', filePopupAttrs.title) + assert_equal('Symbol References', refPopupAttrs.title) + assert_equal(10, line('.', ids[0])) + assert_equal(1, line('.', ids[1])) + assert_equal(3, line('$', ids[1])) + feedkeys("jj\<CR>", 'xt') + assert_equal(12, line('.')) + assert_equal([], popup_list()) + popup_clear() + + # LspShowReferences should start with the current symbol + cursor(12, 6) + :LspPeekReferences + sleep 50m + ids = popup_list() + assert_equal(2, ids->len()) + assert_equal(12, line('.', ids[0])) + assert_equal(3, line('.', ids[1])) + feedkeys("\<CR>", 'xt') + popup_clear() + + bw! + + # empty file + assert_equal('', execute('LspShowReferences')) + + # file without an LSP server + edit a.raku + assert_equal('Error: Language server for "raku" file type supporting "references" feature is not found', + execute('LspShowReferences')->split("\n")[0]) + + :%bw! +enddef + +# Test for LSP diagnostics +def g:Test_LspDiag() + :silent! edit XLspDiag.c + sleep 200m + var lines: list<string> =<< trim END + void blueFunc() + { + int count, j: + count = 20; + j <= count; + j = 10; + MyFunc(); + } + END + setline(1, lines) + g:WaitForServerFileLoad(1) + var bnr: number = bufnr() + :redraw! + :LspDiag show + var qfl: list<dict<any>> = getloclist(0) + assert_equal('quickfix', getwinvar(winnr('$'), '&buftype')) + assert_equal(bnr, qfl[0].bufnr) + assert_equal(3, qfl->len()) + assert_equal([3, 14, 'E'], [qfl[0].lnum, qfl[0].col, qfl[0].type]) + assert_equal([5, 2, 'W'], [qfl[1].lnum, qfl[1].col, qfl[1].type]) + assert_equal([7, 2, 'W'], [qfl[2].lnum, qfl[2].col, qfl[2].type]) + close + g:LspOptionsSet({showDiagInPopup: false}) + normal gg + var output = execute('LspDiag current')->split("\n") + assert_equal('Warn: No diagnostic messages found for current line', output[0]) + :LspDiag first + assert_equal([3, 14], [line('.'), col('.')]) + output = execute('LspDiag current')->split("\n") + assert_equal("Expected ';' at end of declaration (fix available)", output[0]) + :normal! 0 + :LspDiag here + assert_equal([3, 14], [line('.'), col('.')]) + :LspDiag next + assert_equal([5, 2], [line('.'), col('.')]) + :LspDiag next + assert_equal([7, 2], [line('.'), col('.')]) + output = execute('LspDiag next')->split("\n") + assert_equal('Warn: No more diagnostics found', output[0]) + :LspDiag prev + :LspDiag prev + :LspDiag prev + output = execute('LspDiag prev')->split("\n") + assert_equal('Warn: No more diagnostics found', output[0]) + + # Test for maintaining buffer focus + g:LspOptionsSet({keepFocusInDiags: false}) + :LspDiag show + assert_equal('', getwinvar(0, '&buftype')) + :lclose + g:LspOptionsSet({keepFocusInDiags: true}) + + # :[count]LspDiag next + cursor(3, 1) + :2LspDiag next + assert_equal([5, 2], [line('.'), col('.')]) + :2LspDiag next + assert_equal([7, 2], [line('.'), col('.')]) + output = execute(':2LspDiag next')->split("\n") + assert_equal('Warn: No more diagnostics found', output[0]) + + # :[count]LspDiag prev + cursor(7, 2) + :4LspDiag prev + assert_equal([3, 14], [line('.'), col('.')]) + output = execute(':4LspDiag prev')->split("\n") + assert_equal('Warn: No more diagnostics found', output[0]) + + :%d + setline(1, ['void blueFunc()', '{', '}']) + g:WaitForDiags(0) + output = execute('LspDiag show')->split("\n") + assert_match('Warn: No diagnostic messages found for', output[0]) + g:LspOptionsSet({showDiagInPopup: true}) + + popup_clear() + :%bw! +enddef + +# Test for LSP diagnostics handler +def g:Test_LspProcessDiagHandler() + g:LSPTest_modifyDiags = true + g:LspOptionsSet({showDiagInPopup: false}) + + :silent! edit XLspProcessDiag.c + sleep 200m + var lines: list<string> =<< trim END + void blueFunc() + { + int count, j: + } + END + setline(1, lines) + g:WaitForServerFileLoad(1) + :redraw! + normal gg + + :LspDiag first + assert_equal([3, 14], [line('.'), col('.')]) + + var output = execute('LspDiag current')->split("\n") + assert_equal("this is overridden", output[0]) + + g:LspOptionsSet({showDiagInPopup: true}) + g:LSPTest_modifyDiags = false + :%bw! +enddef + +# Diag location list should be automatically updated when the list of diags +# changes. +def g:Test_DiagLocListAutoUpdate() + :silent! edit XdiagLocListAutoUpdate.c + :sleep 200m + setloclist(0, [], 'f') + var lines: list<string> =<< trim END + int i: + int j; + END + setline(1, lines) + var bnr = bufnr() + g:WaitForServerFileLoad(1) + :redraw! + var d = lsp#diag#GetDiagsForBuf()[0] + assert_equal({start: {line: 0, character: 5}, end: {line: 0, character: 6}}, + d.range) + + :LspDiag show + assert_equal(1, line('$')) + wincmd w + setline(2, 'int j:') + redraw! + g:WaitForDiags(2) + var l = lsp#diag#GetDiagsForBuf() + assert_equal({start: {line: 0, character: 5}, end: {line: 0, character: 6}}, + l[0].range) + assert_equal({start: {line: 1, character: 5}, end: {line: 1, character: 6}}, + l[1].range) + wincmd w + assert_equal(2, line('$')) + wincmd w + deletebufline('', 1, '$') + redraw! + g:WaitForDiags(0) + assert_equal([], lsp#diag#GetDiagsForBuf()) + wincmd w + assert_equal([''], getline(1, '$')) + :lclose + + setloclist(0, [], 'f') + :%bw! +enddef + +# Test that the client have been able to configure the server to speak utf-32 +def g:Test_UnicodeColumnCalc() + :silent! edit XUnicodeColumn.c + sleep 200m + var lines: list<string> =<< trim END + int count; + int fn(int a) + { + int 😊😊😊😊; + 😊😊😊😊 = a; + + int b; + b = a; + return count + 1; + } + END + setline(1, lines) + :redraw! + + cursor(5, 1) # 😊😊😊😊 = a; + search('a') + assert_equal([], + execute('LspGotoDefinition')->split("\n")) + assert_equal([2, 12], [line('.'), col('.')]) + + cursor(8, 1) # b = a; + search('a') + assert_equal([], + execute('LspGotoDefinition')->split("\n")) + assert_equal([2, 12], [line('.'), col('.')]) + + :%bw! +enddef + +# Test for multiple LSP diagnostics on the same line +def g:Test_LspDiag_Multi() + :silent! edit XLspDiagMulti.c + sleep 200m + + var bnr: number = bufnr() + + var lines =<< trim END + int i = "a"; + int j = i; + int y = 0; + END + setline(1, lines) + :redraw! + # TODO: Waiting count doesn't include Warning, Info, and Hint diags + if clangdVerMajor > 14 + g:WaitForServerFileLoad(3) + else + g:WaitForServerFileLoad(2) + endif + :LspDiag show + var qfl: list<dict<any>> = getloclist(0) + assert_equal('quickfix', getwinvar(winnr('$'), '&buftype')) + assert_equal(bnr, qfl[0].bufnr) + assert_equal(3, qfl->len()) + if clangdVerMajor > 14 + assert_equal([1, 5, 'E'], [qfl[0].lnum, qfl[0].col, qfl[0].type]) + else + assert_equal([1, 5, 'W'], [qfl[0].lnum, qfl[0].col, qfl[0].type]) + endif + assert_equal([1, 9, 'E'], [qfl[1].lnum, qfl[1].col, qfl[1].type]) + assert_equal([2, 9, 'E'], [qfl[2].lnum, qfl[2].col, qfl[2].type]) + close + + :sleep 100m + cursor(2, 1) + assert_equal('', execute('LspDiag prev')) + assert_equal([1, 9], [line('.'), col('.')]) + + assert_equal('', execute('LspDiag prev')) + assert_equal([1, 5], [line('.'), col('.')]) + + var output = execute('LspDiag prev')->split("\n") + assert_equal('Warn: No more diagnostics found', output[0]) + + assert_equal('', execute('LspDiag prevWrap')) + assert_equal([2, 9], [line('.'), col('.')]) + + cursor(2, 1) + assert_equal('', execute('LspDiag first')) + assert_equal([1, 5], [line('.'), col('.')]) + assert_equal('', execute('LspDiag next')) + assert_equal([1, 9], [line('.'), col('.')]) + cursor(1, 1) + assert_equal('', execute('LspDiag last')) + assert_equal([2, 9], [line('.'), col('.')]) + assert_equal('', execute('LspDiag nextWrap')) + assert_equal([1, 5], [line('.'), col('.')]) + assert_equal('', execute('LspDiag nextWrap')) + assert_equal([1, 9], [line('.'), col('.')]) + popup_clear() + + # Test for :LspDiag here on a line with multiple diagnostics + cursor(1, 1) + :LspDiag here + assert_equal([1, 5], [line('.'), col('.')]) + var ids = popup_list() + assert_equal(1, ids->len()) + assert_match('Incompatible pointer to integer', getbufline(ids[0]->winbufnr(), 1, '$')[0]) + popup_clear() + cursor(1, 6) + :LspDiag here + assert_equal([1, 9], [line('.'), col('.')]) + ids = popup_list() + assert_equal(1, ids->len()) + assert_match('Initializer element is not', getbufline(ids[0]->winbufnr(), 1, '$')[0]) + popup_clear() + + # Line without diagnostics + cursor(3, 1) + output = execute('LspDiag here')->split("\n") + assert_equal('Warn: No more diagnostics found on this line', output[0]) + + g:LspOptionsSet({showDiagInPopup: false}) + for i in range(1, 5) + cursor(1, i) + output = execute('LspDiag current')->split('\n') + assert_match('Incompatible pointer to integer', output[0]) + endfor + for i in range(6, 12) + cursor(1, i) + output = execute('LspDiag current')->split('\n') + assert_match('Initializer element is not ', output[0]) + endfor + g:LspOptionsSet({showDiagInPopup: true}) + + # Check for exact diag ":LspDiag current!" + g:LspOptionsSet({showDiagInPopup: false}) + for i in range(1, 4) + cursor(1, i) + output = execute('LspDiag! current')->split('\n') + assert_equal('Warn: No diagnostic messages found for current position', output[0]) + endfor + + cursor(1, 5) + output = execute('LspDiag! current')->split('\n') + assert_match('Incompatible pointer to integer', output[0]) + + for i in range(6, 8) + cursor(1, i) + output = execute('LspDiag! current')->split('\n') + assert_equal('Warn: No diagnostic messages found for current position', output[0]) + endfor + + for i in range(9, 11) + cursor(1, i) + output = execute('LspDiag! current')->split('\n') + assert_match('Initializer element is not ', output[0]) + endfor + for i in range(12, 12) + cursor(1, i) + output = execute('LspDiag! current')->split('\n') + assert_equal('Warn: No diagnostic messages found for current position', output[0]) + endfor + + g:LspOptionsSet({showDiagInPopup: true}) + + # :[count]LspDiag next + g:LspOptionsSet({showDiagInPopup: false}) + cursor(1, 1) + :2LspDiag next + assert_equal([1, 9], [line('.'), col('.')]) + :2LspDiag next + assert_equal([2, 9], [line('.'), col('.')]) + output = execute(':2LspDiag next')->split("\n") + assert_equal('Warn: No more diagnostics found', output[0]) + + cursor(1, 1) + :99LspDiag next + assert_equal([2, 9], [line('.'), col('.')]) + g:LspOptionsSet({showDiagInPopup: true}) + + # :[count]LspDiag prev + g:LspOptionsSet({showDiagInPopup: false}) + cursor(1, 1) + :2LspDiag prev + assert_equal('Warn: No more diagnostics found', output[0]) + cursor(3, 3) + :2LspDiag prev + assert_equal([1, 9], [line('.'), col('.')]) + :2LspDiag prev + assert_equal([1, 5], [line('.'), col('.')]) + output = execute(':2LspDiag prev')->split("\n") + assert_equal('Warn: No more diagnostics found', output[0]) + + cursor(3, 3) + :99LspDiag prev + assert_equal([1, 5], [line('.'), col('.')]) + g:LspOptionsSet({showDiagInPopup: true}) + + :%bw! +enddef + +# Test for highlight diag inline +def g:Test_LspHighlightDiagInline() + :silent! edit XLspHighlightDiag.c + sleep 200m + setline(1, [ + 'int main()', + '{', + ' struct obj obj', + '', + ' return 1;', + '}', + ]) + + # TODO: Waiting count doesn't include Warning, Info, and Hint diags + g:WaitForDiags(2) + + g:LspOptionsSet({highlightDiagInline: true}) + + var props = prop_list(1) + assert_equal(0, props->len()) + props = prop_list(2) + assert_equal(0, props->len()) + props = prop_list(3) + assert_equal(2, props->len()) + assert_equal([ + {'id': 0, 'col': 12, 'type_bufnr': 0, 'end': 1, 'type': 'LspDiagInlineInfo', 'length': 3, 'start': 1}, + {'id': 0, 'col': 16, 'type_bufnr': 0, 'end': 1, 'type': 'LspDiagInlineError', 'length': 3, 'start': 1} + ], props) + props = prop_list(4) + assert_equal(0, props->len()) + props = prop_list(5) + assert_equal(1, props->len()) + assert_equal([{'id': 0, 'col': 5, 'type_bufnr': 0, 'end': 1, 'type': 'LspDiagInlineError', 'length': 6, 'start': 1}], props) + props = prop_list(6) + assert_equal(0, props->len()) + + g:LspOptionsSet({highlightDiagInline: false}) + props = prop_list(1, {end_lnum: line('$')}) + assert_equal(0, props->len()) + + :%bw! +enddef + +# Test for :LspCodeAction +def g:Test_LspCodeAction() + silent! edit XLspCodeAction.c + sleep 200m + var lines: list<string> =<< trim END + void testFunc() + { + int count; + count == 20; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + cursor(4, 1) + redraw! + :LspCodeAction 1 + assert_equal("\tcount = 20;", getline(4)) + + setline(4, "\tcount = 20:") + cursor(4, 1) + sleep 500m + :LspCodeAction 0 + assert_equal("\tcount = 20:", getline(4)) + + cursor(4, 1) + :LspCodeAction 2 + assert_equal("\tcount = 20:", getline(4)) + + cursor(4, 1) + :LspCodeAction 1 + assert_equal("\tcount = 20;", getline(4)) + bw! + + # pattern and string prefix + silent! edit XLspCodeActionPattern.c + sleep 200m + var lines2: list<string> =<< trim END + void testFunc() + { + int count; + if (count = 1) { + } + } + END + setline(1, lines2) + g:WaitForServerFileLoad(0) + cursor(4, 1) + redraw! + :LspCodeAction use + assert_equal("\tif (count == 1) {", getline(4)) + + setline(4, "\tif (count = 1) {") + cursor(4, 1) + sleep 500m + :LspCodeAction /paren + assert_equal("\tif ((count = 1)) {", getline(4)) + + setline(4, "\tif (count = 1) {") + cursor(4, 1) + sleep 500m + :LspCodeAction NON_EXISTING_PREFIX + assert_equal("\tif (count = 1) {", getline(4)) + + cursor(4, 1) + :LspCodeAction /NON_EXISTING_REGEX + assert_equal("\tif (count = 1) {", getline(4)) + bw! + + # empty file + assert_equal('', execute('LspCodeAction')) + + # file without an LSP server + edit a.raku + assert_equal('Error: Language server for "raku" file type supporting "codeAction" feature is not found', + execute('LspCodeAction')->split("\n")[0]) + + :%bw! +enddef + +# Test for :LspRename +def g:Test_LspRename() + silent! edit XLspRename.c + sleep 200m + var lines: list<string> =<< trim END + void F1(int count) + { + count = 20; + + ++count; + } + + void F2(int count) + { + count = 5; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + cursor(1, 1) + search('count') + redraw! + feedkeys(":LspRename\<CR>er\<CR>", "xt") + redraw! + var expected: list<string> =<< trim END + void F1(int counter) + { + counter = 20; + + ++counter; + } + + void F2(int count) + { + count = 5; + } + END + assert_equal(expected, getline(1, '$')) + + cursor(1, 1) + search('counter') + LspRename countvar + var expected2: list<string> =<< trim END + void F1(int countvar) + { + countvar = 20; + + ++countvar; + } + + void F2(int count) + { + count = 5; + } + END + assert_equal(expected2, getline(1, '$')) + sleep 100m + bw! + + # empty file + assert_equal('', execute('LspRename')) + + # file without an LSP server + edit a.raku + assert_equal('Error: Language server for "raku" file type supporting "rename" feature is not found', + execute('LspRename')->split("\n")[0]) + + :%bw! +enddef + +# Test for :LspSelectionExpand and :LspSelectionShrink +def g:Test_LspSelection() + silent! edit XLspSelection.c + sleep 200m + var lines: list<string> =<< trim END + void fnSel(int count) + { + int i; + for (i = 0; i < 10; i++) { + count++; + } + count = 20; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + # start a block-wise visual mode, LspSelectionExpand should change this to + # a characterwise visual mode. + exe "normal! 1G\<C-V>G\"_y" + cursor(2, 1) + redraw! + :LspSelectionExpand + redraw! + normal! y + assert_equal('v', visualmode()) + assert_equal([2, 8], [line("'<"), line("'>")]) + # start a linewise visual mode, LspSelectionExpand should change this to + # a characterwise visual mode. + exe "normal! 3GViB\"_y" + cursor(4, 29) + redraw! + :LspSelectionExpand + redraw! + normal! y + assert_equal('v', visualmode()) + assert_equal([4, 5, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + + # Expand the visual selection + xnoremap <silent> le <Cmd>LspSelectionExpand<CR> + xnoremap <silent> ls <Cmd>LspSelectionShrink<CR> + cursor(5, 8) + normal vley + assert_equal([5, 8, 5, 12], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleley + assert_equal([5, 8, 5, 14], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleley + assert_equal([4, 30, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleleley + assert_equal([4, 5, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleleleley + assert_equal([2, 1, 8, 1], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleleleleley + assert_equal([1, 1, 8, 1], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleleleleleley + assert_equal([1, 1, 8, 1], [line("'<"), col("'<"), line("'>"), col("'>")]) + + # Shrink the visual selection + cursor(5, 8) + normal vlsy + assert_equal([5, 8, 5, 12], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelsy + assert_equal([5, 8, 5, 12], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelsy + assert_equal([5, 8, 5, 12], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelelsy + assert_equal([5, 8, 5, 14], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelelelsy + assert_equal([4, 30, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelelelelsy + assert_equal([4, 5, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelelelelelsy + assert_equal([2, 1, 8, 1], [line("'<"), col("'<"), line("'>"), col("'>")]) + + xunmap le + xunmap ls + bw! + + # empty file + assert_equal('', execute('LspSelectionExpand')) + + # file without an LSP server + edit a.raku + assert_equal('Error: Language server for "raku" file type supporting "selectionRange" feature is not found', + execute('LspSelectionExpand')->split("\n")[0]) + + :%bw! +enddef + +# Test for :LspGotoDefinition, :LspGotoDeclaration and :LspGotoImpl +def g:Test_LspGotoSymbol() + settagstack(0, {items: []}) + silent! edit XLspGotoSymbol.cpp + sleep 600m + var lines: list<string> =<< trim END + class base { + public: + virtual void print(); + }; + + void base::print() + { + } + + class derived : public base { + public: + void print() {} + }; + + void f1(void) + { + base *bp; + derived d; + bp = &d; + + bp->print(); + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + + cursor(21, 6) + :LspGotoDeclaration + assert_equal([3, 19], [line('.'), col('.')]) + exe "normal! \<C-t>" + assert_equal([21, 6], [line('.'), col('.')]) + assert_equal(1, winnr('$')) + + :LspGotoDefinition + assert_equal([6, 12], [line('.'), col('.')]) + exe "normal! \<C-t>" + assert_equal([21, 6], [line('.'), col('.')]) + assert_equal(1, winnr('$')) + + # Command modifiers + :topleft LspGotoDefinition + assert_equal([6, 12], [line('.'), col('.')]) + assert_equal([1, 2], [winnr(), winnr('$')]) + close + exe "normal! \<C-t>" + assert_equal([21, 6], [line('.'), col('.')]) + + :tab LspGotoDefinition + assert_equal([6, 12], [line('.'), col('.')]) + assert_equal([2, 2, 1], [tabpagenr(), tabpagenr('$'), winnr('$')]) + tabclose + exe "normal! \<C-t>" + assert_equal([21, 6], [line('.'), col('.')]) + + # :LspGotoTypeDef + cursor(21, 2) + :LspGotoTypeDef + assert_equal([1, 7], [line('.'), col('.')]) + exe "normal! \<C-t>" + assert_equal([21, 2], [line('.'), col('.')]) + + # :LspGotoImpl + cursor(21, 6) + :LspGotoImpl + assert_equal([12, 11], [line('.'), col('.')]) + exe "normal! \<C-t>" + assert_equal([21, 6], [line('.'), col('.')]) + + # FIXME: The following tests are failing in Github CI. Comment out for now. + if 0 + # Error cases + :messages clear + cursor(11, 5) + :LspGotoDeclaration + var m = execute('messages')->split("\n") + assert_equal('symbol declaration is not found', m[1]) + :messages clear + :LspGotoDefinition + m = execute('messages')->split("\n") + assert_equal('symbol definition is not found', m[1]) + :messages clear + :LspGotoImpl + m = execute('messages')->split("\n") + assert_equal('symbol implementation is not found', m[1]) + :messages clear + endif + + # Test for LspPeekDeclaration + cursor(21, 6) + var bnum = bufnr() + :LspPeekDeclaration + var plist = popup_list() + assert_true(1, plist->len()) + assert_equal(bnum, plist[0]->winbufnr()) + assert_equal(3, line('.', plist[0])) + popup_clear() + # tag stack should not be changed + assert_fails("normal! \<C-t>", 'E555:') + + # Test for LspPeekDefinition + :LspPeekDefinition + plist = popup_list() + assert_true(1, plist->len()) + assert_equal(bnum, plist[0]->winbufnr()) + assert_equal(6, line('.', plist[0])) + popup_clear() + # tag stack should not be changed + assert_fails("normal! \<C-t>", 'E555:') + + # FIXME: :LspPeekTypeDef and :LspPeekImpl are supported only with clang-14. + # This clangd version is not available in Github CI. + + :%bw! + + # empty file + assert_equal('', execute('LspGotoDefinition')) + assert_equal('', execute('LspGotoDeclaration')) + assert_equal('', execute('LspGotoImpl')) + + # file without an LSP server + edit a.raku + assert_equal('Error: Language server for "raku" file type supporting "definition" feature is not found', + execute('LspGotoDefinition')->split("\n")[0]) + assert_equal('Error: Language server for "raku" file type supporting "declaration" feature is not found', + execute('LspGotoDeclaration')->split("\n")[0]) + assert_equal('Error: Language server for "raku" file type supporting "implementation" feature is not found', + execute('LspGotoImpl')->split("\n")[0]) + + :%bw! +enddef + +# Test for :LspHighlight +def g:Test_LspHighlight() + silent! edit XLspHighlight.c + sleep 200m + var lines: list<string> =<< trim END + void f1(int arg) + { + int i = arg; + arg = 2; + if (arg == 2) { + arg = 3; + } + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + cursor(1, 13) + :LspHighlight + var expected: dict<any> + expected = {id: 0, col: 13, end: 1, type: 'LspTextRef', length: 3, start: 1} + expected.type_bufnr = 0 + assert_equal([expected], prop_list(1)) + expected = {id: 0, col: 11, end: 1, type: 'LspReadRef', length: 3, start: 1} + expected.type_bufnr = 0 + assert_equal([expected], prop_list(3)) + expected = {id: 0, col: 3, end: 1, type: 'LspWriteRef', length: 3, start: 1} + expected.type_bufnr = 0 + assert_equal([expected], prop_list(4)) + :LspHighlightClear + assert_equal([], prop_list(1)) + assert_equal([], prop_list(3)) + assert_equal([], prop_list(4)) + + cursor(5, 3) # if (arg == 2) { + var output = execute('LspHighlight')->split("\n") + assert_equal('Warn: No highlight for the current position', output[0]) + :%bw! +enddef + +# Test for :LspHover +def g:Test_LspHover() + silent! edit XLspHover.c + sleep 200m + var lines: list<string> =<< trim END + int f1(int a) + { + return 0; + } + + void f2(void) + { + f1(5); + char *z = "z"; + f1(z); + } + END + setline(1, lines) + if clangdVerMajor > 14 + g:WaitForServerFileLoad(1) + else + g:WaitForServerFileLoad(0) + endif + cursor(8, 4) + var output = execute(':LspHover')->split("\n") + assert_equal([], output) + var p: list<number> = popup_list() + assert_equal(1, p->len()) + assert_equal(['### function `f1` ', '', '---', '→ `int` ', 'Parameters: ', '- `int a`', '', '---', '```cpp', 'int f1(int a)', '```'], getbufline(winbufnr(p[0]), 1, '$')) + popup_close(p[0]) + cursor(7, 1) + output = execute(':LspHover')->split("\n") + assert_equal('Warn: No documentation found for current keyword', output[0]) + output = execute(':silent LspHover')->split("\n") + assert_equal([], output) + assert_equal([], popup_list()) + + # Show current diagnostic as to open another popup. + # Then we can test that LspHover closes all existing popups + cursor(10, 6) + :LspDiag current + assert_equal(1, popup_list()->len()) + :LspHover + assert_equal(1, popup_list()->len()) + popup_clear() + + # Show hover information in a preview window + g:LspOptionsSet({hoverInPreview: true}) + cursor(8, 4) + :LspHover + assert_equal([2, 2, 'preview'], [winnr('$'), winnr(), win_gettype(1)]) + assert_equal('LspHover', winbufnr(1)->bufname()) + cursor(9, 9) + :LspHover + assert_equal([2, 2, 'preview'], [winnr('$'), winnr(), win_gettype(1)]) + g:LspOptionsSet({hoverInPreview: false}) + :pclose + + :%bw! +enddef + +# Test for :LspShowSignature +def g:Test_LspShowSignature() + silent! edit XLspShowSignature.c + sleep 200m + var lines: list<string> =<< trim END + int MyFunc(int a, int b) + { + return 0; + } + + void f2(void) + { + MyFunc( + } + END + setline(1, lines) + g:WaitForServerFileLoad(2) + cursor(8, 10) + :LspShowSignature + var p: list<number> = popup_list() + var bnr: number = winbufnr(p[0]) + assert_equal(1, p->len()) + assert_equal(['MyFunc(int a, int b) -> int'], getbufline(bnr, 1, '$')) + var expected: dict<any> + expected = {id: 0, col: 8, end: 1, type: 'signature', length: 5, start: 1} + expected.type_bufnr = bnr + assert_equal([expected], prop_list(1, {bufnr: bnr})) + popup_close(p[0]) + + setline(line('.'), ' MyFunc(10, ') + cursor(8, 13) + :LspShowSignature + p = popup_list() + bnr = winbufnr(p[0]) + assert_equal(1, p->len()) + assert_equal(['MyFunc(int a, int b) -> int'], getbufline(bnr, 1, '$')) + expected = {id: 0, col: 15, end: 1, type: 'signature', length: 5, start: 1} + expected.type_bufnr = bnr + assert_equal([expected], prop_list(1, {bufnr: bnr})) + popup_close(p[0]) + :%bw! +enddef + +# Test for :LspSymbolSearch +def g:Test_LspSymbolSearch() + silent! edit XLspSymbolSearch.c + sleep 200m + var lines: list<string> =<< trim END + void lsptest_funcA() + { + } + + void lsptest_funcB() + { + } + + void lsptest_funcC() + { + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + + cursor(1, 1) + feedkeys(":LspSymbolSearch lsptest_funcB\<CR>", "xt") + assert_equal([5, 6], [line('.'), col('.')]) + + cursor(1, 1) + feedkeys(":LspSymbolSearch lsptest_func\<CR>\<Down>\<Down>\<CR>", "xt") + assert_equal([9, 6], [line('.'), col('.')]) + + cursor(1, 1) + feedkeys(":LspSymbolSearch lsptest_func\<CR>A\<BS>B\<CR>", "xt") + assert_equal([5, 6], [line('.'), col('.')]) + + var output = execute(':LspSymbolSearch lsptest_nonexist')->split("\n") + assert_equal('Warn: Symbol "lsptest_nonexist" is not found', output[0]) + + :%bw! +enddef + +# Test for :LspIncomingCalls +def g:Test_LspIncomingCalls() + silent! edit XLspIncomingCalls.c + sleep 200m + var lines: list<string> =<< trim END + void xFuncIncoming(void) + { + } + + void aFuncIncoming(void) + { + xFuncIncoming(); + } + + void bFuncIncoming(void) + { + xFuncIncoming(); + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + cursor(1, 6) + :LspIncomingCalls + assert_equal([1, 2], [winnr(), winnr('$')]) + var l = getline(1, '$') + assert_equal('# Incoming calls to "xFuncIncoming"', l[0]) + assert_match('- xFuncIncoming (XLspIncomingCalls.c \[.*\])', l[1]) + assert_match(' + aFuncIncoming (XLspIncomingCalls.c \[.*\])', l[2]) + assert_match(' + bFuncIncoming (XLspIncomingCalls.c \[.*\])', l[3]) + :%bw! +enddef + +# Test for :LspOutline +def g:Test_LspOutline() + silent! edit XLspOutline.c + sleep 200m + var lines: list<string> =<< trim END + void aFuncOutline(void) + { + } + + void bFuncOutline(void) + { + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + var winid = win_getid() + :LspOutline + assert_equal(2, winnr('$')) + var bnum = winbufnr(winid + 1) + assert_equal('LSP-Outline', bufname(bnum)) + assert_equal(['Function', ' aFuncOutline', ' bFuncOutline'], getbufline(bnum, 4, '$')) + + # Validate position vert topleft + assert_equal(['row', [['leaf', winid + 1], ['leaf', winid]]], winlayout()) + + # Validate default width is 20 + assert_equal(20, winwidth(winid + 1)) + + execute $':{bnum}bw' + + # Validate position vert botright + g:LspOptionsSet({outlineOnRight: true}) + :LspOutline + assert_equal(2, winnr('$')) + bnum = winbufnr(winid + 2) + assert_equal('LSP-Outline', bufname(bnum)) + assert_equal(['Function', ' aFuncOutline', ' bFuncOutline'], getbufline(bnum, 4, '$')) + assert_equal(['row', [['leaf', winid], ['leaf', winid + 2]]], winlayout()) + g:LspOptionsSet({outlineOnRight: false}) + execute $':{bnum}bw' + + # Validate <mods> position botright (below) + :botright LspOutline + assert_equal(2, winnr('$')) + bnum = winbufnr(winid + 3) + assert_equal('LSP-Outline', bufname(bnum)) + assert_equal(['Function', ' aFuncOutline', ' bFuncOutline'], getbufline(bnum, 4, '$')) + assert_equal(['col', [['leaf', winid], ['leaf', winid + 3]]], winlayout()) + execute $':{bnum}bw' + + # Validate that outlineWinSize works for LspOutline + g:LspOptionsSet({outlineWinSize: 40}) + :LspOutline + assert_equal(2, winnr('$')) + bnum = winbufnr(winid + 4) + assert_equal('LSP-Outline', bufname(bnum)) + assert_equal(['Function', ' aFuncOutline', ' bFuncOutline'], getbufline(bnum, 4, '$')) + assert_equal(40, winwidth(winid + 4)) + execute $':{bnum}bw' + g:LspOptionsSet({outlineWinSize: 20}) + + # Validate that <count> works for LspOutline + :37LspOutline + assert_equal(2, winnr('$')) + bnum = winbufnr(winid + 5) + assert_equal('LSP-Outline', bufname(bnum)) + assert_equal(['Function', ' aFuncOutline', ' bFuncOutline'], getbufline(bnum, 4, '$')) + assert_equal(37, winwidth(winid + 5)) + execute $':{bnum}bw' + + :%bw! +enddef + +# Test for setting the 'tagfunc' +def g:Test_LspTagFunc() + var lines: list<string> =<< trim END + void aFuncTag(void) + { + xFuncTag(); + } + + void bFuncTag(void) + { + xFuncTag(); + } + + void xFuncTag(void) + { + } + END + writefile(lines, 'Xtagfunc.c') + :silent! edit Xtagfunc.c + g:WaitForServerFileLoad(1) + :setlocal tagfunc=lsp#lsp#TagFunc + cursor(3, 4) + :exe "normal \<C-]>" + assert_equal([11, 6], [line('.'), col('.')]) + cursor(1, 1) + assert_fails('exe "normal \<C-]>"', 'E433:') + + :set tagfunc& + :%bw! + delete('Xtagfunc.c') +enddef + +# Test for the LspDiagsUpdated autocmd +def g:Test_LspDiagsUpdated_Autocmd() + g:LspAutoCmd = 0 + autocmd_add([{event: 'User', pattern: 'LspDiagsUpdated', cmd: 'g:LspAutoCmd = g:LspAutoCmd + 1'}]) + silent! edit XLspDiagsAutocmd.c + sleep 200m + var lines: list<string> =<< trim END + void aFuncDiag(void) + { + return; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + setline(3, ' return:') + redraw! + g:WaitForDiags(1) + setline(3, ' return;') + redraw! + g:WaitForDiags(0) + :%bw! + autocmd_delete([{event: 'User', pattern: 'LspDiagsUpdated'}]) + assert_equal(5, g:LspAutoCmd) +enddef + +# Test custom notification handlers +def g:Test_LspCustomNotificationHandlers() + + g:LSPTest_customNotificationHandlerReplied = false + + silent! edit XcustomNotification.c + sleep 200m + var lines: list<string> =<< trim END + int a = 1; + int main(void) { + return a; + } + END + setline(1, lines) + g:WaitForAssert(() => assert_equal(true, g:LSPTest_customNotificationHandlerReplied)) + :%bw! +enddef + +def g:Test_ScanFindIdent() + :silent! edit XscanFindIdent.c + sleep 200m + var lines: list<string> =<< trim END + int countFI; + int fnFI(int a) + { + int hello; + hello = a; + return countFI + 1; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + :redraw! + + # LspGotoDefinition et al + cursor(5, 10) + assert_equal([], execute('LspGotoDefinition')->split("\n")) + assert_equal([2, 14], [line('.'), col('.')]) + + cursor(6, 10) + assert_equal([], execute('LspGotoDefinition')->split("\n")) + assert_equal([1, 5], [line('.'), col('.')]) + + # LspShowReferences + cursor(6, 10) + assert_equal([], execute('LspShowReferences')->split("\n")) + :lclose + + # LspRename + cursor(6, 10) + assert_equal([], execute('LspRename counterFI')->split("\n")) + sleep 100m + assert_equal('int counterFI;', getline(1)) + assert_equal(' return counterFI + 1;', getline(6)) + + :%bw! +enddef + +# Test for doing omni completion from the first column +def g:Test_OmniComplete_FirstColumn() + :silent! edit XOmniCompleteFirstColumn.c + sleep 200m + var lines: list<string> =<< trim END + typedef struct Foo_ { + } Foo_t; + + #define FOO 1 + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + feedkeys("G0i\<C-X>\<C-O>", 'xt') + assert_equal('Foo_t#define FOO 1', getline('.')) + :%bw! +enddef + +# Test for doing omni completion with a multibyte character +def g:Test_OmniComplete_Multibyte() + :silent! edit XOmniCompleteMultibyte.c + sleep 200m + var lines: list<string> =<< trim END + #include <string.h> + void Fn(void) + { + int thisVar = 1; + int len = strlen("©©©©©") + thisVar; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + cursor(5, 36) + feedkeys("cwthis\<C-X>\<C-O>", 'xt') + assert_equal(' int len = strlen("©©©©©") + thisVar;', getline('.')) + :%bw! +enddef + +# Test for doing omni completion for a struct field +def g:Test_OmniComplete_Struct() + :silent! edit XOmniCompleteStruct.c + sleep 200m + var lines: list<string> =<< trim END + struct test_ { + int foo; + int bar; + int baz; + }; + void Fn(void) + { + struct test_ myTest; + struct test_ *pTest; + myTest.bar = 10; + pTest->bar = 20; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + cursor(10, 12) + feedkeys("cwb\<C-X>\<C-O>\<C-N>\<C-Y>", 'xt') + assert_equal(' myTest.baz = 10;', getline('.')) + cursor(11, 12) + feedkeys("cw\<C-X>\<C-O>\<C-N>\<C-Y>", 'xt') + assert_equal(' pTest->baz = 20;', getline('.')) + :%bw! +enddef + +# Test for doing omni completion after an opening parenthesis. +# This used to result in an error message. +def g:Test_OmniComplete_AfterParen() + :silent! edit XOmniCompleteAfterParen.c + sleep 200m + var lines: list<string> =<< trim END + #include <stdio.h> + void Fn(void) + { + printf( + } + END + setline(1, lines) + g:WaitForServerFileLoad(2) + redraw! + + cursor(4, 1) + feedkeys("A\<C-X>\<C-O>\<C-Y>", 'xt') + assert_equal(' printf(', getline('.')) + :%bw! +enddef + +# Test for inlay hints +def g:Test_InlayHints() + :silent! edit XinlayHints.c + sleep 200m + var lines: list<string> =<< trim END + void func1(int a, int b) + { + } + + void func2() + { + func1(10, 20); + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + assert_equal([], prop_list(7)) + + :LspInlayHints enable + var p = prop_list(7) + assert_equal([9, 'LspInlayHintsParam'], [p[0].col, p[0].type]) + assert_equal([13, 'LspInlayHintsParam'], [p[1].col, p[1].type]) + + :LspInlayHints disable + assert_equal([], prop_list(7)) + + g:LspOptionsSet({showInlayHints: true}) + assert_equal([9, 'LspInlayHintsParam'], [p[0].col, p[0].type]) + assert_equal([13, 'LspInlayHintsParam'], [p[1].col, p[1].type]) + + g:LspOptionsSet({showInlayHints: false}) + assert_equal([], prop_list(7)) + + :hide enew + :LspInlayHints enable + :bprev + assert_equal([9, 'LspInlayHintsParam'], [p[0].col, p[0].type]) + assert_equal([13, 'LspInlayHintsParam'], [p[1].col, p[1].type]) + + :hide enew + :LspInlayHints disable + :bprev + assert_equal([], prop_list(7)) + + :%bw! +enddef + +# Test for reloading a modified buffer with diags +def g:Test_ReloadBufferWithDiags() + var lines: list<string> =<< trim END + void ReloadBufferFunc1(void) + { + int a: + } + END + writefile(lines, 'Xreloadbuffer.c') + :silent! edit Xreloadbuffer.c + g:WaitForServerFileLoad(1) + var signs = sign_getplaced('%', {group: '*'})[0].signs + assert_equal(3, signs[0].lnum) + append(0, ['', '']) + signs = sign_getplaced('%', {group: '*'})[0].signs + assert_equal(5, signs[0].lnum) + :edit! + sleep 200m + signs = sign_getplaced('%', {group: '*'})[0].signs + assert_equal(3, signs[0].lnum) + + :%bw! + delete('Xreloadbuffer.c') +enddef + +# Test for ":LspDiag" sub commands +def g:Test_LspDiagsSubcmd() + new XLspDiagsSubCmd.raku + + feedkeys(":LspDiag \<C-A>\<CR>", 'xt') + assert_equal('LspDiag first current here highlight last next nextWrap prev prevWrap show', @:) + feedkeys(":LspDiag highlight \<C-A>\<CR>", 'xt') + assert_equal('LspDiag highlight enable disable', @:) + assert_equal(['Error: :LspDiag - Unsupported argument "xyz"'], + execute('LspDiag xyz')->split("\n")) + assert_equal(['Error: :LspDiag - Unsupported argument "first xyz"'], + execute('LspDiag first xyz')->split("\n")) + assert_equal(['Error: :LspDiag - Unsupported argument "current xyz"'], + execute('LspDiag current xyz')->split("\n")) + assert_equal(['Error: :LspDiag - Unsupported argument "here xyz"'], + execute('LspDiag here xyz')->split("\n")) + assert_equal(['Error: Argument required for ":LspDiag highlight"'], + execute('LspDiag highlight')->split("\n")) + assert_equal(['Error: :LspDiag highlight - Unsupported argument "xyz"'], + execute('LspDiag highlight xyz')->split("\n")) + assert_equal(['Error: :LspDiag highlight - Unsupported argument "enable xyz"'], + execute('LspDiag highlight enable xyz')->split("\n")) + assert_equal(['Error: :LspDiag - Unsupported argument "last xyz"'], + execute('LspDiag last xyz')->split("\n")) + assert_equal(['Error: :LspDiag - Unsupported argument "next xyz"'], + execute('LspDiag next xyz')->split("\n")) + assert_equal(['Error: :LspDiag - Unsupported argument "prev xyz"'], + execute('LspDiag prev xyz')->split("\n")) + assert_equal(['Error: :LspDiag - Unsupported argument "show xyz"'], + execute('LspDiag show xyz')->split("\n")) + + :%bw! +enddef + +# Test for the :LspServer command. +def g:Test_LspServer() + new a.raku + assert_equal(['Warn: No Lsp servers found for "a.raku"'], + execute('LspServer debug on')->split("\n")) + assert_equal(['Warn: No Lsp servers found for "a.raku"'], + execute('LspServer restart')->split("\n")) + assert_equal(['Warn: No Lsp servers found for "a.raku"'], + execute('LspServer show status')->split("\n")) + assert_equal(['Warn: No Lsp servers found for "a.raku"'], + execute('LspServer trace verbose')->split("\n")) + assert_equal(['Error: LspServer - Unsupported argument "xyz"'], + execute('LspServer xyz')->split("\n")) + assert_equal(['Error: Argument required for ":LspServer debug"'], + execute('LspServer debug')->split("\n")) + assert_equal(['Error: Unsupported argument "xyz"'], + execute('LspServer debug xyz')->split("\n")) + assert_equal(['Error: Unsupported argument "on xyz"'], + execute('LspServer debug on xyz')->split("\n")) + assert_equal(['Error: Argument required for ":LspServer show"'], + execute('LspServer show')->split("\n")) + assert_equal(['Error: Unsupported argument "xyz"'], + execute('LspServer show xyz')->split("\n")) + assert_equal(['Error: Unsupported argument "status xyz"'], + execute('LspServer show status xyz')->split("\n")) + assert_equal(['Error: Argument required for ":LspServer trace"'], + execute('LspServer trace')->split("\n")) + assert_equal(['Error: Unsupported argument "xyz"'], + execute('LspServer trace xyz')->split("\n")) + assert_equal(['Error: Unsupported argument "verbose xyz"'], + execute('LspServer trace verbose xyz')->split("\n")) + :%bw! +enddef + +# Test for the diagnostics virtual text text property +def g:Test_DiagVirtualText() + if !has('patch-9.0.1157') + # Doesn't support virtual text + return + endif + g:LspOptionsSet({highlightDiagInline: false}) + :silent! edit XdiagVirtualText.c + sleep 200m + var lines: list<string> =<< trim END + void DiagVirtualTextFunc1() + { + int i: + } + END + setline(1, lines) + g:WaitForServerFileLoad(1) + redraw! + + var p = prop_list(1, {end_lnum: line('$')}) + assert_equal(0, p->len()) + + g:LspOptionsSet({showDiagWithVirtualText: true}) + p = prop_list(1, {end_lnum: line('$')}) + assert_equal(1, p->len()) + assert_equal([3, 'LspDiagVirtualTextError'], [p[0].lnum, p[0].type]) + + g:LspOptionsSet({showDiagWithVirtualText: false}) + p = prop_list(1, {end_lnum: line('$')}) + assert_equal(0, p->len()) + + g:LspOptionsSet({highlightDiagInline: true}) + :%bw! +enddef + +# Test for enabling and disabling the "showDiagWithSign" option. +def g:Test_DiagSigns() + :silent! edit Xdiagsigns.c + sleep 200m + var lines: list<string> =<< trim END + void DiagSignsFunc1(void) + { + int a: + } + END + setline(1, lines) + g:WaitForServerFileLoad(1) + redraw! + + var signs = sign_getplaced('%', {group: '*'})[0].signs + assert_equal([1, 3], [signs->len(), signs[0].lnum]) + + g:LspOptionsSet({showDiagWithSign: false}) + signs = sign_getplaced('%', {group: '*'})[0].signs + assert_equal([], signs) + g:LspOptionsSet({showDiagWithSign: true}) + signs = sign_getplaced('%', {group: '*'})[0].signs + assert_equal([1, 3], [signs->len(), signs[0].lnum]) + + # Test for enabling/disabling "autoHighlightDiags" + g:LspOptionsSet({autoHighlightDiags: false}) + signs = sign_getplaced('%', {group: '*'})[0].signs + assert_equal([], signs) + g:LspOptionsSet({autoHighlightDiags: true}) + signs = sign_getplaced('%', {group: '*'})[0].signs + assert_equal([1, 3], [signs->len(), signs[0].lnum]) + + :%bw! +enddef + +# TODO: +# 1. Add a test for autocompletion with a single match while ignoring case. +# After the full matched name is typed, the completion popup should still +# be displayed. e.g. +# +# int MyVar = 1; +# int abc = myvar<C-N><C-Y> +# 2. Add a test for jumping to a non-existing symbol definition, declaration. + +# Start the C language server. Returns true on success and false on failure. +def g:StartLangServer(): bool + return g:StartLangServerWithFile('Xtest.c') +enddef + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/test/common.vim b/vim/pack/downloads/opt/lsp/test/common.vim new file mode 100644 index 0000000..eeb852c --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/common.vim @@ -0,0 +1,166 @@ +vim9script +# Common routines used for running the unit tests + +# Load the LSP plugin. Also enable syntax, file type detection. +def g:LoadLspPlugin() + syntax on + filetype on + filetype plugin on + filetype indent on + + # Set the $LSP_PROFILE environment variable to profile the LSP plugin + var do_profile: bool = false + if exists('$LSP_PROFILE') + do_profile = true + endif + + if do_profile + # profile the LSP plugin + profile start lsp_profile.txt + profile! file */lsp/* + endif + + g:LSPTest = true + source ../plugin/lsp.vim +enddef + +# The WaitFor*() functions are reused from the Vim test suite. +# +# Wait for up to five seconds for "assert" to return zero. "assert" must be a +# (lambda) function containing one assert function. Example: +# call WaitForAssert({-> assert_equal("dead", job_status(job)}) +# +# A second argument can be used to specify a different timeout in msec. +# +# Return zero for success, one for failure (like the assert function). +func g:WaitForAssert(assert, ...) + let timeout = get(a:000, 0, 5000) + if g:WaitForCommon(v:null, a:assert, timeout) < 0 + return 1 + endif + return 0 +endfunc + +# Either "expr" or "assert" is not v:null +# Return the waiting time for success, -1 for failure. +func g:WaitForCommon(expr, assert, timeout) + " using reltime() is more accurate, but not always available + let slept = 0 + if exists('*reltimefloat') + let start = reltime() + endif + + while 1 + if type(a:expr) == v:t_func + let success = a:expr() + elseif type(a:assert) == v:t_func + let success = a:assert() == 0 + else + let success = eval(a:expr) + endif + if success + return slept + endif + + if slept >= a:timeout + break + endif + if type(a:assert) == v:t_func + " Remove the error added by the assert function. + call remove(v:errors, -1) + endif + + sleep 10m + if exists('*reltimefloat') + let slept = float2nr(reltimefloat(reltime(start)) * 1000) + else + let slept += 10 + endif + endwhile + + return -1 " timed out +endfunc + +# Wait for up to five seconds for "expr" to become true. "expr" can be a +# stringified expression to evaluate, or a funcref without arguments. +# Using a lambda works best. Example: +# call WaitFor({-> status == "ok"}) +# +# A second argument can be used to specify a different timeout in msec. +# +# When successful the time slept is returned. +# When running into the timeout an exception is thrown, thus the function does +# not return. +func g:WaitFor(expr, ...) + let timeout = get(a:000, 0, 5000) + let slept = g:WaitForCommon(a:expr, v:null, timeout) + if slept < 0 + throw 'WaitFor() timed out after ' .. timeout .. ' msec' + endif + return slept +endfunc + +# Wait for diagnostic messages from the LSP server. +# Waits for a maximum of (150 * 200) / 1000 = 30 seconds +def g:WaitForDiags(errCount: number) + var retries = 0 + while retries < 200 + var d = lsp#lsp#ErrorCount() + if d.Error == errCount + break + endif + retries += 1 + :sleep 150m + endwhile + + assert_equal(errCount, lsp#lsp#ErrorCount().Error) + if lsp#lsp#ErrorCount().Error != errCount + :LspDiag show + assert_report(getloclist(0)->string()) + :lclose + endif +enddef + +# Wait for the LSP server to load and process a file. This works by waiting +# for a certain number of diagnostic messages from the server. +def g:WaitForServerFileLoad(diagCount: number) + :redraw! + var waitCount = diagCount + if waitCount == 0 + # Introduce a temporary diagnostic + append('$', '-') + redraw! + waitCount = 1 + endif + g:WaitForDiags(waitCount) + if waitCount != diagCount + # Remove the temporary line + deletebufline('%', '$') + redraw! + g:WaitForDiags(0) + endif +enddef + +# Start the language server. Returns true on success and false on failure. +# 'fname' is the name of a dummy file to start the server. +def g:StartLangServerWithFile(fname: string): bool + # Edit a dummy file to start the LSP server + exe ':silent! edit ' .. fname + # Wait for the LSP server to become ready (max 10 seconds) + var maxcount = 100 + while maxcount > 0 && !g:LspServerReady() + :sleep 100m + maxcount -= 1 + endwhile + var serverStatus: bool = g:LspServerReady() + :bw! + + if !serverStatus + writefile(['FAIL: Not able to start the language server'], 'results.txt') + qall! + endif + + return serverStatus +enddef + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/test/dumps/Test_tsserver_completion_1.dump b/vim/pack/downloads/opt/lsp/test/dumps/Test_tsserver_completion_1.dump new file mode 100644 index 0000000..175c1dd --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/dumps/Test_tsserver_completion_1.dump @@ -0,0 +1,10 @@ +|H+0#00e0003#ffffff0|>|c+0#af5f00255&|o|n|s|t| +0#0000000&|h|t@1|p| |=| |r|e|q|u|i|r|e|(|'+0#e000002&|h|t@1|p|'|)+0#0000000&| @44 +| +0#0000e05#a8a8a8255@1|h+0#0000000#ffffff0|t@1|p|.|c|r|e> @64 +|~+0#4040ff13&| @4| +0#0000001#ffd7ff255|c|r|e|a|t|e|S|e|r|v|e|r| |f| | +0#4040ff13#ffffff0@52 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@62 diff --git a/vim/pack/downloads/opt/lsp/test/dumps/Test_tsserver_completion_2.dump b/vim/pack/downloads/opt/lsp/test/dumps/Test_tsserver_completion_2.dump new file mode 100644 index 0000000..3b6ecab --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/dumps/Test_tsserver_completion_2.dump @@ -0,0 +1,10 @@ +|H+0#00e0003#ffffff0|>|c+0#af5f00255&|o|n|s|t| +0#0000000&|h|t@1|p| |=| |r|e|q|u|i|r|e|(|'+0#e000002&|h|t@1|p|'|)+0#0000000&| @44 +| +0#0000e05#a8a8a8255@1|h+0#0000000#ffffff0|t@1|p|.|c|r> @65 +|~+0#4040ff13&| @4| +0#0000001#ffd7ff255|c|r|e|a|t|e|S|e|r|v|e|r| |f| | +0#4040ff13#ffffff0@52 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|~| @73 +|-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@62 diff --git a/vim/pack/downloads/opt/lsp/test/gopls_tests.vim b/vim/pack/downloads/opt/lsp/test/gopls_tests.vim new file mode 100644 index 0000000..bf00c89 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/gopls_tests.vim @@ -0,0 +1,121 @@ +vim9script +# Unit tests for Vim Language Server Protocol (LSP) golang client + +source common.vim + +var lspServers = [{ + filetype: ['go'], + path: exepath('gopls'), + args: ['serve'] + }] +call LspAddServer(lspServers) +echomsg systemlist($'{lspServers[0].path} version') + +# Test for :LspGotoDefinition, :LspGotoDeclaration, etc. +# This test also tests that multiple locations will be +# shown in a list or popup +def g:Test_LspGoto() + :silent! edit Xtest.go + var bnr = bufnr() + + sleep 200m + + var lines =<< trim END + package main + + type A/*goto implementation*/ interface { + Hello() + } + + type B struct{} + + func (b *B) Hello() {} + + type C struct{} + + func (c *C) Hello() {} + + func main() { + } + END + + setline(1, lines) + :redraw! + g:WaitForServerFileLoad(0) + + cursor(9, 10) + :LspGotoDefinition + assert_equal([7, 6], [line('.'), col('.')]) + exe "normal! \<C-t>" + assert_equal([9, 10], [line('.'), col('.')]) + + cursor(9, 13) + :LspGotoImpl + assert_equal([4, 9], [line('.'), col('.')]) + + cursor(13, 13) + :LspGotoImpl + assert_equal([4, 9], [line('.'), col('.')]) + + # Two implementions needs to be shown in a location list + cursor(4, 9) + assert_equal('', execute('LspGotoImpl')) + sleep 200m + var loclist: list<dict<any>> = getloclist(0) + assert_equal('quickfix', getwinvar(winnr('$'), '&buftype')) + assert_equal(2, loclist->len()) + assert_equal(bnr, loclist[0].bufnr) + assert_equal([9, 13, ''], [loclist[0].lnum, loclist[0].col, loclist[0].type]) + assert_equal([13, 13, ''], [loclist[1].lnum, loclist[1].col, loclist[1].type]) + lclose + + # Two implementions needs to be shown in a quickfix list + g:LspOptionsSet({ useQuickfixForLocations: true }) + cursor(4, 9) + assert_equal('', execute('LspGotoImpl')) + sleep 200m + var qfl: list<dict<any>> = getqflist() + assert_equal('quickfix', getwinvar(winnr('$'), '&buftype')) + assert_equal(2, qfl->len()) + assert_equal(bnr, qfl[0].bufnr) + assert_equal([9, 13, ''], [qfl[0].lnum, qfl[0].col, qfl[0].type]) + assert_equal([13, 13, ''], [qfl[1].lnum, qfl[1].col, qfl[1].type]) + cclose + g:LspOptionsSet({ useQuickfixForLocations: false }) + + # Two implementions needs to be peeked in a popup + cursor(4, 9) + :LspPeekImpl + sleep 10m + var ids = popup_list() + assert_equal(2, ids->len()) + var filePopupAttrs = ids[0]->popup_getoptions() + var refPopupAttrs = ids[1]->popup_getoptions() + assert_match('Xtest', filePopupAttrs.title) + assert_match('Implementation', refPopupAttrs.title) + assert_equal(9, line('.', ids[0])) # current line in left panel + assert_equal(2, line('$', ids[1])) # last line in right panel + feedkeys("j\<CR>", 'xt') + assert_equal(13, line('.')) + assert_equal([], popup_list()) + popup_clear() + + # Jump to the first implementation + cursor(4, 9) + assert_equal('', execute(':1LspGotoImpl')) + assert_equal([9, 13], [line('.'), col('.')]) + + # Jump to the second implementation + cursor(4, 9) + assert_equal('', execute(':2LspGotoImpl')) + assert_equal([13, 13], [line('.'), col('.')]) + bw! +enddef + +# Start the gopls language server. Returns true on success and false on +# failure. +def g:StartLangServer(): bool + return g:StartLangServerWithFile('Xtest.go') +enddef + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/test/markdown_tests.vim b/vim/pack/downloads/opt/lsp/test/markdown_tests.vim new file mode 100644 index 0000000..59abfb6 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/markdown_tests.vim @@ -0,0 +1,305 @@ +vim9script + +# Unit tests for the Github Flavored Markdown parser + +import '../autoload/lsp/markdown.vim' as md + +# Test for different markdowns +def g:Test_Markdown() + var tests: list<list<list<any>>> = [ + [ + # Different headings + # Input text + [ + '# First level heading', + '## Second level heading', + '### Third level heading', + '# Heading with leading and trailing whitespaces ', + 'Multiline setext heading ', + 'of level 1', + '===', + 'Multiline setext heading\', + 'of level 2', + '---' + ], + # Expected text + [ + 'First level heading', + '', + 'Second level heading', + '', + 'Third level heading', + '', + 'Heading with leading and trailing whitespaces', + '', + 'Multiline setext heading', + 'of level 1', + '', + 'Multiline setext heading', + 'of level 2' + ], + # Expected text properties + [ + [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 19}], + [], + [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 20}], + [], + [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 19}], + [], + [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 45}], + [], + [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 24}], + [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 10}], + [], + [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 24}], + [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 10}], + ] + ], + [ + # Bold text style + # Input text + [ + 'This **word** should be bold', + '', + '**This line should be bold**', + '', + 'This __word__ should be bold', + '', + '__This line should be bold__' + ], + # Expected text + [ + 'This word should be bold', + '', + 'This line should be bold', + '', + 'This word should be bold', + '', + 'This line should be bold' + ], + # Expected text properties + [ + [{'col': 6, 'type': 'LspMarkdownBold', 'length': 4}], + [], + [{'col': 1, 'type': 'LspMarkdownBold', 'length': 24}], + [], + [{'col': 6, 'type': 'LspMarkdownBold', 'length': 4}], + [], + [{'col': 1, 'type': 'LspMarkdownBold', 'length': 24}] + ] + ], + [ + # Italic text style + # Input text + [ + 'This *word* should be italic', + '', + '*This line should be italic*', + '', + 'This _word_ should be italic', + '', + '_This line should be italic_' + ], + # Expected text + [ + 'This word should be italic', + '', + 'This line should be italic', + '', + 'This word should be italic', + '', + 'This line should be italic' + ], + # Expected text properties + [ + [{'col': 6, 'type': 'LspMarkdownItalic', 'length': 4}], + [], + [{'col': 1, 'type': 'LspMarkdownItalic', 'length': 26}], + [], + [{'col': 6, 'type': 'LspMarkdownItalic', 'length': 4}], + [], + [{'col': 1, 'type': 'LspMarkdownItalic', 'length': 26}] + ], + ], + [ + # strikethrough text style + # Input text + [ + 'This ~word~ should be strikethrough', + '', + '~This line should be strikethrough~' + ], + # Expected text + [ + 'This word should be strikethrough', + '', + 'This line should be strikethrough' + ], + # Expected text properties + [ + [{'col': 6, 'type': 'LspMarkdownStrikeThrough', 'length': 4}], + [], + [{'col': 1, 'type': 'LspMarkdownStrikeThrough', 'length': 33}] + ] + ], + [ + # bold and nested italic text style + # Input text + [ + '**This _word_ should be bold and italic**', + ], + # Expected text + [ + 'This word should be bold and italic', + ], + # Expected text properties + [ + [ + {'col': 1, 'type': 'LspMarkdownBold', 'length': 35}, + {'col': 6, 'type': 'LspMarkdownItalic', 'length': 4} + ] + ] + ], + [ + # all bold and italic text style + # Input text + [ + '***This line should be all bold and italic***', + ], + # Expected text + [ + 'This line should be all bold and italic', + ], + # Expected text properties + [ + [ + {'col': 1, 'type': 'LspMarkdownItalic', 'length': 39}, + {'col': 1, 'type': 'LspMarkdownBold', 'length': 39} + ] + ] + ], + [ + # quoted text + # FIXME: The text is not quoted + # Input text + [ + 'Text that is not quoted', + '> quoted text' + ], + # Expected text + [ + 'Text that is not quoted', + '', + 'quoted text' + ], + # Expected text properties + [ + [], [], [] + ] + ], + [ + # line breaks + # Input text + [ + 'This paragraph contains ', + 'a soft line break', + '', + 'This paragraph contains ', + 'an hard line break', + '', + 'This paragraph contains an emphasis _before_\', + 'an hard line break', + '', + 'This paragraph contains an emphasis ', + '_after_ an hard line break', + '', + 'This paragraph _contains\', + 'an emphasis_ with an hard line break in the middle', + '', + '→ This paragraph contains an hard line break ', + 'and starts with the multibyte character "\u2192"', + '', + 'Line breaks `', + 'do\', + 'not ', + 'occur', + '` inside code spans' + ], + # Expected text + [ + 'This paragraph contains a soft line break', + '', + 'This paragraph contains', + 'an hard line break', + '', + 'This paragraph contains an emphasis before', + 'an hard line break', + '', + 'This paragraph contains an emphasis', + 'after an hard line break', + '', + 'This paragraph contains', + 'an emphasis with an hard line break in the middle', + '', + '→ This paragraph contains an hard line break', + 'and starts with the multibyte character "\u2192"', + '', + 'Line breaks do\ not occur inside code spans' + ], + # Expected text properties + [ + [], + [], + [], + [], + [], + [{'col': 37, 'type': 'LspMarkdownItalic', 'length': 6}], + [], + [], + [], + [{'col': 1, 'type': 'LspMarkdownItalic', 'length': 5}], + [], + [{'col': 16, 'type': 'LspMarkdownItalic', 'length': 8}], + [{'col': 1, 'type': 'LspMarkdownItalic', 'length': 11}], + [], + [], + [], + [], + [{'col': 13, 'type': 'LspMarkdownCode', 'length': 15}] + ] + ], + [ + # non-breaking space characters + # Input text + [ + ' This is text.', + ], + # Expected text + [ + ' This is text.', + ], + # Expected text properties + [ + [] + ] + ], + ] + + var doc: dict<list<any>> + var text_result: list<string> + var props_result: list<list<dict<any>>> + for t in tests + doc = md.ParseMarkdown(t[0]) + text_result = doc.content->deepcopy()->map((_, v) => v.text) + props_result = doc.content->deepcopy()->map((_, v) => v.props) + assert_equal(t[1], text_result, t[0]->string()) + assert_equal(t[2], props_result, t[0]->string()) + endfor +enddef + +# Only here to because the test runner needs it +def g:StartLangServer(): bool + return true +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/test/not_lspserver_related_tests.vim b/vim/pack/downloads/opt/lsp/test/not_lspserver_related_tests.vim new file mode 100644 index 0000000..57c7525 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/not_lspserver_related_tests.vim @@ -0,0 +1,14 @@ +vim9script +# Unit tests for Vim Language Server Protocol (LSP) for various functionality + +# Test for no duplicates in helptags +def g:Test_Helptags() + :helptags ../doc +enddef + +# Only here to because the test runner needs it +def g:StartLangServer(): bool + return true +enddef + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/test/run_tests.cmd b/vim/pack/downloads/opt/lsp/test/run_tests.cmd new file mode 100644 index 0000000..863d06d --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/run_tests.cmd @@ -0,0 +1,17 @@ +@echo off + +REM Script to run the unit-tests for the LSP Vim plugin on MS-Windows + +SETLOCAL +SET VIMPRG="vim.exe" +SET VIM_CMD=%VIMPRG% -u NONE -U NONE -i NONE --noplugin -N --not-a-term + +%VIM_CMD% -c "let g:TestName='clangd_tests.vim'" -S runner.vim + +echo LSP unit test results +type results.txt + +findstr /I FAIL results.txt > nul 2>&1 +if %ERRORLEVEL% EQU 0 echo ERROR: Some test failed. +if %ERRORLEVEL% NEQ 0 echo SUCCESS: All the tests passed. + diff --git a/vim/pack/downloads/opt/lsp/test/run_tests.sh b/vim/pack/downloads/opt/lsp/test/run_tests.sh new file mode 100755 index 0000000..1a1e69b --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/run_tests.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Script to run the unit-tests for the LSP Vim plugin + +VIMPRG=${VIMPRG:=$(which vim)} +if [ -z "$VIMPRG" ]; then + echo "ERROR: vim (\$VIMPRG) is not found in PATH" + exit 1 +fi + +VIM_CMD="$VIMPRG -u NONE -U NONE -i NONE --noplugin -N --not-a-term" + +TESTS="clangd_tests.vim tsserver_tests.vim gopls_tests.vim not_lspserver_related_tests.vim markdown_tests.vim rust_tests.vim" + +RunTestsInFile() { + testfile=$1 + echo "Running tests in $testfile" + $VIM_CMD -c "let g:TestName='$testfile'" -S runner.vim + + if ! [ -f results.txt ]; then + echo "ERROR: Test results file 'results.txt' is not found." + exit 2 + fi + + cat results.txt + + if grep -qw FAIL results.txt; then + echo "ERROR: Some test(s) in $testfile failed." + exit 3 + fi + + echo "SUCCESS: All the tests in $testfile passed." + echo +} + +for testfile in $TESTS +do + RunTestsInFile $testfile +done + +for encoding in "utf-8" "utf-16" "utf-32" +do + export LSP_OFFSET_ENCODING=$encoding + echo "LSP offset encoding: $LSP_OFFSET_ENCODING" + RunTestsInFile clangd_offsetencoding.vim +done + +echo "SUCCESS: All the tests passed." +exit 0 + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/test/runner.vim b/vim/pack/downloads/opt/lsp/test/runner.vim new file mode 100644 index 0000000..14c849a --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/runner.vim @@ -0,0 +1,55 @@ +vim9script +# Script to run a language server unit tests +# The global variable TestName should be set to the name of the file +# containing the tests. + +source common.vim + +def LspRunTests() + :set nomore + :set debug=beep + delete('results.txt') + + # Get the list of test functions in this file and call them + var fns: list<string> = execute('function /^Test_') + ->split("\n") + ->map("v:val->substitute('^def ', '', '')") + ->sort() + if fns->empty() + # No tests are found + writefile(['No tests are found'], 'results.txt') + return + endif + for f in fns + v:errors = [] + v:errmsg = '' + try + :%bw! + exe $'g:{f}' + catch + call add(v:errors, $'Error: Test {f} failed with exception {v:exception} at {v:throwpoint}') + endtry + if v:errmsg != '' + call add(v:errors, $'Error: Test {f} generated error {v:errmsg}') + endif + if !v:errors->empty() + writefile(v:errors, 'results.txt', 'a') + writefile([$'{f}: FAIL'], 'results.txt', 'a') + else + writefile([$'{f}: pass'], 'results.txt', 'a') + endif + endfor +enddef + +try + g:LoadLspPlugin() + exe $'source {g:TestName}' + g:StartLangServer() + LspRunTests() +catch + writefile(['FAIL: Tests in ' .. g:TestName .. ' failed with exception ' .. v:exception .. ' at ' .. v:throwpoint], 'results.txt', 'a') +endtry + +qall! + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/test/rust_tests.vim b/vim/pack/downloads/opt/lsp/test/rust_tests.vim new file mode 100644 index 0000000..6dc2277 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/rust_tests.vim @@ -0,0 +1,137 @@ +vim9script +# Unit tests for LSP rust-analyzer client + +source common.vim +source term_util.vim +source screendump.vim + +var lspServers = [{ + filetype: ['rust'], + path: exepath('rust-analyzer'), + args: [] + }] +call LspAddServer(lspServers) +echomsg systemlist($'{lspServers[0].path} --version') + +def g:Test_LspGotoDef() + settagstack(0, {items: []}) + :cd xrust_tests/src + try + silent! edit ./main.rs + deletebufline('%', 1, '$') + g:WaitForServerFileLoad(0) + var lines: list<string> =<< trim END + fn main() { + } + fn foo() { + } + fn bar() { + foo(); + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + cursor(6, 5) + :LspGotoDefinition + assert_equal([3, 4], [line('.'), col('.')]) + :%bw! + finally + :cd ../.. + endtry +enddef + +# Test for :LspCodeAction creating a file in the current directory +def g:Test_LspCodeAction_CreateFile() + :cd xrust_tests/src + try + silent! edit ./main.rs + deletebufline('%', 1, '$') + g:WaitForServerFileLoad(0) + var lines: list<string> =<< trim END + mod foo; + fn main() { + } + END + setline(1, lines) + g:WaitForServerFileLoad(1) + cursor(1, 1) + :LspCodeAction 1 + g:WaitForServerFileLoad(0) + assert_true(filereadable('foo.rs')) + :%bw! + delete('foo.rs') + finally + :cd ../.. + endtry +enddef + +# Test for :LspCodeAction creating a file in a subdirectory +def g:Test_LspCodeAction_CreateFile_Subdir() + :cd xrust_tests/src + try + silent! edit ./main.rs + deletebufline('%', 1, '$') + g:WaitForServerFileLoad(0) + var lines: list<string> =<< trim END + mod baz; + fn main() { + } + END + setline(1, lines) + g:WaitForServerFileLoad(1) + cursor(1, 1) + :LspCodeAction 2 + g:WaitForServerFileLoad(0) + assert_true(filereadable('baz/mod.rs')) + :%bw! + delete('baz', 'rf') + finally + :cd ../.. + endtry +enddef + +# Test for :LspCodeAction renaming a file +def g:Test_LspCodeAction_RenameFile() + :cd xrust_tests/src + try + silent! edit ./main.rs + deletebufline('%', 1, '$') + g:WaitForServerFileLoad(0) + writefile([], 'foobar.rs') + var lines: list<string> =<< trim END + mod foobar; + fn main() { + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + cursor(1, 5) + :LspRename foobaz + g:WaitForServerFileLoad(0) + assert_true(filereadable('foobaz.rs')) + :%bw! + delete('foobaz.rs') + finally + :cd ../.. + endtry +enddef + +def g:Test_ZZZ_Cleanup() + delete('./xrust_tests', 'rf') +enddef + +# Start the rust-analyzer language server. Returns true on success and false +# on failure. +def g:StartLangServer(): bool + system('cargo new xrust_tests') + :cd xrust_tests/src + var status = false + try + status = g:StartLangServerWithFile('./main.rs') + finally + :cd ../.. + endtry + return status +enddef + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/test/screendump.vim b/vim/pack/downloads/opt/lsp/test/screendump.vim new file mode 100644 index 0000000..68d3c3f --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/screendump.vim @@ -0,0 +1,117 @@ +" Functions shared by tests making screen dumps. + +source term_util.vim + +" Skip the rest if there is no terminal feature at all. +if !has('terminal') + finish +endif + +" Read a dump file "fname" and if "filter" exists apply it to the text. +def ReadAndFilter(fname: string, filter: string): list<string> + var contents = readfile(fname) + + if filereadable(filter) + # do this in the bottom window so that the terminal window is unaffected + wincmd j + enew + setline(1, contents) + exe "source " .. filter + contents = getline(1, '$') + enew! + wincmd k + redraw + endif + + return contents +enddef + + +" Verify that Vim running in terminal buffer "buf" matches the screen dump. +" "options" is passed to term_dumpwrite(). +" Additionally, the "wait" entry can specify the maximum time to wait for the +" screen dump to match in msec (default 1000 msec). +" The file name used is "dumps/{filename}.dump". +" +" To ignore part of the dump, provide a "dumps/{filename}.vim" file with +" Vim commands to be applied to both the reference and the current dump, so +" that parts that are irrelevant are not used for the comparison. The result +" is NOT written, thus "term_dumpdiff()" shows the difference anyway. +" +" Optionally an extra argument can be passed which is prepended to the error +" message. Use this when using the same dump file with different options. +" Returns non-zero when verification fails. +func VerifyScreenDump(buf, filename, options, ...) + let reference = 'dumps/' . a:filename . '.dump' + let filter = 'dumps/' . a:filename . '.vim' + let testfile = 'failed/' . a:filename . '.dump' + + let max_loops = get(a:options, 'wait', 1000) / 10 + + " Starting a terminal to make a screendump is always considered flaky. + let g:test_is_flaky = 1 + + " wait for the pending updates to be handled. + call TermWait(a:buf) + + " Redraw to execute the code that updates the screen. Otherwise we get the + " text and attributes only from the internal buffer. + redraw + + if filereadable(reference) + let refdump = ReadAndFilter(reference, filter) + else + " Must be a new screendump, always fail + let refdump = [] + endif + + let did_mkdir = 0 + if !isdirectory('failed') + let did_mkdir = 1 + call mkdir('failed') + endif + + let i = 0 + while 1 + " leave some time for updating the original window + sleep 10m + call delete(testfile) + call term_dumpwrite(a:buf, testfile, a:options) + let testdump = ReadAndFilter(testfile, filter) + if refdump == testdump + call delete(testfile) + if did_mkdir + call delete('failed', 'd') + endif + break + endif + if i == max_loops + " Leave the failed dump around for inspection. + if filereadable(reference) + let msg = 'See dump file difference: call term_dumpdiff("testdir/' .. testfile .. '", "testdir/' .. reference .. '")' + if a:0 == 1 + let msg = a:1 . ': ' . msg + endif + if len(testdump) != len(refdump) + let msg = msg . '; line count is ' . len(testdump) . ' instead of ' . len(refdump) + endif + else + let msg = 'See new dump file: call term_dumpload("testdir/' .. testfile .. '")' + " no point in retrying + let g:run_nr = 10 + endif + for i in range(len(refdump)) + if i >= len(testdump) + break + endif + if testdump[i] != refdump[i] + let msg = msg . '; difference in line ' . (i + 1) . ': "' . testdump[i] . '"' + endif + endfor + call assert_report(msg) + return 1 + endif + let i += 1 + endwhile + return 0 +endfunc diff --git a/vim/pack/downloads/opt/lsp/test/start_tsserver.vim b/vim/pack/downloads/opt/lsp/test/start_tsserver.vim new file mode 100644 index 0000000..3896c70 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/start_tsserver.vim @@ -0,0 +1,10 @@ +vim9script +source common.vim +g:LoadLspPlugin() +var lspServers = [{ +filetype: ['typescript', 'javascript'], + path: exepath('typescript-language-server'), + args: ['--stdio'] +}] +g:LspAddServer(lspServers) +g:StartLangServerWithFile('Xtest.ts') diff --git a/vim/pack/downloads/opt/lsp/test/term_util.vim b/vim/pack/downloads/opt/lsp/test/term_util.vim new file mode 100644 index 0000000..7b1779f --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/term_util.vim @@ -0,0 +1,125 @@ +" Functions about terminal shared by several tests + +" Wrapper around term_wait(). +" The second argument is the minimum time to wait in msec, 10 if omitted. +func TermWait(buf, ...) + let wait_time = a:0 ? a:1 : 10 + call term_wait(a:buf, wait_time) +endfunc + +" Run Vim with "arguments" in a new terminal window. +" By default uses a size of 20 lines and 75 columns. +" Returns the buffer number of the terminal. +" +" Options is a dictionary, these items are recognized: +" "keep_t_u7" - when 1 do not make t_u7 empty (resetting t_u7 avoids clearing +" parts of line 2 and 3 on the display) +" "rows" - height of the terminal window (max. 20) +" "cols" - width of the terminal window (max. 78) +" "statusoff" - number of lines the status is offset from default +" "wait_for_ruler" - if zero then don't wait for ruler to show +" "no_clean" - if non-zero then remove "--clean" from the command +func RunVimInTerminal(arguments, options) + " If Vim doesn't exit a swap file remains, causing other tests to fail. + " Remove it here. + call delete(".swp") + + if exists('$COLORFGBG') + " Clear $COLORFGBG to avoid 'background' being set to "dark", which will + " only be corrected if the response to t_RB is received, which may be too + " late. + let $COLORFGBG = '' + endif + + " Make a horizontal and vertical split, so that we can get exactly the right + " size terminal window. Works only when the current window is full width. + call assert_equal(&columns, winwidth(0)) + split + vsplit + + " Always do this with 256 colors and a light background. + set t_Co=256 background=light + hi Normal ctermfg=NONE ctermbg=NONE + + " Make the window 20 lines high and 75 columns, unless told otherwise or + " 'termwinsize' is set. + let rows = get(a:options, 'rows', 20) + let cols = get(a:options, 'cols', 75) + let statusoff = get(a:options, 'statusoff', 1) + + if get(a:options, 'keep_t_u7', 0) + let reset_u7 = '' + else + let reset_u7 = ' --cmd "set t_u7=" ' + endif + + let cmd = exepath('vim') .. ' -u NONE --clean --not-a-term --cmd "set enc=utf8"'.. reset_u7 .. a:arguments + + if get(a:options, 'no_clean', 0) + let cmd = substitute(cmd, '--clean', '', '') + endif + + let options = #{curwin: 1} + if &termwinsize == '' + let options.term_rows = rows + let options.term_cols = cols + endif + + " Accept other options whose name starts with 'term_'. + call extend(options, filter(copy(a:options), 'v:key =~# "^term_"')) + + let buf = term_start(cmd, options) + + if &termwinsize == '' + " in the GUI we may end up with a different size, try to set it. + if term_getsize(buf) != [rows, cols] + call term_setsize(buf, rows, cols) + endif + call assert_equal([rows, cols], term_getsize(buf)) + else + let rows = term_getsize(buf)[0] + let cols = term_getsize(buf)[1] + endif + + call TermWait(buf) + + if get(a:options, 'wait_for_ruler', 1) + " Wait for "All" or "Top" of the ruler to be shown in the last line or in + " the status line of the last window. This can be quite slow (e.g. when + " using valgrind). + " If it fails then show the terminal contents for debugging. + try + call g:WaitFor({-> len(term_getline(buf, rows)) >= cols - 1 || len(term_getline(buf, rows - statusoff)) >= cols - 1}) + catch /timed out after/ + let lines = map(range(1, rows), {key, val -> term_getline(buf, val)}) + call assert_report('RunVimInTerminal() failed, screen contents: ' . join(lines, "<NL>")) + endtry + endif + + return buf +endfunc + +" Stop a Vim running in terminal buffer "buf". +func StopVimInTerminal(buf, kill = 1) + call assert_equal("running", term_getstatus(a:buf)) + + " Wait for all the pending updates to terminal to complete + call TermWait(a:buf) + + " CTRL-O : works both in Normal mode and Insert mode to start a command line. + " In Command-line it's inserted, the CTRL-U removes it again. + call term_sendkeys(a:buf, "\<C-O>:\<C-U>qa!\<cr>") + + " Wait for all the pending updates to terminal to complete + call TermWait(a:buf) + + " Wait for the terminal to end. + call WaitForAssert({-> assert_equal("finished", term_getstatus(a:buf))}) + + " If the buffer still exists forcefully wipe it. + if a:kill && bufexists(a:buf) + exe a:buf .. 'bwipe!' + endif +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/vim/pack/downloads/opt/lsp/test/tsserver_tests.vim b/vim/pack/downloads/opt/lsp/test/tsserver_tests.vim new file mode 100644 index 0000000..99e2836 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/test/tsserver_tests.vim @@ -0,0 +1,43 @@ +vim9script +# Unit tests for Vim Language Server Protocol (LSP) typescript client + +source common.vim +source term_util.vim +source screendump.vim + +var lspServers = [{ + filetype: ['typescript', 'javascript'], + path: exepath('typescript-language-server'), + args: ['--stdio'] + }] +call LspAddServer(lspServers) +echomsg systemlist($'{lspServers[0].path} --version') + +# Test for auto-completion. Make sure that only keywords that matches with the +# keyword before the cursor are shown. +# def g:Test_LspCompletion1() +# var lines =<< trim END +# const http = require('http') +# http.cr +# END +# writefile(lines, 'Xcompletion1.js') +# var buf = g:RunVimInTerminal('--cmd "silent so start_tsserver.vim" Xcompletion1.js', {rows: 10, wait_for_ruler: 1}) +# sleep 5 +# term_sendkeys(buf, "GAe") +# g:TermWait(buf) +# g:VerifyScreenDump(buf, 'Test_tsserver_completion_1', {}) +# term_sendkeys(buf, "\<BS>") +# g:TermWait(buf) +# g:VerifyScreenDump(buf, 'Test_tsserver_completion_2', {}) +# +# g:StopVimInTerminal(buf) +# delete('Xcompletion1.js') +# enddef + +# Start the typescript language server. Returns true on success and false on +# failure. +def g:StartLangServer(): bool + return g:StartLangServerWithFile('Xtest.ts') +enddef + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/snippets/plugin/snippets.vim b/vim/pack/downloads/opt/snippets/plugin/snippets.vim new file mode 120000 index 0000000..1b5db26 --- /dev/null +++ b/vim/pack/downloads/opt/snippets/plugin/snippets.vim @@ -0,0 +1 @@ +/home/mccd/.dotfiles/vim/pack/downloads/opt/snippets/snippets.vim
\ No newline at end of file |