├── .github └── workflows │ └── unitests.yml ├── .gitignore ├── LICENSE ├── README.md ├── autoload └── lsp │ ├── buffer.vim │ ├── callhierarchy.vim │ ├── capabilities.vim │ ├── codeaction.vim │ ├── codelens.vim │ ├── completion.vim │ ├── diag.vim │ ├── handlers.vim │ ├── hover.vim │ ├── inlayhints.vim │ ├── lsp.vim │ ├── lspserver.vim │ ├── markdown.vim │ ├── offset.vim │ ├── options.vim │ ├── outline.vim │ ├── selection.vim │ ├── semantichighlight.vim │ ├── signature.vim │ ├── snippet.vim │ ├── symbol.vim │ ├── textedit.vim │ ├── typehierarchy.vim │ └── util.vim ├── doc ├── configs.md └── lsp.txt ├── ftplugin └── lspgfm.vim ├── plugin └── lsp.vim ├── syntax └── lspgfm.vim └── test ├── clangd_offsetencoding.vim ├── clangd_tests.vim ├── common.vim ├── dumps ├── Test_tsserver_completion_1.dump └── Test_tsserver_completion_2.dump ├── gopls_tests.vim ├── markdown_tests.vim ├── not_lspserver_related_tests.vim ├── run_tests.cmd ├── run_tests.sh ├── runner.vim ├── rust_tests.vim ├── screendump.vim ├── start_tsserver.vim ├── term_util.vim └── tsserver_tests.vim /.github/workflows/unitests.yml: -------------------------------------------------------------------------------- 1 | name: unit-tests 2 | on: [push, pull_request] 3 | jobs: 4 | linux: 5 | name: linux 6 | runs-on: ubuntu-22.04 7 | strategy: 8 | matrix: 9 | vim: 10 | - nightly 11 | - v9.0.0000 12 | steps: 13 | - name: Install packages 14 | run: | 15 | sudo apt update 16 | # install clangd language server 17 | sudo apt install -y clangd-15 18 | # install nodejs 19 | sudo apt install -y curl 20 | curl -fsSL https://deb.nodesource.com/setup_18.x | sudo bash - 21 | sudo apt install -y nodejs 22 | # install the typescript language server 23 | sudo npm install -g typescript-language-server typescript 24 | # install the golang language server 25 | sudo apt install -y golang 26 | sudo apt install -y gopls 27 | # install the rust language server 28 | sudo apt install -y cargo rust-src 29 | mkdir -p ~/.local/bin 30 | # Use version v0.3.2011 as the later versions fail the tests 31 | curl -L https://github.com/rust-lang/rust-analyzer/releases/download/2024-06-24/rust-analyzer-x86_64-unknown-linux-gnu.gz | gunzip -c - > ~/.local/bin/rust-analyzer 32 | chmod +x ~/.local/bin/rust-analyzer 33 | - name: Setup Vim 34 | uses: rhysd/action-setup-vim@v1 35 | id: vim 36 | with: 37 | version: ${{ matrix.vim }} 38 | - name: Checkout LSP plugin Code 39 | uses: actions/checkout@v4 40 | - name: Run Tests 41 | run: | 42 | uname -a 43 | ~/.local/bin/rust-analyzer --version 44 | export VIMPRG=${{ steps.vim.outputs.executable }} 45 | $VIMPRG --version 46 | cd test 47 | ./run_tests.sh 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | *.swp 3 | 4 | test/X*.c 5 | test/X*.cpp 6 | # test/Xtest.c 7 | # test/Xtest.cpp 8 | test/results.txt 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Yegappan Lakshmanan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![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) 2 | 3 | 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. 4 | 5 | ## Installation 6 | 7 | You can install this plugin directly from github using the following steps: 8 | 9 | ```bash 10 | mkdir -p $HOME/.vim/pack/downloads/opt 11 | cd $HOME/.vim/pack/downloads/opt 12 | git clone https://github.com/yegappan/lsp 13 | vim -u NONE -c "helptags $HOME/.vim/pack/downloads/opt/lsp/doc" -c q 14 | ``` 15 | 16 | After installing the plugin using the above steps, add the following line to 17 | your $HOME/.vimrc file: 18 | 19 | ```viml 20 | packadd lsp 21 | ``` 22 | 23 | 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.). 24 | 25 | 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. 26 | 27 | ## Features 28 | 29 | The following language server protocol (LSP) features are supported: 30 | 31 | * Code completion 32 | * Jump to definition, declaration, implementation, type definition 33 | * Peek definition, declaration, implementation, type definition and references 34 | * Display warning and error diagnostics 35 | * Find all symbol references 36 | * Document and Workspace symbol search 37 | * Display code outline 38 | * Rename symbol 39 | * Display type and documentation on hover 40 | * Signature help 41 | * Code action 42 | * Display Call hierarchy 43 | * Display Type hierarchy 44 | * Highlight current symbol references 45 | * Formatting code 46 | * Folding code 47 | * Inlay hints 48 | * Visually select symbol block/region 49 | * Semantic Highlight 50 | 51 | ## Configuration 52 | 53 | To use the plugin features with a particular file type(s), you need to first register a LSP server for that file type(s). 54 | 55 | The LSP servers are registered using the `LspAddServer()` function. This function accepts a list of LSP servers. 56 | 57 | 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. 58 | ```viml 59 | 60 | " Clangd language server 61 | call LspAddServer([#{ 62 | \ name: 'clangd', 63 | \ filetype: ['c', 'cpp'], 64 | \ path: '/usr/local/bin/clangd', 65 | \ args: ['--background-index'] 66 | \ }]) 67 | 68 | " Javascript/Typescript language server 69 | call LspAddServer([#{ 70 | \ name: 'typescriptlang', 71 | \ filetype: ['javascript', 'typescript'], 72 | \ path: '/usr/local/bin/typescript-language-server', 73 | \ args: ['--stdio'], 74 | \ }]) 75 | 76 | " Go language server 77 | call LspAddServer([#{ 78 | \ name: 'golang', 79 | \ filetype: ['go', 'gomod'], 80 | \ path: '/usr/local/bin/gopls', 81 | \ args: ['serve'], 82 | \ syncInit: v:true 83 | \ }]) 84 | 85 | " Rust language server 86 | call LspAddServer([#{ 87 | \ name: 'rustlang', 88 | \ filetype: ['rust'], 89 | \ path: '/usr/local/bin/rust-analyzer', 90 | \ args: [], 91 | \ syncInit: v:true 92 | \ }]) 93 | ``` 94 | 95 | The above lines register the language servers for C/C++, Javascript/Typescript, Go and Rust file types. 96 | Refer to [Configs.md](https://github.com/yegappan/lsp/blob/main/doc/configs.md) for various language server specific configuration. 97 | 98 | To register a LSP server, the following information is needed: 99 | 100 | Field|Description 101 | -----|----------- 102 | `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. 103 | `path`|complete path to the LSP server executable (without any arguments). 104 | `args`|a list of command-line arguments passed to the LSP server. Each argument is a separate List item. 105 | `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. 106 | `customNotificationHandlers`|A dictionary of notifications and functions that can be specified to add support for custom language server notifications. 107 | `customRequestHandlers`|A dictionary of request handlers and functions that can be specified to add support for custom language server requests replies. 108 | `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. 109 | 110 | The `LspAddServer()` function accepts a list of LSP servers with the above information. 111 | 112 | Some of the LSP plugin features can be enabled or disabled by using the `LspOptionsSet()` function, detailed in `:help lsp-options`. 113 | Here is an example of configuration with default values: 114 | ```viml 115 | call LspOptionsSet(#{ 116 | \ aleSupport: v:false, 117 | \ autoComplete: v:true, 118 | \ autoHighlight: v:false, 119 | \ autoHighlightDiags: v:true, 120 | \ autoPopulateDiags: v:false, 121 | \ completionMatcher: 'case', 122 | \ completionMatcherValue: 1, 123 | \ diagSignErrorText: 'E>', 124 | \ diagSignHintText: 'H>', 125 | \ diagSignInfoText: 'I>', 126 | \ diagSignWarningText: 'W>', 127 | \ echoSignature: v:false, 128 | \ hideDisabledCodeActions: v:false, 129 | \ highlightDiagInline: v:true, 130 | \ hoverInPreview: v:false, 131 | \ ignoreMissingServer: v:false, 132 | \ keepFocusInDiags: v:true, 133 | \ keepFocusInReferences: v:true, 134 | \ completionTextEdit: v:true, 135 | \ diagVirtualTextAlign: 'above', 136 | \ diagVirtualTextWrap: 'default', 137 | \ noNewlineInCompletion: v:false, 138 | \ omniComplete: v:null, 139 | \ outlineOnRight: v:false, 140 | \ outlineWinSize: 20, 141 | \ popupBorder: v:true, 142 | \ popupBorderHighlight: 'Title', 143 | \ popupBorderHighlightPeek: 'Special', 144 | \ popupBorderSignatureHelp: v:false, 145 | \ popupHighlightSignatureHelp: 'Pmenu;, 146 | \ popupHighlight: 'Normal', 147 | \ semanticHighlight: v:true, 148 | \ showDiagInBalloon: v:true, 149 | \ showDiagInPopup: v:true, 150 | \ showDiagOnStatusLine: v:false, 151 | \ showDiagWithSign: v:true, 152 | \ showDiagWithVirtualText: v:false, 153 | \ showInlayHints: v:false, 154 | \ showSignature: v:true, 155 | \ snippetSupport: v:false, 156 | \ ultisnipsSupport: v:false, 157 | \ useBufferCompletion: v:false, 158 | \ usePopupInCodeAction: v:false, 159 | \ useQuickfixForLocations: v:false, 160 | \ vsnipSupport: v:false, 161 | \ bufferCompletionTimeout: 100, 162 | \ customCompletionKinds: v:false, 163 | \ completionKinds: {}, 164 | \ filterCompletionDuplicates: v:false, 165 | \ condensedCompletionMenu: v:false, 166 | \ }) 167 | ``` 168 | 169 | 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: 170 | ```viml 171 | let lspOpts = #{autoHighlightDiags: v:true} 172 | autocmd User LspSetup call LspOptionsSet(lspOpts) 173 | 174 | let lspServers = [#{ 175 | \ name: 'clang', 176 | \ filetype: ['c', 'cpp'], 177 | \ path: '/usr/local/bin/clangd', 178 | \ args: ['--background-index'] 179 | \ }] 180 | autocmd User LspSetup call LspAddServer(lspServers) 181 | ``` 182 | 183 | ## Supported Commands 184 | 185 | The following commands are provided to use the LSP features. 186 | 187 | Command|Description 188 | -------|----------- 189 | `:LspCodeAction`|Apply the code action supplied by the language server to the diagnostic in the current line. 190 | `:LspCodeLens`|Display a list of code lens commands and apply a selected code lens command to the current file. 191 | `:LspDiag current`|Display the diagnostic message for the current line. 192 | `:LspDiag first`|Jump to the first diagnostic message for the current buffer. 193 | `:LspDiag here`|Jump to the next diagnostic message in the current line. 194 | `:LspDiag highlight disable`|Disable diagnostic message highlights. 195 | `:LspDiag highlight enable`|Enable diagnostic message highlights. 196 | `:LspDiag next`|Jump to the next diagnostic message after the current position. 197 | `:LspDiag nextWrap`|Jump to the next diagnostic message after the current position, wrapping to the first message when the last message is reached. 198 | `:LspDiag prev`|Jump to the previous diagnostic message before the current position. 199 | `:LspDiag prevWrap`|Jump to the previous diagnostic message before the current position, wrapping to the last message when the first message is reached. 200 | `:LspDiag show`|Display the diagnostics messages from the language server for the current buffer in a new location list. 201 | `:LspDocumentSymbol`|Display the symbols in the current file in a popup menu and jump to the selected symbol. 202 | `:LspFold`|Fold the current file. 203 | `: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. 204 | `:LspGotoDeclaration`|Go to the declaration of the keyword under cursor. 205 | `:LspGotoDefinition`|Go to the definition of the keyword under cursor. 206 | `:LspGotoImpl`|Go to the implementation of the keyword under cursor. 207 | `:LspGotoTypeDef`|Go to the type definition of the keyword under cursor. 208 | `:LspHighlight`|Highlight all the matches for the keyword under cursor. 209 | `:LspHighlightClear`|Clear all the matches highlighted by :LspHighlight. 210 | `:LspHover`|Show the documentation for the symbol under the cursor in a popup window. 211 | `:LspIncomingCalls`|Display the list of symbols calling the current symbol. 212 | `:LspOutgoingCalls`|Display the list of symbols called by the current symbol. 213 | `:LspOutline`|Show the list of symbols defined in the current file in a separate window. 214 | `:LspPeekDeclaration`|Open the declaration of the symbol under cursor in the preview window. 215 | `:LspPeekDefinition`|Open the definition of the symbol under cursor in the preview window. 216 | `:LspPeekImpl`|Open the implementation of the symbol under cursor in the preview window. 217 | `:LspPeekReferences`|Display the list of references to the keyword under cursor in a location list associated with the preview window. 218 | `:LspPeekTypeDef`|Open the type definition of the symbol under cursor in the preview window. 219 | `:LspRename`|Rename the current symbol. 220 | `:LspSelectionExpand`|Expand the current symbol range visual selection. 221 | `:LspSelectionShrink`|Shrink the current symbol range visual selection. 222 | `:LspShowAllServers`|Display information about all the registered language servers. 223 | `:LspServer`|Display the capabilities or messages or status of the language server for the current buffer or restart the server. 224 | `:LspShowReferences`|Display the list of references to the keyword under cursor in a new location list. 225 | `:LspShowSignature`|Display the signature of the keyword under cursor. 226 | `:LspSubTypeHierarchy`|Display the sub type hierarchy in a popup window. 227 | `:LspSuperTypeHierarchy`|Display the super type hierarchy in a popup window. 228 | `:LspSwitchSourceHeader`|Switch between a source and a header file. 229 | `:LspSymbolSearch`|Perform a workspace wide search for a symbol. 230 | `:LspWorkspaceAddFolder {folder}`| Add a folder to the workspace. 231 | `:LspWorkspaceListFolders`|Show the list of folders in the workspace. 232 | `:LspWorkspaceRemoveFolder {folder}`|Remove a folder from the workspace. 233 | 234 | ## Similar Vim LSP Plugins 235 | 236 | 1. [vim-lsp: Async Language Server Protocol](https://github.com/prabirshrestha/vim-lsp) 237 | 1. [Coc: Conquer of Completion](https://github.com/neoclide/coc.nvim) 238 | 1. [vim-lsc: Vim Language Server Client](https://github.com/natebosch/vim-lsc) 239 | 1. [LanguageClient-neovim](https://github.com/autozimu/LanguageClient-neovim) 240 | 1. [ALE: Asynchronous Lint Engine](https://github.com/dense-analysis/ale) 241 | 1. [Neovim built-in LSP client](https://neovim.io/doc/user/lsp.html) 242 | 2. [Omnisharp LSP client](https://github.com/OmniSharp/omnisharp-vim) 243 | -------------------------------------------------------------------------------- /autoload/lsp/buffer.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Functions for managing the per-buffer LSP server information 4 | 5 | import './util.vim' 6 | 7 | # A buffer can have one or more attached language servers. The 8 | # "bufnrToServers" Dict contains the list of language servers attached to a 9 | # buffer. The buffer number is the key for the "bufnrToServers" Dict. The 10 | # value is the List of attached language servers. 11 | var bufnrToServers: dict>> = {} 12 | 13 | # Add "lspserver" to "bufnrToServers" map for buffer "bnr". 14 | export def BufLspServerSet(bnr: number, lspserver: dict) 15 | if !bufnrToServers->has_key(bnr) 16 | bufnrToServers[bnr] = [] 17 | endif 18 | 19 | bufnrToServers[bnr]->add(lspserver) 20 | enddef 21 | 22 | # Remove "lspserver" from "bufnrToServers" map for buffer "bnr". 23 | export def BufLspServerRemove(bnr: number, lspserver: dict) 24 | if !bufnrToServers->has_key(bnr) 25 | return 26 | endif 27 | 28 | var servers: list> = bufnrToServers[bnr] 29 | servers = servers->filter((key, srv) => srv.id != lspserver.id) 30 | 31 | if servers->empty() 32 | bufnrToServers->remove(bnr) 33 | else 34 | bufnrToServers[bnr] = servers 35 | endif 36 | enddef 37 | 38 | var SupportedCheckFns = { 39 | callHierarchy: (lspserver) => lspserver.isCallHierarchyProvider, 40 | codeAction: (lspserver) => lspserver.isCodeActionProvider, 41 | codeLens: (lspserver) => lspserver.isCodeLensProvider, 42 | completion: (lspserver) => lspserver.isCompletionProvider, 43 | declaration: (lspserver) => lspserver.isDeclarationProvider, 44 | definition: (lspserver) => lspserver.isDefinitionProvider, 45 | documentFormatting: (lspserver) => lspserver.isDocumentFormattingProvider, 46 | documentHighlight: (lspserver) => lspserver.isDocumentHighlightProvider, 47 | documentSymbol: (lspserver) => lspserver.isDocumentSymbolProvider, 48 | foldingRange: (lspserver) => lspserver.isFoldingRangeProvider, 49 | hover: (lspserver) => lspserver.isHoverProvider, 50 | implementation: (lspserver) => lspserver.isImplementationProvider, 51 | inlayHint: (lspserver) => lspserver.isInlayHintProvider || 52 | lspserver.isClangdInlayHintsProvider, 53 | references: (lspserver) => lspserver.isReferencesProvider, 54 | rename: (lspserver) => lspserver.isRenameProvider, 55 | selectionRange: (lspserver) => lspserver.isSelectionRangeProvider, 56 | semanticTokens: (lspserver) => lspserver.isSemanticTokensProvider, 57 | signatureHelp: (lspserver) => lspserver.isSignatureHelpProvider, 58 | typeDefinition: (lspserver) => lspserver.isTypeDefinitionProvider, 59 | typeHierarchy: (lspserver) => lspserver.isTypeHierarchyProvider, 60 | workspaceSymbol: (lspserver) => lspserver.isWorkspaceSymbolProvider 61 | } 62 | 63 | # Returns the LSP server for the buffer "bnr". If "feature" is specified, 64 | # then returns the LSP server that provides the "feature". 65 | # Returns an empty dict if the server is not found. 66 | export def BufLspServerGet(bnr: number, feature: string = null_string): dict 67 | if !bufnrToServers->has_key(bnr) 68 | return {} 69 | endif 70 | 71 | if bufnrToServers[bnr]->empty() 72 | return {} 73 | endif 74 | 75 | if feature == null_string 76 | return bufnrToServers[bnr][0] 77 | endif 78 | 79 | if !SupportedCheckFns->has_key(feature) 80 | # If this happns it is a programming error, and should be fixed in the 81 | # source code 82 | :throw $'Error: ''{feature}'' is not a valid feature' 83 | endif 84 | 85 | var SupportedCheckFn = SupportedCheckFns[feature] 86 | 87 | var possibleLSPs: list> = [] 88 | 89 | for lspserver in bufnrToServers[bnr] 90 | if !lspserver.ready || !SupportedCheckFn(lspserver) 91 | continue 92 | endif 93 | 94 | possibleLSPs->add(lspserver) 95 | endfor 96 | 97 | if possibleLSPs->empty() 98 | return {} 99 | endif 100 | 101 | # LSP server is configured to be a provider for "feature" 102 | for lspserver in possibleLSPs 103 | var has_feature: bool = lspserver.features->get(feature, false) 104 | if has_feature 105 | return lspserver 106 | endif 107 | endfor 108 | 109 | # Return the first LSP server that supports "feature" and doesn't have it 110 | # disabled 111 | for lspserver in possibleLSPs 112 | if lspserver.featureEnabled(feature) 113 | return lspserver 114 | endif 115 | endfor 116 | 117 | return {} 118 | enddef 119 | 120 | # Returns the LSP server for the buffer "bnr" and with ID "id". Returns an empty 121 | # dict if the server is not found. 122 | export def BufLspServerGetById(bnr: number, id: number): dict 123 | if !bufnrToServers->has_key(bnr) 124 | return {} 125 | endif 126 | 127 | for lspserver in bufnrToServers[bnr] 128 | if lspserver.id == id 129 | return lspserver 130 | endif 131 | endfor 132 | 133 | return {} 134 | enddef 135 | 136 | # Returns the LSP servers for the buffer "bnr". Returns an empty list if the 137 | # servers are not found. 138 | export def BufLspServersGet(bnr: number): list> 139 | if !bufnrToServers->has_key(bnr) 140 | return [] 141 | endif 142 | 143 | return bufnrToServers[bnr] 144 | enddef 145 | 146 | # Returns the LSP server for the current buffer with the optionally "feature". 147 | # Returns an empty dict if the server is not found. 148 | export def CurbufGetServer(feature: string = null_string): dict 149 | return BufLspServerGet(bufnr(), feature) 150 | enddef 151 | 152 | # Returns the LSP servers for the current buffer. Returns an empty list if the 153 | # servers are not found. 154 | export def CurbufGetServers(): list> 155 | return BufLspServersGet(bufnr()) 156 | enddef 157 | 158 | export def BufHasLspServer(bnr: number): bool 159 | var lspserver = BufLspServerGet(bnr) 160 | 161 | return !lspserver->empty() 162 | enddef 163 | 164 | # Returns the LSP server for the current buffer with the optinally "feature" if 165 | # it is running and is ready. 166 | # Returns an empty dict if the server is not found or is not ready. 167 | export def CurbufGetServerChecked(feature: string = null_string): dict 168 | var fname: string = @% 169 | if fname->empty() || &filetype->empty() 170 | return {} 171 | endif 172 | 173 | var lspserver: dict = CurbufGetServer(feature) 174 | if lspserver->empty() 175 | if feature == null_string 176 | util.ErrMsg($'Language server for "{&filetype}" file type is not found') 177 | else 178 | util.ErrMsg($'Language server for "{&filetype}" file type supporting "{feature}" feature is not found') 179 | endif 180 | return {} 181 | endif 182 | if !lspserver.running 183 | util.ErrMsg($'Language server for "{&filetype}" file type is not running') 184 | return {} 185 | endif 186 | if !lspserver.ready 187 | util.ErrMsg($'Language server for "{&filetype}" file type is not ready') 188 | return {} 189 | endif 190 | 191 | return lspserver 192 | enddef 193 | 194 | export def CurbufGetServerByName(name: string): dict 195 | var lspservers: list> = CurbufGetServers() 196 | 197 | for lspserver in lspservers 198 | if lspserver.name == name 199 | return lspserver 200 | endif 201 | endfor 202 | return {} 203 | enddef 204 | 205 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 206 | -------------------------------------------------------------------------------- /autoload/lsp/callhierarchy.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Functions for dealing with call hierarchy (incoming/outgoing calls) 4 | 5 | import './util.vim' 6 | import './buffer.vim' as buf 7 | 8 | # Jump to the location of the symbol under the cursor in the call hierarchy 9 | # tree window. 10 | def CallHierarchyItemJump() 11 | var item: dict = w:LspCallHierItemMap[line('.')].item 12 | util.JumpToLspLocation(item, '') 13 | enddef 14 | 15 | # Refresh the call hierarchy tree for the symbol at index "idx". 16 | def CallHierarchyTreeItemRefresh(idx: number) 17 | var treeItem: dict = w:LspCallHierItemMap[idx] 18 | 19 | if treeItem.open 20 | # Already retrieved the children for this item 21 | return 22 | endif 23 | 24 | if !treeItem->has_key('children') 25 | # First time retrieving the children for the item at index "idx" 26 | var lspserver = buf.BufLspServerGet(w:LspBufnr, 'callHierarchy') 27 | if lspserver->empty() || !lspserver.running 28 | return 29 | endif 30 | 31 | var reply: any 32 | if w:LspCallHierIncoming 33 | reply = lspserver.getIncomingCalls(treeItem.item) 34 | else 35 | reply = lspserver.getOutgoingCalls(treeItem.item) 36 | endif 37 | 38 | treeItem.children = [] 39 | if !reply->empty() 40 | for item in reply 41 | treeItem.children->add({item: w:LspCallHierIncoming ? item.from : 42 | item.to, open: false}) 43 | endfor 44 | endif 45 | endif 46 | 47 | # Clear and redisplay the tree in the window 48 | treeItem.open = true 49 | var save_cursor = getcurpos() 50 | CallHierarchyTreeRefresh() 51 | setpos('.', save_cursor) 52 | enddef 53 | 54 | # Open the call hierarchy tree item under the cursor 55 | def CallHierarchyTreeItemOpen() 56 | CallHierarchyTreeItemRefresh(line('.')) 57 | enddef 58 | 59 | # Refresh the entire call hierarchy tree 60 | def CallHierarchyTreeRefreshCmd() 61 | w:LspCallHierItemMap[2].open = false 62 | w:LspCallHierItemMap[2]->remove('children') 63 | CallHierarchyTreeItemRefresh(2) 64 | enddef 65 | 66 | # Display the incoming call hierarchy tree 67 | def CallHierarchyTreeIncomingCmd() 68 | w:LspCallHierItemMap[2].open = false 69 | w:LspCallHierItemMap[2]->remove('children') 70 | w:LspCallHierIncoming = true 71 | CallHierarchyTreeItemRefresh(2) 72 | enddef 73 | 74 | # Display the outgoing call hierarchy tree 75 | def CallHierarchyTreeOutgoingCmd() 76 | w:LspCallHierItemMap[2].open = false 77 | w:LspCallHierItemMap[2]->remove('children') 78 | w:LspCallHierIncoming = false 79 | CallHierarchyTreeItemRefresh(2) 80 | enddef 81 | 82 | # Close the call hierarchy tree item under the cursor 83 | def CallHierarchyTreeItemClose() 84 | var treeItem: dict = w:LspCallHierItemMap[line('.')] 85 | treeItem.open = false 86 | var save_cursor = getcurpos() 87 | CallHierarchyTreeRefresh() 88 | setpos('.', save_cursor) 89 | enddef 90 | 91 | # Recursively add the call hierarchy items to w:LspCallHierItemMap 92 | def CallHierarchyTreeItemShow(incoming: bool, treeItem: dict, pfx: string) 93 | var item = treeItem.item 94 | var treePfx: string 95 | if treeItem.open && treeItem->has_key('children') 96 | treePfx = has('gui_running') ? '▼' : '-' 97 | else 98 | treePfx = has('gui_running') ? '▶' : '+' 99 | endif 100 | var fname = util.LspUriToFile(item.uri) 101 | var s = $'{pfx}{treePfx} {item.name} ({fname->fnamemodify(":t")} [{fname->fnamemodify(":h")}])' 102 | append('$', s) 103 | w:LspCallHierItemMap->add(treeItem) 104 | if treeItem.open && treeItem->has_key('children') 105 | for child in treeItem.children 106 | CallHierarchyTreeItemShow(incoming, child, $'{pfx} ') 107 | endfor 108 | endif 109 | enddef 110 | 111 | def CallHierarchyTreeRefresh() 112 | :setlocal modifiable 113 | :silent! :%d _ 114 | 115 | setline(1, $'# {w:LspCallHierIncoming ? "Incoming calls to" : "Outgoing calls from"} "{w:LspCallHierarchyTree.item.name}"') 116 | w:LspCallHierItemMap = [{}, {}] 117 | CallHierarchyTreeItemShow(w:LspCallHierIncoming, w:LspCallHierarchyTree, '') 118 | :setlocal nomodifiable 119 | enddef 120 | 121 | def CallHierarchyTreeShow(incoming: bool, prepareItem: dict, 122 | items: list>) 123 | var save_bufnr = bufnr() 124 | var wid = bufwinid('LSP-CallHierarchy') 125 | if wid != -1 126 | wid->win_gotoid() 127 | else 128 | :new LSP-CallHierarchy 129 | :setlocal buftype=nofile 130 | :setlocal bufhidden=wipe 131 | :setlocal noswapfile 132 | :setlocal nonumber nornu 133 | :setlocal fdc=0 signcolumn=no 134 | 135 | :nnoremap CallHierarchyItemJump() 136 | :nnoremap - CallHierarchyTreeItemOpen() 137 | :nnoremap + CallHierarchyTreeItemClose() 138 | :command -buffer LspCallHierarchyRefresh CallHierarchyTreeRefreshCmd() 139 | :command -buffer LspCallHierarchyIncoming CallHierarchyTreeIncomingCmd() 140 | :command -buffer LspCallHierarchyOutgoing CallHierarchyTreeOutgoingCmd() 141 | 142 | :syntax match Comment '^#.*$' 143 | :syntax match Directory '(.*)$' 144 | endif 145 | 146 | w:LspBufnr = save_bufnr 147 | w:LspCallHierIncoming = incoming 148 | w:LspCallHierarchyTree = {} 149 | w:LspCallHierarchyTree.item = prepareItem 150 | w:LspCallHierarchyTree.open = true 151 | w:LspCallHierarchyTree.children = [] 152 | for item in items 153 | w:LspCallHierarchyTree.children->add({item: incoming ? item.from : item.to, open: false}) 154 | endfor 155 | 156 | CallHierarchyTreeRefresh() 157 | 158 | :setlocal nomodified 159 | :setlocal nomodifiable 160 | enddef 161 | 162 | export def IncomingCalls(lspserver: dict) 163 | var prepareReply = lspserver.prepareCallHierarchy() 164 | if prepareReply->empty() 165 | util.WarnMsg('No incoming calls') 166 | return 167 | endif 168 | 169 | var reply = lspserver.getIncomingCalls(prepareReply) 170 | if reply->empty() 171 | util.WarnMsg('No incoming calls') 172 | return 173 | endif 174 | 175 | CallHierarchyTreeShow(true, prepareReply, reply) 176 | enddef 177 | 178 | export def OutgoingCalls(lspserver: dict) 179 | var prepareReply = lspserver.prepareCallHierarchy() 180 | if prepareReply->empty() 181 | util.WarnMsg('No outgoing calls') 182 | return 183 | endif 184 | 185 | var reply = lspserver.getOutgoingCalls(prepareReply) 186 | if reply->empty() 187 | util.WarnMsg('No outgoing calls') 188 | return 189 | endif 190 | 191 | CallHierarchyTreeShow(false, prepareReply, reply) 192 | enddef 193 | 194 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 195 | -------------------------------------------------------------------------------- /autoload/lsp/capabilities.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Functions for managing the LSP server and client capabilities 4 | 5 | import './options.vim' as opt 6 | 7 | # Process the server capabilities 8 | # interface ServerCapabilities 9 | export def ProcessServerCaps(lspserver: dict, caps: dict) 10 | var serverEncoding = 'utf-16' 11 | if lspserver.caps->has_key('positionEncoding') 12 | serverEncoding = lspserver.caps.positionEncoding 13 | elseif lspserver.caps->has_key('~additionalInitResult_offsetEncoding') 14 | serverEncoding = lspserver.caps['~additionalInitResult_offsetEncoding'] 15 | endif 16 | 17 | if lspserver.forceOffsetEncoding != '' 18 | serverEncoding = lspserver.forceOffsetEncoding 19 | endif 20 | 21 | # one of 'utf-8', 'utf-16' or 'utf-32' 22 | if serverEncoding == 'utf-8' 23 | lspserver.posEncoding = 8 24 | elseif serverEncoding == 'utf-16' 25 | lspserver.posEncoding = 16 26 | else 27 | lspserver.posEncoding = 32 28 | endif 29 | 30 | if has('patch-9.0.1629') && lspserver.posEncoding != 32 31 | lspserver.needOffsetEncoding = true 32 | else 33 | lspserver.needOffsetEncoding = false 34 | endif 35 | 36 | # completionProvider 37 | if lspserver.caps->has_key('completionProvider') 38 | lspserver.isCompletionProvider = true 39 | if lspserver.caps.completionProvider->has_key('resolveProvider') 40 | lspserver.isCompletionResolveProvider = 41 | lspserver.caps.completionProvider.resolveProvider 42 | else 43 | lspserver.isCompletionResolveProvider = false 44 | endif 45 | else 46 | lspserver.isCompletionProvider = false 47 | lspserver.isCompletionResolveProvider = false 48 | endif 49 | 50 | # definitionProvider 51 | if lspserver.caps->has_key('definitionProvider') 52 | if lspserver.caps.definitionProvider->type() == v:t_bool 53 | lspserver.isDefinitionProvider = lspserver.caps.definitionProvider 54 | else 55 | lspserver.isDefinitionProvider = true 56 | endif 57 | else 58 | lspserver.isDefinitionProvider = false 59 | endif 60 | 61 | # declarationProvider 62 | if lspserver.caps->has_key('declarationProvider') 63 | if lspserver.caps.declarationProvider->type() == v:t_bool 64 | lspserver.isDeclarationProvider = lspserver.caps.declarationProvider 65 | else 66 | lspserver.isDeclarationProvider = true 67 | endif 68 | else 69 | lspserver.isDeclarationProvider = false 70 | endif 71 | 72 | # typeDefinitionProvider 73 | if lspserver.caps->has_key('typeDefinitionProvider') 74 | if lspserver.caps.typeDefinitionProvider->type() == v:t_bool 75 | lspserver.isTypeDefinitionProvider = lspserver.caps.typeDefinitionProvider 76 | else 77 | lspserver.isTypeDefinitionProvider = true 78 | endif 79 | else 80 | lspserver.isTypeDefinitionProvider = false 81 | endif 82 | 83 | # implementationProvider 84 | if lspserver.caps->has_key('implementationProvider') 85 | if lspserver.caps.implementationProvider->type() == v:t_bool 86 | lspserver.isImplementationProvider = lspserver.caps.implementationProvider 87 | else 88 | lspserver.isImplementationProvider = true 89 | endif 90 | else 91 | lspserver.isImplementationProvider = false 92 | endif 93 | 94 | # signatureHelpProvider 95 | if lspserver.caps->has_key('signatureHelpProvider') 96 | lspserver.isSignatureHelpProvider = true 97 | else 98 | lspserver.isSignatureHelpProvider = false 99 | endif 100 | 101 | # hoverProvider 102 | if lspserver.caps->has_key('hoverProvider') 103 | if lspserver.caps.hoverProvider->type() == v:t_bool 104 | lspserver.isHoverProvider = lspserver.caps.hoverProvider 105 | else 106 | lspserver.isHoverProvider = true 107 | endif 108 | else 109 | lspserver.isHoverProvider = false 110 | endif 111 | 112 | # referencesProvider 113 | if lspserver.caps->has_key('referencesProvider') 114 | if lspserver.caps.referencesProvider->type() == v:t_bool 115 | lspserver.isReferencesProvider = lspserver.caps.referencesProvider 116 | else 117 | lspserver.isReferencesProvider = true 118 | endif 119 | else 120 | lspserver.isReferencesProvider = false 121 | endif 122 | 123 | # documentHighlightProvider 124 | if lspserver.caps->has_key('documentHighlightProvider') 125 | if lspserver.caps.documentHighlightProvider->type() == v:t_bool 126 | lspserver.isDocumentHighlightProvider = 127 | lspserver.caps.documentHighlightProvider 128 | else 129 | lspserver.isDocumentHighlightProvider = true 130 | endif 131 | else 132 | lspserver.isDocumentHighlightProvider = false 133 | endif 134 | 135 | # documentSymbolProvider 136 | if lspserver.caps->has_key('documentSymbolProvider') 137 | if lspserver.caps.documentSymbolProvider->type() == v:t_bool 138 | lspserver.isDocumentSymbolProvider = 139 | lspserver.caps.documentSymbolProvider 140 | else 141 | lspserver.isDocumentSymbolProvider = true 142 | endif 143 | else 144 | lspserver.isDocumentSymbolProvider = false 145 | endif 146 | 147 | # documentFormattingProvider 148 | if lspserver.caps->has_key('documentFormattingProvider') 149 | if lspserver.caps.documentFormattingProvider->type() == v:t_bool 150 | lspserver.isDocumentFormattingProvider = 151 | lspserver.caps.documentFormattingProvider 152 | else 153 | lspserver.isDocumentFormattingProvider = true 154 | endif 155 | else 156 | lspserver.isDocumentFormattingProvider = false 157 | endif 158 | 159 | # callHierarchyProvider 160 | if lspserver.caps->has_key('callHierarchyProvider') 161 | if lspserver.caps.callHierarchyProvider->type() == v:t_bool 162 | lspserver.isCallHierarchyProvider = 163 | lspserver.caps.callHierarchyProvider 164 | else 165 | lspserver.isCallHierarchyProvider = true 166 | endif 167 | else 168 | lspserver.isCallHierarchyProvider = false 169 | endif 170 | 171 | # semanticTokensProvider 172 | if lspserver.caps->has_key('semanticTokensProvider') 173 | lspserver.isSemanticTokensProvider = true 174 | lspserver.semanticTokensLegend = 175 | lspserver.caps.semanticTokensProvider.legend 176 | lspserver.semanticTokensRange = 177 | lspserver.caps.semanticTokensProvider->get('range', false) 178 | if lspserver.caps.semanticTokensProvider->has_key('full') 179 | if lspserver.caps.semanticTokensProvider.full->type() == v:t_bool 180 | lspserver.semanticTokensFull = 181 | lspserver.caps.semanticTokensProvider.full 182 | lspserver.semanticTokensDelta = false 183 | else 184 | lspserver.semanticTokensFull = true 185 | if lspserver.caps.semanticTokensProvider.full->has_key('delta') 186 | lspserver.semanticTokensDelta = 187 | lspserver.caps.semanticTokensProvider.full.delta 188 | else 189 | lspserver.semanticTokensDelta = false 190 | endif 191 | endif 192 | else 193 | lspserver.semanticTokensFull = false 194 | lspserver.semanticTokensDelta = false 195 | endif 196 | else 197 | lspserver.isSemanticTokensProvider = false 198 | endif 199 | 200 | # typeHierarchyProvider 201 | if lspserver.caps->has_key('typeHierarchyProvider') 202 | lspserver.isTypeHierarchyProvider = true 203 | else 204 | lspserver.isTypeHierarchyProvider = false 205 | endif 206 | 207 | # renameProvider 208 | if lspserver.caps->has_key('renameProvider') 209 | if lspserver.caps.renameProvider->type() == v:t_bool 210 | lspserver.isRenameProvider = lspserver.caps.renameProvider 211 | else 212 | lspserver.isRenameProvider = true 213 | endif 214 | else 215 | lspserver.isRenameProvider = false 216 | endif 217 | 218 | # codeActionProvider 219 | if lspserver.caps->has_key('codeActionProvider') 220 | if lspserver.caps.codeActionProvider->type() == v:t_bool 221 | lspserver.isCodeActionProvider = lspserver.caps.codeActionProvider 222 | else 223 | lspserver.isCodeActionProvider = true 224 | endif 225 | else 226 | lspserver.isCodeActionProvider = false 227 | endif 228 | 229 | # codeLensProvider 230 | if lspserver.caps->has_key('codeLensProvider') 231 | lspserver.isCodeLensProvider = true 232 | var codeLensProvider = lspserver.caps.codeLensProvider 233 | if codeLensProvider->type() == v:t_dict 234 | && codeLensProvider->has_key('resolveProvider') 235 | lspserver.isCodeLensResolveProvider = codeLensProvider.resolveProvider 236 | else 237 | lspserver.isCodeLensResolveProvider = false 238 | endif 239 | else 240 | lspserver.isCodeLensProvider = false 241 | endif 242 | 243 | # workspaceSymbolProvider 244 | if lspserver.caps->has_key('workspaceSymbolProvider') 245 | if lspserver.caps.workspaceSymbolProvider->type() == v:t_bool 246 | lspserver.isWorkspaceSymbolProvider = 247 | lspserver.caps.workspaceSymbolProvider 248 | else 249 | lspserver.isWorkspaceSymbolProvider = true 250 | endif 251 | else 252 | lspserver.isWorkspaceSymbolProvider = false 253 | endif 254 | 255 | # selectionRangeProvider 256 | if lspserver.caps->has_key('selectionRangeProvider') 257 | if lspserver.caps.selectionRangeProvider->type() == v:t_bool 258 | lspserver.isSelectionRangeProvider = 259 | lspserver.caps.selectionRangeProvider 260 | else 261 | lspserver.isSelectionRangeProvider = true 262 | endif 263 | else 264 | lspserver.isSelectionRangeProvider = false 265 | endif 266 | 267 | # foldingRangeProvider 268 | if lspserver.caps->has_key('foldingRangeProvider') 269 | if lspserver.caps.foldingRangeProvider->type() == v:t_bool 270 | lspserver.isFoldingRangeProvider = lspserver.caps.foldingRangeProvider 271 | else 272 | lspserver.isFoldingRangeProvider = true 273 | endif 274 | else 275 | lspserver.isFoldingRangeProvider = false 276 | endif 277 | 278 | # inlayHintProvider 279 | if lspserver.caps->has_key('inlayHintProvider') 280 | if lspserver.caps.inlayHintProvider->type() == v:t_bool 281 | lspserver.isInlayHintProvider = lspserver.caps.inlayHintProvider 282 | else 283 | lspserver.isInlayHintProvider = true 284 | endif 285 | else 286 | lspserver.isInlayHintProvider = false 287 | endif 288 | 289 | # clangdInlayHintsProvider 290 | if lspserver.caps->has_key('clangdInlayHintsProvider') 291 | lspserver.isClangdInlayHintsProvider = 292 | lspserver.caps.clangdInlayHintsProvider 293 | else 294 | lspserver.isClangdInlayHintsProvider = false 295 | endif 296 | 297 | # textDocumentSync capabilities 298 | lspserver.supportsDidSave = false 299 | # Default to TextDocumentSyncKind.None 300 | lspserver.textDocumentSync = 0 301 | if lspserver.caps->has_key('textDocumentSync') 302 | if lspserver.caps.textDocumentSync->type() == v:t_bool 303 | || lspserver.caps.textDocumentSync->type() == v:t_number 304 | lspserver.supportsDidSave = lspserver.caps.textDocumentSync 305 | lspserver.textDocumentSync = lspserver.caps.textDocumentSync 306 | elseif lspserver.caps.textDocumentSync->type() == v:t_dict 307 | # "save" 308 | if lspserver.caps.textDocumentSync->has_key('save') 309 | if lspserver.caps.textDocumentSync.save->type() == v:t_bool 310 | || lspserver.caps.textDocumentSync.save->type() == v:t_number 311 | lspserver.supportsDidSave = lspserver.caps.textDocumentSync.save 312 | elseif lspserver.caps.textDocumentSync.save->type() == v:t_dict 313 | lspserver.supportsDidSave = true 314 | endif 315 | endif 316 | # "change" 317 | if lspserver.caps.textDocumentSync->has_key('change') 318 | lspserver.textDocumentSync = lspserver.caps.textDocumentSync.change 319 | endif 320 | endif 321 | endif 322 | enddef 323 | 324 | # Return all the LSP client capabilities 325 | export def GetClientCaps(): dict 326 | # client capabilities (ClientCapabilities) 327 | var clientCaps: dict = { 328 | general: { 329 | # Currently we always send character count as position offset, 330 | # which meanas only utf-32 is supported. 331 | # Adding utf-16 simply for good mesure, as I'm scared some servers will 332 | # give up if they don't support utf-32 only. 333 | positionEncodings: ['utf-32', 'utf-16'] 334 | }, 335 | textDocument: { 336 | callHierarchy: { 337 | dynamicRegistration: false 338 | }, 339 | codeAction: { 340 | dynamicRegistration: false, 341 | codeActionLiteralSupport: { 342 | codeActionKind: { 343 | valueSet: ['', 'quickfix', 'refactor', 'refactor.extract', 344 | 'refactor.inline', 'refactor.rewrite', 'source', 345 | 'source.organizeImports'] 346 | } 347 | }, 348 | isPreferredSupport: true, 349 | disabledSupport: true 350 | }, 351 | codeLens: { 352 | dynamicRegistration: false 353 | }, 354 | completion: { 355 | dynamicRegistration: false, 356 | completionItem: { 357 | documentationFormat: ['markdown', 'plaintext'], 358 | resolveSupport: { 359 | properties: ['detail', 'documentation'] 360 | }, 361 | snippetSupport: opt.lspOptions.snippetSupport, 362 | insertReplaceSupport: false 363 | }, 364 | completionItemKind: { 365 | valueSet: range(1, 25) 366 | } 367 | }, 368 | declaration: { 369 | dynamicRegistration: false, 370 | linkSupport: true 371 | }, 372 | definition: { 373 | dynamicRegistration: false, 374 | linkSupport: true 375 | }, 376 | documentHighlight: { 377 | dynamicRegistration: false 378 | }, 379 | documentSymbol: { 380 | dynamicRegistration: false, 381 | symbolKind: { 382 | valueSet: range(1, 25) 383 | }, 384 | hierarchicalDocumentSymbolSupport: true, 385 | labelSupport: false 386 | }, 387 | foldingRange: { 388 | dynamicRegistration: false, 389 | rangeLimit: 5000, 390 | lineFoldingOnly: true, 391 | foldingRangeKind: { 392 | valueSet: ['comment', 'imports', 'region'] 393 | }, 394 | foldingRange: { 395 | collapsedText: true 396 | } 397 | }, 398 | formatting: { 399 | dynamicRegistration: false 400 | }, 401 | hover: { 402 | dynamicRegistration: false, 403 | contentFormat: ['markdown', 'plaintext'] 404 | }, 405 | implementation: { 406 | dynamicRegistration: false, 407 | linkSupport: true 408 | }, 409 | inlayHint: { 410 | dynamicRegistration: false 411 | }, 412 | publishDiagnostics: { 413 | relatedInformation: false, 414 | versionSupport: true, 415 | codeDescriptionSupport: true, 416 | dataSupport: true 417 | }, 418 | rangeFormatting: { 419 | dynamicRegistration: false 420 | }, 421 | references: { 422 | dynamicRegistration: false 423 | }, 424 | rename: { 425 | dynamicRegistration: false, 426 | prepareSupport: false, 427 | }, 428 | selectionRange: { 429 | dynamicRegistration: false, 430 | }, 431 | signatureHelp: { 432 | dynamicRegistration: false, 433 | signatureInformation: { 434 | documentationFormat: ['markdown', 'plaintext'], 435 | activeParameterSupport: true 436 | } 437 | }, 438 | semanticTokens: { 439 | dynamicRegistration: false, 440 | requests: { 441 | range: false, 442 | full: { 443 | delta: true 444 | } 445 | }, 446 | tokenTypes: [ 447 | 'type', 'class', 'enum', 'interface', 'struct', 'typeParameter', 448 | 'parameter', 'variable', 'property', 'enumMember', 'event', 449 | 'function', 'method', 'macro', 'keyword', 'modifier', 'comment', 450 | 'string', 'number', 'regexp', 'operator' 451 | ], 452 | tokenModifiers: [ 453 | 'declaration', 'definition', 'readonly', 'static', 'deprecated', 454 | 'abstract', 'async', 'modification', 'documentation', 455 | 'defaultLibrary' 456 | ], 457 | formats: ['relative'], 458 | overlappingTokenSupport: false, 459 | multilineTokenSupport: false, 460 | serverCancelSupport: false, 461 | augmentsSyntaxTokens: true 462 | }, 463 | synchronization: { 464 | dynamicRegistration: false, 465 | didSave: true, 466 | willSave: false, 467 | WillSaveWaitUntil: false 468 | }, 469 | typeDefinition: { 470 | dynamicRegistration: false, 471 | linkSupport: true 472 | }, 473 | typeHierarchy: { 474 | dynamicRegistration: false, 475 | } 476 | }, 477 | window: {}, 478 | workspace: { 479 | workspaceFolders: true, 480 | applyEdit: true, 481 | workspaceEdit: { 482 | resourceOperations: ['rename', 'create', 'delete'] 483 | }, 484 | configuration: true, 485 | symbol: { 486 | dynamicRegistration: false 487 | } 488 | }, 489 | # This is the way clangd expects to be informated about supported encodings: 490 | # https://clangd.llvm.org/extensions#utf-8-offsets 491 | offsetEncoding: ['utf-32', 'utf-16'] 492 | } 493 | 494 | # Vim patch 1629 is needed to properly encode/decode UTF-16 offsets 495 | if has('patch-9.0.1629') 496 | clientCaps.general.positionEncodings = ['utf-32', 'utf-16', 'utf-8'] 497 | clientCaps.offsetEncoding = ['utf-32', 'utf-16', 'utf-8'] 498 | endif 499 | 500 | return clientCaps 501 | enddef 502 | 503 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 504 | -------------------------------------------------------------------------------- /autoload/lsp/codeaction.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Functions related to handling LSP code actions to fix diagnostics. 4 | 5 | import './util.vim' 6 | import './textedit.vim' 7 | import './options.vim' as opt 8 | 9 | var CommandHandlers: dict 10 | 11 | export def RegisterCmdHandler(cmd: string, Handler: func) 12 | CommandHandlers[cmd] = Handler 13 | enddef 14 | 15 | export def DoCommand(lspserver: dict, cmd: dict) 16 | if cmd->has_key('command') && CommandHandlers->has_key(cmd.command) 17 | var CmdHandler: func = CommandHandlers[cmd.command] 18 | try 19 | call CmdHandler(cmd) 20 | catch 21 | util.ErrMsg($'"{cmd.command}" handler raised exception {v:exception}') 22 | endtry 23 | else 24 | lspserver.executeCommand(cmd) 25 | endif 26 | enddef 27 | 28 | # Apply the code action selected by the user. 29 | export def HandleCodeAction(lspserver: dict, selAction: dict) 30 | # textDocument/codeAction can return either Command[] or CodeAction[]. 31 | # If it is a CodeAction, it can have either an edit, a command or both. 32 | # Edits should be executed first. 33 | # Both Command and CodeAction interfaces has "command" member 34 | # so we should check "command" type - for Command it will be "string" 35 | if selAction->has_key('edit') 36 | || (selAction->has_key('command') && selAction.command->type() == v:t_dict) 37 | # selAction is a CodeAction instance, apply edit and command 38 | if selAction->has_key('edit') 39 | # apply edit first 40 | textedit.ApplyWorkspaceEdit(selAction.edit) 41 | endif 42 | if selAction->has_key('command') 43 | DoCommand(lspserver, selAction.command) 44 | endif 45 | else 46 | # selAction is a Command instance, apply it directly 47 | DoCommand(lspserver, selAction) 48 | endif 49 | enddef 50 | 51 | # Process the list of code actions returned by the LSP server, ask the user to 52 | # choose one action from the list and then apply it. 53 | # If "query" is a number, then apply the corresponding action in the list. 54 | # If "query" is a regular expression starting with "/", then apply the action 55 | # matching the search string in the list. 56 | # If "query" is a regular string, then apply the action matching the string. 57 | # If "query" is an empty string, then if the "usePopupInCodeAction" option is 58 | # configured by the user, then display the list of items in a popup menu. 59 | # Otherwise display the items in an input list and prompt the user to select 60 | # an action. 61 | export def ApplyCodeAction(lspserver: dict, actionlist: list>, query: string): void 62 | var actions = actionlist 63 | 64 | if opt.lspOptions.hideDisabledCodeActions 65 | actions = actions->filter((ix, act) => !act->has_key('disabled')) 66 | endif 67 | 68 | if actions->empty() 69 | # no action can be performed 70 | util.WarnMsg('No code action is available') 71 | return 72 | endif 73 | 74 | var text: list = [] 75 | var act: dict 76 | for i in actions->len()->range() 77 | act = actions[i] 78 | var t: string = act.title->substitute('\r\n', '\\r\\n', 'g') 79 | t = t->substitute('\n', '\\n', 'g') 80 | text->add(printf(" %d. %s ", i + 1, t)) 81 | endfor 82 | 83 | var choice: number 84 | 85 | var query_ = query->trim() 86 | if query_ =~ '^\d\+$' # digit 87 | choice = query_->str2nr() 88 | elseif query_ =~ '^/' # regex 89 | choice = 1 + util.Indexof(actions, (i, a) => a.title =~ query_[1 : ]) 90 | elseif query_ != '' # literal string 91 | choice = 1 + util.Indexof(actions, (i, a) => a.title[0 : query_->len() - 1] == query_) 92 | elseif opt.lspOptions.usePopupInCodeAction 93 | # Use a popup menu to show the code action 94 | var popupAttrs = opt.PopupConfigure('CodeAction', { 95 | pos: 'botleft', 96 | line: 'cursor-1', 97 | col: 'cursor', 98 | zindex: 1000, 99 | cursorline: 1, 100 | mapping: 0, 101 | wrap: 0, 102 | title: 'Code action', 103 | callback: (_, result) => { 104 | # Invalid item selected or closed the popup 105 | if result <= 0 || result > text->len() 106 | return 107 | endif 108 | 109 | # Do the code action 110 | HandleCodeAction(lspserver, actions[result - 1]) 111 | }, 112 | filter: (winid, key) => { 113 | if key == 'h' || key == 'l' 114 | winid->popup_close(-1) 115 | elseif key->str2nr() > 0 116 | # assume less than 10 entries are present 117 | winid->popup_close(key->str2nr()) 118 | else 119 | return popup_filter_menu(winid, key) 120 | endif 121 | return 1 122 | }, 123 | }) 124 | popup_create(text, popupAttrs) 125 | else 126 | choice = inputlist(['Code action:'] + text) 127 | endif 128 | 129 | if choice < 1 || choice > text->len() 130 | return 131 | endif 132 | 133 | HandleCodeAction(lspserver, actions[choice - 1]) 134 | enddef 135 | 136 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 137 | -------------------------------------------------------------------------------- /autoload/lsp/codelens.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | import './codeaction.vim' 4 | 5 | # Functions related to handling LSP code lens 6 | 7 | export def ProcessCodeLens(lspserver: dict, bnr: number, codeLensItems: list>) 8 | var text: list = [] 9 | for i in codeLensItems->len()->range() 10 | var item = codeLensItems[i] 11 | if !item->has_key('command') 12 | # resolve the code lens 13 | item = lspserver.resolveCodeLens(bnr, item) 14 | if item->empty() 15 | continue 16 | endif 17 | codeLensItems[i] = item 18 | endif 19 | text->add(printf("%d. %s\t| L%s:%s", i + 1, item.command.title, 20 | item.range.start.line + 1, 21 | getline(item.range.start.line + 1))) 22 | endfor 23 | 24 | var choice = inputlist(['Code Lens:'] + text) 25 | if choice < 1 || choice > codeLensItems->len() 26 | return 27 | endif 28 | 29 | codeaction.DoCommand(lspserver, codeLensItems[choice - 1].command) 30 | enddef 31 | 32 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 33 | -------------------------------------------------------------------------------- /autoload/lsp/handlers.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Handlers for messages from the LSP server 4 | # Refer to https://microsoft.github.io/language-server-protocol/specification 5 | # for the Language Server Protocol (LSP) specification. 6 | 7 | import './util.vim' 8 | import './diag.vim' 9 | import './textedit.vim' 10 | 11 | # Process various reply messages from the LSP server 12 | export def ProcessReply(lspserver: dict, req: dict, reply: dict): void 13 | util.ErrMsg($'Unsupported reply received from LSP server: {reply->string()} for request: {req->string()}') 14 | enddef 15 | 16 | # process a diagnostic notification message from the LSP server 17 | # Notification: textDocument/publishDiagnostics 18 | # Param: PublishDiagnosticsParams 19 | def ProcessDiagNotif(lspserver: dict, reply: dict): void 20 | var params = reply.params 21 | diag.DiagNotification(lspserver, params.uri, params.diagnostics) 22 | enddef 23 | 24 | # Convert LSP message type to a string 25 | def LspMsgTypeToString(lspMsgType: number): string 26 | var msgStrMap: list = ['', 'Error', 'Warning', 'Info', 'Log'] 27 | var mtype: string = 'Log' 28 | if lspMsgType > 0 && lspMsgType < 5 29 | mtype = msgStrMap[lspMsgType] 30 | endif 31 | return mtype 32 | enddef 33 | 34 | # process a show notification message from the LSP server 35 | # Notification: window/showMessage 36 | # Param: ShowMessageParams 37 | def ProcessShowMsgNotif(lspserver: dict, reply: dict) 38 | var msgType = reply.params.type 39 | if msgType >= 4 40 | # ignore log messages from the LSP server (too chatty) 41 | # TODO: Add a configuration to control the message level that will be 42 | # displayed. Also store these messages and provide a command to display 43 | # them. 44 | return 45 | endif 46 | if msgType == 1 47 | util.ErrMsg($'Lsp({lspserver.name}) {reply.params.message}') 48 | elseif msgType == 2 49 | util.WarnMsg($'Lsp({lspserver.name}) {reply.params.message}') 50 | elseif msgType == 3 51 | util.InfoMsg($'Lsp({lspserver.name}) {reply.params.message}') 52 | endif 53 | enddef 54 | 55 | # process a log notification message from the LSP server 56 | # Notification: window/logMessage 57 | # Param: LogMessageParams 58 | def ProcessLogMsgNotif(lspserver: dict, reply: dict) 59 | var params = reply.params 60 | var mtype = LspMsgTypeToString(params.type) 61 | lspserver.addMessage(mtype, params.message) 62 | enddef 63 | 64 | # process the log trace notification messages 65 | # Notification: $/logTrace 66 | # Param: LogTraceParams 67 | def ProcessLogTraceNotif(lspserver: dict, reply: dict) 68 | lspserver.addMessage('trace', reply.params.message) 69 | enddef 70 | 71 | # process unsupported notification messages 72 | def ProcessUnsupportedNotif(lspserver: dict, reply: dict) 73 | util.WarnMsg($'Unsupported notification message received from the LSP server ({lspserver.name}), message = {reply->string()}') 74 | enddef 75 | 76 | # Dict to process telemetry notification messages only once per filetype 77 | var telemetryProcessed: dict = {} 78 | # process unsupported notification messages only once 79 | def ProcessUnsupportedNotifOnce(lspserver: dict, reply: dict) 80 | if !telemetryProcessed->get(&ft, false) 81 | ProcessUnsupportedNotif(lspserver, reply) 82 | telemetryProcessed->extend({[&ft]: true}) 83 | endif 84 | enddef 85 | 86 | # process notification messages from the LSP server 87 | export def ProcessNotif(lspserver: dict, reply: dict): void 88 | var lsp_notif_handlers: dict = 89 | { 90 | 'window/showMessage': ProcessShowMsgNotif, 91 | 'window/logMessage': ProcessLogMsgNotif, 92 | 'textDocument/publishDiagnostics': ProcessDiagNotif, 93 | '$/logTrace': ProcessLogTraceNotif, 94 | 'telemetry/event': ProcessUnsupportedNotifOnce, 95 | } 96 | 97 | # Explicitly ignored notification messages (many of them are specific to a 98 | # particular language server) 99 | var lsp_ignored_notif_handlers: list = 100 | [ 101 | '$/progress', 102 | '$/status/report', 103 | '$/status/show', 104 | # PHP intelephense server sends the "indexingStarted" and 105 | # "indexingEnded" notifications which is not in the LSP specification. 106 | 'indexingStarted', 107 | 'indexingEnded', 108 | # Java language server sends the 'language/status' notification which is 109 | # not in the LSP specification. 110 | 'language/status', 111 | # Typescript language server sends the '$/typescriptVersion' 112 | # notification which is not in the LSP specification. 113 | '$/typescriptVersion', 114 | # Dart language server sends the '$/analyzerStatus' notification which 115 | # is not in the LSP specification. 116 | '$/analyzerStatus', 117 | # pyright language server notifications 118 | 'pyright/beginProgress', 119 | 'pyright/reportProgress', 120 | 'pyright/endProgress', 121 | 'eslint/status', 122 | 'taplo/didChangeSchemaAssociation', 123 | 'sqlLanguageServer.finishSetup', 124 | # ccls language server notifications 125 | '$ccls/publishSkippedRanges', 126 | '$ccls/publishSemanticHighlight', 127 | # omnisharp language server notifications 128 | 'o#/backgrounddiagnosticstatus', 129 | 'o#/msbuildprojectdiagnostics', 130 | 'o#/projectadded', 131 | 'o#/projectchanged', 132 | 'o#/projectconfiguration', 133 | 'o#/projectdiagnosticstatus', 134 | 'o#/unresolveddependencies', 135 | '@/tailwindCSS/projectInitialized' 136 | ] 137 | 138 | if lsp_notif_handlers->has_key(reply.method) 139 | lsp_notif_handlers[reply.method](lspserver, reply) 140 | elseif lspserver.customNotificationHandlers->has_key(reply.method) 141 | lspserver.customNotificationHandlers[reply.method](lspserver, reply) 142 | elseif lsp_ignored_notif_handlers->index(reply.method) == -1 143 | ProcessUnsupportedNotif(lspserver, reply) 144 | endif 145 | enddef 146 | 147 | # process the workspace/applyEdit LSP server request 148 | # Request: "workspace/applyEdit" 149 | # Param: ApplyWorkspaceEditParams 150 | def ProcessApplyEditReq(lspserver: dict, request: dict) 151 | # interface ApplyWorkspaceEditParams 152 | if !request->has_key('params') 153 | return 154 | endif 155 | var workspaceEditParams: dict = request.params 156 | if workspaceEditParams->has_key('label') 157 | util.InfoMsg($'Workspace edit {workspaceEditParams.label}') 158 | endif 159 | textedit.ApplyWorkspaceEdit(workspaceEditParams.edit) 160 | # TODO: Need to return the proper result of the edit operation 161 | lspserver.sendResponse(request, {applied: true}, {}) 162 | enddef 163 | 164 | # process the workspace/workspaceFolders LSP server request 165 | # Request: "workspace/workspaceFolders" 166 | # Param: none 167 | def ProcessWorkspaceFoldersReq(lspserver: dict, request: dict) 168 | if !lspserver->has_key('workspaceFolders') 169 | lspserver.sendResponse(request, null, {}) 170 | return 171 | endif 172 | if lspserver.workspaceFolders->empty() 173 | lspserver.sendResponse(request, [], {}) 174 | else 175 | lspserver.sendResponse(request, 176 | \ lspserver.workspaceFolders->copy()->map('{name: v:val->fnamemodify(":t"), uri: util.LspFileToUri(v:val)}'), 177 | \ {}) 178 | endif 179 | enddef 180 | 181 | # process the workspace/configuration LSP server request 182 | # Request: "workspace/configuration" 183 | # Param: ConfigurationParams 184 | def ProcessWorkspaceConfiguration(lspserver: dict, request: dict) 185 | var items = request.params.items 186 | var response = items->map((_, item) => lspserver.workspaceConfigGet(item)) 187 | 188 | # Server expect null value if no config is given 189 | if response->type() == v:t_list && response->len() == 1 190 | && response[0]->type() == v:t_dict 191 | && response[0] == null_dict 192 | response[0] = null 193 | endif 194 | 195 | lspserver.sendResponse(request, response, {}) 196 | enddef 197 | 198 | # process the window/workDoneProgress/create LSP server request 199 | # Request: "window/workDoneProgress/create" 200 | # Param: none 201 | def ProcessWorkDoneProgressCreate(lspserver: dict, request: dict) 202 | lspserver.sendResponse(request, null, {}) 203 | enddef 204 | 205 | # process the window/showMessageRequest LSP server request 206 | # Request: "window/showMessageRequest" 207 | # Param: ShowMessageRequestParams 208 | def ProcessShowMessageRequest(lspserver: dict, req: dict) 209 | var params: dict = req.params 210 | if params->has_key('actions') 211 | var actions: list> = params.actions 212 | if actions->empty() 213 | util.WarnMsg($'Empty actions in showMessage request {params.message}') 214 | lspserver.sendResponse(req, null, {}) 215 | return 216 | endif 217 | 218 | # Generate a list of strings from the action titles 219 | var text: list = [] 220 | var act: dict 221 | for i in actions->len()->range() 222 | act = actions[i] 223 | var t: string = act.title->substitute('\r\n', '\\r\\n', 'g') 224 | t = t->substitute('\n', '\\n', 'g') 225 | text->add(printf(" %d. %s ", i + 1, t)) 226 | endfor 227 | 228 | # Ask the user to choose one of the actions 229 | var choice: number = inputlist([params.message] + text) 230 | if choice < 1 || choice > text->len() 231 | lspserver.sendResponse(req, null, {}) 232 | return 233 | endif 234 | lspserver.sendResponse(req, actions[choice - 1], {}) 235 | else 236 | # No actions in the message. Simply display the message. 237 | ProcessShowMsgNotif(lspserver, req) 238 | lspserver.sendResponse(req, null, {}) 239 | endif 240 | enddef 241 | 242 | # process the client/registerCapability LSP server request 243 | # Request: "client/registerCapability" 244 | # Param: RegistrationParams 245 | def ProcessClientRegisterCap(lspserver: dict, request: dict) 246 | lspserver.sendResponse(request, null, {}) 247 | enddef 248 | 249 | # process the client/unregisterCapability LSP server request 250 | # Request: "client/unregisterCapability" 251 | # Param: UnregistrationParams 252 | def ProcessClientUnregisterCap(lspserver: dict, request: dict) 253 | lspserver.sendResponse(request, null, {}) 254 | enddef 255 | 256 | # process a request message from the server 257 | export def ProcessRequest(lspserver: dict, request: dict) 258 | var lspRequestHandlers: dict = 259 | { 260 | 'client/registerCapability': ProcessClientRegisterCap, 261 | 'client/unregisterCapability': ProcessClientUnregisterCap, 262 | 'window/workDoneProgress/create': ProcessWorkDoneProgressCreate, 263 | 'window/showMessageRequest': ProcessShowMessageRequest, 264 | 'workspace/applyEdit': ProcessApplyEditReq, 265 | 'workspace/configuration': ProcessWorkspaceConfiguration, 266 | 'workspace/workspaceFolders': ProcessWorkspaceFoldersReq 267 | # TODO: Handle the following requests from the server: 268 | # workspace/codeLens/refresh 269 | # workspace/diagnostic/refresh 270 | # workspace/inlayHint/refresh 271 | # workspace/inlineValue/refresh 272 | # workspace/semanticTokens/refresh 273 | } 274 | 275 | # Explicitly ignored requests 276 | var lspIgnoredRequestHandlers: list = 277 | [ 278 | # Eclipse java language server sends the 279 | # 'workspace/executeClientCommand' request (to reload bundles) which is 280 | # not in the LSP specification. 281 | 'workspace/executeClientCommand', 282 | ] 283 | 284 | if lspRequestHandlers->has_key(request.method) 285 | lspRequestHandlers[request.method](lspserver, request) 286 | elseif lspserver.customRequestHandlers->has_key(request.method) 287 | lspserver.customRequestHandlers[request.method](lspserver, request) 288 | elseif lspIgnoredRequestHandlers->index(request.method) == -1 289 | util.ErrMsg($'Unsupported request message received from the LSP server ({lspserver.name}), message = {request->string()}') 290 | endif 291 | enddef 292 | 293 | # process one or more LSP server messages 294 | export def ProcessMessages(lspserver: dict): void 295 | var idx: number 296 | var len: number 297 | var content: string 298 | var msg: dict 299 | var req: dict 300 | 301 | msg = lspserver.data 302 | if msg->has_key('result') || msg->has_key('error') 303 | # response message from the server 304 | req = lspserver.requests->get(msg.id->string(), {}) 305 | if !req->empty() 306 | # Remove the corresponding stored request message 307 | lspserver.requests->remove(msg.id->string()) 308 | 309 | if msg->has_key('result') 310 | lspserver.processReply(req, msg) 311 | else 312 | # request failed 313 | var emsg: string = msg.error.message 314 | emsg ..= $', code = {msg.error.code}' 315 | if msg.error->has_key('data') 316 | emsg ..= $', data = {msg.error.data->string()}' 317 | endif 318 | util.ErrMsg($'request {req.method} failed ({emsg})') 319 | endif 320 | endif 321 | elseif msg->has_key('id') && msg->has_key('method') 322 | # request message from the server 323 | lspserver.processRequest(msg) 324 | elseif msg->has_key('method') 325 | # notification message from the server 326 | lspserver.processNotif(msg) 327 | else 328 | util.ErrMsg($'Unsupported message ({msg->string()})') 329 | endif 330 | enddef 331 | 332 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 333 | -------------------------------------------------------------------------------- /autoload/lsp/hover.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Functions related to displaying hover symbol information. 4 | 5 | import './util.vim' 6 | import './options.vim' as opt 7 | 8 | # Util used to compute the hoverText from textDocument/hover reply 9 | def GetHoverText(lspserver: dict, hoverResult: any): list 10 | if hoverResult->empty() 11 | return ['', ''] 12 | endif 13 | 14 | # MarkupContent 15 | if hoverResult.contents->type() == v:t_dict 16 | && hoverResult.contents->has_key('kind') 17 | if hoverResult.contents.kind == 'plaintext' 18 | return [hoverResult.contents.value->split("\n"), 'text'] 19 | endif 20 | 21 | if hoverResult.contents.kind == 'markdown' 22 | return [hoverResult.contents.value->split("\n"), 'lspgfm'] 23 | endif 24 | 25 | lspserver.errorLog( 26 | $'{strftime("%m/%d/%y %T")}: Unsupported hover contents kind ({hoverResult.contents.kind})' 27 | ) 28 | return ['', ''] 29 | endif 30 | 31 | # MarkedString 32 | if hoverResult.contents->type() == v:t_dict 33 | && hoverResult.contents->has_key('value') 34 | return [ 35 | [$'``` {hoverResult.contents.language}'] 36 | + hoverResult.contents.value->split("\n") 37 | + ['```'], 38 | 'lspgfm' 39 | ] 40 | endif 41 | 42 | # MarkedString 43 | if hoverResult.contents->type() == v:t_string 44 | return [hoverResult.contents->split("\n"), 'lspgfm'] 45 | endif 46 | 47 | # interface MarkedString[] 48 | if hoverResult.contents->type() == v:t_list 49 | var hoverText: list = [] 50 | for e in hoverResult.contents 51 | if !hoverText->empty() 52 | hoverText->extend(['- - -']) 53 | endif 54 | 55 | if e->type() == v:t_string 56 | hoverText->extend(e->split("\n")) 57 | else 58 | hoverText->extend([$'``` {e.language}']) 59 | hoverText->extend(e.value->split("\n")) 60 | hoverText->extend(['```']) 61 | endif 62 | endfor 63 | 64 | return [hoverText, 'lspgfm'] 65 | endif 66 | 67 | lspserver.errorLog( 68 | $'{strftime("%m/%d/%y %T")}: Unsupported hover reply ({hoverResult})' 69 | ) 70 | return ['', ''] 71 | enddef 72 | 73 | # Key filter function for the hover popup window. 74 | # Only keys to scroll the popup window are supported. 75 | def HoverWinFilterKey(hoverWin: number, key: string): bool 76 | var keyHandled = false 77 | 78 | if key == "\" 79 | || key == "\" 80 | || key == "\" 81 | || key == "\" 82 | || key == "\" 83 | || key == "\" 84 | || key == "\" 85 | || key == "\" 86 | || key == "\" 87 | || key == "\" 88 | # scroll the hover popup window 89 | win_execute(hoverWin, $'normal! {key}') 90 | keyHandled = true 91 | endif 92 | 93 | if key == "\" 94 | hoverWin->popup_close() 95 | keyHandled = true 96 | endif 97 | 98 | return keyHandled 99 | enddef 100 | 101 | # process the 'textDocument/hover' reply from the LSP server 102 | # Result: Hover | null 103 | export def HoverReply(lspserver: dict, hoverResult: any, cmdmods: string): void 104 | var [hoverText, hoverKind] = GetHoverText(lspserver, hoverResult) 105 | 106 | # Nothing to show 107 | if hoverText->empty() 108 | if cmdmods !~ 'silent' 109 | util.WarnMsg($'No documentation found for current keyword') 110 | endif 111 | return 112 | endif 113 | 114 | if opt.lspOptions.hoverInPreview 115 | execute $':silent! {cmdmods} pedit LspHover' 116 | :wincmd P 117 | :setlocal buftype=nofile 118 | :setlocal bufhidden=delete 119 | bufnr()->deletebufline(1, '$') 120 | hoverText->append(0) 121 | [1, 1]->cursor() 122 | exe $'setlocal ft={hoverKind}' 123 | :wincmd p 124 | else 125 | popup_clear() 126 | var popupAttrs = opt.PopupConfigure('Hover', { 127 | moved: 'any', 128 | close: 'click', 129 | fixed: true, 130 | maxwidth: 80, 131 | filter: HoverWinFilterKey, 132 | padding: [0, 1, 0, 1] 133 | }) 134 | var winid = hoverText->popup_atcursor(popupAttrs) 135 | win_execute(winid, $'setlocal ft={hoverKind}') 136 | endif 137 | enddef 138 | 139 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 140 | -------------------------------------------------------------------------------- /autoload/lsp/inlayhints.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Functions for dealing with inlay hints 4 | 5 | import './util.vim' 6 | import './buffer.vim' as buf 7 | import './options.vim' as opt 8 | 9 | # Initialize the highlight group and the text property type used for 10 | # inlay hints. 11 | export def InitOnce() 12 | hlset([ 13 | {name: 'LspInlayHintsType', default: true, linksto: 'Label'}, 14 | {name: 'LspInlayHintsParam', default: true, linksto: 'Conceal'} 15 | ]) 16 | prop_type_add('LspInlayHintsType', {highlight: 'LspInlayHintsType'}) 17 | prop_type_add('LspInlayHintsParam', {highlight: 'LspInlayHintsParam'}) 18 | 19 | autocmd_add([{group: 'LspCmds', 20 | event: 'User', 21 | pattern: 'LspOptionsChanged', 22 | cmd: 'LspInlayHintsOptionsChanged()'}]) 23 | enddef 24 | 25 | # Clear all the inlay hints text properties in the current buffer 26 | def InlayHintsClear(bnr: number) 27 | prop_remove({type: 'LspInlayHintsType', bufnr: bnr, all: true}) 28 | prop_remove({type: 'LspInlayHintsParam', bufnr: bnr, all: true}) 29 | enddef 30 | 31 | # LSP inlay hints reply message handler 32 | export def InlayHintsReply(lspserver: dict, bnr: number, inlayHints: any) 33 | if inlayHints->empty() 34 | return 35 | endif 36 | 37 | InlayHintsClear(bnr) 38 | 39 | if mode() != 'n' 40 | # Update inlay hints only in normal mode 41 | return 42 | endif 43 | 44 | for hint in inlayHints 45 | var label = '' 46 | if hint.label->type() == v:t_list 47 | label = hint.label->copy()->map((_, v) => v.value)->join('') 48 | else 49 | label = hint.label 50 | endif 51 | 52 | # add a space before or after the label 53 | var padLeft: bool = hint->get('paddingLeft', false) 54 | var padRight: bool = hint->get('paddingRight', false) 55 | if padLeft 56 | label = $' {label}' 57 | endif 58 | if padRight 59 | label = $'{label} ' 60 | endif 61 | 62 | var kind = hint->has_key('kind') ? hint.kind->string() : '1' 63 | try 64 | lspserver.decodePosition(bnr, hint.position) 65 | var byteIdx = util.GetLineByteFromPos(bnr, hint.position) 66 | if kind == "'type'" || kind == '1' 67 | prop_add(hint.position.line + 1, byteIdx + 1, 68 | {type: 'LspInlayHintsType', text: label, bufnr: bnr}) 69 | elseif kind == "'parameter'" || kind == '2' 70 | prop_add(hint.position.line + 1, byteIdx + 1, 71 | {type: 'LspInlayHintsParam', text: label, bufnr: bnr}) 72 | endif 73 | catch /E966\|E964/ # Invalid lnum | Invalid col 74 | # Inlay hints replies arrive asynchronously and the document might have 75 | # been modified in the mean time. As the reply is stale, ignore invalid 76 | # line number and column number errors. 77 | endtry 78 | endfor 79 | enddef 80 | 81 | # Timer callback to display the inlay hints. 82 | def InlayHintsTimerCb(lspserver: dict, bnr: number, timerid: number) 83 | lspserver.inlayHintsShow(bnr) 84 | setbufvar(bnr, 'LspInlayHintsNeedsUpdate', false) 85 | enddef 86 | 87 | # Update all the inlay hints. A timer is used to throttle the updates. 88 | def LspInlayHintsUpdate(bnr: number) 89 | if !bnr->getbufvar('LspInlayHintsNeedsUpdate', true) 90 | return 91 | endif 92 | 93 | var timerid = bnr->getbufvar('LspInlayHintsTimer', -1) 94 | if timerid != -1 95 | timerid->timer_stop() 96 | setbufvar(bnr, 'LspInlayHintsTimer', -1) 97 | endif 98 | 99 | var lspserver: dict = buf.BufLspServerGet(bnr, 'inlayHint') 100 | if lspserver->empty() 101 | return 102 | endif 103 | 104 | if get(g:, 'LSPTest') 105 | # When running tests, update the inlay hints immediately 106 | InlayHintsTimerCb(lspserver, bnr, -1) 107 | else 108 | timerid = timer_start(300, function('InlayHintsTimerCb', [lspserver, bnr])) 109 | setbufvar(bnr, 'LspInlayHintsTimer', timerid) 110 | endif 111 | enddef 112 | 113 | # Text is modified. Need to update the inlay hints. 114 | def LspInlayHintsChanged(bnr: number) 115 | setbufvar(bnr, 'LspInlayHintsNeedsUpdate', true) 116 | enddef 117 | 118 | # Trigger an update of the inlay hints in the current buffer. 119 | export def LspInlayHintsUpdateNow(bnr: number) 120 | setbufvar(bnr, 'LspInlayHintsNeedsUpdate', true) 121 | LspInlayHintsUpdate(bnr) 122 | enddef 123 | 124 | # Stop updating the inlay hints. 125 | def LspInlayHintsUpdateStop(bnr: number) 126 | var timerid = bnr->getbufvar('LspInlayHintsTimer', -1) 127 | if timerid != -1 128 | timerid->timer_stop() 129 | setbufvar(bnr, 'LspInlayHintsTimer', -1) 130 | endif 131 | enddef 132 | 133 | # Do buffer-local initialization for displaying inlay hints 134 | export def BufferInit(lspserver: dict, bnr: number) 135 | if !lspserver.isInlayHintProvider && !lspserver.isClangdInlayHintsProvider 136 | # no support for inlay hints 137 | return 138 | endif 139 | 140 | # Inlays hints are disabled 141 | if !opt.lspOptions.showInlayHints 142 | || !lspserver.featureEnabled('inlayHint') 143 | return 144 | endif 145 | 146 | var acmds: list> = [] 147 | 148 | # Update the inlay hints (if needed) when the cursor is not moved for some 149 | # time. 150 | acmds->add({bufnr: bnr, 151 | event: ['CursorHold'], 152 | group: 'LspInlayHints', 153 | cmd: $'LspInlayHintsUpdate({bnr})'}) 154 | # After the text in the current buffer is modified, the inlay hints need to 155 | # be updated. 156 | acmds->add({bufnr: bnr, 157 | event: ['TextChanged'], 158 | group: 'LspInlayHints', 159 | cmd: $'LspInlayHintsChanged({bnr})'}) 160 | # Editing a file should trigger an inlay hint update. 161 | acmds->add({bufnr: bnr, 162 | event: ['BufReadPost'], 163 | group: 'LspInlayHints', 164 | cmd: $'LspInlayHintsUpdateNow({bnr})'}) 165 | # Inlay hints need not be updated if a buffer is no longer active. 166 | acmds->add({bufnr: bnr, 167 | event: ['BufLeave'], 168 | group: 'LspInlayHints', 169 | cmd: $'LspInlayHintsUpdateStop({bnr})'}) 170 | 171 | # Inlay hints maybe a bit delayed if it was a sync init lsp server. 172 | if lspserver.syncInit 173 | acmds->add({bufnr: bnr, 174 | event: ['User'], 175 | group: 'LspAttached', 176 | cmd: $'LspInlayHintsUpdateNow({bnr})'}) 177 | endif 178 | 179 | autocmd_add(acmds) 180 | enddef 181 | 182 | # Track the current inlay hints enabled/disabled state. Used when the 183 | # "showInlayHints" option value is changed. 184 | var save_showInlayHints = opt.lspOptions.showInlayHints 185 | 186 | # Enable inlay hints. For all the buffers with an attached language server 187 | # that supports inlay hints, refresh the inlay hints. 188 | export def InlayHintsEnable() 189 | opt.lspOptions.showInlayHints = true 190 | for binfo in getbufinfo() 191 | var lspservers: list> = buf.BufLspServersGet(binfo.bufnr) 192 | if lspservers->empty() 193 | continue 194 | endif 195 | for lspserver in lspservers 196 | if !lspserver.ready 197 | || !lspserver.featureEnabled('inlayHint') 198 | || (!lspserver.isInlayHintProvider && 199 | !lspserver.isClangdInlayHintsProvider) 200 | continue 201 | endif 202 | BufferInit(lspserver, binfo.bufnr) 203 | LspInlayHintsUpdateNow(binfo.bufnr) 204 | endfor 205 | endfor 206 | save_showInlayHints = true 207 | enddef 208 | 209 | # Disable inlay hints for the current Vim session. Clear the inlay hints in 210 | # all the buffers. 211 | export def InlayHintsDisable() 212 | opt.lspOptions.showInlayHints = false 213 | for binfo in getbufinfo() 214 | var lspserver: dict = buf.BufLspServerGet(binfo.bufnr, 'inlayHint') 215 | if lspserver->empty() 216 | continue 217 | endif 218 | LspInlayHintsUpdateStop(binfo.bufnr) 219 | :silent! autocmd_delete([{bufnr: binfo.bufnr, group: 'LspInlayHints'}]) 220 | InlayHintsClear(binfo.bufnr) 221 | endfor 222 | save_showInlayHints = false 223 | enddef 224 | 225 | # Toggle (enable or disable) inlay hints. 226 | export def InlayHintsToggle() 227 | if opt.lspOptions.showInlayHints 228 | InlayHintsDisable() 229 | else 230 | InlayHintsEnable() 231 | endif 232 | enddef 233 | 234 | # Some options are changed. If 'showInlayHints' option is changed, then 235 | # either enable or disable inlay hints. 236 | export def LspInlayHintsOptionsChanged() 237 | if save_showInlayHints && !opt.lspOptions.showInlayHints 238 | InlayHintsDisable() 239 | elseif !save_showInlayHints && opt.lspOptions.showInlayHints 240 | InlayHintsEnable() 241 | endif 242 | enddef 243 | 244 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 245 | -------------------------------------------------------------------------------- /autoload/lsp/offset.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | import './util.vim' 4 | 5 | # Functions for encoding and decoding the LSP position offsets. Language 6 | # servers support either UTF-8 or UTF-16 or UTF-32 position offsets. The 7 | # character related Vim functions use the UTF-32 position offset. The 8 | # encoding used is negotiated during the language server initialization. 9 | 10 | # Encode the UTF-32 character offset in the LSP position "pos" to the encoding 11 | # negotiated with the language server. 12 | # 13 | # Modifies in-place the UTF-32 offset in pos.character to a UTF-8 or UTF-16 or 14 | # UTF-32 offset. 15 | export def EncodePosition(lspserver: dict, bnr: number, pos: dict) 16 | if has('patch-9.0.1629') 17 | if lspserver.posEncoding == 32 || bnr <= 0 18 | # LSP client plugin also uses utf-32 encoding 19 | return 20 | endif 21 | 22 | :silent! bnr->bufload() 23 | var text = bnr->getbufline(pos.line + 1)->get(0, '') 24 | if text->empty() 25 | return 26 | endif 27 | 28 | if lspserver.posEncoding == 16 29 | pos.character = text->utf16idx(pos.character, true, true) 30 | else 31 | pos.character = text->byteidxcomp(pos.character) 32 | endif 33 | endif 34 | enddef 35 | 36 | # Decode the character offset in the LSP position "pos" using the encoding 37 | # negotiated with the language server to a UTF-32 offset. 38 | # 39 | # Modifies in-place the UTF-8 or UTF-16 or UTF-32 offset in pos.character to a 40 | # UTF-32 offset. 41 | export def DecodePosition(lspserver: dict, bnr: number, pos: dict) 42 | if has('patch-9.0.1629') 43 | if lspserver.posEncoding == 32 || bnr <= 0 44 | # LSP client plugin also uses utf-32 encoding 45 | return 46 | endif 47 | 48 | :silent! bnr->bufload() 49 | var text = bnr->getbufline(pos.line + 1)->get(0, '') 50 | # If the line is empty then don't decode the character position. 51 | if text->empty() 52 | return 53 | endif 54 | 55 | # If the character position is out-of-bounds, then don't decode the 56 | # character position. 57 | var textLen = 0 58 | if lspserver.posEncoding == 16 59 | textLen = text->strutf16len(true) 60 | else 61 | textLen = text->strlen() 62 | endif 63 | 64 | if pos.character > textLen 65 | return 66 | endif 67 | 68 | if pos.character == textLen 69 | pos.character = text->strchars() 70 | else 71 | if lspserver.posEncoding == 16 72 | pos.character = text->charidx(pos.character, true, true) 73 | else 74 | pos.character = text->charidx(pos.character, true) 75 | endif 76 | endif 77 | endif 78 | enddef 79 | 80 | # Encode the start and end UTF-32 character offsets in the LSP range "range" 81 | # to the encoding negotiated with the language server. 82 | # 83 | # Modifies in-place the UTF-32 offset in range.start.character and 84 | # range.end.character to a UTF-8 or UTF-16 or UTF-32 offset. 85 | export def EncodeRange(lspserver: dict, bnr: number, 86 | range: dict>) 87 | if has('patch-9.0.1629') 88 | if lspserver.posEncoding == 32 89 | return 90 | endif 91 | 92 | EncodePosition(lspserver, bnr, range.start) 93 | EncodePosition(lspserver, bnr, range.end) 94 | endif 95 | enddef 96 | 97 | # Decode the start and end character offsets in the LSP range "range" to 98 | # UTF-32 offsets. 99 | # 100 | # Modifies in-place the offset value in range.start.character and 101 | # range.end.character to a UTF-32 offset. 102 | export def DecodeRange(lspserver: dict, bnr: number, 103 | range: dict>) 104 | if has('patch-9.0.1629') 105 | if lspserver.posEncoding == 32 106 | return 107 | endif 108 | 109 | DecodePosition(lspserver, bnr, range.start) 110 | DecodePosition(lspserver, bnr, range.end) 111 | endif 112 | enddef 113 | 114 | # Encode the range in the LSP position "location" to the encoding negotiated 115 | # with the language server. 116 | # 117 | # Modifies in-place the UTF-32 offset in location.range to a UTF-8 or UTF-16 118 | # or UTF-32 offset. 119 | export def EncodeLocation(lspserver: dict, location: dict) 120 | if has('patch-9.0.1629') 121 | if lspserver.posEncoding == 32 122 | return 123 | endif 124 | 125 | var bnr = 0 126 | if location->has_key('targetUri') 127 | # LocationLink 128 | bnr = util.LspUriToBufnr(location.targetUri) 129 | if bnr > 0 130 | # We use only the "targetSelectionRange" item. The 131 | # "originSelectionRange" and the "targetRange" items are not used. 132 | lspserver.encodeRange(bnr, location.targetSelectionRange) 133 | endif 134 | else 135 | # Location 136 | bnr = util.LspUriToBufnr(location.uri) 137 | if bnr > 0 138 | lspserver.encodeRange(bnr, location.range) 139 | endif 140 | endif 141 | endif 142 | enddef 143 | 144 | # Decode the range in the LSP location "location" to UTF-32. 145 | # 146 | # Modifies in-place the offset value in location.range to a UTF-32 offset. 147 | export def DecodeLocation(lspserver: dict, location: dict) 148 | if has('patch-9.0.1629') 149 | if lspserver.posEncoding == 32 150 | return 151 | endif 152 | 153 | var bnr = 0 154 | if location->has_key('targetUri') 155 | # LocationLink 156 | bnr = util.LspUriToBufnr(location.targetUri) 157 | # We use only the "targetSelectionRange" item. The 158 | # "originSelectionRange" and the "targetRange" items are not used. 159 | lspserver.decodeRange(bnr, location.targetSelectionRange) 160 | else 161 | # Location 162 | bnr = util.LspUriToBufnr(location.uri) 163 | lspserver.decodeRange(bnr, location.range) 164 | endif 165 | endif 166 | enddef 167 | 168 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 169 | -------------------------------------------------------------------------------- /autoload/lsp/options.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | export const COMPLETIONMATCHER_CASE = 1 4 | export const COMPLETIONMATCHER_ICASE = 2 5 | export const COMPLETIONMATCHER_FUZZY = 3 6 | 7 | # LSP plugin options 8 | # User can override these by calling the OptionsSet() function. 9 | export var lspOptions: dict = { 10 | # Enable ale diagnostics support. 11 | # If true, diagnostics will be sent to ale, which will be responsible for 12 | # showing them. 13 | aleSupport: false, 14 | 15 | # In insert mode, complete the current symbol automatically 16 | # Otherwise, use omni-completion 17 | autoComplete: true, 18 | 19 | # In normal mode, highlight the current symbol automatically 20 | autoHighlight: false, 21 | 22 | # Automatically highlight diagnostics messages from LSP server 23 | autoHighlightDiags: true, 24 | 25 | # Automatically populate the location list with new diagnostics 26 | autoPopulateDiags: false, 27 | 28 | # icase | fuzzy | case match for language servers that replies with a full 29 | # list of completion items 30 | completionMatcher: 'case', 31 | 32 | # Due to a bug in the earlier versions of Vim, cannot use the 33 | # COMPLETIONMATCHER_CASE constant here for initialization. 34 | completionMatcherValue: 1, 35 | 36 | # diagnostics signs options 37 | diagSignErrorText: 'E>', 38 | diagSignHintText: 'H>', 39 | diagSignInfoText: 'I>', 40 | diagSignWarningText: 'W>', 41 | 42 | # In insert mode, echo the current symbol signature in the status line 43 | # instead of showing it in a popup 44 | echoSignature: false, 45 | 46 | # hide disabled code actions 47 | hideDisabledCodeActions: false, 48 | 49 | # Highlight diagnostics inline 50 | highlightDiagInline: true, 51 | 52 | # Show the symbol documentation in the preview window instead of in a popup 53 | hoverInPreview: false, 54 | 55 | # Don't print message when a configured language server is missing. 56 | ignoreMissingServer: false, 57 | 58 | # Focus on the location list window after ":LspDiag show" 59 | keepFocusInDiags: true, 60 | 61 | # Focus on the location list window after LspShowReferences 62 | keepFocusInReferences: true, 63 | 64 | # If true, apply the LSP server supplied text edits after a completion. 65 | # If a snippet plugin is going to apply the text edits, then set this to 66 | # false to avoid applying the text edits twice. 67 | completionTextEdit: true, 68 | 69 | # Alignment of virtual diagnostic text, when showDiagWithVirtualText is true 70 | # Allowed values: 'above' | 'below' | 'after' (default is 'above') 71 | diagVirtualTextAlign: 'above', 72 | 73 | # Wrapping of virtual diagnostic text, when showDiagWithVirtualText is true. 74 | # Allowed valuse: 'default' | 'truncate' | 'wrap' (default is 'default') 75 | diagVirtualTextWrap: 'default', 76 | 77 | # Suppress adding a new line on completion selection with 78 | noNewlineInCompletion: false, 79 | 80 | # Omni-completion support. To keep backward compatibility, this option is 81 | # set to null by default instead of false. 82 | omniComplete: null, 83 | 84 | # Open outline window on right side 85 | outlineOnRight: false, 86 | 87 | # Outline window size 88 | outlineWinSize: 20, 89 | 90 | popupBorder: false, 91 | 92 | popupBorderChars: ['─', '│', '─', '│', '╭', '╮', '╯', '╰'], 93 | 94 | popupBorderHighlight: 'LspPopupBorder', 95 | 96 | popupHighlight: 'LspPopup', 97 | 98 | # Optional overrideable popup options: 99 | # popupBorderCodeAction 100 | # popupBorderHighlightCodeAction 101 | # popupHighlightCodeAction 102 | # popupBorderCompletion 103 | # popupBorderHighlightCompletion 104 | # popupHighlightCompletion 105 | # popupBorderDiag 106 | # popupBorderHighlightDiag 107 | # popupHighlightDiag 108 | # popupBorderHover 109 | # popupBorderHighlightHover 110 | # popupHighlightHover 111 | # popupBorderPeek 112 | # popupBorderHighlightPeek 113 | # popupHighlightPeek 114 | # popupBorderSignatureHelp 115 | # popupBorderHighlightSignatureHelp 116 | # popupHighlightSignatureHelp 117 | # popupBorderSymbolMenu 118 | # popupBorderHighlightSymbolMenu 119 | # popupHighlightSymbolMenu 120 | # popupBorderSymbolMenuInput 121 | # popupBorderHighlightSymbolMenuInput 122 | # popupHighlightSymbolMenuInput 123 | # popupBorderTypeHierarchy 124 | # popupBorderHighlightTypeHierarchy 125 | # popupHighlightTypeHierarchy 126 | 127 | # Enable semantic highlighting 128 | semanticHighlight: false, 129 | 130 | # Show diagnostic text in a balloon when the mouse is over the diagnostic 131 | showDiagInBalloon: true, 132 | 133 | # Make diagnostics show in a popup instead of echoing 134 | showDiagInPopup: true, 135 | 136 | # Suppress diagnostic hover from appearing when the mouse is over the line 137 | # Show a diagnostic message on a status line 138 | showDiagOnStatusLine: false, 139 | 140 | # Show a diagnostic messages using signs 141 | showDiagWithSign: true, 142 | 143 | # Show a diagnostic messages with virtual text 144 | showDiagWithVirtualText: false, 145 | 146 | # enable inlay hints 147 | showInlayHints: false, 148 | 149 | # In insert mode, show the current symbol signature automatically 150 | showSignature: true, 151 | 152 | # enable snippet completion support 153 | snippetSupport: false, 154 | 155 | # enable SirVer/ultisnips completion support 156 | ultisnipsSupport: false, 157 | 158 | # add to autocomplition list current buffer words 159 | useBufferCompletion: false, 160 | 161 | # Use a floating menu to show the code action menu instead of asking for 162 | # input 163 | usePopupInCodeAction: false, 164 | 165 | # ShowReferences in a quickfix list instead of a location list` 166 | useQuickfixForLocations: false, 167 | 168 | # enable hrsh7th/vim-vsnip completion support 169 | vsnipSupport: false, 170 | 171 | # Limit the time autocompletion searches for words in current buffer (in 172 | # milliseconds) 173 | bufferCompletionTimeout: 100, 174 | 175 | # Enable support for custom completion kinds 176 | customCompletionKinds: false, 177 | 178 | # A dictionary with all completion kinds that you want to customize 179 | completionKinds: {}, 180 | 181 | # Filter duplicate completion items 182 | filterCompletionDuplicates: false, 183 | 184 | # Condenses the completion menu items to single (key-)words (plus kind) 185 | condensedCompletionMenu: false, 186 | 187 | # Ignore >ItemsIsIncomplete< messages from misbehaving servers: 188 | ignoreCompleteItemsIsIncomplete: [], 189 | } 190 | 191 | # set the LSP plugin options from the user provided option values 192 | export def OptionsSet(opts: dict) 193 | lspOptions->extend(opts) 194 | if !has('patch-9.0.0178') 195 | lspOptions.showInlayHints = false 196 | endif 197 | if !has('patch-9.0.1157') 198 | lspOptions.showDiagWithVirtualText = false 199 | endif 200 | 201 | # For faster comparison, convert the 'completionMatcher' option value from a 202 | # string to a number. 203 | if lspOptions.completionMatcher == 'icase' 204 | lspOptions.completionMatcherValue = COMPLETIONMATCHER_ICASE 205 | elseif lspOptions.completionMatcher == 'fuzzy' 206 | lspOptions.completionMatcherValue = COMPLETIONMATCHER_FUZZY 207 | else 208 | lspOptions.completionMatcherValue = COMPLETIONMATCHER_CASE 209 | endif 210 | 211 | # Apply the changed options 212 | if exists('#LspCmds#User#LspOptionsChanged') 213 | :doautocmd LspCmds User LspOptionsChanged 214 | endif 215 | enddef 216 | 217 | # return a copy of the LSP plugin options 218 | export def OptionsGet(): dict 219 | return lspOptions->deepcopy() 220 | enddef 221 | 222 | def PopupOptionGet(type: string, optionName: string): any 223 | return get(lspOptions, 224 | optionName .. type, # e.g. popupHighlightHover 225 | get(lspOptions, optionName)) # e.g. popupHighlight 226 | enddef 227 | 228 | # Set generic configurable popup options. These may be overridden by popup type 229 | # if users have configured those options, e.g. popupHighlightHover will be used 230 | # as the highlight group for "Hover" type popups if configured, otherwise hover 231 | # popups will fall back to the standard popupHighlight option. 232 | export def PopupConfigure(type: string, popupAttrs: dict): dict 233 | popupAttrs.highlight = PopupOptionGet(type, 'popupHighlight') 234 | if PopupOptionGet(type, 'popupBorder') 235 | popupAttrs.border = [] 236 | popupAttrs.borderchars = lspOptions.popupBorderChars 237 | popupAttrs.borderhighlight = [PopupOptionGet(type, 'popupBorderHighlight')] 238 | endif 239 | return popupAttrs 240 | enddef 241 | 242 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 243 | -------------------------------------------------------------------------------- /autoload/lsp/outline.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | import './util.vim' 4 | import './options.vim' as opt 5 | 6 | # jump to a symbol selected in the outline window 7 | def OutlineJumpToSymbol() 8 | var lnum: number = line('.') - 1 9 | if w:lspSymbols.lnumTable[lnum]->empty() 10 | return 11 | endif 12 | 13 | var slnum: number = w:lspSymbols.lnumTable[lnum].lnum 14 | var scol: number = w:lspSymbols.lnumTable[lnum].col 15 | var fname: string = w:lspSymbols.filename 16 | 17 | # Highlight the selected symbol 18 | prop_remove({type: 'LspOutlineHighlight'}) 19 | var col: number = getline('.')->match('\S') + 1 20 | prop_add(line('.'), col, {type: 'LspOutlineHighlight', 21 | length: w:lspSymbols.lnumTable[lnum].name->len()}) 22 | 23 | # disable the outline window refresh 24 | skipRefresh = true 25 | 26 | # If the file is already opened in a window, jump to it. Otherwise open it 27 | # in another window 28 | var wid: number = fname->bufwinid() 29 | if wid == -1 30 | # Find a window showing a normal buffer and use it 31 | for w in getwininfo() 32 | if w.winid->getwinvar('&buftype')->empty() 33 | wid = w.winid 34 | wid->win_gotoid() 35 | break 36 | endif 37 | endfor 38 | if wid == -1 39 | var symWinid: number = win_getid() 40 | :rightbelow vnew 41 | # retain the fixed symbol window width 42 | win_execute(symWinid, 'vertical resize 20') 43 | endif 44 | 45 | exe $'edit {fname}' 46 | else 47 | wid->win_gotoid() 48 | endif 49 | [slnum, scol]->cursor() 50 | skipRefresh = false 51 | enddef 52 | 53 | # Skip refreshing the outline window. Used to prevent recursive updates to the 54 | # outline window 55 | var skipRefresh: bool = false 56 | 57 | export def SkipOutlineRefresh(): bool 58 | return skipRefresh 59 | enddef 60 | 61 | def AddSymbolText(bnr: number, 62 | symbolTypeTable: dict>>, 63 | pfx: string, 64 | text: list, 65 | lnumMap: list>, 66 | children: bool) 67 | var prefix: string = pfx .. ' ' 68 | for [symType, symbols] in symbolTypeTable->items() 69 | if !children 70 | # Add an empty line for the top level symbol types. For types in the 71 | # children symbols, don't add the empty line. 72 | text->extend(['']) 73 | lnumMap->extend([{}]) 74 | endif 75 | if children 76 | text->extend([prefix .. symType]) 77 | prefix ..= ' ' 78 | else 79 | text->extend([symType]) 80 | endif 81 | lnumMap->extend([{}]) 82 | for s in symbols 83 | text->add(prefix .. s.name) 84 | # remember the line number for the symbol 85 | var s_start = s.range.start 86 | var start_col: number = util.GetLineByteFromPos(bnr, s_start) + 1 87 | lnumMap->add({name: s.name, lnum: s_start.line + 1, 88 | col: start_col}) 89 | s.outlineLine = lnumMap->len() 90 | if s->has_key('children') && !s.children->empty() 91 | AddSymbolText(bnr, s.children, prefix, text, lnumMap, true) 92 | endif 93 | endfor 94 | endfor 95 | enddef 96 | 97 | # update the symbols displayed in the outline window 98 | export def UpdateOutlineWindow(fname: string, 99 | symbolTypeTable: dict>>, 100 | symbolLineTable: list>) 101 | var wid: number = bufwinid('LSP-Outline') 102 | if wid == -1 103 | return 104 | endif 105 | 106 | # stop refreshing the outline window recursively 107 | skipRefresh = true 108 | 109 | var prevWinID: number = win_getid() 110 | wid->win_gotoid() 111 | 112 | # if the file displayed in the outline window is same as the new file, then 113 | # save and restore the cursor position 114 | var symbols = wid->getwinvar('lspSymbols', {}) 115 | var saveCursor: list = [] 116 | if !symbols->empty() && symbols.filename == fname 117 | saveCursor = getcurpos() 118 | endif 119 | 120 | :setlocal modifiable 121 | :silent! :%d _ 122 | setline(1, ['# LSP Outline View', 123 | $'# {fname->fnamemodify(":t")} ({fname->fnamemodify(":h")})']) 124 | 125 | # First two lines in the buffer display comment information 126 | var lnumMap: list> = [{}, {}] 127 | var text: list = [] 128 | AddSymbolText(fname->bufnr(), symbolTypeTable, '', text, lnumMap, false) 129 | text->append('$') 130 | w:lspSymbols = {filename: fname, lnumTable: lnumMap, 131 | symbolsByLine: symbolLineTable} 132 | :setlocal nomodifiable 133 | 134 | if !saveCursor->empty() 135 | saveCursor->setpos('.') 136 | endif 137 | 138 | prevWinID->win_gotoid() 139 | 140 | # Highlight the current symbol 141 | OutlineHighlightCurrentSymbol() 142 | 143 | # re-enable refreshing the outline window 144 | skipRefresh = false 145 | enddef 146 | 147 | def OutlineHighlightCurrentSymbol() 148 | var fname: string = expand('%')->fnamemodify(':p') 149 | if fname->empty() || &filetype->empty() 150 | return 151 | endif 152 | 153 | var wid: number = bufwinid('LSP-Outline') 154 | if wid == -1 155 | return 156 | endif 157 | 158 | # Check whether the symbols for this file are displayed in the outline 159 | # window 160 | var lspSymbols = wid->getwinvar('lspSymbols', {}) 161 | if lspSymbols->empty() || lspSymbols.filename != fname 162 | return 163 | endif 164 | 165 | var symbolTable: list> = lspSymbols.symbolsByLine 166 | 167 | # line number to locate the symbol 168 | var lnum: number = line('.') 169 | 170 | # Find the symbol for the current line number (binary search) 171 | var left: number = 0 172 | var right: number = symbolTable->len() - 1 173 | var mid: number 174 | while left <= right 175 | mid = (left + right) / 2 176 | var r = symbolTable[mid].range 177 | if lnum >= (r.start.line + 1) && lnum <= (r.end.line + 1) 178 | break 179 | endif 180 | if lnum > (r.start.line + 1) 181 | left = mid + 1 182 | else 183 | right = mid - 1 184 | endif 185 | endwhile 186 | 187 | # clear the highlighting in the outline window 188 | var bnr: number = wid->winbufnr() 189 | prop_remove({bufnr: bnr, type: 'LspOutlineHighlight'}) 190 | 191 | if left > right 192 | # symbol not found 193 | return 194 | endif 195 | 196 | # Highlight the selected symbol 197 | var col: number = 198 | bnr->getbufline(symbolTable[mid].outlineLine)->get(0, '')->match('\S') + 1 199 | prop_add(symbolTable[mid].outlineLine, col, 200 | {bufnr: bnr, type: 'LspOutlineHighlight', 201 | length: symbolTable[mid].name->len()}) 202 | 203 | # if the line is not visible, then scroll the outline window to make the 204 | # line visible 205 | var wininfo = wid->getwininfo() 206 | if symbolTable[mid].outlineLine < wininfo[0].topline 207 | || symbolTable[mid].outlineLine > wininfo[0].botline 208 | var cmd: string = $'call cursor({symbolTable[mid].outlineLine}, 1) | normal z.' 209 | win_execute(wid, cmd) 210 | endif 211 | enddef 212 | 213 | # when the outline window is closed, do the cleanup 214 | def OutlineCleanup() 215 | # Remove the outline autocommands 216 | :silent! autocmd_delete([{group: 'LSPOutline'}]) 217 | 218 | :silent! syntax clear LSPTitle 219 | enddef 220 | 221 | # open the symbol outline window 222 | export def OpenOutlineWindow(cmdmods: string, winsize: number) 223 | var wid: number = bufwinid('LSP-Outline') 224 | if wid != -1 225 | return 226 | endif 227 | 228 | var prevWinID: number = win_getid() 229 | 230 | var mods = cmdmods 231 | if mods->empty() 232 | if opt.lspOptions.outlineOnRight 233 | mods = ':vert :botright' 234 | else 235 | mods = ':vert :topleft' 236 | endif 237 | endif 238 | 239 | var size = winsize 240 | if size == 0 241 | size = opt.lspOptions.outlineWinSize 242 | endif 243 | 244 | execute $'{mods} :{size}new LSP-Outline' 245 | :setlocal modifiable 246 | :setlocal noreadonly 247 | :silent! :%d _ 248 | :setlocal buftype=nofile 249 | :setlocal bufhidden=delete 250 | :setlocal noswapfile nobuflisted 251 | :setlocal nonumber norelativenumber fdc=0 nowrap winfixheight winfixwidth 252 | :setlocal shiftwidth=2 253 | :setlocal foldenable 254 | :setlocal foldcolumn=4 255 | :setlocal foldlevel=4 256 | :setlocal foldmethod=indent 257 | setline(1, ['# File Outline']) 258 | :nnoremap q :quit 259 | :nnoremap :call OutlineJumpToSymbol() 260 | :setlocal nomodifiable 261 | 262 | # highlight all the symbol types 263 | :syntax keyword LSPTitle File Module Namespace Package Class Method Property 264 | :syntax keyword LSPTitle Field Constructor Enum Interface Function Variable 265 | :syntax keyword LSPTitle Constant String Number Boolean Array Object Key Null 266 | :syntax keyword LSPTitle EnumMember Struct Event Operator TypeParameter 267 | 268 | if str2nr(&t_Co) > 2 269 | :highlight clear LSPTitle 270 | :highlight default link LSPTitle Title 271 | endif 272 | 273 | prop_type_add('LspOutlineHighlight', {bufnr: bufnr(), highlight: 'Search', override: true}) 274 | 275 | try 276 | autocmd_delete([{group: 'LSPOutline', event: '*'}]) 277 | catch /E367:/ 278 | endtry 279 | var acmds: list> 280 | 281 | # Refresh or add the symbols in a buffer to the outline window 282 | acmds->add({event: 'BufEnter', 283 | group: 'LSPOutline', 284 | pattern: '*', 285 | cmd: 'call g:LspRequestDocSymbols()'}) 286 | 287 | # when the outline window is closed, do the cleanup 288 | acmds->add({event: 'BufUnload', 289 | group: 'LSPOutline', 290 | pattern: 'LSP-Outline', 291 | cmd: 'OutlineCleanup()'}) 292 | 293 | # Highlight the current symbol when the cursor is not moved for sometime 294 | acmds->add({event: 'CursorHold', 295 | group: 'LSPOutline', 296 | pattern: '*', 297 | cmd: 'OutlineHighlightCurrentSymbol()'}) 298 | 299 | autocmd_add(acmds) 300 | 301 | prevWinID->win_gotoid() 302 | enddef 303 | 304 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 305 | -------------------------------------------------------------------------------- /autoload/lsp/selection.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Functions related to handling LSP range selection. 4 | 5 | import './util.vim' 6 | 7 | # Visually (character-wise) select the text in a range 8 | def SelectText(bnr: number, range: dict>) 9 | var rstart = range.start 10 | var rend = range.end 11 | var start_col: number = util.GetLineByteFromPos(bnr, rstart) + 1 12 | var end_col: number = util.GetLineByteFromPos(bnr, rend) 13 | 14 | :normal! v"_y 15 | setcharpos("'<", [0, rstart.line + 1, start_col, 0]) 16 | setcharpos("'>", [0, rend.line + 1, end_col, 0]) 17 | :normal! gv 18 | enddef 19 | 20 | # Process the range selection reply from LSP server and start a new selection 21 | export def SelectionStart(lspserver: dict, sel: list>) 22 | if sel->empty() 23 | return 24 | endif 25 | 26 | var bnr: number = bufnr() 27 | 28 | # save the reply for expanding or shrinking the selected text. 29 | lspserver.selection = {bnr: bnr, selRange: sel[0], index: 0} 30 | 31 | SelectText(bnr, sel[0].range) 32 | enddef 33 | 34 | # Locate the range in the LSP reply at a specified level 35 | def GetSelRangeAtLevel(selRange: dict, level: number): dict 36 | var r: dict = selRange 37 | var idx: number = 0 38 | 39 | while idx != level 40 | if !r->has_key('parent') 41 | break 42 | endif 43 | r = r.parent 44 | idx += 1 45 | endwhile 46 | 47 | return r 48 | enddef 49 | 50 | # Returns true if the current visual selection matches a range in the 51 | # selection reply from LSP. 52 | def SelectionFromLSP(range: dict, startpos: list, endpos: list): bool 53 | var rstart = range.start 54 | var rend = range.end 55 | return startpos[1] == rstart.line + 1 56 | && endpos[1] == rend.line + 1 57 | && startpos[2] == rstart.character + 1 58 | && endpos[2] == rend.character 59 | enddef 60 | 61 | # Expand or Shrink the current selection or start a new one. 62 | export def SelectionModify(lspserver: dict, expand: bool) 63 | var fname: string = @% 64 | var bnr: number = bufnr() 65 | 66 | if mode() == 'v' && !lspserver.selection->empty() 67 | && lspserver.selection.bnr == bnr 68 | && !lspserver.selection->empty() 69 | # Already in characterwise visual mode and the previous LSP selection 70 | # reply for this buffer is available. Modify the current selection. 71 | 72 | var selRange: dict = lspserver.selection.selRange 73 | var startpos: list = getcharpos('v') 74 | var endpos: list = getcharpos('.') 75 | var idx: number = lspserver.selection.index 76 | 77 | # Locate the range in the LSP reply for the current selection 78 | selRange = GetSelRangeAtLevel(selRange, lspserver.selection.index) 79 | 80 | # If the current selection is present in the LSP reply, then modify the 81 | # selection 82 | if SelectionFromLSP(selRange.range, startpos, endpos) 83 | if expand 84 | # expand the selection 85 | if selRange->has_key('parent') 86 | selRange = selRange.parent 87 | lspserver.selection.index = idx + 1 88 | endif 89 | else 90 | # shrink the selection 91 | if idx > 0 92 | idx -= 1 93 | selRange = GetSelRangeAtLevel(lspserver.selection.selRange, idx) 94 | lspserver.selection.index = idx 95 | endif 96 | endif 97 | 98 | SelectText(bnr, selRange.range) 99 | return 100 | endif 101 | endif 102 | 103 | # Start a new selection 104 | lspserver.selectionRange(fname) 105 | enddef 106 | 107 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 108 | -------------------------------------------------------------------------------- /autoload/lsp/semantichighlight.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # LSP semantic highlighting functions 4 | 5 | import './offset.vim' 6 | import './options.vim' as opt 7 | import './buffer.vim' as buf 8 | 9 | # Map token type names to higlight group/text property type names 10 | var TokenTypeMap: dict = { 11 | 'namespace': 'LspSemanticNamespace', 12 | 'type': 'LspSemanticType', 13 | 'class': 'LspSemanticClass', 14 | 'enum': 'LspSemanticEnum', 15 | 'interface': 'LspSemanticInterface', 16 | 'struct': 'LspSemanticStruct', 17 | 'typeParameter': 'LspSemanticTypeParameter', 18 | 'parameter': 'LspSemanticParameter', 19 | 'variable': 'LspSemanticVariable', 20 | 'property': 'LspSemanticProperty', 21 | 'enumMember': 'LspSemanticEnumMember', 22 | 'event': 'LspSemanticEvent', 23 | 'function': 'LspSemanticFunction', 24 | 'method': 'LspSemanticMethod', 25 | 'macro': 'LspSemanticMacro', 26 | 'keyword': 'LspSemanticKeyword', 27 | 'modifier': 'LspSemanticModifier', 28 | 'comment': 'LspSemanticComment', 29 | 'string': 'LspSemanticString', 30 | 'number': 'LspSemanticNumber', 31 | 'regexp': 'LspSemanticRegexp', 32 | 'operator': 'LspSemanticOperator', 33 | 'decorator': 'LspSemanticDecorator' 34 | } 35 | 36 | export def InitOnce() 37 | # Define the default semantic token type highlight groups 38 | hlset([ 39 | {name: 'LspSemanticNamespace', default: true, linksto: 'Type'}, 40 | {name: 'LspSemanticType', default: true, linksto: 'Type'}, 41 | {name: 'LspSemanticClass', default: true, linksto: 'Type'}, 42 | {name: 'LspSemanticEnum', default: true, linksto: 'Type'}, 43 | {name: 'LspSemanticInterface', default: true, linksto: 'TypeDef'}, 44 | {name: 'LspSemanticStruct', default: true, linksto: 'Type'}, 45 | {name: 'LspSemanticTypeParameter', default: true, linksto: 'Type'}, 46 | {name: 'LspSemanticParameter', default: true, linksto: 'Identifier'}, 47 | {name: 'LspSemanticVariable', default: true, linksto: 'Identifier'}, 48 | {name: 'LspSemanticProperty', default: true, linksto: 'Identifier'}, 49 | {name: 'LspSemanticEnumMember', default: true, linksto: 'Constant'}, 50 | {name: 'LspSemanticEvent', default: true, linksto: 'Identifier'}, 51 | {name: 'LspSemanticFunction', default: true, linksto: 'Function'}, 52 | {name: 'LspSemanticMethod', default: true, linksto: 'Function'}, 53 | {name: 'LspSemanticMacro', default: true, linksto: 'Macro'}, 54 | {name: 'LspSemanticKeyword', default: true, linksto: 'Keyword'}, 55 | {name: 'LspSemanticModifier', default: true, linksto: 'Type'}, 56 | {name: 'LspSemanticComment', default: true, linksto: 'Comment'}, 57 | {name: 'LspSemanticString', default: true, linksto: 'String'}, 58 | {name: 'LspSemanticNumber', default: true, linksto: 'Number'}, 59 | {name: 'LspSemanticRegexp', default: true, linksto: 'String'}, 60 | {name: 'LspSemanticOperator', default: true, linksto: 'Operator'}, 61 | {name: 'LspSemanticDecorator', default: true, linksto: 'Macro'} 62 | ]) 63 | 64 | for hlName in TokenTypeMap->values() 65 | prop_type_add(hlName, {highlight: hlName, combine: true}) 66 | endfor 67 | enddef 68 | 69 | def ParseSemanticTokenMods(lspserverTokenMods: list, tokenMods: number): string 70 | var n = tokenMods 71 | var tokenMod: number 72 | var str = '' 73 | 74 | while n > 0 75 | tokenMod = float2nr(log10(and(n, invert(n - 1))) / log10(2)) 76 | str = $'{str}{lspserverTokenMods[tokenMod]},' 77 | n = and(n, n - 1) 78 | endwhile 79 | 80 | return str 81 | enddef 82 | 83 | # Apply the edit operations in a semantic tokens delta update message 84 | # (SemanticTokensDelta) from the language server. 85 | # 86 | # The previous list of tokens are stored in the buffer-local 87 | # LspSemanticTokensData variable. After applying the edits in 88 | # semTokens.edits, the new set of tokens are returned in semTokens.data. 89 | def ApplySemanticTokenEdits(bnr: number, semTokens: dict) 90 | if semTokens.edits->empty() 91 | return 92 | endif 93 | 94 | # Need to sort the edits and apply the last edit first. 95 | semTokens.edits->sort((a: dict, b: dict) => a.start - b.start) 96 | 97 | # TODO: Remove this code 98 | # var d = bnr->getbufvar('LspSemanticTokensData', []) 99 | # for e in semTokens.edits 100 | # var insertData = e->get('data', []) 101 | # d = (e.start > 0 ? d[: e.start - 1] : []) + insertData + 102 | # d[e.start + e.deleteCount :] 103 | # endfor 104 | # semTokens.data = d 105 | 106 | var oldTokens = bnr->getbufvar('LspSemanticTokensData', []) 107 | var newTokens = [] 108 | var idx = 0 109 | for e in semTokens.edits 110 | if e.start > 0 111 | newTokens->extend(oldTokens[idx : e.start - 1]) 112 | endif 113 | newTokens->extend(e->get('data', [])) 114 | idx = e.start + e.deleteCount 115 | endfor 116 | newTokens->extend(oldTokens[idx : ]) 117 | semTokens.data = newTokens 118 | enddef 119 | 120 | # Process a list of semantic tokens and return the corresponding text 121 | # properties for highlighting. 122 | def ProcessSemanticTokens(lspserver: dict, bnr: number, tokens: list): dict>> 123 | var props: dict>> = {} 124 | var tokenLine: number = 0 125 | var startChar: number = 0 126 | var length: number = 0 127 | var tokenType: number = 0 128 | var tokenMods: number = 0 129 | var prevTokenLine = 0 130 | var lnum = 1 131 | var charIdx = 0 132 | 133 | var lspserverTokenTypes: list = 134 | lspserver.semanticTokensLegend.tokenTypes 135 | var lspserverTokenMods: list = 136 | lspserver.semanticTokensLegend.tokenModifiers 137 | 138 | # Each semantic token uses 5 items in the tokens List 139 | var i = 0 140 | while i < tokens->len() 141 | tokenLine = tokens[i] 142 | # tokenLine is relative to the previous token line number 143 | lnum += tokenLine 144 | if prevTokenLine != lnum 145 | # this token is on a different line from the previous token 146 | charIdx = 0 147 | prevTokenLine = lnum 148 | endif 149 | startChar = tokens[i + 1] 150 | charIdx += startChar 151 | length = tokens[i + 2] 152 | tokenType = tokens[i + 3] 153 | tokenMods = tokens[i + 4] 154 | 155 | var typeStr = lspserverTokenTypes[tokenType] 156 | var modStr = ParseSemanticTokenMods(lspserverTokenMods, tokenMods) 157 | 158 | # Decode the semantic token line number, column number and length to 159 | # UTF-32 encoding. 160 | var r = { 161 | start: { 162 | line: lnum - 1, 163 | character: charIdx 164 | }, 165 | end: { 166 | line: lnum - 1, 167 | character: charIdx + length 168 | } 169 | } 170 | offset.DecodeRange(lspserver, bnr, r) 171 | 172 | if !props->has_key(typeStr) 173 | props[typeStr] = [] 174 | endif 175 | props[typeStr]->add([lnum, r.start.character + 1, lnum, r.end.character + 1]) 176 | 177 | i += 5 178 | endwhile 179 | 180 | return props 181 | enddef 182 | 183 | # Parse the semantic highlight reply from the language server and update the 184 | # text properties 185 | export def UpdateTokens(lspserver: dict, bnr: number, semTokens: dict) 186 | 187 | if semTokens->has_key('edits') 188 | # Delta semantic update. Need to sort the edits and apply the last edit 189 | # first. 190 | ApplySemanticTokenEdits(bnr, semTokens) 191 | endif 192 | 193 | # Cache the semantic tokens in a buffer-local variable, it will be used 194 | # later for a delta update. 195 | setbufvar(bnr, 'LspSemanticResultId', semTokens->get('resultId', '')) 196 | if !semTokens->has_key('data') 197 | return 198 | endif 199 | setbufvar(bnr, 'LspSemanticTokensData', semTokens.data) 200 | 201 | var props: dict>> 202 | props = ProcessSemanticTokens(lspserver, bnr, semTokens.data) 203 | 204 | # First clear all the previous text properties 205 | if has('patch-9.0.0233') 206 | prop_remove({types: TokenTypeMap->values(), bufnr: bnr, all: true}) 207 | else 208 | for propName in TokenTypeMap->values() 209 | prop_remove({type: propName, bufnr: bnr, all: true}) 210 | endfor 211 | endif 212 | 213 | if props->empty() 214 | return 215 | endif 216 | 217 | # Apply the new text properties 218 | for tokenType in TokenTypeMap->keys() 219 | if props->has_key(tokenType) 220 | prop_add_list({bufnr: bnr, type: TokenTypeMap[tokenType]}, props[tokenType]) 221 | endif 222 | endfor 223 | enddef 224 | 225 | # Update the semantic highlighting for buffer "bnr" 226 | def LspUpdateSemanticHighlight(bnr: number) 227 | var lspserver: dict = buf.BufLspServerGet(bnr, 'semanticTokens') 228 | if lspserver->empty() 229 | return 230 | endif 231 | 232 | lspserver.semanticHighlightUpdate(bnr) 233 | enddef 234 | 235 | # Initialize the semantic highlighting for the buffer 'bnr' 236 | export def BufferInit(lspserver: dict, bnr: number) 237 | if !opt.lspOptions.semanticHighlight || !lspserver.isSemanticTokensProvider 238 | # no support for semantic highlighting 239 | return 240 | endif 241 | 242 | # Highlight all the semantic tokens 243 | LspUpdateSemanticHighlight(bnr) 244 | 245 | # buffer-local autocmds for semantic highlighting 246 | var acmds: list> = [] 247 | 248 | acmds->add({bufnr: bnr, 249 | event: 'TextChanged', 250 | group: 'LSPBufferAutocmds', 251 | cmd: $'LspUpdateSemanticHighlight({bnr})'}) 252 | acmds->add({bufnr: bnr, 253 | event: 'BufUnload', 254 | group: 'LSPBufferAutocmds', 255 | cmd: $"b:LspSemanticTokensData = [] | b:LspSemanticResultId = ''"}) 256 | 257 | autocmd_add(acmds) 258 | enddef 259 | 260 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 261 | -------------------------------------------------------------------------------- /autoload/lsp/signature.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Functions related to handling LSP symbol signature help. 4 | 5 | import './options.vim' as opt 6 | import './util.vim' 7 | import './buffer.vim' as buf 8 | 9 | # close the signature popup window 10 | def CloseSignaturePopup(lspserver: dict) 11 | if lspserver.signaturePopup != -1 12 | lspserver.signaturePopup->popup_close() 13 | endif 14 | lspserver.signaturePopup = -1 15 | enddef 16 | 17 | def CloseCurBufSignaturePopup() 18 | var lspserver: dict = buf.CurbufGetServer('signatureHelp') 19 | if lspserver->empty() 20 | return 21 | endif 22 | 23 | CloseSignaturePopup(lspserver) 24 | enddef 25 | 26 | # Show the signature using "textDocument/signatureHelp" LSP method 27 | # Invoked from an insert-mode mapping, so return an empty string. 28 | def g:LspShowSignature(): string 29 | var lspserver: dict = buf.CurbufGetServerChecked('signatureHelp') 30 | if lspserver->empty() 31 | return '' 32 | endif 33 | 34 | # first send all the changes in the current buffer to the LSP server 35 | listener_flush() 36 | lspserver.showSignature() 37 | return '' 38 | enddef 39 | 40 | export def InitOnce() 41 | hlset([{name: 'LspSigActiveParameter', default: true, linksto: 'LineNr'}]) 42 | enddef 43 | 44 | # Initialize the signature triggers for the current buffer 45 | export def BufferInit(lspserver: dict) 46 | if !lspserver.isSignatureHelpProvider 47 | || !lspserver.caps.signatureHelpProvider->has_key('triggerCharacters') 48 | # no support for signature help 49 | return 50 | endif 51 | 52 | if !opt.lspOptions.showSignature 53 | || !lspserver.featureEnabled('signatureHelp') 54 | # Show signature support is disabled 55 | return 56 | endif 57 | 58 | # map characters that trigger signature help 59 | for ch in lspserver.caps.signatureHelpProvider.triggerCharacters 60 | var mapChar = ch 61 | if ch =~ ' ' 62 | mapChar = '' 63 | endif 64 | exe $"inoremap {mapChar} {mapChar}=g:LspShowSignature()" 65 | endfor 66 | 67 | # close the signature popup when leaving insert mode 68 | autocmd_add([{bufnr: bufnr(), 69 | event: 'InsertLeave', 70 | cmd: 'CloseCurBufSignaturePopup()'}]) 71 | enddef 72 | 73 | # process the 'textDocument/signatureHelp' reply from the LSP server and 74 | # display the symbol signature help. 75 | # Result: SignatureHelp | null 76 | export def SignatureHelp(lspserver: dict, sighelp: any): void 77 | if sighelp->empty() 78 | CloseSignaturePopup(lspserver) 79 | return 80 | endif 81 | 82 | if sighelp.signatures->len() <= 0 83 | CloseSignaturePopup(lspserver) 84 | return 85 | endif 86 | 87 | var sigidx: number = 0 88 | if sighelp->has_key('activeSignature') 89 | sigidx = sighelp.activeSignature 90 | endif 91 | 92 | var sig: dict = sighelp.signatures[sigidx] 93 | var text: string = sig.label 94 | var hllen: number = 0 95 | var startcol: number = 0 96 | if sig->has_key('parameters') && sighelp->has_key('activeParameter') 97 | var params: list> = sig.parameters 98 | var params_len: number = params->len() 99 | var activeParam: number = sighelp.activeParameter 100 | if params_len > 0 && activeParam < params_len 101 | var paramInfo: dict = params[activeParam] 102 | var label: any = paramInfo.label 103 | if label->type() == v:t_string 104 | # label string 105 | var label_str: string = label 106 | hllen = label_str->len() 107 | startcol = text->stridx(label_str) 108 | else 109 | # [inclusive start offset, exclusive end offset] 110 | var label_offset: list = params[activeParam].label 111 | var start_offset: number = label_offset[0] 112 | var end_offset: number = label_offset[1] 113 | 114 | if has('patch-9.0.1629') 115 | # Convert UTF-16 offsets 116 | startcol = text->byteidx(start_offset, true) 117 | var endcol: number = text->byteidx(end_offset, true) 118 | hllen = endcol - startcol 119 | else 120 | startcol = start_offset 121 | hllen = end_offset - start_offset 122 | endif 123 | endif 124 | endif 125 | endif 126 | 127 | if opt.lspOptions.echoSignature 128 | :echon "\r\r" 129 | :echon '' 130 | :echon text->strpart(0, startcol) 131 | :echoh LspSigActiveParameter 132 | :echon text->strpart(startcol, hllen) 133 | :echoh None 134 | :echon text->strpart(startcol + hllen) 135 | else 136 | # Close the previous signature popup and open a new one 137 | lspserver.signaturePopup->popup_close() 138 | 139 | var popupAttrs = opt.PopupConfigure('SignatureHelp', { 140 | padding: [0, 1, 0, 1], 141 | moved: [col('.') - 1, 9999999], 142 | pos: 'botright' 143 | }) 144 | var popupID = text->popup_atcursor(popupAttrs) 145 | var bnr: number = popupID->winbufnr() 146 | prop_type_add('signature', {bufnr: bnr, highlight: 'LspSigActiveParameter'}) 147 | if hllen > 0 148 | prop_add(1, startcol + 1, {bufnr: bnr, length: hllen, type: 'signature'}) 149 | endif 150 | lspserver.signaturePopup = popupID 151 | endif 152 | enddef 153 | 154 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 155 | -------------------------------------------------------------------------------- /autoload/lsp/snippet.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Snippet support 4 | 5 | # Integration with the UltiSnips plugin 6 | export def CompletionUltiSnips(prefix: string, items: list>) 7 | call UltiSnips#SnippetsInCurrentScope(1) 8 | for key in matchfuzzy(g:current_ulti_dict_info->keys(), prefix) 9 | var item = g:current_ulti_dict_info[key] 10 | var parts = split(item.location, ':') 11 | var txt = parts[0]->readfile()[parts[1]->str2nr() : parts[1]->str2nr() + 20] 12 | var restxt = item.description .. "\n\n" 13 | for line in txt 14 | if line->empty() || line[0 : 6] == "snippet" 15 | break 16 | else 17 | restxt = restxt .. line .. "\n" 18 | endif 19 | endfor 20 | items->add({ 21 | label: key, 22 | data: { 23 | entryNames: [key], 24 | }, 25 | kind: 15, 26 | documentation: restxt, 27 | }) 28 | endfor 29 | enddef 30 | 31 | # Integration with the vim-vsnip plugin 32 | export def CompletionVsnip(items: list>) 33 | def Pattern(abbr: string): string 34 | var chars = escape(abbr, '\/?')->split('\zs') 35 | var chars_pattern = '\%(\V' .. chars->join('\m\|\V') .. '\m\)' 36 | var separator = chars[0] =~ '\a' ? '\<' : '' 37 | return $'{separator}\V{chars[0]}\m{chars_pattern}*$' 38 | enddef 39 | 40 | if charcol('.') == 1 41 | return 42 | endif 43 | var starttext = getline('.')->slice(0, charcol('.') - 1) 44 | for item in vsnip#get_complete_items(bufnr()) 45 | var match = starttext->matchstrpos(Pattern(item.abbr)) 46 | if match[0] != '' 47 | var user_data = item.user_data->json_decode() 48 | var documentation = [] 49 | for line in vsnip#to_string(user_data.vsnip.snippet)->split("\n") 50 | documentation->add(line) 51 | endfor 52 | items->add({ 53 | label: item.abbr, 54 | filterText: item.word, 55 | insertTextFormat: 2, 56 | textEdit: { 57 | newText: user_data.vsnip.snippet->join("\n"), 58 | range: { 59 | start: { 60 | line: line('.'), 61 | character: match[1], 62 | }, 63 | ['end']: { 64 | line: line('.'), 65 | character: match[2], 66 | }, 67 | }, 68 | }, 69 | data: { 70 | entryNames: [item.word], 71 | }, 72 | kind: 15, 73 | documentation: { 74 | kind: 'markdown', 75 | value: documentation->join("\n"), 76 | }, 77 | }) 78 | endif 79 | endfor 80 | enddef 81 | 82 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 83 | -------------------------------------------------------------------------------- /autoload/lsp/textedit.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | import './util.vim' 4 | 5 | # sort the list of edit operations in the descending order of line and column 6 | # numbers. 7 | # 'a': {'A': [lnum, col], 'B': [lnum, col]} 8 | # 'b': {'A': [lnum, col], 'B': [lnum, col]} 9 | def Edit_sort_func(a: dict, b: dict): number 10 | # line number 11 | if a.A[0] != b.A[0] 12 | return b.A[0] - a.A[0] 13 | endif 14 | # column number 15 | if a.A[1] != b.A[1] 16 | return b.A[1] - a.A[1] 17 | endif 18 | 19 | # Assume that the LSP sorted the lines correctly to begin with 20 | return b.idx - a.idx 21 | enddef 22 | 23 | # Replaces text in a range with new text. 24 | # 25 | # CAUTION: Changes in-place! 26 | # 27 | # 'lines': Original list of strings 28 | # 'A': Start position; [line, col] 29 | # 'B': End position [line, col] 30 | # 'new_lines' A list of strings to replace the original 31 | # 32 | # returns the modified 'lines' 33 | def Set_lines(lines: list, A: list, B: list, 34 | new_lines: list): list 35 | var i_0: number = A[0] 36 | 37 | # If it extends past the end, truncate it to the end. This is because the 38 | # way the LSP describes the range including the last newline is by 39 | # specifying a line number after what we would call the last line. 40 | var numlines: number = lines->len() 41 | var i_n = [B[0], numlines - 1]->min() 42 | 43 | if i_0 < 0 || i_0 >= numlines || i_n < 0 || i_n >= numlines 44 | #util.WarnMsg("set_lines: Invalid range, A = " .. A->string() 45 | # .. ", B = " .. B->string() .. ", numlines = " .. numlines 46 | # .. ", new lines = " .. new_lines->string()) 47 | var msg = $"set_lines: Invalid range, A = {A->string()}" 48 | msg ..= $", B = {B->string()}, numlines = {numlines}" 49 | msg ..= $", new lines = {new_lines->string()}" 50 | util.WarnMsg(msg) 51 | return lines 52 | endif 53 | 54 | # save the prefix and suffix text before doing the replacements 55 | var prefix: string = '' 56 | var suffix: string = lines[i_n][B[1] :] 57 | if A[1] > 0 58 | prefix = lines[i_0][0 : A[1] - 1] 59 | endif 60 | 61 | var new_lines_len: number = new_lines->len() 62 | 63 | #echomsg $"i_0 = {i_0}, i_n = {i_n}, new_lines = {string(new_lines)}" 64 | var n: number = i_n - i_0 + 1 65 | if n != new_lines_len 66 | if n > new_lines_len 67 | # remove the deleted lines 68 | lines->remove(i_0, i_0 + n - new_lines_len - 1) 69 | else 70 | # add empty lines for newly the added lines (will be replaced with the 71 | # actual lines below) 72 | lines->extend(repeat([''], new_lines_len - n), i_0) 73 | endif 74 | endif 75 | #echomsg $"lines(1) = {string(lines)}" 76 | 77 | # replace the previous lines with the new lines 78 | for i in new_lines_len->range() 79 | lines[i_0 + i] = new_lines[i] 80 | endfor 81 | #echomsg $"lines(2) = {string(lines)}" 82 | 83 | # append the suffix (if any) to the last line 84 | if suffix != '' 85 | var i = i_0 + new_lines_len - 1 86 | lines[i] = lines[i] .. suffix 87 | endif 88 | #echomsg $"lines(3) = {string(lines)}" 89 | 90 | # prepend the prefix (if any) to the first line 91 | if prefix != '' 92 | lines[i_0] = prefix .. lines[i_0] 93 | endif 94 | #echomsg $"lines(4) = {string(lines)}" 95 | 96 | return lines 97 | enddef 98 | 99 | # Apply set of text edits to the specified buffer 100 | # The text edit logic is ported from the Neovim lua implementation 101 | export def ApplyTextEdits(bnr: number, text_edits: list>): void 102 | if text_edits->empty() 103 | return 104 | endif 105 | 106 | # if the buffer is not loaded, load it and make it a listed buffer 107 | :silent! bnr->bufload() 108 | setbufvar(bnr, '&buflisted', true) 109 | 110 | var start_line: number = 4294967295 # 2 ^ 32 111 | var finish_line: number = -1 112 | var updated_edits: list> = [] 113 | var start_row: number 114 | var start_col: number 115 | var end_row: number 116 | var end_col: number 117 | 118 | # create a list of buffer positions where the edits have to be applied. 119 | var idx = 0 120 | for e in text_edits 121 | # Adjust the start and end columns for multibyte characters 122 | var r = e.range 123 | var rstart: dict = r.start 124 | var rend: dict = r.end 125 | start_row = rstart.line 126 | start_col = util.GetCharIdxWithoutCompChar(bnr, rstart) 127 | end_row = rend.line 128 | end_col = util.GetCharIdxWithoutCompChar(bnr, rend) 129 | start_line = [rstart.line, start_line]->min() 130 | finish_line = [rend.line, finish_line]->max() 131 | 132 | updated_edits->add({A: [start_row, start_col], 133 | B: [end_row, end_col], 134 | idx: idx, 135 | lines: e.newText->split("\n", true)}) 136 | idx += 1 137 | endfor 138 | 139 | # Reverse sort the edit operations by descending line and column numbers so 140 | # that they can be applied without interfering with each other. 141 | updated_edits->sort('Edit_sort_func') 142 | 143 | var lines: list = bnr->getbufline(start_line + 1, finish_line + 1) 144 | var fix_eol: bool = bnr->getbufvar('&fixeol') 145 | var set_eol = fix_eol && bnr->getbufinfo()[0].linecount <= finish_line + 1 146 | if !lines->empty() && set_eol && lines[-1]->len() != 0 147 | lines->add('') 148 | endif 149 | 150 | #echomsg $'lines(1) = {string(lines)}' 151 | #echomsg updated_edits 152 | 153 | for e in updated_edits 154 | var A: list = [e.A[0] - start_line, e.A[1]] 155 | var B: list = [e.B[0] - start_line, e.B[1]] 156 | lines = Set_lines(lines, A, B, e.lines) 157 | endfor 158 | 159 | #echomsg $'lines(2) = {string(lines)}' 160 | 161 | # If the last line is empty and we need to set EOL, then remove it. 162 | if !lines->empty() && set_eol && lines[-1]->len() == 0 163 | lines->remove(-1) 164 | endif 165 | 166 | #echomsg $'ApplyTextEdits: start_line = {start_line}, finish_line = {finish_line}' 167 | #echomsg $'lines = {string(lines)}' 168 | 169 | # if the buffer is empty, appending lines before the first line adds an 170 | # extra empty line at the end. Delete the empty line after appending the 171 | # lines. 172 | var dellastline: bool = false 173 | if start_line == 0 && bnr->getbufinfo()[0].linecount == 1 && 174 | bnr->getbufline(1)->get(0, '')->empty() 175 | dellastline = true 176 | endif 177 | 178 | # Now we apply the textedits to the actual buffer. 179 | # In theory we could just delete all old lines and append the new lines. 180 | # This would however cause the cursor to change position: It will always be 181 | # on the last line added. 182 | # 183 | # Luckily there is an even simpler solution, that has no cursor sideeffects. 184 | # 185 | # Logically this method is split into the following three cases: 186 | # 187 | # 1. The number of new lines is equal to the number of old lines: 188 | # Just replace the lines inline with setbufline() 189 | # 190 | # 2. The number of new lines is greater than the old ones: 191 | # First append the missing lines at the **end** of the range, then use 192 | # setbufline() again. This does not cause the cursor to change position. 193 | # 194 | # 3. The number of new lines is less than before: 195 | # First use setbufline() to replace the lines that we can replace. 196 | # Then remove superfluous lines. 197 | # 198 | # Luckily, the three different cases exist only logically, we can reduce 199 | # them to a single case practically, because appendbufline() does not append 200 | # anything if an empty list is passed just like deletebufline() does not 201 | # delete anything, if the last line of the range is before the first line. 202 | # We just need to be careful with all indices. 203 | appendbufline(bnr, finish_line + 1, lines[finish_line - start_line + 1 : -1]) 204 | setbufline(bnr, start_line + 1, lines) 205 | deletebufline(bnr, start_line + 1 + lines->len(), finish_line + 1) 206 | 207 | if dellastline 208 | bnr->deletebufline(bnr->getbufinfo()[0].linecount) 209 | endif 210 | enddef 211 | 212 | # interface TextDocumentEdit 213 | def ApplyTextDocumentEdit(textDocEdit: dict) 214 | var bnr: number = util.LspUriToBufnr(textDocEdit.textDocument.uri) 215 | if bnr == -1 216 | util.ErrMsg($'Text Document edit, buffer {textDocEdit.textDocument.uri} is not found') 217 | return 218 | endif 219 | ApplyTextEdits(bnr, textDocEdit.edits) 220 | enddef 221 | 222 | # interface CreateFile 223 | # Create the "createFile.uri" file 224 | def FileCreate(createFile: dict) 225 | var fname: string = util.LspUriToFile(createFile.uri) 226 | var opts: dict = createFile->get('options', {}) 227 | var ignoreIfExists: bool = opts->get('ignoreIfExists', true) 228 | var overwrite: bool = opts->get('overwrite', false) 229 | 230 | # LSP Spec: Overwrite wins over `ignoreIfExists` 231 | if fname->filereadable() && ignoreIfExists && !overwrite 232 | return 233 | endif 234 | 235 | fname->fnamemodify(':p:h')->mkdir('p') 236 | []->writefile(fname) 237 | fname->bufadd() 238 | enddef 239 | 240 | # interface DeleteFile 241 | # Delete the "deleteFile.uri" file 242 | def FileDelete(deleteFile: dict) 243 | var fname: string = util.LspUriToFile(deleteFile.uri) 244 | var opts: dict = deleteFile->get('options', {}) 245 | var recursive: bool = opts->get('recursive', false) 246 | var ignoreIfNotExists: bool = opts->get('ignoreIfNotExists', true) 247 | 248 | if !fname->filereadable() && ignoreIfNotExists 249 | return 250 | endif 251 | 252 | var flags: string = '' 253 | if recursive 254 | # # NOTE: is this a dangerous operation? The LSP server can send a 255 | # # DeleteFile message to recursively delete all the files in the disk. 256 | # flags = 'rf' 257 | util.ErrMsg($'Recursively deleting files is not supported') 258 | return 259 | elseif fname->isdirectory() 260 | flags = 'd' 261 | endif 262 | var bnr: number = fname->bufadd() 263 | fname->delete(flags) 264 | exe $'{bnr}bwipe!' 265 | enddef 266 | 267 | # interface RenameFile 268 | # Rename file "renameFile.oldUri" to "renameFile.newUri" 269 | def FileRename(renameFile: dict) 270 | var old_fname: string = util.LspUriToFile(renameFile.oldUri) 271 | var new_fname: string = util.LspUriToFile(renameFile.newUri) 272 | 273 | var opts: dict = renameFile->get('options', {}) 274 | var overwrite: bool = opts->get('overwrite', false) 275 | var ignoreIfExists: bool = opts->get('ignoreIfExists', true) 276 | 277 | if new_fname->filereadable() && (!overwrite || ignoreIfExists) 278 | return 279 | endif 280 | 281 | old_fname->rename(new_fname) 282 | enddef 283 | 284 | # interface WorkspaceEdit 285 | export def ApplyWorkspaceEdit(workspaceEdit: dict) 286 | if workspaceEdit->has_key('documentChanges') 287 | for change in workspaceEdit.documentChanges 288 | if change->has_key('kind') 289 | if change.kind == 'create' 290 | FileCreate(change) 291 | elseif change.kind == 'delete' 292 | FileDelete(change) 293 | elseif change.kind == 'rename' 294 | FileRename(change) 295 | else 296 | util.ErrMsg($'Unsupported change in workspace edit [{change.kind}]') 297 | endif 298 | else 299 | ApplyTextDocumentEdit(change) 300 | endif 301 | endfor 302 | return 303 | endif 304 | 305 | if !workspaceEdit->has_key('changes') 306 | return 307 | endif 308 | 309 | for [uri, changes] in workspaceEdit.changes->items() 310 | var bnr: number = util.LspUriToBufnr(uri) 311 | if bnr == 0 312 | # file is not present 313 | continue 314 | endif 315 | 316 | # interface TextEdit 317 | ApplyTextEdits(bnr, changes) 318 | endfor 319 | enddef 320 | 321 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 322 | -------------------------------------------------------------------------------- /autoload/lsp/typehierarchy.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Functions for dealing with type hierarchy (super types/sub types) 4 | 5 | import './options.vim' as opt 6 | import './util.vim' 7 | import './symbol.vim' 8 | 9 | # Parse the type hierarchy in "typeHier" and displays a tree of type names 10 | # in the current buffer. This function is called recursively to display the 11 | # super/sub type hierarchy. 12 | # 13 | # Returns the line number where the next type name should be added. 14 | def TypeTreeGenerate(isSuper: bool, typeHier: dict, pfx_arg: string, 15 | typeTree: list, typeUriMap: list>) 16 | 17 | var itemHasChildren = false 18 | if isSuper 19 | if typeHier->has_key('parents') && !typeHier.parents->empty() 20 | itemHasChildren = true 21 | endif 22 | else 23 | if typeHier->has_key('children') && !typeHier.children->empty() 24 | itemHasChildren = true 25 | endif 26 | endif 27 | 28 | var itemBranchPfx: string 29 | if itemHasChildren 30 | itemBranchPfx = '▾ ' 31 | else 32 | itemBranchPfx = pfx_arg->empty() ? '' : ' ' 33 | endif 34 | 35 | var typestr: string 36 | var kindstr = symbol.SymbolKindToName(typeHier.kind) 37 | if kindstr != '' 38 | typestr = $'{pfx_arg}{itemBranchPfx}{typeHier.name} ({kindstr[0]})' 39 | else 40 | typestr = $'{pfx_arg}{itemBranchPfx}{typeHier.name}' 41 | endif 42 | typeTree->add(typestr) 43 | typeUriMap->add(typeHier) 44 | 45 | # last item to process 46 | if !itemHasChildren 47 | return 48 | endif 49 | 50 | var items: list> 51 | items = isSuper ? typeHier.parents : typeHier.children 52 | 53 | for item in items 54 | TypeTreeGenerate(isSuper, item, $'{pfx_arg}| ', typeTree, typeUriMap) 55 | endfor 56 | enddef 57 | 58 | # Display a popup with the file containing a type and highlight the line and 59 | # the type name. 60 | def UpdateTypeHierFileInPopup(lspserver: dict, typeUriMap: list>) 61 | if lspserver.typeHierPopup->winbufnr() == -1 62 | return 63 | endif 64 | 65 | lspserver.typeHierFilePopup->popup_close() 66 | 67 | var n = line('.', lspserver.typeHierPopup) - 1 68 | var fname: string = util.LspUriToFile(typeUriMap[n].uri) 69 | 70 | var bnr = fname->bufadd() 71 | if bnr == 0 72 | return 73 | endif 74 | 75 | var popupAttrs = opt.PopupConfigure('TypeHierarchy', { 76 | title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})", 77 | wrap: false, 78 | fixed: true, 79 | minheight: 10, 80 | maxheight: 10, 81 | minwidth: winwidth(0) - 38, 82 | maxwidth: winwidth(0) - 38, 83 | cursorline: true, 84 | line: 'cursor+1', 85 | col: 1 86 | }) 87 | lspserver.typeHierFilePopup = popup_create(bnr, popupAttrs) 88 | var cmds =<< trim eval END 89 | [{typeUriMap[n].range.start.line + 1}, 1]->cursor() 90 | :normal! z. 91 | END 92 | win_execute(lspserver.typeHierFilePopup, cmds) 93 | 94 | lspserver.typeHierFilePopup->clearmatches() 95 | var start_col = util.GetLineByteFromPos(bnr, 96 | typeUriMap[n].selectionRange.start) + 1 97 | var end_col = util.GetLineByteFromPos(bnr, typeUriMap[n].selectionRange.end) 98 | var pos = [[typeUriMap[n].selectionRange.start.line + 1, 99 | start_col, end_col - start_col + 1]] 100 | matchaddpos('Search', pos, 10, -1, {window: lspserver.typeHierFilePopup}) 101 | enddef 102 | 103 | def TypeHierPopupFilter(lspserver: dict, typeUriMap: list>, 104 | popupID: number, key: string): bool 105 | popupID->popup_filter_menu(key) 106 | if lspserver.typeHierPopup->winbufnr() == -1 107 | # popup is closed 108 | if lspserver.typeHierFilePopup->winbufnr() != -1 109 | lspserver.typeHierFilePopup->popup_close() 110 | endif 111 | lspserver.typeHierFilePopup = -1 112 | lspserver.typeHierPopup = -1 113 | else 114 | UpdateTypeHierFileInPopup(lspserver, typeUriMap) 115 | endif 116 | 117 | return true 118 | enddef 119 | 120 | def TypeHierPopupCallback(lspserver: dict, typeUriMap: list>, 121 | popupID: number, selIdx: number) 122 | if lspserver.typeHierFilePopup->winbufnr() != -1 123 | lspserver.typeHierFilePopup->popup_close() 124 | endif 125 | lspserver.typeHierFilePopup = -1 126 | lspserver.typeHierPopup = -1 127 | 128 | if selIdx <= 0 129 | # popup is canceled 130 | return 131 | endif 132 | 133 | # Save the current cursor location in the tag stack. 134 | util.PushCursorToTagStack() 135 | util.JumpToLspLocation(typeUriMap[selIdx - 1], '') 136 | enddef 137 | 138 | # Show the super or sub type hierarchy items "types" as a tree in a popup 139 | # window 140 | export def ShowTypeHierarchy(lspserver: dict, isSuper: bool, types: dict) 141 | 142 | if lspserver.typeHierPopup->winbufnr() != -1 143 | # If the type hierarchy popup window is already present, close it. 144 | lspserver.typeHierPopup->popup_close() 145 | endif 146 | 147 | var typeTree: list 148 | var typeUriMap: list> 149 | 150 | # Generate a tree of the type hierarchy items 151 | TypeTreeGenerate(isSuper, types, '', typeTree, typeUriMap) 152 | 153 | # Display a popup window with the type hierarchy tree and a popup window for 154 | # the file. 155 | var popupAttrs = opt.PopupConfigure('TypeHierarchy', { 156 | title: $'{isSuper ? "Super" : "Sub"}Type Hierarchy', 157 | wrap: 0, 158 | pos: 'topleft', 159 | line: 'cursor+1', 160 | col: winwidth(0) - 34, 161 | minheight: 10, 162 | maxheight: 10, 163 | minwidth: 30, 164 | maxwidth: 30, 165 | mapping: false, 166 | fixed: true, 167 | filter: function(TypeHierPopupFilter, [lspserver, typeUriMap]), 168 | callback: function(TypeHierPopupCallback, [lspserver, typeUriMap]) 169 | }) 170 | lspserver.typeHierPopup = popup_menu(typeTree, popupAttrs) 171 | UpdateTypeHierFileInPopup(lspserver, typeUriMap) 172 | enddef 173 | 174 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 175 | -------------------------------------------------------------------------------- /autoload/lsp/util.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Display an info message 4 | export def InfoMsg(msg: string) 5 | :echohl Question 6 | :echomsg $'Info: {msg}' 7 | :echohl None 8 | enddef 9 | 10 | # Display a warning message 11 | export def WarnMsg(msg: string) 12 | :echohl WarningMsg 13 | :echomsg $'Warn: {msg}' 14 | :echohl None 15 | enddef 16 | 17 | # Display an error message 18 | export def ErrMsg(msg: string) 19 | :echohl Error 20 | :echomsg $'Error: {msg}' 21 | :echohl None 22 | enddef 23 | 24 | # Lsp server trace log directory 25 | var lsp_log_dir: string 26 | if has('unix') 27 | lsp_log_dir = '/tmp/' 28 | else 29 | lsp_log_dir = $TEMP .. '\\' 30 | endif 31 | 32 | # Log a message from the LSP server. stderr is true for logging messages 33 | # from the standard error and false for stdout. 34 | export def TraceLog(fname: string, stderr: bool, msg: string) 35 | if stderr 36 | writefile(msg->split("\n"), $'{lsp_log_dir}{fname}', 'a') 37 | else 38 | writefile([msg], $'{lsp_log_dir}{fname}', 'a') 39 | endif 40 | enddef 41 | 42 | # Empty out the LSP server trace logs 43 | export def ClearTraceLogs(fname: string) 44 | writefile([], $'{lsp_log_dir}{fname}') 45 | enddef 46 | 47 | # Open the LSP server debug messages file. 48 | export def ServerMessagesShow(fname: string) 49 | var fullname = $'{lsp_log_dir}{fname}' 50 | if !filereadable(fullname) 51 | WarnMsg($'File {fullname} is not found') 52 | return 53 | endif 54 | var wid = fullname->bufwinid() 55 | if wid == -1 56 | exe $'split {fullname}' 57 | else 58 | win_gotoid(wid) 59 | endif 60 | setlocal autoread 61 | setlocal bufhidden=wipe 62 | setlocal nomodified 63 | setlocal nomodifiable 64 | enddef 65 | 66 | # Parse a LSP Location or LocationLink type and return a List with two items. 67 | # The first item is the DocumentURI and the second item is the Range. 68 | export def LspLocationParse(lsploc: dict): list 69 | if lsploc->has_key('targetUri') 70 | # LocationLink 71 | return [lsploc.targetUri, lsploc.targetSelectionRange] 72 | else 73 | # Location 74 | return [lsploc.uri, lsploc.range] 75 | endif 76 | enddef 77 | 78 | # Convert a LSP file URI (file://) to a Vim file name 79 | export def LspUriToFile(uri: string): string 80 | # Replace all the %xx numbers (e.g. %20 for space) in the URI to character 81 | var uri_decoded: string = substitute(uri, '%\(\x\x\)', 82 | '\=nr2char(str2nr(submatch(1), 16))', 'g') 83 | 84 | # File URIs on MS-Windows start with file:///[a-zA-Z]:' 85 | if uri_decoded =~? '^file:///\a:' 86 | # MS-Windows URI 87 | uri_decoded = uri_decoded[8 : ] 88 | uri_decoded = uri_decoded->substitute('/', '\\', 'g') 89 | # On GNU/Linux (pattern not end with `:`) 90 | elseif uri_decoded =~? '^file:///\a' 91 | uri_decoded = uri_decoded[7 : ] 92 | endif 93 | 94 | return uri_decoded 95 | enddef 96 | 97 | # Convert a LSP file URI (file://) to a Vim buffer number. 98 | # If the file is not in a Vim buffer, then adds the buffer. 99 | # Returns 0 on error. 100 | export def LspUriToBufnr(uri: string): number 101 | return LspUriToFile(uri)->bufadd() 102 | enddef 103 | 104 | # Returns if the URI refers to a remote file (e.g. ssh://) 105 | # Credit: vim-lsp plugin 106 | export def LspUriRemote(uri: string): bool 107 | return uri =~ '^\w\+::' || uri =~ '^[a-z][a-z0-9+.-]*://' 108 | enddef 109 | 110 | var resolvedUris = {} 111 | 112 | # Convert a Vim filename to an LSP URI (file://) 113 | export def LspFileToUri(fname: string): string 114 | var fname_full: string = fname->fnamemodify(':p') 115 | 116 | if resolvedUris->has_key(fname_full) 117 | return resolvedUris[fname_full] 118 | endif 119 | 120 | var uri: string = fname_full 121 | 122 | if has("win32unix") 123 | # We're in Cygwin, convert POSIX style paths to Windows style. 124 | # The substitution is to remove the '^@' escape character from the end of 125 | # line. 126 | uri = system($'cygpath -m {uri}')->substitute('^\(\p*\).*$', '\=submatch(1)', "") 127 | endif 128 | 129 | var on_windows: bool = false 130 | if uri =~? '^\a:' 131 | on_windows = true 132 | endif 133 | 134 | if on_windows 135 | # MS-Windows 136 | uri = uri->substitute('\\', '/', 'g') 137 | endif 138 | 139 | uri = uri->substitute('\([^A-Za-z0-9-._~:/]\)', 140 | '\=printf("%%%02x", char2nr(submatch(1)))', 'g') 141 | 142 | if on_windows 143 | uri = $'file:///{uri}' 144 | else 145 | uri = $'file://{uri}' 146 | endif 147 | 148 | resolvedUris[fname_full] = uri 149 | return uri 150 | enddef 151 | 152 | # Convert a Vim buffer number to an LSP URI (file://) 153 | export def LspBufnrToUri(bnr: number): string 154 | return LspFileToUri(bnr->bufname()) 155 | enddef 156 | 157 | # Returns the byte number of the specified LSP position in buffer "bnr". 158 | # LSP's line and characters are 0-indexed. 159 | # Vim's line and columns are 1-indexed. 160 | # Returns a zero-indexed column. 161 | export def GetLineByteFromPos(bnr: number, pos: dict): number 162 | var col: number = pos.character 163 | # When on the first character, we can ignore the difference between byte and 164 | # character 165 | if col <= 0 166 | return col 167 | endif 168 | 169 | # Need a loaded buffer to read the line and compute the offset 170 | :silent! bnr->bufload() 171 | 172 | var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '') 173 | if ltext->empty() 174 | return col 175 | endif 176 | 177 | var byteIdx = ltext->byteidxcomp(col) 178 | if byteIdx != -1 179 | return byteIdx 180 | endif 181 | 182 | return col 183 | enddef 184 | 185 | # Get the index of the character at [pos.line, pos.character] in buffer "bnr" 186 | # without counting the composing characters. The LSP server counts composing 187 | # characters as separate characters whereas Vim string indexing ignores the 188 | # composing characters. 189 | export def GetCharIdxWithoutCompChar(bnr: number, pos: dict): number 190 | var col: number = pos.character 191 | # When on the first character, nothing to do. 192 | if col <= 0 193 | return col 194 | endif 195 | 196 | # Need a loaded buffer to read the line and compute the offset 197 | :silent! bnr->bufload() 198 | 199 | var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '') 200 | if ltext->empty() 201 | return col 202 | endif 203 | 204 | # Convert the character index that includes composing characters as separate 205 | # characters to a byte index and then back to a character index ignoring the 206 | # composing characters. 207 | var byteIdx = ltext->byteidxcomp(col) 208 | if byteIdx != -1 209 | if byteIdx == ltext->strlen() 210 | # Byte index points to the byte after the last byte. 211 | return ltext->strcharlen() 212 | else 213 | return ltext->charidx(byteIdx, false) 214 | endif 215 | endif 216 | 217 | return col 218 | enddef 219 | 220 | # Get the index of the character at [pos.line, pos.character] in buffer "bnr" 221 | # counting the composing characters as separate characters. The LSP server 222 | # counts composing characters as separate characters whereas Vim string 223 | # indexing ignores the composing characters. 224 | export def GetCharIdxWithCompChar(ltext: string, charIdx: number): number 225 | # When on the first character, nothing to do. 226 | if charIdx <= 0 || ltext->empty() 227 | return charIdx 228 | endif 229 | 230 | # Convert the character index that doesn't include composing characters as 231 | # separate characters to a byte index and then back to a character index 232 | # that includes the composing characters as separate characters 233 | var byteIdx = ltext->byteidx(charIdx) 234 | if byteIdx != -1 235 | if byteIdx == ltext->strlen() 236 | return ltext->strchars() 237 | else 238 | return ltext->charidx(byteIdx, true) 239 | endif 240 | endif 241 | 242 | return charIdx 243 | enddef 244 | 245 | # push the current location on to the tag stack 246 | export def PushCursorToTagStack() 247 | settagstack(winnr(), {items: [ 248 | { 249 | bufnr: bufnr(), 250 | from: getpos('.'), 251 | matchnr: 1, 252 | tagname: expand('') 253 | }]}, 't') 254 | enddef 255 | 256 | # Jump to the LSP "location". The "location" contains the file name, line 257 | # number and character number. The user specified window command modifiers 258 | # (e.g. topleft) are in "cmdmods". 259 | export def JumpToLspLocation(location: dict, cmdmods: string) 260 | var [uri, range] = LspLocationParse(location) 261 | var fname = LspUriToFile(uri) 262 | 263 | # jump to the file and line containing the symbol 264 | var bnr: number = fname->bufnr() 265 | if cmdmods->empty() 266 | if bnr == bufnr() 267 | # Set the previous cursor location mark. Instead of using setpos(), m' is 268 | # used so that the current location is added to the jump list. 269 | :normal m' 270 | else 271 | var wid = fname->bufwinid() 272 | if wid != -1 273 | wid->win_gotoid() 274 | else 275 | if bnr != -1 276 | # Reuse an existing buffer. If the current buffer has unsaved changes 277 | # and 'hidden' is not set or if the current buffer is a special 278 | # buffer, then open the buffer in a new window. 279 | if (&modified && !&hidden) || &buftype != '' 280 | exe $'belowright sbuffer {bnr}' 281 | else 282 | exe $'buf {bnr}' 283 | endif 284 | else 285 | if (&modified && !&hidden) || &buftype != '' 286 | # if the current buffer has unsaved changes and 'hidden' is not set, 287 | # or if the current buffer is a special buffer, then open the file 288 | # in a new window 289 | exe $'belowright split {fname}' 290 | else 291 | exe $'edit {fname}' 292 | endif 293 | endif 294 | endif 295 | endif 296 | else 297 | if bnr == -1 298 | exe $'{cmdmods} split {fname}' 299 | else 300 | # Use "sbuffer" so that the 'switchbuf' option settings are used. 301 | exe $'{cmdmods} sbuffer {bnr}' 302 | endif 303 | endif 304 | var rstart = range.start 305 | setcursorcharpos(rstart.line + 1, 306 | GetCharIdxWithoutCompChar(bufnr(), rstart) + 1) 307 | :normal! zv 308 | enddef 309 | 310 | # indexof() function is not present in older Vim 9 versions. So use this 311 | # function. 312 | export def Indexof(list: list, CallbackFn: func(number, any): bool): number 313 | var ix = 0 314 | for val in list 315 | if CallbackFn(ix, val) 316 | return ix 317 | endif 318 | ix += 1 319 | endfor 320 | return -1 321 | enddef 322 | 323 | # Find the nearest root directory containing a file or directory name from the 324 | # list of names in "files" starting with the directory "startDir". 325 | # Based on a similar implementation in the vim-lsp plugin. 326 | # Searches upwards starting with the directory "startDir". 327 | # If a file name ends with '/' or '\', then it is a directory name, otherwise 328 | # it is a file name. 329 | # Returns '' if none of the file and directory names in "files" can be found 330 | # in one of the parent directories. 331 | export def FindNearestRootDir(startDir: string, files: list): string 332 | var foundDirs: dict = {} 333 | 334 | for file in files 335 | if file->type() != v:t_string || file->empty() 336 | continue 337 | endif 338 | var isDir = file[-1 : ] == '/' || file[-1 : ] == '\' 339 | var relPath: string 340 | if isDir 341 | relPath = finddir(file, $'{startDir};') 342 | else 343 | relPath = findfile(file, $'{startDir};') 344 | endif 345 | if relPath->empty() 346 | continue 347 | endif 348 | var rootDir = relPath->fnamemodify(isDir ? ':p:h:h' : ':p:h') 349 | foundDirs[rootDir] = true 350 | endfor 351 | if foundDirs->empty() 352 | return '' 353 | endif 354 | 355 | # Sort the directory names by length 356 | var sortedList: list = foundDirs->keys()->sort((a, b) => { 357 | return b->len() - a->len() 358 | }) 359 | 360 | # choose the longest matching path (the nearest directory from "startDir") 361 | return sortedList[0] 362 | enddef 363 | 364 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 365 | -------------------------------------------------------------------------------- /ftplugin/lspgfm.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | import autoload 'lsp/markdown.vim' as md 4 | 5 | # Update the preview window with the github flavored markdown text 6 | def UpdatePreviewWindowContents(bnr: number, contentList: list>) 7 | :silent! bnr->deletebufline(1, '$') 8 | 9 | var lines: list = [] 10 | var props: dict>> 11 | var lnum = 0 12 | 13 | # Each item in "contentList" is a Dict with the following items: 14 | # text: text for this line 15 | # props: list of text properties. Each list item is a Dict. See 16 | # |popup-props| for more information. 17 | # 18 | # Need to convert the text properties from the format used by 19 | # popup_settext() to that used by prop_add_list(). 20 | for entry in contentList 21 | lines->add(entry.text) 22 | lnum += 1 23 | if entry->has_key('props') 24 | for p in entry.props 25 | if !props->has_key(p.type) 26 | props[p.type] = [] 27 | endif 28 | if p->has_key('end_lnum') 29 | props[p.type]->add([lnum, p.col, p.end_lnum, p.end_col]) 30 | else 31 | props[p.type]->add([lnum, p.col, lnum, p.col + p.length]) 32 | endif 33 | endfor 34 | endif 35 | endfor 36 | setbufline(bnr, 1, lines) 37 | for prop_type in props->keys() 38 | prop_add_list({type: prop_type}, props[prop_type]) 39 | endfor 40 | enddef 41 | 42 | # Render the github flavored markdown text. 43 | # Text can be displayed either in a popup window or in a preview window. 44 | def RenderGitHubMarkdownText() 45 | var bnr: number = bufnr() 46 | var winId: number = win_getid() 47 | var document: dict> 48 | var inPreviewWindow = false 49 | 50 | if win_gettype() == 'preview' 51 | inPreviewWindow = true 52 | endif 53 | 54 | try 55 | if !inPreviewWindow 56 | winId = bnr->getbufinfo()[0].popups[0] 57 | endif 58 | # parse the github markdown content and convert it into a list of text and 59 | # list of associated text properties. 60 | document = md.ParseMarkdown(bnr->getbufline(1, '$'), winId->winwidth()) 61 | catch /.*/ 62 | b:markdown_fallback = v:true 63 | return 64 | endtry 65 | 66 | b:lsp_syntax = document.syntax 67 | md.list_pattern->setbufvar(bnr, '&formatlistpat') 68 | var settings = 'linebreak breakindent breakindentopt=list:-1' 69 | win_execute(winId, $'setlocal {settings}') 70 | if inPreviewWindow 71 | UpdatePreviewWindowContents(bnr, document.content) 72 | else 73 | winId->popup_settext(document.content) 74 | endif 75 | enddef 76 | RenderGitHubMarkdownText() 77 | 78 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 79 | -------------------------------------------------------------------------------- /plugin/lsp.vim: -------------------------------------------------------------------------------- 1 | if !has('vim9script') || v:version < 900 2 | " Needs Vim version 9.0 and above 3 | finish 4 | endif 5 | 6 | vim9script noclear 7 | 8 | # Language Server Protocol (LSP) plugin for vim 9 | 10 | if get(g:, 'loaded_lsp', false) 11 | finish 12 | endif 13 | g:loaded_lsp = true 14 | 15 | import '../autoload/lsp/options.vim' 16 | import autoload '../autoload/lsp/lsp.vim' 17 | 18 | # Set LSP plugin options from 'opts'. 19 | def g:LspOptionsSet(opts: dict) 20 | options.OptionsSet(opts) 21 | enddef 22 | 23 | # Return a copy of all the LSP plugin options 24 | def g:LspOptionsGet(): dict 25 | return options.OptionsGet() 26 | enddef 27 | 28 | # Add one or more LSP servers in 'serverList' 29 | def g:LspAddServer(serverList: list>) 30 | lsp.AddServer(serverList) 31 | enddef 32 | 33 | # Register 'Handler' callback function for LSP command 'cmd'. 34 | def g:LspRegisterCmdHandler(cmd: string, Handler: func) 35 | lsp.RegisterCmdHandler(cmd, Handler) 36 | enddef 37 | 38 | # Returns true if the language server for the current buffer is initialized 39 | # and ready to accept requests. 40 | def g:LspServerReady(): bool 41 | return lsp.ServerReady() 42 | enddef 43 | 44 | # Returns true if the language server for 'ftype' file type is running 45 | def g:LspServerRunning(ftype: string): bool 46 | return lsp.ServerRunning(ftype) 47 | enddef 48 | 49 | augroup LSPAutoCmds 50 | au! 51 | autocmd BufNewFile,BufReadPost,FileType * lsp.AddFile(expand('')->str2nr()) 52 | # Note that when BufWipeOut is invoked, the current buffer may be different 53 | # from the buffer getting wiped out. 54 | autocmd BufWipeOut * lsp.RemoveFile(expand('')->str2nr()) 55 | autocmd BufWinEnter * lsp.BufferLoadedInWin(expand('')->str2nr()) 56 | augroup END 57 | 58 | # TODO: Is it needed to shutdown all the LSP servers when exiting Vim? 59 | # This takes some time. 60 | # autocmd VimLeavePre * call lsp.StopAllServers() 61 | 62 | # LSP commands 63 | command! -nargs=? -bar -range LspCodeAction lsp.CodeAction(, , ) 64 | command! -nargs=0 -bar LspCodeLens lsp.CodeLens() 65 | command! -nargs=+ -bar -bang -count -complete=customlist,lsp.LspDiagComplete LspDiag lsp.LspDiagCmd(, , false) 66 | command! -nargs=0 -bar -bang LspDiagCurrent lsp.LspShowCurrentDiag(false) 67 | command! -nargs=0 -bar LspDiagFirst lsp.JumpToDiag('first') 68 | command! -nargs=0 -bar LspDiagLast lsp.JumpToDiag('last') 69 | command! -nargs=0 -bar -count=1 LspDiagNext lsp.JumpToDiag('next', ) 70 | command! -nargs=0 -bar -count=1 LspDiagNextWrap lsp.JumpToDiag('nextWrap', ) 71 | command! -nargs=0 -bar -count=1 LspDiagPrev lsp.JumpToDiag('prev', ) 72 | command! -nargs=0 -bar -count=1 LspDiagPrevWrap lsp.JumpToDiag('prevWrap', ) 73 | command! -nargs=0 -bar LspDiagShow lsp.ShowDiagnostics() 74 | command! -nargs=0 -bar LspDiagHere lsp.JumpToDiag('here') 75 | command! -nargs=0 -bar LspDocumentSymbol lsp.ShowDocSymbols() 76 | command! -nargs=0 -bar LspFold lsp.FoldDocument() 77 | 78 | command! -nargs=0 -bar -range=% LspFormat lsp.TextDocFormat(, , ) 79 | def g:LspFormatFunc(type: string, visual_mode = v:false) 80 | if visual_mode 81 | exe "normal! gv:LspFormat\" 82 | elseif type ==# 'line' 83 | exe "normal! '[V']:LspFormat\" 84 | elseif type ==# 'char' 85 | exe "normal! `[v`]:LspFormat\" 86 | endif 87 | enddef 88 | nnoremap (LspFormat) set operatorfunc=LspFormatFuncg@ 89 | vnoremap (LspFormat) LspFormat 90 | 91 | command! -nargs=0 -bar -count LspGotoDeclaration lsp.GotoDeclaration(v:false, , ) 92 | command! -nargs=0 -bar -count LspGotoDefinition lsp.GotoDefinition(v:false, , ) 93 | command! -nargs=0 -bar -count LspGotoImpl lsp.GotoImplementation(v:false, , ) 94 | command! -nargs=0 -bar -count LspGotoTypeDef lsp.GotoTypedef(v:false, , ) 95 | command! -nargs=0 -bar LspHighlight call LspDocHighlight(bufnr(), ) 96 | command! -nargs=0 -bar LspHighlightClear call LspDocHighlightClear() 97 | command! -nargs=? -bar LspHover lsp.Hover() 98 | command! -nargs=1 -bar -complete=customlist,lsp.LspInlayHintsComplete LspInlayHints lsp.InlayHints() 99 | command! -nargs=0 -bar LspIncomingCalls lsp.IncomingCalls() 100 | command! -nargs=0 -bar LspOutgoingCalls lsp.OutgoingCalls() 101 | command! -nargs=0 -bar -count LspOutline lsp.Outline(, ) 102 | command! -nargs=0 -bar -count LspPeekDeclaration lsp.GotoDeclaration(v:true, , ) 103 | command! -nargs=0 -bar -count LspPeekDefinition lsp.GotoDefinition(v:true, , ) 104 | command! -nargs=0 -bar -count LspPeekImpl lsp.GotoImplementation(v:true, , ) 105 | command! -nargs=0 -bar LspPeekReferences lsp.ShowReferences(v:true) 106 | command! -nargs=0 -bar -count LspPeekTypeDef lsp.GotoTypedef(v:true, , ) 107 | command! -nargs=? -bar LspRename lsp.Rename() 108 | command! -nargs=0 -bar LspSelectionExpand lsp.SelectionExpand() 109 | command! -nargs=0 -bar LspSelectionShrink lsp.SelectionShrink() 110 | command! -nargs=+ -bar -complete=customlist,lsp.LspServerComplete LspServer lsp.LspServerCmd() 111 | command! -nargs=0 -bar LspShowReferences lsp.ShowReferences(v:false) 112 | command! -nargs=0 -bar LspShowAllServers lsp.ShowAllServers() 113 | command! -nargs=0 -bar LspShowSignature call LspShowSignature() 114 | command! -nargs=0 -bar LspSubTypeHierarchy lsp.TypeHierarchy(0) 115 | command! -nargs=0 -bar LspSuperTypeHierarchy lsp.TypeHierarchy(1) 116 | # Clangd specifc extension to switch from one C/C++ source file to a 117 | # corresponding header file 118 | command! -nargs=0 -bar LspSwitchSourceHeader lsp.SwitchSourceHeader() 119 | command! -nargs=? -bar LspSymbolSearch lsp.SymbolSearch(, ) 120 | command! -nargs=1 -bar -complete=dir LspWorkspaceAddFolder lsp.AddWorkspaceFolder() 121 | command! -nargs=0 -bar LspWorkspaceListFolders lsp.ListWorkspaceFolders() 122 | command! -nargs=1 -bar -complete=dir LspWorkspaceRemoveFolder lsp.RemoveWorkspaceFolder() 123 | 124 | # Add the GUI menu entries 125 | if has('gui_running') 126 | anoremenu L&sp.Goto.Definition :LspGotoDefinition 127 | anoremenu L&sp.Goto.Declaration :LspGotoDeclaration 128 | anoremenu L&sp.Goto.Implementation :LspGotoImpl 129 | anoremenu L&sp.Goto.TypeDef :LspGotoTypeDef 130 | 131 | anoremenu L&sp.Show\ Signature :LspShowSignature 132 | anoremenu L&sp.Show\ References :LspShowReferences 133 | anoremenu L&sp.Show\ Detail :LspHover 134 | anoremenu L&sp.Outline :LspOutline 135 | 136 | anoremenu L&sp.Goto\ Symbol :LspDocumentSymbol 137 | anoremenu L&sp.Symbol\ Search :LspSymbolSearch 138 | anoremenu L&sp.Outgoing\ Calls :LspOutgoingCalls 139 | anoremenu L&sp.Incoming\ Calls :LspIncomingCalls 140 | anoremenu L&sp.Rename :LspRename 141 | anoremenu L&sp.Code\ Action :LspCodeAction 142 | 143 | anoremenu L&sp.Highlight\ Symbol :LspHighlight 144 | anoremenu L&sp.Highlight\ Clear :LspHighlightClear 145 | 146 | # Diagnostics 147 | anoremenu L&sp.Diagnostics.Current :LspDiag current 148 | anoremenu L&sp.Diagnostics.Show\ All :LspDiag show 149 | anoremenu L&sp.Diagnostics.First :LspDiag first 150 | anoremenu L&sp.Diagnostics.Last :LspDiag last 151 | anoremenu L&sp.Diagnostics.Next :LspDiag next 152 | anoremenu L&sp.Diagnostics.Previous :LspDiag prev 153 | anoremenu L&sp.Diagnostics.This :LspDiag here 154 | 155 | if &mousemodel =~ 'popup' 156 | anoremenu PopUp.L&sp.Go\ to\ Definition 157 | \ :LspGotoDefinition 158 | anoremenu PopUp.L&sp.Go\ to\ Declaration 159 | \ :LspGotoDeclaration 160 | anoremenu PopUp.L&sp.Find\ All\ References 161 | \ :LspShowReferences 162 | anoremenu PopUp.L&sp.Show\ Detail 163 | \ :LspHover 164 | anoremenu PopUp.L&sp.Highlight\ Symbol 165 | \ :LspHighlight 166 | anoremenu PopUp.L&sp.Highlight\ Clear 167 | \ :LspHighlightClear 168 | endif 169 | endif 170 | 171 | # Invoke autocmd to register LSP servers and to set LSP options 172 | if exists('#User#LspSetup') 173 | :doautocmd User LspSetup 174 | endif 175 | 176 | # vim: shiftwidth=2 softtabstop=2 177 | -------------------------------------------------------------------------------- /syntax/lspgfm.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | if get(b:, 'markdown_fallback', v:false) 4 | runtime! syntax/markdown.vim 5 | finish 6 | endif 7 | 8 | var group: dict = {} 9 | for region in get(b:, 'lsp_syntax', []) 10 | if !group->has_key(region.lang) 11 | group[region.lang] = region.lang->substitute('\(^.\|_\a\)', '\u&', 'g') 12 | try 13 | exe $'syntax include @{group[region.lang]} syntax/{region.lang}.vim' 14 | catch /.*/ 15 | group[region.lang] = '' 16 | endtry 17 | endif 18 | if !group[region.lang]->empty() 19 | exe $'syntax region lspCodeBlock start="{region.start}" end="{region.end}" contains=@{group[region.lang]}' 20 | endif 21 | endfor 22 | 23 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 24 | -------------------------------------------------------------------------------- /test/clangd_offsetencoding.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # Unit tests for language server protocol offset encoding using clangd 3 | 4 | source common.vim 5 | 6 | # Start the C language server. Returns true on success and false on failure. 7 | def g:StartLangServer(): bool 8 | if has('patch-9.0.1629') 9 | return g:StartLangServerWithFile('Xtest.c') 10 | endif 11 | return false 12 | enddef 13 | 14 | if !has('patch-9.0.1629') 15 | # Need patch 9.0.1629 to properly encode/decode the UTF-16 offsets 16 | finish 17 | endif 18 | 19 | var lspOpts = {autoComplete: false} 20 | g:LspOptionsSet(lspOpts) 21 | 22 | var lspServers = [{ 23 | filetype: ['c', 'cpp'], 24 | path: (exepath('clangd-15') ?? exepath('clangd')), 25 | args: ['--background-index', 26 | '--clang-tidy', 27 | $'--offset-encoding={$LSP_OFFSET_ENCODING}'] 28 | }] 29 | call LspAddServer(lspServers) 30 | 31 | # Test for :LspCodeAction with symbols containing multibyte and composing 32 | # characters 33 | def g:Test_LspCodeAction_multibyte() 34 | silent! edit XLspCodeAction_mb.c 35 | sleep 200m 36 | var lines =<< trim END 37 | #include 38 | void fn(int aVar) 39 | { 40 | printf("aVar = %d\n", aVar); 41 | printf("😊😊😊😊 = %d\n", aVar): 42 | printf("áb́áb́ = %d\n", aVar): 43 | printf("ą́ą́ą́ą́ = %d\n", aVar): 44 | } 45 | END 46 | setline(1, lines) 47 | g:WaitForServerFileLoad(3) 48 | :redraw! 49 | cursor(5, 5) 50 | redraw! 51 | :LspCodeAction 1 52 | assert_equal(' printf("😊😊😊😊 = %d\n", aVar);', getline(5)) 53 | cursor(6, 5) 54 | redraw! 55 | :LspCodeAction 1 56 | assert_equal(' printf("áb́áb́ = %d\n", aVar);', getline(6)) 57 | cursor(7, 5) 58 | redraw! 59 | :LspCodeAction 1 60 | assert_equal(' printf("ą́ą́ą́ą́ = %d\n", aVar);', getline(7)) 61 | 62 | :%bw! 63 | enddef 64 | 65 | # Test for ":LspDiag show" when using multibyte and composing characters 66 | def g:Test_LspDiagShow_multibyte() 67 | :silent! edit XLspDiagShow_mb.c 68 | sleep 200m 69 | var lines =<< trim END 70 | #include 71 | void fn(int aVar) 72 | { 73 | printf("aVar = %d\n", aVar); 74 | printf("😊😊😊😊 = %d\n". aVar); 75 | printf("áb́áb́ = %d\n". aVar); 76 | printf("ą́ą́ą́ą́ = %d\n". aVar); 77 | } 78 | END 79 | setline(1, lines) 80 | g:WaitForServerFileLoad(3) 81 | :redraw! 82 | :LspDiag show 83 | var qfl: list> = getloclist(0) 84 | assert_equal([5, 37], [qfl[0].lnum, qfl[0].col]) 85 | assert_equal([6, 33], [qfl[1].lnum, qfl[1].col]) 86 | assert_equal([7, 41], [qfl[2].lnum, qfl[2].col]) 87 | :lclose 88 | :%bw! 89 | enddef 90 | 91 | # Test for :LspFormat when using multibyte and composing characters 92 | def g:Test_LspFormat_multibyte() 93 | :silent! edit XLspFormat_mb.c 94 | sleep 200m 95 | var lines =<< trim END 96 | void fn(int aVar) 97 | { 98 | int 😊😊😊😊 = aVar + 1; 99 | int áb́áb́ = aVar + 1; 100 | int ą́ą́ą́ą́ = aVar + 1; 101 | } 102 | END 103 | setline(1, lines) 104 | g:WaitForServerFileLoad(0) 105 | :redraw! 106 | :LspFormat 107 | var expected =<< trim END 108 | void fn(int aVar) { 109 | int 😊😊😊😊 = aVar + 1; 110 | int áb́áb́ = aVar + 1; 111 | int ą́ą́ą́ą́ = aVar + 1; 112 | } 113 | END 114 | assert_equal(expected, getline(1, '$')) 115 | :%bw! 116 | enddef 117 | 118 | # Test for :LspGotoDefinition when using multibyte and composing characters 119 | def g:Test_LspGotoDefinition_multibyte() 120 | :silent! edit XLspGotoDefinition_mb.c 121 | sleep 200m 122 | var lines: list =<< trim END 123 | #include 124 | void fn(int aVar) 125 | { 126 | printf("aVar = %d\n", aVar); 127 | printf("😊😊😊😊 = %d\n", aVar); 128 | printf("áb́áb́ = %d\n", aVar); 129 | printf("ą́ą́ą́ą́ = %d\n", aVar); 130 | } 131 | END 132 | setline(1, lines) 133 | g:WaitForServerFileLoad(0) 134 | redraw! 135 | 136 | for [lnum, colnr] in [[4, 27], [5, 39], [6, 35], [7, 43]] 137 | cursor(lnum, colnr) 138 | :LspGotoDefinition 139 | assert_equal([2, 13], [line('.'), col('.')]) 140 | endfor 141 | 142 | :%bw! 143 | enddef 144 | 145 | # Test for :LspGotoDefinition when using multibyte and composing characters 146 | def g:Test_LspGotoDefinition_after_multibyte() 147 | :silent! edit XLspGotoDef_after_mb.c 148 | sleep 200m 149 | var lines =<< trim END 150 | void fn(int aVar) 151 | { 152 | /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int αβγδ, bVar; 153 | /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int 😊😊😊😊, cVar; 154 | /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int áb́áb́, dVar; 155 | /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int ą́ą́ą́ą́, eVar; 156 | bVar = 1; 157 | cVar = 2; 158 | dVar = 3; 159 | eVar = 4; 160 | aVar = αβγδ + 😊😊😊😊 + áb́áb́ + ą́ą́ą́ą́ + bVar; 161 | } 162 | END 163 | setline(1, lines) 164 | g:WaitForServerFileLoad(0) 165 | :redraw! 166 | cursor(7, 5) 167 | :LspGotoDefinition 168 | assert_equal([3, 88], [line('.'), col('.')]) 169 | cursor(8, 5) 170 | :LspGotoDefinition 171 | assert_equal([4, 96], [line('.'), col('.')]) 172 | cursor(9, 5) 173 | :LspGotoDefinition 174 | assert_equal([5, 92], [line('.'), col('.')]) 175 | cursor(10, 5) 176 | :LspGotoDefinition 177 | assert_equal([6, 100], [line('.'), col('.')]) 178 | cursor(11, 12) 179 | :LspGotoDefinition 180 | assert_equal([3, 78], [line('.'), col('.')]) 181 | cursor(11, 23) 182 | :LspGotoDefinition 183 | assert_equal([4, 78], [line('.'), col('.')]) 184 | cursor(11, 42) 185 | :LspGotoDefinition 186 | assert_equal([5, 78], [line('.'), col('.')]) 187 | cursor(11, 57) 188 | :LspGotoDefinition 189 | assert_equal([6, 78], [line('.'), col('.')]) 190 | 191 | :%bw! 192 | enddef 193 | 194 | # Test for doing omni completion for symbols with multibyte and composing 195 | # characters 196 | def g:Test_OmniComplete_multibyte() 197 | :silent! edit XOmniComplete_mb.c 198 | sleep 200m 199 | var lines: list =<< trim END 200 | void Func1(void) 201 | { 202 | int 😊😊😊😊, aVar; 203 | int áb́áb́, bVar; 204 | int ą́ą́ą́ą́, cVar; 205 | 206 | 207 | 208 | } 209 | END 210 | setline(1, lines) 211 | g:WaitForServerFileLoad(0) 212 | redraw! 213 | 214 | cursor(6, 4) 215 | feedkeys("aaV\\ = 😊😊\\;", 'xt') 216 | assert_equal(' aVar = 😊😊😊😊;', getline('.')) 217 | cursor(7, 4) 218 | feedkeys("abV\\ = áb́\\;", 'xt') 219 | assert_equal(' bVar = áb́áb́;', getline('.')) 220 | cursor(8, 4) 221 | feedkeys("acV\\ = ą́ą́\\;", 'xt') 222 | assert_equal(' cVar = ą́ą́ą́ą́;', getline('.')) 223 | feedkeys("oáb́\\ = ą́ą́\\;", 'xt') 224 | assert_equal(' áb́áb́ = ą́ą́ą́ą́;', getline('.')) 225 | feedkeys("oą́ą́\\ = áb́\\;", 'xt') 226 | assert_equal(' ą́ą́ą́ą́ = áb́áb́;', getline('.')) 227 | :%bw! 228 | enddef 229 | 230 | # Test for :LspOutline with multibyte and composing characters 231 | def g:Test_Outline_multibyte() 232 | silent! edit XLspOutline_mb.c 233 | sleep 200m 234 | var lines: list =<< trim END 235 | typedef void 😊😊😊😊; 236 | typedef void áb́áb́; 237 | typedef void ą́ą́ą́ą́; 238 | 239 | 😊😊😊😊 Func1() 240 | { 241 | } 242 | 243 | áb́áb́ Func2() 244 | { 245 | } 246 | 247 | ą́ą́ą́ą́ Func3() 248 | { 249 | } 250 | END 251 | setline(1, lines) 252 | g:WaitForServerFileLoad(0) 253 | redraw! 254 | 255 | cursor(1, 1) 256 | :LspOutline 257 | assert_equal(2, winnr('$')) 258 | 259 | :wincmd w 260 | cursor(5, 1) 261 | feedkeys("\", 'xt') 262 | assert_equal([2, 5, 18], [winnr(), line('.'), col('.')]) 263 | 264 | :wincmd w 265 | cursor(6, 1) 266 | feedkeys("\", 'xt') 267 | assert_equal([2, 9, 14], [winnr(), line('.'), col('.')]) 268 | 269 | :wincmd w 270 | cursor(7, 1) 271 | feedkeys("\", 'xt') 272 | assert_equal([2, 13, 22], [winnr(), line('.'), col('.')]) 273 | 274 | :wincmd w 275 | cursor(10, 1) 276 | feedkeys("\", 'xt') 277 | assert_equal([2, 1, 14], [winnr(), line('.'), col('.')]) 278 | 279 | :wincmd w 280 | cursor(11, 1) 281 | feedkeys("\", 'xt') 282 | assert_equal([2, 2, 14], [winnr(), line('.'), col('.')]) 283 | 284 | :wincmd w 285 | cursor(12, 1) 286 | feedkeys("\", 'xt') 287 | assert_equal([2, 3, 14], [winnr(), line('.'), col('.')]) 288 | 289 | :%bw! 290 | enddef 291 | 292 | # Test for :LspRename with multibyte and composing characters 293 | def g:Test_LspRename_multibyte() 294 | silent! edit XLspRename_mb.c 295 | sleep 200m 296 | var lines: list =<< trim END 297 | #include 298 | void fn(int aVar) 299 | { 300 | printf("aVar = %d\n", aVar); 301 | printf("😊😊😊😊 = %d\n", aVar); 302 | printf("áb́áb́ = %d\n", aVar); 303 | printf("ą́ą́ą́ą́ = %d\n", aVar); 304 | } 305 | END 306 | setline(1, lines) 307 | g:WaitForServerFileLoad(0) 308 | redraw! 309 | cursor(2, 12) 310 | :LspRename bVar 311 | redraw! 312 | var expected: list =<< trim END 313 | #include 314 | void fn(int bVar) 315 | { 316 | printf("aVar = %d\n", bVar); 317 | printf("😊😊😊😊 = %d\n", bVar); 318 | printf("áb́áb́ = %d\n", bVar); 319 | printf("ą́ą́ą́ą́ = %d\n", bVar); 320 | } 321 | END 322 | assert_equal(expected, getline(1, '$')) 323 | :%bw! 324 | enddef 325 | 326 | # Test for :LspShowReferences when using multibyte and composing characters 327 | def g:Test_LspShowReferences_multibyte() 328 | :silent! edit XLspShowReferences_mb.c 329 | sleep 200m 330 | var lines: list =<< trim END 331 | #include 332 | void fn(int aVar) 333 | { 334 | printf("aVar = %d\n", aVar); 335 | printf("😊😊😊😊 = %d\n", aVar); 336 | printf("áb́áb́ = %d\n", aVar); 337 | printf("ą́ą́ą́ą́ = %d\n", aVar); 338 | } 339 | END 340 | setline(1, lines) 341 | g:WaitForServerFileLoad(0) 342 | redraw! 343 | cursor(4, 27) 344 | :LspShowReferences 345 | var qfl: list> = getloclist(0) 346 | assert_equal([2, 13], [qfl[0].lnum, qfl[0].col]) 347 | assert_equal([4, 27], [qfl[1].lnum, qfl[1].col]) 348 | assert_equal([5, 39], [qfl[2].lnum, qfl[2].col]) 349 | assert_equal([6, 35], [qfl[3].lnum, qfl[3].col]) 350 | assert_equal([7, 43], [qfl[4].lnum, qfl[4].col]) 351 | :lclose 352 | 353 | :%bw! 354 | enddef 355 | 356 | # Test for :LspSymbolSearch when using multibyte and composing characters 357 | def g:Test_LspSymbolSearch_multibyte() 358 | silent! edit XLspSymbolSearch_mb.c 359 | sleep 200m 360 | var lines: list =<< trim END 361 | typedef void 😊😊😊😊; 362 | typedef void áb́áb́; 363 | typedef void ą́ą́ą́ą́; 364 | 365 | 😊😊😊😊 Func1() 366 | { 367 | } 368 | 369 | áb́áb́ Func2() 370 | { 371 | } 372 | 373 | ą́ą́ą́ą́ Func3() 374 | { 375 | } 376 | END 377 | setline(1, lines) 378 | g:WaitForServerFileLoad(0) 379 | 380 | cursor(1, 1) 381 | feedkeys(":LspSymbolSearch Func1\", "xt") 382 | assert_equal([5, 18], [line('.'), col('.')]) 383 | cursor(1, 1) 384 | feedkeys(":LspSymbolSearch Func2\", "xt") 385 | assert_equal([9, 14], [line('.'), col('.')]) 386 | cursor(1, 1) 387 | feedkeys(":LspSymbolSearch Func3\", "xt") 388 | assert_equal([13, 22], [line('.'), col('.')]) 389 | 390 | :%bw! 391 | enddef 392 | 393 | # Test for setting the 'tagfunc' with multibyte and composing characters in 394 | # symbols 395 | def g:Test_LspTagFunc_multibyte() 396 | var lines =<< trim END 397 | void fn(int aVar) 398 | { 399 | int 😊😊😊😊, bVar; 400 | int áb́áb́, cVar; 401 | int ą́ą́ą́ą́, dVar; 402 | bVar = 10; 403 | cVar = 10; 404 | dVar = 10; 405 | } 406 | END 407 | writefile(lines, 'Xtagfunc_mb.c') 408 | :silent! edit! Xtagfunc_mb.c 409 | g:WaitForServerFileLoad(0) 410 | :setlocal tagfunc=lsp#lsp#TagFunc 411 | cursor(6, 5) 412 | :exe "normal \" 413 | assert_equal([3, 27], [line('.'), col('.')]) 414 | cursor(7, 5) 415 | :exe "normal \" 416 | assert_equal([4, 23], [line('.'), col('.')]) 417 | cursor(8, 5) 418 | :exe "normal \" 419 | assert_equal([5, 31], [line('.'), col('.')]) 420 | :set tagfunc& 421 | 422 | :%bw! 423 | delete('Xtagfunc_mb.c') 424 | enddef 425 | 426 | # Test for the :LspSuperTypeHierarchy and :LspSubTypeHierarchy commands with 427 | # multibyte and composing characters 428 | def g:Test_LspTypeHier_multibyte() 429 | silent! edit XLspTypeHier_mb.cpp 430 | sleep 200m 431 | var lines =<< trim END 432 | /* αβ😊😊ááą́ą́ */ class parent { 433 | }; 434 | 435 | /* αβ😊😊ááą́ą́ */ class child : public parent { 436 | }; 437 | 438 | /* αβ😊😊ááą́ą́ */ class grandchild : public child { 439 | }; 440 | END 441 | setline(1, lines) 442 | g:WaitForServerFileLoad(0) 443 | redraw! 444 | 445 | cursor(1, 42) 446 | :LspSubTypeHierarchy 447 | call feedkeys("\", 'xt') 448 | assert_equal([1, 36], [line('.'), col('.')]) 449 | cursor(1, 42) 450 | 451 | :LspSubTypeHierarchy 452 | call feedkeys("\\", 'xt') 453 | assert_equal([4, 42], [line('.'), col('.')]) 454 | 455 | cursor(1, 42) 456 | :LspSubTypeHierarchy 457 | call feedkeys("\\\", 'xt') 458 | assert_equal([7, 42], [line('.'), col('.')]) 459 | 460 | cursor(7, 42) 461 | :LspSuperTypeHierarchy 462 | call feedkeys("\", 'xt') 463 | assert_equal([7, 36], [line('.'), col('.')]) 464 | 465 | cursor(7, 42) 466 | :LspSuperTypeHierarchy 467 | call feedkeys("\\", 'xt') 468 | assert_equal([4, 36], [line('.'), col('.')]) 469 | 470 | cursor(7, 42) 471 | :LspSuperTypeHierarchy 472 | call feedkeys("\\\", 'xt') 473 | assert_equal([1, 36], [line('.'), col('.')]) 474 | 475 | :%bw! 476 | enddef 477 | 478 | # vim: shiftwidth=2 softtabstop=2 noexpandtab 479 | -------------------------------------------------------------------------------- /test/common.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # Common routines used for running the unit tests 3 | 4 | # Load the LSP plugin. Also enable syntax, file type detection. 5 | def g:LoadLspPlugin() 6 | syntax on 7 | filetype on 8 | filetype plugin on 9 | filetype indent on 10 | 11 | # Set the $LSP_PROFILE environment variable to profile the LSP plugin 12 | var do_profile: bool = false 13 | if exists('$LSP_PROFILE') 14 | do_profile = true 15 | endif 16 | 17 | if do_profile 18 | # profile the LSP plugin 19 | profile start lsp_profile.txt 20 | profile! file */lsp/* 21 | endif 22 | 23 | g:LSPTest = true 24 | source ../plugin/lsp.vim 25 | enddef 26 | 27 | # The WaitFor*() functions are reused from the Vim test suite. 28 | # 29 | # Wait for up to five seconds for "assert" to return zero. "assert" must be a 30 | # (lambda) function containing one assert function. Example: 31 | # call WaitForAssert({-> assert_equal("dead", job_status(job)}) 32 | # 33 | # A second argument can be used to specify a different timeout in msec. 34 | # 35 | # Return zero for success, one for failure (like the assert function). 36 | func g:WaitForAssert(assert, ...) 37 | let timeout = get(a:000, 0, 5000) 38 | if g:WaitForCommon(v:null, a:assert, timeout) < 0 39 | return 1 40 | endif 41 | return 0 42 | endfunc 43 | 44 | # Either "expr" or "assert" is not v:null 45 | # Return the waiting time for success, -1 for failure. 46 | func g:WaitForCommon(expr, assert, timeout) 47 | " using reltime() is more accurate, but not always available 48 | let slept = 0 49 | if exists('*reltimefloat') 50 | let start = reltime() 51 | endif 52 | 53 | while 1 54 | if type(a:expr) == v:t_func 55 | let success = a:expr() 56 | elseif type(a:assert) == v:t_func 57 | let success = a:assert() == 0 58 | else 59 | let success = eval(a:expr) 60 | endif 61 | if success 62 | return slept 63 | endif 64 | 65 | if slept >= a:timeout 66 | break 67 | endif 68 | if type(a:assert) == v:t_func 69 | " Remove the error added by the assert function. 70 | call remove(v:errors, -1) 71 | endif 72 | 73 | sleep 10m 74 | if exists('*reltimefloat') 75 | let slept = float2nr(reltimefloat(reltime(start)) * 1000) 76 | else 77 | let slept += 10 78 | endif 79 | endwhile 80 | 81 | return -1 " timed out 82 | endfunc 83 | 84 | # Wait for up to five seconds for "expr" to become true. "expr" can be a 85 | # stringified expression to evaluate, or a funcref without arguments. 86 | # Using a lambda works best. Example: 87 | # call WaitFor({-> status == "ok"}) 88 | # 89 | # A second argument can be used to specify a different timeout in msec. 90 | # 91 | # When successful the time slept is returned. 92 | # When running into the timeout an exception is thrown, thus the function does 93 | # not return. 94 | func g:WaitFor(expr, ...) 95 | let timeout = get(a:000, 0, 5000) 96 | let slept = g:WaitForCommon(a:expr, v:null, timeout) 97 | if slept < 0 98 | throw 'WaitFor() timed out after ' .. timeout .. ' msec' 99 | endif 100 | return slept 101 | endfunc 102 | 103 | # Wait for diagnostic messages from the LSP server. 104 | # Waits for a maximum of (150 * 200) / 1000 = 30 seconds 105 | def g:WaitForDiags(errCount: number) 106 | var retries = 0 107 | while retries < 200 108 | var d = lsp#lsp#ErrorCount() 109 | if d.Error == errCount 110 | break 111 | endif 112 | retries += 1 113 | :sleep 150m 114 | endwhile 115 | 116 | assert_equal(errCount, lsp#lsp#ErrorCount().Error) 117 | if lsp#lsp#ErrorCount().Error != errCount 118 | :LspDiag show 119 | assert_report(getloclist(0)->string()) 120 | :lclose 121 | endif 122 | enddef 123 | 124 | # Wait for the LSP server to load and process a file. This works by waiting 125 | # for a certain number of diagnostic messages from the server. 126 | def g:WaitForServerFileLoad(diagCount: number) 127 | :redraw! 128 | var waitCount = diagCount 129 | if waitCount == 0 130 | # Introduce a temporary diagnostic 131 | append('$', '-') 132 | redraw! 133 | waitCount = 1 134 | endif 135 | g:WaitForDiags(waitCount) 136 | if waitCount != diagCount 137 | # Remove the temporary line 138 | deletebufline('%', '$') 139 | redraw! 140 | g:WaitForDiags(0) 141 | endif 142 | enddef 143 | 144 | # Start the language server. Returns true on success and false on failure. 145 | # 'fname' is the name of a dummy file to start the server. 146 | def g:StartLangServerWithFile(fname: string): bool 147 | # Edit a dummy file to start the LSP server 148 | exe ':silent! edit ' .. fname 149 | # Wait for the LSP server to become ready (max 10 seconds) 150 | var maxcount = 100 151 | while maxcount > 0 && !g:LspServerReady() 152 | :sleep 100m 153 | maxcount -= 1 154 | endwhile 155 | var serverStatus: bool = g:LspServerReady() 156 | :bw! 157 | 158 | if !serverStatus 159 | writefile(['FAIL: Not able to start the language server'], 'results.txt') 160 | qall! 161 | endif 162 | 163 | return serverStatus 164 | enddef 165 | 166 | # vim: shiftwidth=2 softtabstop=2 noexpandtab 167 | -------------------------------------------------------------------------------- /test/dumps/Test_tsserver_completion_1.dump: -------------------------------------------------------------------------------- 1 | |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 2 | | +0#0000e05#a8a8a8255@1|h+0#0000000#ffffff0|t@1|p|.|c|r|e> @64 3 | |~+0#4040ff13&| @4| +0#0000001#ffd7ff255|c|r|e|a|t|e|S|e|r|v|e|r| |f| | +0#4040ff13#ffffff0@52 4 | |~| @73 5 | |~| @73 6 | |~| @73 7 | |~| @73 8 | |~| @73 9 | |~| @73 10 | |-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@62 11 | -------------------------------------------------------------------------------- /test/dumps/Test_tsserver_completion_2.dump: -------------------------------------------------------------------------------- 1 | |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 2 | | +0#0000e05#a8a8a8255@1|h+0#0000000#ffffff0|t@1|p|.|c|r> @65 3 | |~+0#4040ff13&| @4| +0#0000001#ffd7ff255|c|r|e|a|t|e|S|e|r|v|e|r| |f| | +0#4040ff13#ffffff0@52 4 | |~| @73 5 | |~| @73 6 | |~| @73 7 | |~| @73 8 | |~| @73 9 | |~| @73 10 | |-+2#0000000&@1| |I|N|S|E|R|T| |-@1| +0&&@62 11 | -------------------------------------------------------------------------------- /test/gopls_tests.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # Unit tests for Vim Language Server Protocol (LSP) golang client 3 | 4 | source common.vim 5 | 6 | var lspServers = [{ 7 | filetype: ['go'], 8 | path: exepath('gopls'), 9 | args: ['serve'] 10 | }] 11 | call LspAddServer(lspServers) 12 | echomsg systemlist($'{lspServers[0].path} version') 13 | 14 | # Test for :LspGotoDefinition, :LspGotoDeclaration, etc. 15 | # This test also tests that multiple locations will be 16 | # shown in a list or popup 17 | def g:Test_LspGoto() 18 | :silent! edit Xtest.go 19 | var bnr = bufnr() 20 | 21 | sleep 200m 22 | 23 | var lines =<< trim END 24 | package main 25 | 26 | type A/*goto implementation*/ interface { 27 | Hello() 28 | } 29 | 30 | type B struct{} 31 | 32 | func (b *B) Hello() {} 33 | 34 | type C struct{} 35 | 36 | func (c *C) Hello() {} 37 | 38 | func main() { 39 | } 40 | END 41 | 42 | setline(1, lines) 43 | :redraw! 44 | g:WaitForServerFileLoad(0) 45 | 46 | cursor(9, 10) 47 | :LspGotoDefinition 48 | assert_equal([7, 6], [line('.'), col('.')]) 49 | exe "normal! \" 50 | assert_equal([9, 10], [line('.'), col('.')]) 51 | 52 | cursor(9, 13) 53 | :LspGotoImpl 54 | assert_equal([4, 9], [line('.'), col('.')]) 55 | 56 | cursor(13, 13) 57 | :LspGotoImpl 58 | assert_equal([4, 9], [line('.'), col('.')]) 59 | 60 | # Two implementions needs to be shown in a location list 61 | cursor(4, 9) 62 | assert_equal('', execute('LspGotoImpl')) 63 | sleep 200m 64 | var loclist: list> = getloclist(0) 65 | assert_equal('quickfix', getwinvar(winnr('$'), '&buftype')) 66 | assert_equal(2, loclist->len()) 67 | assert_equal(bnr, loclist[0].bufnr) 68 | assert_equal([9, 13, ''], [loclist[0].lnum, loclist[0].col, loclist[0].type]) 69 | assert_equal([13, 13, ''], [loclist[1].lnum, loclist[1].col, loclist[1].type]) 70 | lclose 71 | 72 | # Two implementions needs to be shown in a quickfix list 73 | g:LspOptionsSet({ useQuickfixForLocations: true }) 74 | cursor(4, 9) 75 | assert_equal('', execute('LspGotoImpl')) 76 | sleep 200m 77 | var qfl: list> = getqflist() 78 | assert_equal('quickfix', getwinvar(winnr('$'), '&buftype')) 79 | assert_equal(2, qfl->len()) 80 | assert_equal(bnr, qfl[0].bufnr) 81 | assert_equal([9, 13, ''], [qfl[0].lnum, qfl[0].col, qfl[0].type]) 82 | assert_equal([13, 13, ''], [qfl[1].lnum, qfl[1].col, qfl[1].type]) 83 | cclose 84 | g:LspOptionsSet({ useQuickfixForLocations: false }) 85 | 86 | # Two implementions needs to be peeked in a popup 87 | cursor(4, 9) 88 | :LspPeekImpl 89 | sleep 10m 90 | var ids = popup_list() 91 | assert_equal(2, ids->len()) 92 | var filePopupAttrs = ids[0]->popup_getoptions() 93 | var refPopupAttrs = ids[1]->popup_getoptions() 94 | assert_match('Xtest', filePopupAttrs.title) 95 | assert_match('Implementation', refPopupAttrs.title) 96 | assert_equal(9, line('.', ids[0])) # current line in left panel 97 | assert_equal(2, line('$', ids[1])) # last line in right panel 98 | feedkeys("j\", 'xt') 99 | assert_equal(13, line('.')) 100 | assert_equal([], popup_list()) 101 | popup_clear() 102 | 103 | # Jump to the first implementation 104 | cursor(4, 9) 105 | assert_equal('', execute(':1LspGotoImpl')) 106 | assert_equal([9, 13], [line('.'), col('.')]) 107 | 108 | # Jump to the second implementation 109 | cursor(4, 9) 110 | assert_equal('', execute(':2LspGotoImpl')) 111 | assert_equal([13, 13], [line('.'), col('.')]) 112 | bw! 113 | enddef 114 | 115 | # Test for :LspFold command 116 | def g:Test_LspFold() 117 | :silent! edit XLspFold1.go 118 | sleep 200m 119 | var lines =<< trim END 120 | package main 121 | 122 | // Some comment 123 | // Some other comment 124 | 125 | func plus(a int, b int) int { 126 | return a + b 127 | } 128 | 129 | func main() { 130 | } 131 | END 132 | 133 | setline(1, lines) 134 | :redraw! 135 | g:WaitForServerFileLoad(0) 136 | 137 | :LspFold 138 | sleep 50m 139 | assert_equal(1, foldlevel(3)) 140 | var r = [0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0] 141 | assert_equal(r, range(1, 11)->map((_, v) => foldlevel(v))) 142 | r = [-1, -1, 3, 3, -1, 6, 6, -1, -1, -1, -1] 143 | assert_equal(r, range(1, 11)->map((_, v) => foldclosed(v))) 144 | r = [-1, -1, 4, 4, -1, 7, 7, -1, -1, -1, -1] 145 | assert_equal(r, range(1, 11)->map((_, v) => foldclosedend(v))) 146 | 147 | v:errmsg = '' 148 | bw! 149 | enddef 150 | 151 | # Start the gopls language server. Returns true on success and false on 152 | # failure. 153 | def g:StartLangServer(): bool 154 | return g:StartLangServerWithFile('Xtest.go') 155 | enddef 156 | 157 | # vim: shiftwidth=2 softtabstop=2 noexpandtab 158 | -------------------------------------------------------------------------------- /test/markdown_tests.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | 3 | # Unit tests for the Github Flavored Markdown parser 4 | 5 | import '../autoload/lsp/markdown.vim' as md 6 | 7 | # Test for different markdowns 8 | def g:Test_Markdown() 9 | var tests: list>> = [ 10 | [ 11 | # Different headings 12 | # Input text 13 | [ 14 | '# First level heading', 15 | '## Second level heading', 16 | '### Third level heading', 17 | '# Heading with leading and trailing whitespaces ', 18 | 'Multiline setext heading ', 19 | 'of level 1', 20 | '===', 21 | 'Multiline setext heading\', 22 | 'of level 2', 23 | '---' 24 | ], 25 | # Expected text 26 | [ 27 | 'First level heading', 28 | '', 29 | 'Second level heading', 30 | '', 31 | 'Third level heading', 32 | '', 33 | 'Heading with leading and trailing whitespaces', 34 | '', 35 | 'Multiline setext heading', 36 | 'of level 1', 37 | '', 38 | 'Multiline setext heading', 39 | 'of level 2' 40 | ], 41 | # Expected text properties 42 | [ 43 | [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 19}], 44 | [], 45 | [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 20}], 46 | [], 47 | [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 19}], 48 | [], 49 | [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 45}], 50 | [], 51 | [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 24}], 52 | [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 10}], 53 | [], 54 | [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 24}], 55 | [{'col': 1, 'type': 'LspMarkdownHeading', 'length': 10}], 56 | ] 57 | ], 58 | [ 59 | # Bold text style 60 | # Input text 61 | [ 62 | 'This **word** should be bold', 63 | '', 64 | '**This line should be bold**', 65 | '', 66 | 'This __word__ should be bold', 67 | '', 68 | '__This line should be bold__' 69 | ], 70 | # Expected text 71 | [ 72 | 'This word should be bold', 73 | '', 74 | 'This line should be bold', 75 | '', 76 | 'This word should be bold', 77 | '', 78 | 'This line should be bold' 79 | ], 80 | # Expected text properties 81 | [ 82 | [{'col': 6, 'type': 'LspMarkdownBold', 'length': 4}], 83 | [], 84 | [{'col': 1, 'type': 'LspMarkdownBold', 'length': 24}], 85 | [], 86 | [{'col': 6, 'type': 'LspMarkdownBold', 'length': 4}], 87 | [], 88 | [{'col': 1, 'type': 'LspMarkdownBold', 'length': 24}] 89 | ] 90 | ], 91 | [ 92 | # Italic text style 93 | # Input text 94 | [ 95 | 'This *word* should be italic', 96 | '', 97 | '*This line should be italic*', 98 | '', 99 | 'This _word_ should be italic', 100 | '', 101 | '_This line should be italic_' 102 | ], 103 | # Expected text 104 | [ 105 | 'This word should be italic', 106 | '', 107 | 'This line should be italic', 108 | '', 109 | 'This word should be italic', 110 | '', 111 | 'This line should be italic' 112 | ], 113 | # Expected text properties 114 | [ 115 | [{'col': 6, 'type': 'LspMarkdownItalic', 'length': 4}], 116 | [], 117 | [{'col': 1, 'type': 'LspMarkdownItalic', 'length': 26}], 118 | [], 119 | [{'col': 6, 'type': 'LspMarkdownItalic', 'length': 4}], 120 | [], 121 | [{'col': 1, 'type': 'LspMarkdownItalic', 'length': 26}] 122 | ], 123 | ], 124 | [ 125 | # strikethrough text style 126 | # Input text 127 | [ 128 | 'This ~word~ should be strikethrough', 129 | '', 130 | '~This line should be strikethrough~' 131 | ], 132 | # Expected text 133 | [ 134 | 'This word should be strikethrough', 135 | '', 136 | 'This line should be strikethrough' 137 | ], 138 | # Expected text properties 139 | [ 140 | [{'col': 6, 'type': 'LspMarkdownStrikeThrough', 'length': 4}], 141 | [], 142 | [{'col': 1, 'type': 'LspMarkdownStrikeThrough', 'length': 33}] 143 | ] 144 | ], 145 | [ 146 | # bold and nested italic text style 147 | # Input text 148 | [ 149 | '**This _word_ should be bold and italic**', 150 | ], 151 | # Expected text 152 | [ 153 | 'This word should be bold and italic', 154 | ], 155 | # Expected text properties 156 | [ 157 | [ 158 | {'col': 1, 'type': 'LspMarkdownBold', 'length': 35}, 159 | {'col': 6, 'type': 'LspMarkdownItalic', 'length': 4} 160 | ] 161 | ] 162 | ], 163 | [ 164 | # all bold and italic text style 165 | # Input text 166 | [ 167 | '***This line should be all bold and italic***', 168 | ], 169 | # Expected text 170 | [ 171 | 'This line should be all bold and italic', 172 | ], 173 | # Expected text properties 174 | [ 175 | [ 176 | {'col': 1, 'type': 'LspMarkdownItalic', 'length': 39}, 177 | {'col': 1, 'type': 'LspMarkdownBold', 'length': 39} 178 | ] 179 | ] 180 | ], 181 | [ 182 | # quoted text 183 | # FIXME: The text is not quoted 184 | # Input text 185 | [ 186 | 'Text that is not quoted', 187 | '> quoted text' 188 | ], 189 | # Expected text 190 | [ 191 | 'Text that is not quoted', 192 | '', 193 | 'quoted text' 194 | ], 195 | # Expected text properties 196 | [ 197 | [], [], [] 198 | ] 199 | ], 200 | [ 201 | # line breaks 202 | # Input text 203 | [ 204 | 'This paragraph contains ', 205 | 'a soft line break', 206 | '', 207 | 'This paragraph contains ', 208 | 'an hard line break', 209 | '', 210 | 'This paragraph contains an emphasis _before_\', 211 | 'an hard line break', 212 | '', 213 | 'This paragraph contains an emphasis ', 214 | '_after_ an hard line break', 215 | '', 216 | 'This paragraph _contains\', 217 | 'an emphasis_ with an hard line break in the middle', 218 | '', 219 | '→ This paragraph contains an hard line break ', 220 | 'and starts with the multibyte character "\u2192"', 221 | '', 222 | 'Line breaks `', 223 | 'do\', 224 | 'not ', 225 | 'occur', 226 | '` inside code spans' 227 | ], 228 | # Expected text 229 | [ 230 | 'This paragraph contains a soft line break', 231 | '', 232 | 'This paragraph contains', 233 | 'an hard line break', 234 | '', 235 | 'This paragraph contains an emphasis before', 236 | 'an hard line break', 237 | '', 238 | 'This paragraph contains an emphasis', 239 | 'after an hard line break', 240 | '', 241 | 'This paragraph contains', 242 | 'an emphasis with an hard line break in the middle', 243 | '', 244 | '→ This paragraph contains an hard line break', 245 | 'and starts with the multibyte character "\u2192"', 246 | '', 247 | 'Line breaks do\ not occur inside code spans' 248 | ], 249 | # Expected text properties 250 | [ 251 | [], 252 | [], 253 | [], 254 | [], 255 | [], 256 | [{'col': 37, 'type': 'LspMarkdownItalic', 'length': 6}], 257 | [], 258 | [], 259 | [], 260 | [{'col': 1, 'type': 'LspMarkdownItalic', 'length': 5}], 261 | [], 262 | [{'col': 16, 'type': 'LspMarkdownItalic', 'length': 8}], 263 | [{'col': 1, 'type': 'LspMarkdownItalic', 'length': 11}], 264 | [], 265 | [], 266 | [], 267 | [], 268 | [{'col': 13, 'type': 'LspMarkdownCode', 'length': 15}] 269 | ] 270 | ], 271 | [ 272 | # non-breaking space characters 273 | # Input text 274 | [ 275 | '  This is text.', 276 | ], 277 | # Expected text 278 | [ 279 | ' This is text.', 280 | ], 281 | # Expected text properties 282 | [ 283 | [] 284 | ] 285 | ], 286 | ] 287 | 288 | var doc: dict> 289 | var text_result: list 290 | var props_result: list>> 291 | for t in tests 292 | doc = md.ParseMarkdown(t[0]) 293 | text_result = doc.content->deepcopy()->map((_, v) => v.text) 294 | props_result = doc.content->deepcopy()->map((_, v) => v.props) 295 | assert_equal(t[1], text_result, t[0]->string()) 296 | assert_equal(t[2], props_result, t[0]->string()) 297 | endfor 298 | enddef 299 | 300 | # Only here to because the test runner needs it 301 | def g:StartLangServer(): bool 302 | return true 303 | enddef 304 | 305 | # vim: tabstop=8 shiftwidth=2 softtabstop=2 noexpandtab 306 | -------------------------------------------------------------------------------- /test/not_lspserver_related_tests.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # Unit tests for Vim Language Server Protocol (LSP) for various functionality 3 | 4 | # Test for no duplicates in helptags 5 | def g:Test_Helptags() 6 | :helptags ../doc 7 | enddef 8 | 9 | # Only here to because the test runner needs it 10 | def g:StartLangServer(): bool 11 | return true 12 | enddef 13 | 14 | # vim: shiftwidth=2 softtabstop=2 noexpandtab 15 | -------------------------------------------------------------------------------- /test/run_tests.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Script to run the unit-tests for the LSP Vim plugin on MS-Windows 4 | 5 | SETLOCAL 6 | SET VIMPRG="vim.exe" 7 | SET VIM_CMD=%VIMPRG% -u NONE -U NONE -i NONE --noplugin -N --not-a-term 8 | 9 | %VIM_CMD% -c "let g:TestName='clangd_tests.vim'" -S runner.vim 10 | 11 | echo LSP unit test results 12 | type results.txt 13 | 14 | findstr /I FAIL results.txt > nul 2>&1 15 | if %ERRORLEVEL% EQU 0 echo ERROR: Some test failed. 16 | if %ERRORLEVEL% NEQ 0 echo SUCCESS: All the tests passed. 17 | 18 | -------------------------------------------------------------------------------- /test/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to run the unit-tests for the LSP Vim plugin 4 | 5 | VIMPRG=${VIMPRG:=$(which vim)} 6 | if [ -z "$VIMPRG" ]; then 7 | echo "ERROR: vim (\$VIMPRG) is not found in PATH" 8 | exit 1 9 | fi 10 | 11 | VIM_CMD="$VIMPRG -u NONE -U NONE -i NONE --noplugin -N --not-a-term" 12 | 13 | TESTS="clangd_tests.vim tsserver_tests.vim gopls_tests.vim not_lspserver_related_tests.vim markdown_tests.vim rust_tests.vim" 14 | 15 | RunTestsInFile() { 16 | testfile=$1 17 | echo "Running tests in $testfile" 18 | $VIM_CMD -c "let g:TestName='$testfile'" -S runner.vim 19 | 20 | if ! [ -f results.txt ]; then 21 | echo "ERROR: Test results file 'results.txt' is not found." 22 | exit 2 23 | fi 24 | 25 | cat results.txt 26 | 27 | if grep -qw FAIL results.txt; then 28 | echo "ERROR: Some test(s) in $testfile failed." 29 | exit 3 30 | fi 31 | 32 | echo "SUCCESS: All the tests in $testfile passed." 33 | echo 34 | } 35 | 36 | for testfile in $TESTS 37 | do 38 | RunTestsInFile $testfile 39 | done 40 | 41 | for encoding in "utf-8" "utf-16" "utf-32" 42 | do 43 | export LSP_OFFSET_ENCODING=$encoding 44 | echo "LSP offset encoding: $LSP_OFFSET_ENCODING" 45 | RunTestsInFile clangd_offsetencoding.vim 46 | done 47 | 48 | echo "SUCCESS: All the tests passed." 49 | exit 0 50 | 51 | # vim: shiftwidth=2 softtabstop=2 noexpandtab 52 | -------------------------------------------------------------------------------- /test/runner.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # Script to run a language server unit tests 3 | # The global variable TestName should be set to the name of the file 4 | # containing the tests. 5 | 6 | source common.vim 7 | 8 | def LspRunTests() 9 | :set nomore 10 | :set debug=beep 11 | delete('results.txt') 12 | 13 | # Get the list of test functions in this file and call them 14 | var fns: list = execute('function /^Test_') 15 | ->split("\n") 16 | ->map("v:val->substitute('^def ', '', '')") 17 | ->sort() 18 | if fns->empty() 19 | # No tests are found 20 | writefile(['No tests are found'], 'results.txt') 21 | return 22 | endif 23 | for f in fns 24 | v:errors = [] 25 | v:errmsg = '' 26 | try 27 | :%bw! 28 | exe $'g:{f}' 29 | catch 30 | call add(v:errors, $'Error: Test {f} failed with exception {v:exception} at {v:throwpoint}') 31 | endtry 32 | if v:errmsg != '' 33 | call add(v:errors, $'Error: Test {f} generated error {v:errmsg}') 34 | endif 35 | if !v:errors->empty() 36 | writefile(v:errors, 'results.txt', 'a') 37 | writefile([$'{f}: FAIL'], 'results.txt', 'a') 38 | else 39 | writefile([$'{f}: pass'], 'results.txt', 'a') 40 | endif 41 | endfor 42 | enddef 43 | 44 | try 45 | g:LoadLspPlugin() 46 | exe $'source {g:TestName}' 47 | g:StartLangServer() 48 | LspRunTests() 49 | catch 50 | writefile(['FAIL: Tests in ' .. g:TestName .. ' failed with exception ' .. v:exception .. ' at ' .. v:throwpoint], 'results.txt', 'a') 51 | endtry 52 | 53 | qall! 54 | 55 | # vim: shiftwidth=2 softtabstop=2 noexpandtab 56 | -------------------------------------------------------------------------------- /test/rust_tests.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # Unit tests for LSP rust-analyzer client 3 | 4 | source common.vim 5 | source term_util.vim 6 | source screendump.vim 7 | 8 | var lspServers = [{ 9 | filetype: ['rust'], 10 | path: exepath('rust-analyzer'), 11 | args: [] 12 | }] 13 | call LspAddServer(lspServers) 14 | echomsg systemlist($'{lspServers[0].path} --version') 15 | 16 | def g:Test_LspGotoDef() 17 | settagstack(0, {items: []}) 18 | :cd xrust_tests/src 19 | try 20 | silent! edit ./main.rs 21 | deletebufline('%', 1, '$') 22 | g:WaitForServerFileLoad(0) 23 | var lines: list =<< trim END 24 | fn main() { 25 | } 26 | fn foo() { 27 | } 28 | fn bar() { 29 | foo(); 30 | } 31 | END 32 | setline(1, lines) 33 | g:WaitForServerFileLoad(0) 34 | cursor(6, 5) 35 | :LspGotoDefinition 36 | assert_equal([3, 4], [line('.'), col('.')]) 37 | :%bw! 38 | finally 39 | :cd ../.. 40 | endtry 41 | enddef 42 | 43 | # Test for :LspCodeAction creating a file in the current directory 44 | def g:Test_LspCodeAction_CreateFile() 45 | :cd xrust_tests/src 46 | try 47 | silent! edit ./main.rs 48 | deletebufline('%', 1, '$') 49 | g:WaitForServerFileLoad(0) 50 | var lines: list =<< trim END 51 | mod foo; 52 | fn main() { 53 | } 54 | END 55 | setline(1, lines) 56 | g:WaitForServerFileLoad(1) 57 | cursor(1, 1) 58 | :LspCodeAction 1 59 | g:WaitForServerFileLoad(0) 60 | assert_true(filereadable('foo.rs')) 61 | :%bw! 62 | delete('foo.rs') 63 | finally 64 | :cd ../.. 65 | endtry 66 | enddef 67 | 68 | # Test for :LspCodeAction creating a file in a subdirectory 69 | def g:Test_LspCodeAction_CreateFile_Subdir() 70 | :cd xrust_tests/src 71 | try 72 | silent! edit ./main.rs 73 | deletebufline('%', 1, '$') 74 | g:WaitForServerFileLoad(0) 75 | var lines: list =<< trim END 76 | mod baz; 77 | fn main() { 78 | } 79 | END 80 | setline(1, lines) 81 | g:WaitForServerFileLoad(1) 82 | cursor(1, 1) 83 | :LspCodeAction 2 84 | g:WaitForServerFileLoad(0) 85 | assert_true(filereadable('baz/mod.rs')) 86 | :%bw! 87 | delete('baz', 'rf') 88 | finally 89 | :cd ../.. 90 | endtry 91 | enddef 92 | 93 | # Test for :LspCodeAction renaming a file 94 | def g:Test_LspCodeAction_RenameFile() 95 | :cd xrust_tests/src 96 | try 97 | silent! edit ./main.rs 98 | deletebufline('%', 1, '$') 99 | g:WaitForServerFileLoad(0) 100 | writefile([], 'foobar.rs') 101 | var lines: list =<< trim END 102 | mod foobar; 103 | fn main() { 104 | } 105 | END 106 | setline(1, lines) 107 | g:WaitForServerFileLoad(0) 108 | cursor(1, 5) 109 | :LspRename foobaz 110 | g:WaitForServerFileLoad(0) 111 | assert_true(filereadable('foobaz.rs')) 112 | :%bw! 113 | delete('foobaz.rs') 114 | finally 115 | :cd ../.. 116 | endtry 117 | enddef 118 | 119 | def g:Test_ZZZ_Cleanup() 120 | delete('./xrust_tests', 'rf') 121 | enddef 122 | 123 | # Start the rust-analyzer language server. Returns true on success and false 124 | # on failure. 125 | def g:StartLangServer(): bool 126 | system('cargo new xrust_tests') 127 | :cd xrust_tests/src 128 | var status = false 129 | try 130 | status = g:StartLangServerWithFile('./main.rs') 131 | finally 132 | :cd ../.. 133 | endtry 134 | return status 135 | enddef 136 | 137 | # vim: shiftwidth=2 softtabstop=2 noexpandtab 138 | -------------------------------------------------------------------------------- /test/screendump.vim: -------------------------------------------------------------------------------- 1 | " Functions shared by tests making screen dumps. 2 | 3 | source term_util.vim 4 | 5 | " Skip the rest if there is no terminal feature at all. 6 | if !has('terminal') 7 | finish 8 | endif 9 | 10 | " Read a dump file "fname" and if "filter" exists apply it to the text. 11 | def ReadAndFilter(fname: string, filter: string): list 12 | var contents = readfile(fname) 13 | 14 | if filereadable(filter) 15 | # do this in the bottom window so that the terminal window is unaffected 16 | wincmd j 17 | enew 18 | setline(1, contents) 19 | exe "source " .. filter 20 | contents = getline(1, '$') 21 | enew! 22 | wincmd k 23 | redraw 24 | endif 25 | 26 | return contents 27 | enddef 28 | 29 | 30 | " Verify that Vim running in terminal buffer "buf" matches the screen dump. 31 | " "options" is passed to term_dumpwrite(). 32 | " Additionally, the "wait" entry can specify the maximum time to wait for the 33 | " screen dump to match in msec (default 1000 msec). 34 | " The file name used is "dumps/{filename}.dump". 35 | " 36 | " To ignore part of the dump, provide a "dumps/{filename}.vim" file with 37 | " Vim commands to be applied to both the reference and the current dump, so 38 | " that parts that are irrelevant are not used for the comparison. The result 39 | " is NOT written, thus "term_dumpdiff()" shows the difference anyway. 40 | " 41 | " Optionally an extra argument can be passed which is prepended to the error 42 | " message. Use this when using the same dump file with different options. 43 | " Returns non-zero when verification fails. 44 | func VerifyScreenDump(buf, filename, options, ...) 45 | let reference = 'dumps/' . a:filename . '.dump' 46 | let filter = 'dumps/' . a:filename . '.vim' 47 | let testfile = 'failed/' . a:filename . '.dump' 48 | 49 | let max_loops = get(a:options, 'wait', 1000) / 10 50 | 51 | " Starting a terminal to make a screendump is always considered flaky. 52 | let g:test_is_flaky = 1 53 | 54 | " wait for the pending updates to be handled. 55 | call TermWait(a:buf) 56 | 57 | " Redraw to execute the code that updates the screen. Otherwise we get the 58 | " text and attributes only from the internal buffer. 59 | redraw 60 | 61 | if filereadable(reference) 62 | let refdump = ReadAndFilter(reference, filter) 63 | else 64 | " Must be a new screendump, always fail 65 | let refdump = [] 66 | endif 67 | 68 | let did_mkdir = 0 69 | if !isdirectory('failed') 70 | let did_mkdir = 1 71 | call mkdir('failed') 72 | endif 73 | 74 | let i = 0 75 | while 1 76 | " leave some time for updating the original window 77 | sleep 10m 78 | call delete(testfile) 79 | call term_dumpwrite(a:buf, testfile, a:options) 80 | let testdump = ReadAndFilter(testfile, filter) 81 | if refdump == testdump 82 | call delete(testfile) 83 | if did_mkdir 84 | call delete('failed', 'd') 85 | endif 86 | break 87 | endif 88 | if i == max_loops 89 | " Leave the failed dump around for inspection. 90 | if filereadable(reference) 91 | let msg = 'See dump file difference: call term_dumpdiff("testdir/' .. testfile .. '", "testdir/' .. reference .. '")' 92 | if a:0 == 1 93 | let msg = a:1 . ': ' . msg 94 | endif 95 | if len(testdump) != len(refdump) 96 | let msg = msg . '; line count is ' . len(testdump) . ' instead of ' . len(refdump) 97 | endif 98 | else 99 | let msg = 'See new dump file: call term_dumpload("testdir/' .. testfile .. '")' 100 | " no point in retrying 101 | let g:run_nr = 10 102 | endif 103 | for i in range(len(refdump)) 104 | if i >= len(testdump) 105 | break 106 | endif 107 | if testdump[i] != refdump[i] 108 | let msg = msg . '; difference in line ' . (i + 1) . ': "' . testdump[i] . '"' 109 | endif 110 | endfor 111 | call assert_report(msg) 112 | return 1 113 | endif 114 | let i += 1 115 | endwhile 116 | return 0 117 | endfunc 118 | -------------------------------------------------------------------------------- /test/start_tsserver.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | source common.vim 3 | g:LoadLspPlugin() 4 | var lspServers = [{ 5 | filetype: ['typescript', 'javascript'], 6 | path: exepath('typescript-language-server'), 7 | args: ['--stdio'] 8 | }] 9 | g:LspAddServer(lspServers) 10 | g:StartLangServerWithFile('Xtest.ts') 11 | -------------------------------------------------------------------------------- /test/term_util.vim: -------------------------------------------------------------------------------- 1 | " Functions about terminal shared by several tests 2 | 3 | " Wrapper around term_wait(). 4 | " The second argument is the minimum time to wait in msec, 10 if omitted. 5 | func TermWait(buf, ...) 6 | let wait_time = a:0 ? a:1 : 10 7 | call term_wait(a:buf, wait_time) 8 | endfunc 9 | 10 | " Run Vim with "arguments" in a new terminal window. 11 | " By default uses a size of 20 lines and 75 columns. 12 | " Returns the buffer number of the terminal. 13 | " 14 | " Options is a dictionary, these items are recognized: 15 | " "keep_t_u7" - when 1 do not make t_u7 empty (resetting t_u7 avoids clearing 16 | " parts of line 2 and 3 on the display) 17 | " "rows" - height of the terminal window (max. 20) 18 | " "cols" - width of the terminal window (max. 78) 19 | " "statusoff" - number of lines the status is offset from default 20 | " "wait_for_ruler" - if zero then don't wait for ruler to show 21 | " "no_clean" - if non-zero then remove "--clean" from the command 22 | func RunVimInTerminal(arguments, options) 23 | " If Vim doesn't exit a swap file remains, causing other tests to fail. 24 | " Remove it here. 25 | call delete(".swp") 26 | 27 | if exists('$COLORFGBG') 28 | " Clear $COLORFGBG to avoid 'background' being set to "dark", which will 29 | " only be corrected if the response to t_RB is received, which may be too 30 | " late. 31 | let $COLORFGBG = '' 32 | endif 33 | 34 | " Make a horizontal and vertical split, so that we can get exactly the right 35 | " size terminal window. Works only when the current window is full width. 36 | call assert_equal(&columns, winwidth(0)) 37 | split 38 | vsplit 39 | 40 | " Always do this with 256 colors and a light background. 41 | set t_Co=256 background=light 42 | hi Normal ctermfg=NONE ctermbg=NONE 43 | 44 | " Make the window 20 lines high and 75 columns, unless told otherwise or 45 | " 'termwinsize' is set. 46 | let rows = get(a:options, 'rows', 20) 47 | let cols = get(a:options, 'cols', 75) 48 | let statusoff = get(a:options, 'statusoff', 1) 49 | 50 | if get(a:options, 'keep_t_u7', 0) 51 | let reset_u7 = '' 52 | else 53 | let reset_u7 = ' --cmd "set t_u7=" ' 54 | endif 55 | 56 | let cmd = exepath('vim') .. ' -u NONE --clean --not-a-term --cmd "set enc=utf8"'.. reset_u7 .. a:arguments 57 | 58 | if get(a:options, 'no_clean', 0) 59 | let cmd = substitute(cmd, '--clean', '', '') 60 | endif 61 | 62 | let options = #{curwin: 1} 63 | if &termwinsize == '' 64 | let options.term_rows = rows 65 | let options.term_cols = cols 66 | endif 67 | 68 | " Accept other options whose name starts with 'term_'. 69 | call extend(options, filter(copy(a:options), 'v:key =~# "^term_"')) 70 | 71 | let buf = term_start(cmd, options) 72 | 73 | if &termwinsize == '' 74 | " in the GUI we may end up with a different size, try to set it. 75 | if term_getsize(buf) != [rows, cols] 76 | call term_setsize(buf, rows, cols) 77 | endif 78 | call assert_equal([rows, cols], term_getsize(buf)) 79 | else 80 | let rows = term_getsize(buf)[0] 81 | let cols = term_getsize(buf)[1] 82 | endif 83 | 84 | call TermWait(buf) 85 | 86 | if get(a:options, 'wait_for_ruler', 1) 87 | " Wait for "All" or "Top" of the ruler to be shown in the last line or in 88 | " the status line of the last window. This can be quite slow (e.g. when 89 | " using valgrind). 90 | " If it fails then show the terminal contents for debugging. 91 | try 92 | call g:WaitFor({-> len(term_getline(buf, rows)) >= cols - 1 || len(term_getline(buf, rows - statusoff)) >= cols - 1}) 93 | catch /timed out after/ 94 | let lines = map(range(1, rows), {key, val -> term_getline(buf, val)}) 95 | call assert_report('RunVimInTerminal() failed, screen contents: ' . join(lines, "")) 96 | endtry 97 | endif 98 | 99 | return buf 100 | endfunc 101 | 102 | " Stop a Vim running in terminal buffer "buf". 103 | func StopVimInTerminal(buf, kill = 1) 104 | call assert_equal("running", term_getstatus(a:buf)) 105 | 106 | " Wait for all the pending updates to terminal to complete 107 | call TermWait(a:buf) 108 | 109 | " CTRL-O : works both in Normal mode and Insert mode to start a command line. 110 | " In Command-line it's inserted, the CTRL-U removes it again. 111 | call term_sendkeys(a:buf, "\:\qa!\") 112 | 113 | " Wait for all the pending updates to terminal to complete 114 | call TermWait(a:buf) 115 | 116 | " Wait for the terminal to end. 117 | call WaitForAssert({-> assert_equal("finished", term_getstatus(a:buf))}) 118 | 119 | " If the buffer still exists forcefully wipe it. 120 | if a:kill && bufexists(a:buf) 121 | exe a:buf .. 'bwipe!' 122 | endif 123 | endfunc 124 | 125 | " vim: shiftwidth=2 sts=2 expandtab 126 | -------------------------------------------------------------------------------- /test/tsserver_tests.vim: -------------------------------------------------------------------------------- 1 | vim9script 2 | # Unit tests for Vim Language Server Protocol (LSP) typescript client 3 | 4 | source common.vim 5 | source term_util.vim 6 | source screendump.vim 7 | 8 | var lspServers = [{ 9 | filetype: ['typescript', 'javascript'], 10 | path: exepath('typescript-language-server'), 11 | args: ['--stdio'] 12 | }] 13 | call LspAddServer(lspServers) 14 | echomsg systemlist($'{lspServers[0].path} --version') 15 | 16 | # Test for auto-completion. Make sure that only keywords that matches with the 17 | # keyword before the cursor are shown. 18 | # def g:Test_LspCompletion1() 19 | # var lines =<< trim END 20 | # const http = require('http') 21 | # http.cr 22 | # END 23 | # writefile(lines, 'Xcompletion1.js') 24 | # var buf = g:RunVimInTerminal('--cmd "silent so start_tsserver.vim" Xcompletion1.js', {rows: 10, wait_for_ruler: 1}) 25 | # sleep 5 26 | # term_sendkeys(buf, "GAe") 27 | # g:TermWait(buf) 28 | # g:VerifyScreenDump(buf, 'Test_tsserver_completion_1', {}) 29 | # term_sendkeys(buf, "\") 30 | # g:TermWait(buf) 31 | # g:VerifyScreenDump(buf, 'Test_tsserver_completion_2', {}) 32 | # 33 | # g:StopVimInTerminal(buf) 34 | # delete('Xcompletion1.js') 35 | # enddef 36 | 37 | # Start the typescript language server. Returns true on success and false on 38 | # failure. 39 | def g:StartLangServer(): bool 40 | return g:StartLangServerWithFile('Xtest.ts') 41 | enddef 42 | 43 | # vim: shiftwidth=2 softtabstop=2 noexpandtab 44 | --------------------------------------------------------------------------------