├── .github └── workflows │ ├── integration_tests.yml │ └── vint.yml ├── .gitignore ├── .vintrc.yml ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── after └── plugin │ └── lsc.vim ├── autoload └── lsc │ ├── capabilities.vim │ ├── channel.vim │ ├── complete.vim │ ├── config.vim │ ├── convert.vim │ ├── cursor.vim │ ├── diagnostics.vim │ ├── diff.vim │ ├── edit.vim │ ├── file.vim │ ├── highlights.vim │ ├── message.vim │ ├── params.vim │ ├── protocol.vim │ ├── reference.vim │ ├── search.vim │ ├── server.vim │ ├── signaturehelp.vim │ ├── uri.vim │ └── util.vim ├── doc └── lsc.txt ├── plugin └── lsc.vim └── test ├── diff_test.vim ├── initialization_input ├── integration ├── .gitignore ├── bin │ └── stub_server.dart ├── lib │ ├── stub_lsp.dart │ ├── test_bed.dart │ └── vim_remote.dart ├── pubspec.yaml ├── test │ ├── complete_test.dart │ ├── did_open_test.dart │ ├── did_save_test.dart │ ├── disabled_server_test.dart │ ├── early_exit_test.dart │ ├── edit_test.dart │ ├── highlight_test.dart │ ├── stderr_test.dart │ ├── vim_test.dart │ └── workspace_configuration_test.dart └── vimrc ├── travis.sh └── uri_test.vim /.github/workflows/integration_tests.yml: -------------------------------------------------------------------------------- 1 | name: integration tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | dart-tests: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | 17 | name: Run integration tests 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: dart-lang/setup-dart@v1.3 22 | with: 23 | sdk: 2.19.0 24 | - name: checkout 25 | uses: actions/checkout@v2 26 | - name: dart pub upgrade 27 | working-directory: test/integration 28 | run: dart pub upgrade 29 | - uses: cachix/install-nix-action@v17 30 | with: 31 | nix_path: nixpkgs=channel:nixos-unstable 32 | - uses: workflow/nix-shell-action@v3.0.2 33 | with: 34 | packages: xvfb-run,vim_configurable 35 | script: cd test/integration/; xvfb-run dart test 36 | -------------------------------------------------------------------------------- /.github/workflows/vint.yml: -------------------------------------------------------------------------------- 1 | name: vint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | vim-vint: 13 | name: Lint the viml with vint 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Run vint 21 | uses: ludvighz/vint-action@v1 22 | with: 23 | path: . 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /.vintrc.yml: -------------------------------------------------------------------------------- 1 | cmdargs: 2 | severity: style_problem 3 | color: true 4 | policies: 5 | ProhibitImplicitScopeVariable: 6 | enabled: true 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Nate Bosch 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.5.0 2 | 3 | **Bug fixes** 4 | - Fix occasional errors stemming from interleaving the handling of multiple 5 | messages from the server. 6 | - Support the `DocumentHighlightOptions` format for 7 | `documentHighlightsProvider`. 8 | - Avoid a state where no document highlights reference calls are made following 9 | a server restart. 10 | - Avoid creating public functions for callbacks which should be temporary. 11 | - Fix file edits in buffers that aren't current when the current buffer is 12 | shorter than the buffer being edited. 13 | 14 | **Minor breaking changes** 15 | - Remove `noselect` from the default `completeopts` used during autocompletion. 16 | Re-enable this with `set completeopts+=noselect`. This changes the initial 17 | behavior of some keys when autocomplete starts. 18 | 19 | **Enhancements** 20 | - Improve performance when the language server sends many diagnostics updates. 21 | - Improve performance for periods of high message throughput. 22 | - Add `g:lsc_diagnostic_highlights` config, and commands to enable and disable 23 | diagnostic highlights. 24 | - Add `g:lsc_block_complete_triggers` to override server config and disable 25 | autocomplete after specific characters. 26 | 27 | # 0.4.0 28 | 29 | **Bug fixes** 30 | - Allow use with version that don't have the `trim()` function. 31 | - Avoid sending unnecessary `didClose` notifications for buffers of the wrong 32 | filetype. 33 | - Fix `getbufinfo` calls for loaded buffers. 34 | - Fix completions starting at the beginning of the line when the server did not 35 | send any items containing a `textEdit` field. 36 | - Truncate diagnostics at 1 character shorter for when `ruler` is used. 37 | - Normalize windows file path separators to create valid URIs. 38 | - Don't send `textDocument/didSave` notifications if the server does not 39 | advertise it as a capability. 40 | - Fix edits when there are folds in the buffer. 41 | 42 | **Minor breaking changes** 43 | - Server dictionaries no longer expose their full `init_results`, or their call 44 | logs. 45 | - Default keybinding for `LSClientSignatureHelp` changed to `gm`. 46 | - Notifications which are not handled by the client are ignored instead of 47 | reported. 48 | 49 | **Enhancements** 50 | - More tolerant towards buggy language servers that omit the `result` field on 51 | response message. 52 | - Add `g:lsc_autocomplete_length` to configure how many word characters to wait 53 | for before starting autocomplete. Set to `0` or `v:false` to disable 54 | autocomplete on anything other than trigger characters, or to a number to vary 55 | the length before autocomplete from the default of `3`. 56 | - Add support for skipping calls by adding a `message_hook` and returning 57 | `lsc#config#skip()`. 58 | - Add `'notifications'` support in server config to add callbacks for server 59 | specific notifications. 60 | - After calling `:LSClientAllDiagnostics` the quickfix list will be kept up to 61 | date with all diagnostics across the project, until it is set by some other 62 | tool. 63 | - Add support for a `workspace_config` server configuration key which causes a 64 | `workspace/didChangeConfiguration` notification on server startup. 65 | - Use the popup window for hover. 66 | - Add autocmd `User LSCDiagnosticsChange` to trigger when diagnostics are 67 | received. 68 | - Add support for a custom action menu with `g:LSC_action_menu`. 69 | - Sort completion suggestions that match by prefix higher than those that match 70 | by substring. 71 | - Include Completion item `detail` field in the preview window. 72 | - Strikethrough deprecated completion options in the menu. 73 | - Improve performance of finding incremental changes for file syncing in very 74 | large files when lua support is available. 75 | - Send `hover.contentFormat` to prefer plaintext content which should be more 76 | readable for most users. 77 | - Use full words for completion item kinds instead of single letters. 78 | - Avoid overwriting location list if it is in use for something other than LSC 79 | diagnostics. 80 | - Asynchronously loop over server messages to avoid long synchronous pauses 81 | between handling user input. 82 | 83 | # 0.3.2 84 | 85 | - `:LSClientShowHover` is now bound with `keywordprg` instead of by mapping `K`. 86 | If the `g:lsc_auto_map` manually specifies a binding of `'K'` it should be 87 | dropped to pick up the default, or switched to `v:true` to use `keywordprg` 88 | instead. If the mapping is set to a string it will continue to be mapped as 89 | usual, if it is mapped to `0` or `v:false` no mapping will occur, if it is set 90 | to `1` or `v:true` then `keywordprg` will be set. `:LSClientShowHover` also 91 | now allows an argument but it will always be ignored. 92 | - Added `g:lsc_enable_dagnostics`. Set to `v:false` to ignore all diagnostics 93 | sent by the server. 94 | - Notifications with a method starting with `$/` will be silently dropped 95 | instead of showing a message. 96 | - Fix a bug where `au_group_id` was not initialized correctly. 97 | - Ignore a `null` or empty `insertText` during completion. 98 | - Add support for `window/showMessageRequest`. 99 | - Add support for `:LSClientSignatureHelp` which calls 100 | `textDocument/signatureHelp`. 101 | - Add highlighting group `lscCurrentParameter` which is used to highlight the 102 | active parameter in the signature help. 103 | - Send `textDocument/didSave` notifications on `BufWritePost` if the server does 104 | not indicate they should be skipped. 105 | - Call `initialized` in response to the `initialize` result. 106 | - Fix a bug with responding to `window/showMessage` notifications. 107 | - Wait to call 'exit' until the 'shutdown' response comes back. 108 | - Update tag stack when jumping to definition. 109 | - Add special handling for window/progress messages 110 | - Add `:LSClientLineDiagnostics` to print diagnostics for the current line. 111 | - Allow server commands to be specified as lists. 112 | - Allow filtering what level of logs are echoed. 113 | - Fix a bug with incremental text change syncing when there are multi-byte 114 | characters in the buffer. 115 | - Fix some bugs with editing in a buffer for a file which has not been written, 116 | and would be written to a directory that does not exist. 117 | - Add `:LSClientGoToDefinitionSplit` to go to definitions in a split window 118 | (depending on `switchbuf`). 119 | - Fix a bug where some uses of the quick fix list would modify paths under the 120 | home directory and make it impossible to jump to them. 121 | - Fix a race condition where the same message from the server may be handled 122 | twice. 123 | 124 | # 0.3.1 125 | 126 | - Allow using the default map but overriding or omitting a subset of the keys. 127 | - Set `completefunc` even when autocomplete is enabled. 128 | - Don't eagerly attempt to fetch completions when autocomplete is disabled. 129 | Allow fetching completions at any time when the manual completion function is 130 | triggered. 131 | - Add support for passing a pattern to `:LSClientFindCodeActions`. When exactly 132 | one action has a `title` that matches the pattern it will be run 133 | automatically. 134 | - Bug fix: Handle workspace edits that have double quotes. 135 | - Add support for `CodeAction` literals. 136 | - Bug fix: Correctly truncate multi-byte or wide character diagnostics. 137 | - Bug fix: Allow duplicate words in completions (overloads). 138 | 139 | # 0.3.0 140 | 141 | - Add support for neovim. 142 | - Add highlighting based on references to the symbol under the cursor. Disable 143 | with `let g:lsc_reference_highlights = v:false`. Customize highlighting with 144 | the group `lscReference`. 145 | - Add `LSClientNextReference` and `LSClientPreviousReference` command to jump to 146 | references when highlighting is enabled. 147 | - Bug Fix: Capitalize variable handling message hooks to allow `funcref` values. 148 | - Bug Fix: Order edits before applying them so that the offsets are correctly 149 | matched to the original file. 150 | - Add `LSClientWorkspaceSymbol`. 151 | - Add `LSClientFindImplementations`. 152 | - Enable incremental sync by default. 153 | - Enable apply edit by default. 154 | - Improve the preview height for hover text which has few lines but they wrap. 155 | - Bug Fix: Include diagnostics for the current line with code actions requests. 156 | 157 | # 0.2.10 158 | 159 | - Add `:LSClientDocumentSymbol` command to populate the quickfix list with 160 | symbols in the current document. 161 | - Bug Fix: Clear highlighting when entering buffers that don't fire 162 | `BufWinEnter` but do fire `BufEnter`. Restore highlights when opening a buffer 163 | that in a window that previously had cleared highlights. 164 | 165 | # 0.2.9+1 166 | 167 | - Fix error in calling function message hooks. 168 | 169 | # 0.2.9 170 | 171 | - Add an argument to `lsc#edit#findCodeActions` to pass a callback to choose an 172 | action. 173 | - Save and restore window view when applying a workspace edit. 174 | - Bug fix: Handle zero width edits at the end of a line. 175 | - Add support for `textDocument/rename`. 176 | - Support `TextEdit`s in non-current buffers. 177 | - Add `lsc#diagnostics#count()` 178 | - Add `:LSClientAllDiagnostics` which populates the quickfix list with 179 | diagnostics across all files that have been sent by any server. 180 | - Bug fix: Don't make callback functions global. 181 | - Reduce performance impact of a large number of files with empty diagnostics. 182 | - Allow `message_hooks` values to be a `dict` which gets merged into `params`. 183 | Supports inner values which are functions that get called to resolve values. 184 | - Support highlights for multi-line diagnostics. 185 | - Split up large messages into chunks to avoid potential deadlocks where output 186 | buffer becomes full but it isn't read. 187 | - Improve performance of incremental diff. 188 | - Print messages received on stderr. 189 | - Don't open an empty quickfix list when no references are found. 190 | - Don't mask `hlsearch` with diagnostics. 191 | 192 | # 0.2.8 193 | 194 | - Don't track files which are not `modifiable`. 195 | - Bug Fix: Fix jumping from quickfix item to files under home directory. 196 | - Only update diagnostic under cursor when the change is for the current file. 197 | - Add support for additional per-server config. 198 | - Bug Fix: If using `g:lsc_enable_incremental_sync` correctly handles multiple 199 | blank lines in a row. 200 | - Add support for overriding the `params` for certain methods. 201 | - Bug Fix: Correct paths on Windows. 202 | - Bug Fix: Allow restarting a server which failed to start initially. 203 | - Add experimental support for `textDocument/codeActions` and 204 | `workspace/applyEdit` 205 | 206 | # 0.2.7 207 | 208 | - Add support for `TextDocumentSyncKind.Incremental`. There is a new setting 209 | called `g:lsc_enable_incremental_sync` which is defaulted to `v:false` to 210 | allow the client to attempt incremental syncs. This feature is experimental. 211 | - Bug Fix: Use buffer filetype rather than current filetype when flushing file 212 | changes for background buffers. 213 | - Update the diagnostic under the cursor when diagnostics change for the file. 214 | - Bug Fix: Don't change diagnostic highlights while in visual mode. 215 | 216 | # 0.2.6 217 | 218 | - Send file updates after re-reading already open file. Fixes some cases where 219 | the server has a different idea of the file content than the editor. 220 | - Avoid clearing and readding the same diagnostic matches each time diagnostics 221 | change in some other file. 222 | - Avoid doing work on diagnostics for unopened files. 223 | - Bug Fix: Use the correct diagnostics when updating the location list for 224 | windows other than the current window. 225 | - Add `LSCServerStatus()` function which returns a string representing the state 226 | of the language server for the current filetype. 227 | - `:LSCRestartServer` can now restart servers that failed, rather than just 228 | those which are currently running. 229 | - Bug Fix: Always send `didOpen` calls with the content they have at the time of 230 | initialization rather than what they had when the buffer was read. Fixes some 231 | cases where an edit before the server is read would get lost. 232 | - Bug Fix: Handle case where a `GoToDefinition` is an empty list rather than 233 | null. 234 | - Bug Fix: Handle case where initialization call gets a null response. 235 | - Bug Fix: Avoid breaking further callbacks when a message handler throws. 236 | - Bug Fix: Handle `MarkedString` and `List` results to 237 | `textDocument/hover` calls. 238 | - Add experimental support for communicating over a TCP channel. Configure the 239 | command as a "host:port" pair. 240 | - Bug Fix: Handle null completions response. 241 | - Bug Fix: Don't include an 'id' field for messages which are notifications. 242 | - Add support for `window/showMessage` and `window/logMessage`. 243 | - Use `` with `doautocmd`. 244 | - Bug Fix: Check for `lsc_flush_timer` before stopping it. 245 | - Show an error if a user triggered call fails. 246 | - Bug Fix: URI encode file paths. 247 | 248 | # 0.2.5 249 | 250 | - Add autocmds `LSCAutocomplete` before firing completion, and `LSCShowPreview` 251 | after opening the preview window. 252 | - Change Info and Hint diagnostic default highlight to `SpellCap`. 253 | - Append diagnostic code to the message. 254 | 255 | # 0.2.4 256 | 257 | - Bug Fix: Handle completion items with empty detail. 258 | - `LSClientShowHover` now reuses the window it already opened rather than 259 | closing it and splitting again to allow for maintaining layout. 260 | - Add optional configuration `g:lsc_preview_split_direction` to override 261 | `splitbelow`. 262 | - Add docs. 263 | 264 | # 0.2.3 265 | 266 | - `redraw` after jumping to definition in another file. 267 | - Allow configuring trace level with `g:lsc_trace_level`. May be one of 'off', 268 | 'messages', or 'verbose'. Defaults to 'off'. 269 | - Bug fix: Avoid deep stack during large spikes of messages. Switch from 270 | recursion to a while loop. 271 | - Add `LSClientDisable`, `LSClientEnable` to disable or re-enable the client 272 | for the current filetype during a session. 273 | - Add `LSClientShowHover` to display hover information in a preview window. 274 | - Add support for automatically mapping keys only in buffers for tracked 275 | filetypes. 276 | 277 | # 0.2.2 278 | 279 | - Completion Improvements: 280 | - Bug fix: Don't leave an extra character when completing after typing 3 281 | characters. 282 | - Filter completions after typing 3 characters. 283 | - Add configuration to disable autocomplete. 284 | - Bug Fix: Don't block future completion attempts after an empty suggestion 285 | list. 286 | - Use the `triggerCharacters` suggested by the server instead of `.` 287 | exclusively. 288 | - Use only the first line of suggestion detail in completion menu 289 | - Bug Fix: Send and allow a space before header content. 290 | 291 | # 0.2.1 292 | 293 | - Handle language server restarts: 294 | - Clean up local state when a language server exits. 295 | - Call `didOpen` for all open files when a language server (re)starts. 296 | - Add LSClientRestart command to restart the server for the current filetype. 297 | 298 | # 0.2.0 299 | 300 | - More detail in completion suggestions, doc comment in preview window. 301 | - Sort diagnostics in location list. 302 | - **Breaking**: Allow only 1 server command per filetype. 303 | - Add commands for GoToDefinition and FindReferences 304 | - Bug fix: Don't try to read lines from unreadable buffer. 305 | 306 | # 0.1.3 307 | 308 | - Bug fix: Newlines in diagnostics are replace with '\n' to avoid multiline 309 | messages 310 | - Add support for `textDocument/references` request. References are shown in 311 | quickfix list. 312 | - Bug fix: Support receiving diagnostics for files which are not opened in a 313 | buffer 314 | 315 | # 0.1.2 316 | 317 | - Bug fix: Leave a jump in the jumplist when moving to a definition in the same 318 | file 319 | - Completion improvements: 320 | - Overwrite `completeopt` before completion for a better experience. 321 | - Avoid completion requests while already giving suggestions 322 | - Improve heuristics for start of completion range 323 | - Flush file changes after completion 324 | - Bug fix: Don't change window highlights when in select mode 325 | - Bug fix: Location list is cleared when switching to a non-tracked filetype, 326 | and kept up to date across windows and tabs showing the same buffer 327 | 328 | # 0.1.1 329 | 330 | - Call initialize first for better protocol compliance 331 | - Use a relative path where possible when jumping to definition 332 | - Only display 'message' field for errors 333 | - Bug Fix: Less likely to delete inserted text when trying to complete 334 | - Bug Fix: More likely to try to complete when not following a '.' 335 | - Populate location list with diagnostics 336 | - Bug fix: Don't try to 'edit' the current file 337 | 338 | # 0.1.0 339 | 340 | Experimental first release - there are protocol bugs, for instance this does not 341 | call the required `initialize` method. Only known to work with the 342 | `dart_language_server` implementation. 343 | 344 | Supports: 345 | - Diagnostic highlights 346 | - Autocomplete suggestions 347 | - Jump to definition 348 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 vim-lsc authors 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vim Language Server Client 2 | 3 | Adds language-aware tooling to vim by communicating with a language server 4 | following the [language server protocol][]. For more information see 5 | [langserver.org][]. 6 | 7 | [language server protocol]: https://github.com/Microsoft/language-server-protocol 8 | [langserver.org]: http://langserver.org/ 9 | 10 | ## Installation 11 | 12 | Install with your plugin management method of choice. If you don't have a 13 | preference check out [vim-plug][]. Install a language server and ensure it is 14 | executable from your `$PATH`. 15 | 16 | vim-lsc is compatible with vim 8, and neovim. 17 | 18 | Note: When using neovim be sure to use `set shortmess-=F` to avoid suppressing 19 | error messages from this plugin. 20 | 21 | [vim-plug]:https://github.com/junegunn/vim-plug 22 | 23 | ## Configuration 24 | 25 | Map a filetype to the command that starts the language server for that filetype 26 | in your `vimrc`. 27 | 28 | ```viml 29 | let g:lsc_server_commands = {'dart': 'dart_language_server'} 30 | ``` 31 | 32 | To disable autocomplete in favor of manual completion also add 33 | 34 | ```viml 35 | let g:lsc_enable_autocomplete = v:false 36 | ``` 37 | 38 | Most interactive features are triggered by commands. You can use 39 | `g:lsc_auto_map` to have them automatically mapped for the buffers which have a 40 | language server enabled. You can use the default mappings by setting it to 41 | `v:true`, or specify your own mappings in a dict. 42 | 43 | Most keys take strings or lists of strings which are the keys bound to that 44 | command in normal mode. The `'ShowHover'` key can also be `v:true` in which case 45 | it sets `keywordprg` instead of a keybind (`keywordprg` maps `K`). The 46 | 'Completion' key sets a completion function for manual invocation, and should be 47 | either `'completefunc'` or `'omnifunc'` (see `:help complete-functions`). 48 | 49 | ```viml 50 | " Use all the defaults (recommended): 51 | let g:lsc_auto_map = v:true 52 | 53 | " Apply the defaults with a few overrides: 54 | let g:lsc_auto_map = {'defaults': v:true, 'FindReferences': 'r'} 55 | 56 | " Setting a value to a blank string leaves that command unmapped: 57 | let g:lsc_auto_map = {'defaults': v:true, 'FindImplementations': ''} 58 | 59 | " ... or set only the commands you want mapped without defaults. 60 | " Complete default mappings are: 61 | let g:lsc_auto_map = { 62 | \ 'GoToDefinition': '', 63 | \ 'GoToDefinitionSplit': [']', ''], 64 | \ 'FindReferences': 'gr', 65 | \ 'NextReference': '', 66 | \ 'PreviousReference': '', 67 | \ 'FindImplementations': 'gI', 68 | \ 'FindCodeActions': 'ga', 69 | \ 'Rename': 'gR', 70 | \ 'ShowHover': v:true, 71 | \ 'DocumentSymbol': 'go', 72 | \ 'WorkspaceSymbol': 'gS', 73 | \ 'SignatureHelp': 'gm', 74 | \ 'Completion': 'completefunc', 75 | \} 76 | ``` 77 | 78 | During the initialization call LSP supports a `trace` argument which configures 79 | logging on the server. Set this with `g:lsc_trace_level`. Valid values are 80 | `'off'`, `'messages'`, or `'verbose'`. Defaults to `'off'`. 81 | 82 | ## Features 83 | 84 | The protocol does not require that every language server supports every feature 85 | so support may vary. 86 | 87 | All communication with the server is asynchronous and will not block the editor. 88 | For requests that trigger an action the response might be silently ignored if it 89 | can no longer be used - you can abort most operations that are too slow by 90 | moving the cursor. 91 | 92 | The client can be temporarily disabled for a session with `LSClientDisable` and 93 | re-enabled with `LSClientEnable`. At any time the server can be exited and 94 | restarted with `LSClientRestartServer` - this sends a request for the server to 95 | exit rather than kill it's process so a completely unresponsive server should be 96 | killed manually instead. 97 | 98 | ### Diagnostics 99 | 100 | Errors, warnings, and hints reported by the server are highlighted in the 101 | buffer. When the cursor is on a line with a diagnostic the message will be 102 | displayed. If there are multiple diagnostics on a line the one closest to the 103 | cursor will be displayed. 104 | 105 | Diagnostics are also reported in the location list for each window which has the 106 | buffer open. **Tip:** Use `:lbefore` or `:lafter` to jump to the next diagnostic 107 | before or after the cursor position. If the location list was overwritten it can 108 | be restored with `:LSCLientWindowDiagnostics`. 109 | 110 | Run `:LSClientAllDiagnostics` to populate, and maintain, a list of all 111 | diagnostics across the project in the quickfix list. 112 | 113 | ### Autocomplete 114 | 115 | When more than 3 word characters or a trigger character are typed a request for 116 | autocomplete suggestions is sent to the server. If the server responds before 117 | the cursor moves again the options will be provided using vim's built in 118 | completion. 119 | 120 | Note: By default `completeopt` includes `preview` and completion items include 121 | documentation in the preview window. Close the window after completion with 122 | `` or disable with `set completeopt-=preview`. To automatically close 123 | the documentation window use the following: 124 | 125 | ```viml 126 | autocmd CompleteDone * silent! pclose 127 | ``` 128 | 129 | Disable autocomplete with `let g:lsc_enable_autocomplete = v:false`. A 130 | completion function is available at `lsc#complete#complete` (set to 131 | `completefunc` if applying the default keymap). This is synchronous and has a 132 | cap of 5 seconds to wait for the server to respond. It can be used whether 133 | autocomplete is enabled or not. 134 | 135 | ### Reference Highlights 136 | 137 | If the server supports the `textDocument/documentHighlight` call references to 138 | the element under the cursor throughout the document will be highlighted. 139 | Disable with `let g:lsc_reference_highlights = v:false` or customize the 140 | highlighting with the group `lscReference`. Use `` 141 | (`:LSClientNextReference`) or `` (`:LSClientPReviousReference`) to jump to 142 | other reference to the currently highlighted element. 143 | 144 | ### Jump to definition 145 | 146 | While the cursor is on any identifier call `LSClientGoToDefinition` (`` if 147 | using the default mappings) to jump to the location of the definition. If the 148 | cursor moves before the server responds the response will be ignored. 149 | 150 | ### Find references 151 | 152 | While the cursor is on any identifier call `LSClientFindReferences` (`gr` if 153 | using the default mappings) to populate the quickfix list with usage locations. 154 | 155 | ### Find implementations 156 | 157 | While the cursor is on any identifier call `LSClientFindImplementations` (`gI` 158 | if using the default mappings) to populate the quickfix list with implementation 159 | locations. 160 | 161 | ### Document Symbols 162 | 163 | Call `LSClientDocumentSymbol` (`go` if using the default mappings) to populate 164 | the quickfix list with the locations of all symbols in that document. 165 | 166 | ### Workspace Symbol Search 167 | 168 | Call `LSClientWorkspaceSymbol` with a no arguments, or with a single String 169 | argument. (`gS` if using the default mappings) to query the server for symbols 170 | matching a search string. Results will populate the quickfix list. 171 | 172 | ### Hover 173 | 174 | While the cursor is on any identifier call `LSClientShowHover` (`K` if using the 175 | default mappings, bound through `keywordprg`) to request hover text and show it 176 | in a popup or preview window. 177 | Override the direction of the split by setting `g:lsc_preview_split_direction` 178 | to either `'below'` or `'above'`. Quickly close the preview without switching 179 | buffers with ``. See `:help preview-window`. 180 | 181 | ### Code Actions 182 | 183 | Call `LSClientFindCodeActions` (`ga` if using the default mappings) to look for 184 | code actions available at the cursor location and run one by entering the number 185 | of the chosen action. 186 | 187 | ### Signature help 188 | 189 | Call `LSClientSignatureHelp` (`gm` if using the default mappings) to get help while writing 190 | a function call. The currently active parameter is highlighted with the group 191 | `lscCurrentParameter`. 192 | 193 | ## Integrations with other plugins 194 | 195 | - [vista.vim](https://github.com/liuchengxu/vista.vim) 196 | - [deoplete-vim-lsc](https://github.com/hrsh7th/deoplete-vim-lsc) 197 | - [vim-vsnip](https://github.com/hrsh7th/vim-vsnip) 198 | - [vim-vsnip-integ](https://github.com/hrsh7th/vim-vsnip-integ) 199 | 200 | -------------------------------------------------------------------------------- /after/plugin/lsc.vim: -------------------------------------------------------------------------------- 1 | if exists('g:lsc_registered_commands') | finish | endif 2 | let g:lsc_registered_commands = 1 3 | 4 | if !exists('g:lsc_server_commands') | finish | endif 5 | 6 | for [s:filetype, s:config] in items(g:lsc_server_commands) 7 | call RegisterLanguageServer(s:filetype, s:config) 8 | endfor 9 | -------------------------------------------------------------------------------- /autoload/lsc/capabilities.vim: -------------------------------------------------------------------------------- 1 | " Returns a client specific view of capabilities. 2 | " 3 | " Capabilities are filtered down to those this client cares about, and 4 | " strucutured to direclty answer the questions we have rather than use the LSP 5 | " types. 6 | function! lsc#capabilities#normalize(capabilities) abort 7 | let l:normalized = lsc#capabilities#defaults() 8 | if has_key(a:capabilities, 'completionProvider') && 9 | \ type(a:capabilities.completionProvider) != type(v:null) 10 | let l:completion_provider = a:capabilities.completionProvider 11 | if has_key(l:completion_provider, 'triggerCharacters') 12 | let l:normalized.completion.triggerCharacters = 13 | \ l:completion_provider['triggerCharacters'] 14 | let l:blocked = get(g:, 'lsc_block_complete_triggers', []) 15 | if !empty(l:blocked) 16 | call filter(l:normalized.completion.triggerCharacters, 17 | \ 'index(l:blocked, v:val) < 0') 18 | endif 19 | endif 20 | endif 21 | if has_key(a:capabilities, 'textDocumentSync') 22 | let l:text_document_sync = a:capabilities['textDocumentSync'] 23 | let l:incremental = v:false 24 | if type(l:text_document_sync) == type({}) 25 | if has_key(l:text_document_sync, 'change') 26 | let l:incremental = l:text_document_sync['change'] == 2 27 | endif 28 | let l:normalized.textDocumentSync.sendDidSave = 29 | \ has_key(l:text_document_sync, 'save') 30 | else 31 | let l:incremental = l:text_document_sync == 2 32 | endif 33 | let l:normalized.textDocumentSync.incremental = l:incremental 34 | endif 35 | let l:document_highlight_provider = 36 | \ get(a:capabilities, 'documentHighlightProvider', v:false) 37 | if type(l:document_highlight_provider) == type({}) 38 | let l:normalized.referenceHighlights = v:true 39 | else 40 | let l:normalized.referenceHighlights = l:document_highlight_provider 41 | endif 42 | return l:normalized 43 | endfunction 44 | 45 | function! lsc#capabilities#defaults() abort 46 | return { 47 | \ 'completion': {'triggerCharacters': []}, 48 | \ 'textDocumentSync': { 49 | \ 'incremental': v:false, 50 | \ 'sendDidSave': v:false, 51 | \ }, 52 | \ 'referenceHighlights': v:false, 53 | \} 54 | endfunction 55 | -------------------------------------------------------------------------------- /autoload/lsc/channel.vim: -------------------------------------------------------------------------------- 1 | function! lsc#channel#open(command, Callback, ErrCallback, OnExit) abort 2 | let l:c = s:Channel() 3 | if type(a:command) == type('') && a:command =~# '[^:]\+:\d\+' 4 | if exists('*ch_open') 5 | let l:channel_options = {'mode': 'raw', 6 | \ 'callback': {_, message -> a:Callback(message)}, 7 | \ 'close_cb': {_ -> a:OnExit()}} 8 | call s:WrapVim(ch_open(a:command, l:channel_options), l:c) 9 | return l:c 10 | elseif exists('*sockconnect') 11 | let l:channel_options = { 12 | \ 'on_data': {_, data, __ -> a:Callback(join(data, "\n"))}} 13 | let l:channel = sockconnect('tcp', a:command, l:channel_options) 14 | call s:WrapNeovim(l:channel, l:c) 15 | return l:c 16 | else 17 | call lsc#message#error('No support for sockets for '.a:command) 18 | return v:null 19 | endif 20 | endif 21 | if exists('*job_start') 22 | let l:job_options = {'in_io': 'pipe', 'in_mode': 'raw', 23 | \ 'out_io': 'pipe', 'out_mode': 'raw', 24 | \ 'out_cb': {_, message -> a:Callback(message)}, 25 | \ 'err_io': 'pipe', 'err_mode': 'nl', 26 | \ 'err_cb': {_, message -> a:ErrCallback(message)}, 27 | \ 'exit_cb': {_, __ -> a:OnExit()}} 28 | let l:job = job_start(a:command, l:job_options) 29 | call s:WrapVim(job_getchannel(l:job), l:c) 30 | return l:c 31 | elseif exists('*jobstart') 32 | let l:job_options = { 33 | \ 'on_stdout': {_, data, __ -> a:Callback(join(data, "\n"))}, 34 | \ 'on_stderr': {_, data, __ -> 35 | \ data == [''] ? v:null : a:ErrCallback(join(data, "\n"))}, 36 | \ 'on_exit': {_, __, ___ -> a:OnExit()}} 37 | let l:job = jobstart(a:command, l:job_options) 38 | call s:WrapNeovim(l:job, l:c) 39 | return l:c 40 | endif 41 | 42 | call lsc#message#error('Cannot start '.a:command) 43 | return v:null 44 | endfunction 45 | 46 | function! s:Channel() abort 47 | let l:c = {'send_buffer': ''} 48 | 49 | function! l:c.send(message) abort 50 | let l:self.send_buffer .= a:message 51 | call l:self.__flush() 52 | endfunction 53 | 54 | function! l:c.__flush(...) abort 55 | if len(l:self.send_buffer) <= 1024 56 | call l:self._send(l:self.send_buffer) 57 | let l:self.send_buffer = '' 58 | else 59 | let l:to_send = l:self.send_buffer[:1023] 60 | let l:self.send_buffer = l:self.send_buffer[1024:] 61 | call l:self._send(l:to_send) 62 | call timer_start(0, l:self.__flush) 63 | endif 64 | endfunction 65 | 66 | return l:c 67 | endfunction 68 | 69 | function! s:WrapVim(vim_channel, c) abort 70 | let a:c._channel = a:vim_channel 71 | function! a:c._send(data) abort 72 | call ch_sendraw(l:self._channel, a:data) 73 | endfunction 74 | endfunction 75 | 76 | function! s:WrapNeovim(nvim_job, c) abort 77 | let a:c['_job'] = a:nvim_job 78 | function! a:c._send(data) abort 79 | call jobsend(l:self._job, a:data) 80 | endfunction 81 | endfunction 82 | -------------------------------------------------------------------------------- /autoload/lsc/complete.vim: -------------------------------------------------------------------------------- 1 | " Use InsertCharPre to reliably know what is typed, but don't send the 2 | " completion request until the file reflects the inserted character. Track typed 3 | " characters in `s:next_char` and use CursorMovedI to act on the change. 4 | " 5 | " Every typed character can potentially start a completion request: 6 | " - "Trigger" characters (as specified during initialization) always start a 7 | " completion request when they are typed 8 | " - Characters that match '\w' start a completion in words of at least length 3 9 | 10 | function! lsc#complete#insertCharPre() abort 11 | let s:next_char = v:char 12 | endfunction 13 | 14 | function! lsc#complete#textChanged() abort 15 | if &paste | return | endif 16 | if !g:lsc_enable_autocomplete | return | endif 17 | " This may be or similar if not due to a character typed 18 | if empty(s:next_char) | return | endif 19 | call s:typedCharacter() 20 | let s:next_char = '' 21 | endfunction 22 | 23 | function! s:typedCharacter() abort 24 | if s:isTrigger(s:next_char) || s:isCompletable() 25 | call s:startCompletion(v:true) 26 | endif 27 | endfunction 28 | 29 | if !exists('s:initialized') 30 | let s:next_char = '' 31 | let s:initialized = v:true 32 | endif 33 | 34 | " Clean state associated with a server. 35 | function! lsc#complete#clean(filetype) abort 36 | for l:buffer in getbufinfo({'bufloaded': v:true}) 37 | if getbufvar(l:buffer.bufnr, '&filetype') != a:filetype | continue | endif 38 | call setbufvar(l:buffer.bufnr, 'lsc_is_completing', v:false) 39 | endfor 40 | endfunction 41 | 42 | function! s:isTrigger(char) abort 43 | for l:server in lsc#server#current() 44 | if index(l:server.capabilities.completion.triggerCharacters, a:char) >= 0 45 | return v:true 46 | endif 47 | endfor 48 | return v:false 49 | endfunction 50 | 51 | augroup LscCompletion 52 | autocmd! 53 | autocmd CompleteDone * let b:lsc_is_completing = v:false 54 | \ | silent! unlet b:lsc_completion | let s:next_char = '' 55 | augroup END 56 | 57 | " Whether the cursor follows a minimum count of word characters, and completion 58 | " isn't already in progress. 59 | " 60 | " Minimum length can be configured with `g:lsc_autocomplete_length`. 61 | function! s:isCompletable() abort 62 | if exists('b:lsc_is_completing') && b:lsc_is_completing 63 | return v:false 64 | endif 65 | if s:next_char !~# '\w' | return v:false | endif 66 | let l:cur_col = col('.') 67 | let l:min_length = exists('g:lsc_autocomplete_length') ? 68 | \ g:lsc_autocomplete_length : 3 69 | if l:min_length == v:false | return v:false | endif 70 | if l:cur_col < (l:min_length + 1) | return v:false | endif 71 | let l:word = getline('.')[l:cur_col - (l:min_length + 1):l:cur_col - 2] 72 | return l:word =~# '^\w*$' 73 | endfunction 74 | 75 | function! s:startCompletion(isAuto) abort 76 | let b:lsc_is_completing = v:true 77 | call lsc#file#flushChanges() 78 | let l:params = lsc#params#documentPosition() 79 | " TODO handle multiple servers 80 | let l:server = lsc#server#forFileType(&filetype)[0] 81 | call l:server.request('textDocument/completion', l:params, 82 | \ lsc#util#gateResult('Complete', 83 | \ function('OnResult', [a:isAuto]), 84 | \ function('OnSkip', [bufnr('%')]))) 85 | endfunction 86 | 87 | function! s:OnResult(isAuto, completion) abort 88 | let l:items = [] 89 | if type(a:completion) == type([]) 90 | let l:items = a:completion 91 | elseif type(a:completion) == type({}) 92 | let l:items = a:completion.items 93 | endif 94 | if (a:isAuto) 95 | call s:SuggestCompletions(l:items) 96 | else 97 | let b:lsc_completion = l:items 98 | endif 99 | endfunction 100 | 101 | function! s:OnSkip(bufnr, completion) abort 102 | call setbufvar(a:bufnr, 'lsc_is_completing', v:false) 103 | endfunction 104 | 105 | function! s:SuggestCompletions(items) abort 106 | if mode() !=# 'i' || len(a:items) == 0 107 | let b:lsc_is_completing = v:false 108 | return 109 | endif 110 | let l:start = s:FindStart(a:items) 111 | let l:base = l:start != col('.') 112 | \ ? getline('.')[l:start - 1:col('.') - 2] 113 | \ : '' 114 | let l:completion_items = s:CompletionItems(l:base, a:items) 115 | call s:SetCompleteOpt() 116 | if exists('#User#LSCAutocomplete') 117 | doautocmd User LSCAutocomplete 118 | endif 119 | call complete(l:start, l:completion_items) 120 | endfunction 121 | 122 | function! s:SetCompleteOpt() abort 123 | if type(g:lsc_auto_completeopt) == type('') 124 | " Set completeopt locally exactly like the user wants 125 | execute 'setl completeopt='.g:lsc_auto_completeopt 126 | elseif (type(g:lsc_auto_completeopt) == type(v:true) 127 | \ || type(g:lsc_auto_completeopt) == type(0)) 128 | \ && g:lsc_auto_completeopt 129 | " Set the options that impact behavior for autocomplete use cases without 130 | " touching other like `preview` 131 | setl completeopt-=longest 132 | setl completeopt+=menu,menuone,noinsert 133 | endif 134 | endfunction 135 | 136 | function! lsc#complete#complete(findstart, base) abort 137 | if a:findstart 138 | if !exists('b:lsc_completion') 139 | let l:searchStart = reltime() 140 | call s:startCompletion(v:false) 141 | let l:timeout = get(g:, 'lsc_complete_timeout', 5) 142 | while !exists('b:lsc_completion') 143 | \ && reltimefloat(reltime(l:searchStart)) <= l:timeout 144 | sleep 100m 145 | endwhile 146 | if !exists('b:lsc_completion') || len(b:lsc_completion) == 0 147 | return -3 148 | endif 149 | return s:FindStart(b:lsc_completion) - 1 150 | endif 151 | else 152 | " We'll get an error if b:lsc_completion doesn't exist, which is good, 153 | " we want to be vocal about such failures. 154 | return s:CompletionItems(a:base, b:lsc_completion) 155 | endif 156 | endfunction 157 | 158 | " Finds the 1-based index of the first character in the completion. 159 | function! s:FindStart(completion_items) abort 160 | for l:item in a:completion_items 161 | if has_key(l:item, 'textEdit') 162 | \ && type(l:item.textEdit) == type({}) 163 | return l:item.textEdit.range.start.character + 1 164 | endif 165 | endfor 166 | return s:GuessCompletionStart() 167 | endfunction 168 | 169 | " Finds the 1-based index of the character after the last non word character 170 | " behind the cursor. 171 | function! s:GuessCompletionStart() abort 172 | let l:search = col('.') - 2 173 | let l:line = getline('.') 174 | while l:search > 0 175 | let l:char = l:line[l:search] 176 | if l:char !~# '\w' 177 | return l:search + 2 178 | endif 179 | let l:search -= 1 180 | endwhile 181 | return 1 182 | endfunction 183 | 184 | " Filter and convert LSP completion items into the format used by vim. 185 | " 186 | " a:base is the portion of the portion of the word typed so far, matching the 187 | " argument to `completefunc` the second time it is called. 188 | " 189 | " If a non-empty base is passed, only the items which contain the base somewhere 190 | " whithin the completion will be used. Preference is given first to the 191 | " completions which match by a case-sensitive prefix, then by case-insensitive 192 | " prefix, then case-insensitive substring. 193 | function! s:CompletionItems(base, lsp_items) abort 194 | let l:prefix_case_matches = [] 195 | let l:prefix_matches = [] 196 | let l:substring_matches = [] 197 | 198 | let l:prefix_base = '^'.a:base 199 | 200 | for l:lsp_item in a:lsp_items 201 | let l:vim_item = s:CompletionItemWord(l:lsp_item) 202 | if l:vim_item.word =~# l:prefix_base 203 | call add(l:prefix_case_matches, l:vim_item) 204 | elseif l:vim_item.word =~? l:prefix_base 205 | call add(l:prefix_matches, l:vim_item) 206 | elseif l:vim_item.word =~? a:base 207 | call add(l:substring_matches, l:vim_item) 208 | else 209 | continue 210 | endif 211 | call s:FinishItem(l:lsp_item, l:vim_item) 212 | endfor 213 | 214 | return l:prefix_case_matches + l:prefix_matches + l:substring_matches 215 | endfunction 216 | 217 | " Normalize the multiple potential fields which may convey the text to insert 218 | " from the LSP item into a vim formatted completion. 219 | function! s:CompletionItemWord(lsp_item) abort 220 | let l:item = {'abbr': a:lsp_item.label, 'icase': 1, 'dup': 1} 221 | if has_key(a:lsp_item, 'textEdit') 222 | \ && type(a:lsp_item.textEdit) == type({}) 223 | \ && has_key(a:lsp_item.textEdit, 'newText') 224 | let l:item.word = a:lsp_item.textEdit.newText 225 | elseif has_key(a:lsp_item, 'insertText') 226 | \ && !empty(a:lsp_item.insertText) 227 | let l:item.word = a:lsp_item.insertText 228 | else 229 | let l:item.word = a:lsp_item.label 230 | endif 231 | if has_key(a:lsp_item, 'insertTextFormat') && a:lsp_item.insertTextFormat == 2 232 | let l:item.user_data = json_encode({ 233 | \ 'snippet': l:item.word, 234 | \ 'snippet_trigger': l:item.word 235 | \ }) 236 | let l:item.word = a:lsp_item.label 237 | endif 238 | return l:item 239 | endfunction 240 | 241 | " Fill out the non-word fields of the vim completion item from an LSP item. 242 | " 243 | " Deprecated suggestions get a strike-through on their `abbr`. 244 | " The `kind` field is translated from LSP numeric values into a single letter 245 | " vim kind identifier. 246 | " The `menu` and `info` vim fields are normalized from the `detail` and 247 | " `documentation` LSP fields. 248 | function! s:FinishItem(lsp_item, vim_item) abort 249 | if get(a:lsp_item, 'deprecated', v:false) || 250 | \ index(get(a:lsp_item, 'tags', []), 1) >=0 251 | let a:vim_item.abbr = 252 | \ substitute(a:vim_item.word, '.', "\\0\", 'g') 253 | endif 254 | if has_key(a:lsp_item, 'kind') 255 | let a:vim_item.kind = s:CompletionItemKind(a:lsp_item.kind) 256 | endif 257 | if has_key(a:lsp_item, 'detail') && a:lsp_item.detail != v:null 258 | let l:detail_lines = split(a:lsp_item.detail, "\n") 259 | if len(l:detail_lines) > 0 260 | let a:vim_item.menu = l:detail_lines[0] 261 | let a:vim_item.info = a:lsp_item.detail 262 | endif 263 | endif 264 | if has_key(a:lsp_item, 'documentation') 265 | let l:documentation = a:lsp_item.documentation 266 | if has_key(a:vim_item, 'info') 267 | let a:vim_item.info .= "\n\n" 268 | else 269 | let a:vim_item.info = '' 270 | endif 271 | if type(l:documentation) == type('') 272 | let a:vim_item.info .= l:documentation 273 | elseif type(l:documentation) == type({}) 274 | \ && has_key(l:documentation, 'value') 275 | let a:vim_item.info .= l:documentation.value 276 | endif 277 | endif 278 | endfunction 279 | 280 | function! s:CompletionItemKind(lsp_kind) abort 281 | if a:lsp_kind == 1 282 | return 'Text' 283 | elseif a:lsp_kind == 2 284 | return 'Method' 285 | elseif a:lsp_kind == 3 286 | return 'Function' 287 | elseif a:lsp_kind == 4 288 | return 'Constructor' 289 | elseif a:lsp_kind == 5 290 | return 'Field' 291 | elseif a:lsp_kind == 6 292 | return 'Variable' 293 | elseif a:lsp_kind == 7 294 | return 'Class' 295 | elseif a:lsp_kind == 8 296 | return 'Interface' 297 | elseif a:lsp_kind == 9 298 | return 'Module' 299 | elseif a:lsp_kind == 10 300 | return 'Property' 301 | elseif a:lsp_kind == 11 302 | return 'Unit' 303 | elseif a:lsp_kind == 12 304 | return 'Value' 305 | elseif a:lsp_kind == 13 306 | return 'Enum' 307 | elseif a:lsp_kind == 14 308 | return 'Keyword' 309 | elseif a:lsp_kind == 15 310 | return 'Snippet' 311 | elseif a:lsp_kind == 16 312 | return 'Color' 313 | elseif a:lsp_kind == 17 314 | return 'File' 315 | elseif a:lsp_kind == 18 316 | return 'Reference' 317 | elseif a:lsp_kind == 19 318 | return 'Folder' 319 | elseif a:lsp_kind == 20 320 | return 'EnumMember' 321 | elseif a:lsp_kind == 21 322 | return 'Constant' 323 | elseif a:lsp_kind == 22 324 | return 'Struct' 325 | elseif a:lsp_kind == 23 326 | return 'Event' 327 | elseif a:lsp_kind == 24 328 | return 'Operator' 329 | elseif a:lsp_kind == 25 330 | return 'TypeParameter' 331 | else 332 | return '' 333 | endif 334 | endfunction 335 | -------------------------------------------------------------------------------- /autoload/lsc/config.vim: -------------------------------------------------------------------------------- 1 | if !exists('s:initialized') 2 | let s:initialized = v:true 3 | let s:default_maps = { 4 | \ 'GoToDefinition': '', 5 | \ 'GoToDefinitionSplit': [']', ''], 6 | \ 'FindReferences': 'gr', 7 | \ 'NextReference': '', 8 | \ 'PreviousReference': '', 9 | \ 'FindImplementations': 'gI', 10 | \ 'FindCodeActions': 'ga', 11 | \ 'Rename': 'gR', 12 | \ 'ShowHover': v:true, 13 | \ 'DocumentSymbol': 'go', 14 | \ 'WorkspaceSymbol': 'gS', 15 | \ 'SignatureHelp': 'gm', 16 | \ 'Completion': 'completefunc', 17 | \} 18 | let s:skip_marker = {} 19 | endif 20 | 21 | function! s:ApplyDefaults(config) abort 22 | if type(a:config) == type(v:true) || type(a:config) == type(0) 23 | return s:default_maps 24 | endif 25 | if type(a:config) != type({}) 26 | \ || !has_key(a:config, 'defaults') 27 | \ || !a:config.defaults 28 | return a:config 29 | endif 30 | let l:merged = deepcopy(s:default_maps) 31 | for l:pair in items(a:config) 32 | if l:pair[0] ==# 'defaults' | continue | endif 33 | if empty(l:pair[1]) 34 | unlet l:merged[l:pair[0]] 35 | else 36 | let l:merged[l:pair[0]] = l:pair[1] 37 | endif 38 | endfor 39 | return l:merged 40 | endfunction 41 | 42 | function! lsc#config#mapKeys() abort 43 | if !exists('g:lsc_auto_map') 44 | \ || (type(g:lsc_auto_map) == type(v:true) && !g:lsc_auto_map) 45 | \ || (type(g:lsc_auto_map) == type(0) && !g:lsc_auto_map) 46 | return 47 | endif 48 | let l:maps = s:ApplyDefaults(g:lsc_auto_map) 49 | if type(l:maps) != type({}) 50 | call lsc#message#error('g:lsc_auto_map must be a bool or dict') 51 | return 52 | endif 53 | 54 | for l:command in [ 55 | \ 'GoToDefinition', 56 | \ 'GoToDefinitionSplit', 57 | \ 'FindReferences', 58 | \ 'NextReference', 59 | \ 'PreviousReference', 60 | \ 'FindImplementations', 61 | \ 'FindCodeActions', 62 | \ 'ShowHover', 63 | \ 'DocumentSymbol', 64 | \ 'WorkspaceSymbol', 65 | \ 'SignatureHelp', 66 | \] + (get(g:, 'lsc_enable_apply_edit', 1) ? ['Rename'] : []) 67 | let l:lhs = get(l:maps, l:command, []) 68 | if type(l:lhs) != type('') && type(l:lhs) != type([]) 69 | continue 70 | endif 71 | for l:m in type(l:lhs) == type([]) ? l:lhs : [l:lhs] 72 | execute 'nnoremap '.l:m.' :LSClient'.l:command.'' 73 | endfor 74 | endfor 75 | if has_key(l:maps, 'Completion') && 76 | \ type(l:maps['Completion']) == type('') && 77 | \ len(l:maps['Completion']) > 0 78 | execute 'setlocal '.l:maps['Completion'].'=lsc#complete#complete' 79 | endif 80 | if has_key(l:maps, 'ShowHover') 81 | let l:show_hover = l:maps['ShowHover'] 82 | if type(l:show_hover) == type(v:true) || type(l:show_hover) == type(0) 83 | if l:show_hover 84 | setlocal keywordprg=:LSClientShowHover 85 | endif 86 | endif 87 | endif 88 | endfunction 89 | 90 | " Wraps [Callback] with a function that will first translate a result through a 91 | " user provided translation. 92 | function! lsc#config#responseHook(server, method, Callback) abort 93 | if !has_key(a:server.config, 'response_hooks') | return a:Callback | endif 94 | let l:hooks = a:server.config.response_hooks 95 | if !has_key(l:hooks, a:method) | return a:Callback | endif 96 | let l:Hook = l:hooks[a:method] 97 | return {result -> a:Callback(l:Hook(result))} 98 | endfunction 99 | 100 | function! lsc#config#messageHook(server, method, params) abort 101 | if !has_key(a:server.config, 'message_hooks') | return a:params | endif 102 | let l:hooks = a:server.config.message_hooks 103 | if !has_key(l:hooks, a:method) | return a:params | endif 104 | let l:Hook = l:hooks[a:method] 105 | if type(l:Hook) == type({_->_}) 106 | return s:RunHookFunction(l:Hook, a:method, a:params) 107 | elseif type(l:Hook) == type({}) 108 | return s:MergeHookDict(l:Hook, a:method, a:params) 109 | else 110 | call lsc#message#error('Message hook must be a function or a dict. '. 111 | \' Invalid config for '.a:method) 112 | return a:params 113 | endif 114 | endfunction 115 | 116 | function! s:RunHookFunction(Hook, method, params) abort 117 | try 118 | return a:Hook(a:method, a:params) 119 | catch 120 | call lsc#message#error('Failed to run message hook for '.a:method. 121 | \': '.v:exception) 122 | return a:params 123 | endtry 124 | endfunction 125 | 126 | function! s:MergeHookDict(hook, method, params) abort 127 | let l:resolved = s:ResolveHookDict(a:hook, a:method, a:params) 128 | for l:key in keys(l:resolved) 129 | let a:params[l:key] = l:resolved[l:key] 130 | endfor 131 | return a:params 132 | endfunction 133 | 134 | " If any key at any level within [hook] is a function, run it with [method] and 135 | " [params] as arguments. 136 | function! s:ResolveHookDict(hook, method, params) abort 137 | if !s:HasFunction(a:hook) | return a:hook | endif 138 | let l:copied = deepcopy(a:hook) 139 | for l:key in keys(a:hook) 140 | if type(a:hook[l:key]) == type({}) 141 | let l:copied[l:key] = s:ResolveHookDict(a:hook[l:key], a:method, a:params) 142 | elseif type(a:hook[l:key]) == type({_->_}) 143 | let l:Func = a:hook[l:key] 144 | let l:copied[l:key] = Func(a:method, a:params) 145 | endif 146 | endfor 147 | return l:copied 148 | endfunction 149 | 150 | function! s:HasFunction(hook) abort 151 | for l:Value in values(a:hook) 152 | if type(l:Value) == type({}) && s:HasFunction(l:Value) 153 | return v:true 154 | elseif type(l:Value) == type({_->_}) 155 | return v:true 156 | endif 157 | endfor 158 | return v:false 159 | endfunction 160 | 161 | " Whether a message of type [type] should be echoed 162 | " 163 | " By default messages are shown at "Info" or higher, this can be overrided per 164 | " server. 165 | function! lsc#config#shouldEcho(server, type) abort 166 | let l:threshold = 3 167 | if has_key(a:server.config, 'log_level') 168 | if type(a:server.config.log_level) == type(0) 169 | let l:threshold = a:server.config.log_level 170 | else 171 | let l:config = a:server.config.log_level 172 | if l:config ==# 'Error' 173 | let l:threshold = 1 174 | elseif l:config ==# 'Warning' 175 | let l:threshold = 2 176 | elseif l:config ==# 'Info' 177 | let l:threshold = 3 178 | elseif l:config ==# 'Log' 179 | let l:threshold = 4 180 | endif 181 | endif 182 | endif 183 | return a:type <= l:threshold 184 | endfunction 185 | 186 | " A maker from returns from "message_hook" functions indicating that a call 187 | " should not be made. 188 | function! lsc#config#skip() abort 189 | return s:skip_marker 190 | endfunction 191 | 192 | function! lsc#config#handleNotification(server, method, params) abort 193 | if !has_key(a:server.config, 'notifications') | return | endif 194 | let l:hooks = a:server.config.notifications 195 | if !has_key(l:hooks, a:method) | return | endif 196 | let l:Hook = l:hooks[a:method] 197 | if type(l:Hook) != type({_->_}) 198 | call lsc#message#error('Notification handlers must be functions: '.a:method) 199 | unlet l:hooks[a:method] 200 | return 201 | endif 202 | try 203 | call l:Hook(a:method, a:params) 204 | catch 205 | call lsc#message#error('Failed to run callback for '.a:method. 206 | \': '.v:exception) 207 | endtry 208 | endfunction 209 | -------------------------------------------------------------------------------- /autoload/lsc/convert.vim: -------------------------------------------------------------------------------- 1 | function! lsc#convert#rangeToHighlights(range) abort 2 | let l:start = a:range.start 3 | let l:end = a:range.end 4 | if l:end.line > l:start.line 5 | let l:ranges =[[ 6 | \ l:start.line + 1, 7 | \ l:start.character + 1, 8 | \ 99]] 9 | " Matches render wrong until a `redraw!` if lines are mixed with ranges 10 | let l:line_hacks = map(range(l:start.line + 2, l:end.line), {_, l->[l,0,99]}) 11 | call extend(l:ranges, l:line_hacks) 12 | call add(l:ranges, [ 13 | \ l:end.line + 1, 14 | \ 1, 15 | \ l:end.character]) 16 | else 17 | let l:ranges = [[ 18 | \ l:start.line + 1, 19 | \ l:start.character + 1, 20 | \ l:end.character - l:start.character]] 21 | endif 22 | return l:ranges 23 | endfunction 24 | 25 | " Convert an LSP SymbolInformation to a quick fix item. 26 | " 27 | " Both representations are dictionaries. 28 | " 29 | " SymbolInformation: 30 | " 'location': 31 | " 'uri': file:// URI 32 | " 'range': {'start': {'line', 'characater'}, 'end': {'line', 'character'}} 33 | " 'name': The symbol's name 34 | " 'kind': Integer kind 35 | " 'containerName': The element this symbol is inside 36 | " 37 | " QuickFix Item: (as used) 38 | " 'filename': The file path of the symbol's location 39 | " 'lnum': line number 40 | " 'col': column number 41 | " 'text': "SymbolName" [kind] (in containerName)? 42 | function! lsc#convert#quickFixSymbol(symbol) abort 43 | let l:item = {'lnum': a:symbol.location.range.start.line + 1, 44 | \ 'col': a:symbol.location.range.start.character + 1, 45 | \ 'filename': lsc#uri#documentPath(a:symbol.location.uri)} 46 | let l:text = '"'.a:symbol.name.'"' 47 | if !empty(a:symbol.kind) 48 | let l:text .= ' ['.lsc#convert#symbolKind(a:symbol.kind).']' 49 | endif 50 | let l:containerName = get(a:symbol, 'containerName', '') 51 | if !empty(l:containerName) 52 | let l:text .= ' in '.l:containerName 53 | endif 54 | let l:item.text = l:text 55 | return l:item 56 | endfunction 57 | 58 | function! lsc#convert#symbolKind(kind) abort 59 | if a:kind == 1 60 | return 'File' 61 | endif 62 | if a:kind == 2 63 | return 'Module' 64 | endif 65 | if a:kind == 3 66 | return 'Namespace' 67 | endif 68 | if a:kind == 4 69 | return 'Package' 70 | endif 71 | if a:kind == 5 72 | return 'Class' 73 | endif 74 | if a:kind == 6 75 | return 'Method' 76 | endif 77 | if a:kind == 7 78 | return 'Property' 79 | endif 80 | if a:kind == 8 81 | return 'Field' 82 | endif 83 | if a:kind == 9 84 | return 'Constructor' 85 | endif 86 | if a:kind == 10 87 | return 'Enum' 88 | endif 89 | if a:kind == 11 90 | return 'Interface' 91 | endif 92 | if a:kind == 12 93 | return 'Function' 94 | endif 95 | if a:kind == 13 96 | return 'Variable' 97 | endif 98 | if a:kind == 14 99 | return 'Constant' 100 | endif 101 | if a:kind == 15 102 | return 'String' 103 | endif 104 | if a:kind == 16 105 | return 'Number' 106 | endif 107 | if a:kind == 17 108 | return 'Boolean' 109 | endif 110 | if a:kind == 18 111 | return 'Array' 112 | endif 113 | if a:kind == 19 114 | return 'Object' 115 | endif 116 | if a:kind == 20 117 | return 'Key' 118 | endif 119 | if a:kind == 21 120 | return 'Null' 121 | endif 122 | if a:kind == 22 123 | return 'EnumMember' 124 | endif 125 | if a:kind == 23 126 | return 'Struct' 127 | endif 128 | if a:kind == 24 129 | return 'Event' 130 | endif 131 | if a:kind == 25 132 | return 'Operator' 133 | endif 134 | if a:kind == 26 135 | return 'TypeParameter' 136 | endif 137 | endfunction 138 | -------------------------------------------------------------------------------- /autoload/lsc/cursor.vim: -------------------------------------------------------------------------------- 1 | if !exists('s:initialized') 2 | let s:initialized = v:true 3 | let s:highlights_request = 0 4 | let s:pending = {} 5 | endif 6 | 7 | function! lsc#cursor#onMove() abort 8 | call lsc#cursor#showDiagnostic() 9 | call s:HighlightReferences(v:false) 10 | endfunction 11 | 12 | function! lsc#cursor#onWinEnter() abort 13 | call s:HighlightReferences(v:false) 14 | endfunction 15 | 16 | function! lsc#cursor#showDiagnostic() abort 17 | if !get(g:, 'lsc_enable_diagnostics', v:true) | return | endif 18 | if !get(g:, 'lsc_diagnostic_highlights', v:true) | return | endif 19 | let l:diagnostic = lsc#diagnostics#underCursor() 20 | if has_key(l:diagnostic, 'message') 21 | let l:max_width = &columns - 1 " Avoid edge of terminal 22 | let l:has_ruler = &ruler && 23 | \ (&laststatus == 0 || (&laststatus == 1 && winnr('$') < 2)) 24 | if l:has_ruler | let l:max_width -= 18 | endif 25 | if &showcmd | let l:max_width -= 11 | endif 26 | let l:message = strtrans(l:diagnostic.message) 27 | if strdisplaywidth(l:message) > l:max_width 28 | let l:max_width -= 1 " 1 character for ellipsis 29 | let l:truncated = strcharpart(l:message, 0, l:max_width) 30 | " Trim by character until a satisfactory display width. 31 | while strdisplaywidth(l:truncated) > l:max_width 32 | let l:truncated = strcharpart(l:truncated, 0, strchars(l:truncated) - 1) 33 | endwhile 34 | echo l:truncated."\u2026" 35 | else 36 | echo l:message 37 | endif 38 | else 39 | echo '' 40 | endif 41 | endfunction 42 | 43 | function! lsc#cursor#onChangesFlushed() abort 44 | let l:mode = mode() 45 | if l:mode ==# 'n' || l:mode ==# 'no' 46 | call s:HighlightReferences(v:true) 47 | endif 48 | endfunction 49 | 50 | function! s:HighlightReferences(force_in_highlight) abort 51 | if exists('g:lsc_reference_highlights') && !g:lsc_reference_highlights 52 | return 53 | endif 54 | if !s:CanHighlightReferences() | return | endif 55 | if !a:force_in_highlight && 56 | \ exists('w:lsc_references') && 57 | \ lsc#cursor#isInReference(w:lsc_references) >= 0 58 | return 59 | endif 60 | if has_key(s:pending, &filetype) && s:pending[&filetype] 61 | return 62 | endif 63 | let s:highlights_request += 1 64 | let l:params = lsc#params#documentPosition() 65 | " TODO handle multiple servers 66 | let l:server = lsc#server#forFileType(&filetype)[0] 67 | let s:pending[&filetype] = l:server.request('textDocument/documentHighlight', 68 | \ l:params, funcref('HandleHighlights', 69 | \ [s:highlights_request, getcurpos(), bufnr('%'), &filetype])) 70 | endfunction 71 | 72 | function! s:CanHighlightReferences() abort 73 | for l:server in lsc#server#current() 74 | if l:server.capabilities.referenceHighlights 75 | return v:true 76 | endif 77 | endfor 78 | return v:false 79 | endfunction 80 | 81 | function! s:HandleHighlights(request_number, old_pos, old_buf_nr, 82 | \ request_filetype, highlights) abort 83 | if !has_key(s:pending, a:request_filetype) || !s:pending[a:request_filetype] 84 | return 85 | endif 86 | let s:pending[a:request_filetype] = v:false 87 | if bufnr('%') != a:old_buf_nr | return | endif 88 | if a:request_number != s:highlights_request | return | endif 89 | call lsc#cursor#clean() 90 | if empty(a:highlights) | return | endif 91 | call map(a:highlights, {_, reference -> s:ConvertReference(reference)}) 92 | call sort(a:highlights, function('CompareRange')) 93 | if lsc#cursor#isInReference(a:highlights) == -1 94 | if a:old_pos != getcurpos() 95 | call s:HighlightReferences(v:true) 96 | endif 97 | return 98 | endif 99 | 100 | let w:lsc_references = a:highlights 101 | let w:lsc_reference_matches = [] 102 | for l:reference in a:highlights 103 | let l:match = matchaddpos('lscReference', l:reference.ranges, -5) 104 | call add(w:lsc_reference_matches, l:match) 105 | endfor 106 | endfunction 107 | 108 | function! lsc#cursor#clean() abort 109 | let s:pending[&filetype] = v:false 110 | if exists('w:lsc_reference_matches') 111 | for l:current_match in w:lsc_reference_matches 112 | silent! call matchdelete(l:current_match) 113 | endfor 114 | unlet w:lsc_reference_matches 115 | unlet w:lsc_references 116 | endif 117 | endfunction 118 | 119 | " Returns the index of the reference the cursor is positioned in, or -1 if it is 120 | " not in any reference. 121 | function! lsc#cursor#isInReference(references) abort 122 | let l:line = line('.') 123 | let l:col = col('.') 124 | let l:idx = 0 125 | for l:reference in a:references 126 | for l:range in l:reference.ranges 127 | if l:line == l:range[0] 128 | \ && l:col >= l:range[1] 129 | \ && l:col < l:range[1] + l:range[2] 130 | return l:idx 131 | endif 132 | endfor 133 | let l:idx += 1 134 | endfor 135 | return -1 136 | endfunction 137 | 138 | function! s:ConvertReference(reference) abort 139 | return {'ranges': lsc#convert#rangeToHighlights(a:reference.range)} 140 | endfunction 141 | 142 | function! s:CompareRange(r1, r2) abort 143 | let l:line_1 = a:r1.ranges[0][0] 144 | let l:line_2 = a:r2.ranges[0][0] 145 | if l:line_1 != l:line_2 | return l:line_1 > l:line_2 ? 1 : -1 | endif 146 | let l:col_1 = a:r1.ranges[0][1] 147 | let l:col_2 = a:r2.ranges[0][1] 148 | return l:col_1 - l:col_2 149 | endfunction 150 | -------------------------------------------------------------------------------- /autoload/lsc/diagnostics.vim: -------------------------------------------------------------------------------- 1 | if !exists('s:file_diagnostics') 2 | " file path -> Diagnostics 3 | " 4 | " Diagnostics are dictionaries with: 5 | " 'Highlights()': Highlight groups and ranges 6 | " 'ByLine()': Nested dictionaries with the structure: 7 | " { line: [{ 8 | " message: Human readable message with code 9 | " range: LSP Range object 10 | " severity: String label for severity 11 | " }] 12 | " } 13 | " 'ListItems()': QuickFix or Location list items 14 | let s:file_diagnostics = {} 15 | endif 16 | 17 | function! lsc#diagnostics#clean(filetype) abort 18 | for l:buffer in getbufinfo({'bufloaded': v:true}) 19 | if getbufvar(l:buffer.bufnr, '&filetype') != a:filetype | continue | endif 20 | call lsc#diagnostics#setForFile(lsc#file#normalize(l:buffer.name), []) 21 | endfor 22 | endfunction 23 | 24 | " Finds the highlight group given a diagnostic severity level 25 | function! s:SeverityGroup(severity) abort 26 | return 'lscDiagnostic'.s:SeverityLabel(a:severity) 27 | endfunction 28 | 29 | " Finds the human readable label for a diagnsotic severity level 30 | function! s:SeverityLabel(severity) abort 31 | if a:severity == 1 | return 'Error' 32 | elseif a:severity == 2 | return 'Warning' 33 | elseif a:severity == 3 | return 'Info' 34 | elseif a:severity == 4 | return 'Hint' 35 | else | return '' 36 | endif 37 | endfunction 38 | 39 | " Finds the location list type given a diagnostic severity level 40 | function! s:SeverityType(severity) abort 41 | if a:severity == 1 | return 'E' 42 | elseif a:severity == 2 | return 'W' 43 | elseif a:severity == 3 | return 'I' 44 | elseif a:severity == 4 | return 'H' 45 | else | return '' 46 | endif 47 | endfunction 48 | 49 | function! s:DiagnosticMessage(diagnostic) abort 50 | let l:message = a:diagnostic.message 51 | if has_key(a:diagnostic, 'code') 52 | let l:message = l:message.' ['.a:diagnostic.code.']' 53 | endif 54 | return l:message 55 | endfunction 56 | 57 | function! lsc#diagnostics#forFile(file_path) abort 58 | if !has_key(s:file_diagnostics, a:file_path) 59 | return s:EmptyDiagnostics() 60 | endif 61 | return s:file_diagnostics[a:file_path] 62 | endfunction 63 | 64 | function! lsc#diagnostics#setForFile(file_path, diagnostics) abort 65 | if (exists('g:lsc_enable_diagnostics') && !g:lsc_enable_diagnostics) 66 | \ || (empty(a:diagnostics) && !has_key(s:file_diagnostics, a:file_path)) 67 | return 68 | endif 69 | let l:visible_change = v:true 70 | if !empty(a:diagnostics) 71 | if has_key(s:file_diagnostics, a:file_path) && 72 | \ s:file_diagnostics[a:file_path].lsp_diagnostics == a:diagnostics 73 | return 74 | endif 75 | if exists('s:highest_used_diagnostic') 76 | if lsc#file#compare(s:highest_used_diagnostic, a:file_path) >= 0 77 | if len( 78 | \ get( 79 | \ get(s:file_diagnostics, a:file_path, {}), 80 | \ 'lsp_diagnostics', [] 81 | \ ) 82 | \) > len(a:diagnostics) 83 | unlet s:highest_used_diagnostic 84 | endif 85 | else 86 | let l:visible_change = v:false 87 | endif 88 | endif 89 | let s:file_diagnostics[a:file_path] = 90 | \ s:Diagnostics(a:file_path, a:diagnostics) 91 | else 92 | unlet s:file_diagnostics[a:file_path] 93 | if exists('s:highest_used_diagnostic') 94 | if lsc#file#compare(s:highest_used_diagnostic, a:file_path) >= 0 95 | unlet s:highest_used_diagnostic 96 | else 97 | let l:visible_change = v:false 98 | endif 99 | endif 100 | endif 101 | let l:bufnr = lsc#file#bufnr(a:file_path) 102 | if l:bufnr != -1 103 | call s:UpdateWindowStates(a:file_path) 104 | call lsc#highlights#updateDisplayed(l:bufnr) 105 | endif 106 | if l:visible_change 107 | if exists('s:quickfix_debounce') 108 | call timer_stop(s:quickfix_debounce) 109 | endif 110 | let s:quickfix_debounce = timer_start(100, funcref('UpdateQuickFix')) 111 | endif 112 | if exists('#User#LSCDiagnosticsChange') 113 | doautocmd User LSCDiagnosticsChange 114 | endif 115 | if(a:file_path ==# lsc#file#fullPath()) 116 | call lsc#cursor#showDiagnostic() 117 | endif 118 | endfunction 119 | 120 | " Updates location list for all windows showing [file_path]. 121 | " 122 | " If a window already has a location list which aren't LSC owned diagnostics 123 | " the list is left as is. If there is no location list or the list is LSC owned 124 | " diagnostics, check if it is stale and update it to the new diagnostics. 125 | function! s:UpdateWindowStates(file_path) abort 126 | let l:diagnostics = lsc#diagnostics#forFile(a:file_path) 127 | for l:window_id in win_findbuf(lsc#file#bufnr(a:file_path)) 128 | call s:UpdateWindowState(l:window_id, l:diagnostics) 129 | endfor 130 | endfunction 131 | 132 | function! lsc#diagnostics#updateCurrentWindow() abort 133 | let l:diagnostics = lsc#diagnostics#forFile(lsc#file#fullPath()) 134 | if exists('w:lsc_diagnostics') && w:lsc_diagnostics is l:diagnostics 135 | return 136 | endif 137 | call s:UpdateWindowState(win_getid(), l:diagnostics) 138 | endfunction 139 | 140 | function! s:UpdateWindowState(window_id, diagnostics) abort 141 | call settabwinvar(0, a:window_id, 'lsc_diagnostics', a:diagnostics) 142 | let l:list_info = getloclist(a:window_id, {'changedtick': 1}) 143 | let l:new_list = get(l:list_info, 'changedtick', 0) == 0 144 | if l:new_list 145 | call s:CreateLocationList(a:window_id, a:diagnostics.ListItems()) 146 | else 147 | call s:UpdateLocationList(a:window_id, a:diagnostics.ListItems()) 148 | endif 149 | endfunction 150 | 151 | function! s:CreateLocationList(window_id, items) abort 152 | call setloclist(a:window_id, [], ' ', { 153 | \ 'title': 'LSC Diagnostics', 154 | \ 'items': a:items, 155 | \}) 156 | let l:new_id = getloclist(a:window_id, {'id': 0}).id 157 | call settabwinvar(0, a:window_id, 'lsc_location_list_id', l:new_id) 158 | endfunction 159 | 160 | " Update an existing location list to contain new items. 161 | " 162 | " If the LSC diagnostics location list is not reachable with `lolder` or 163 | " `lhistory` the update will silently fail. 164 | function! s:UpdateLocationList(window_id, items) abort 165 | let l:list_id = gettabwinvar(0, a:window_id, 'lsc_location_list_id', -1) 166 | call setloclist(a:window_id, [], 'r', { 167 | \ 'id': l:list_id, 168 | \ 'items': a:items, 169 | \}) 170 | endfunction 171 | 172 | function! lsc#diagnostics#showLocationList() abort 173 | let l:window_id = win_getid() 174 | if &filetype ==# 'qf' 175 | let l:list_window = get(getloclist(0, {'filewinid': 0}), 'filewinid', 0) 176 | if l:list_window != 0 177 | let l:window_id = l:list_window 178 | endif 179 | endif 180 | let l:list_id = gettabwinvar(0, l:window_id, 'lsc_location_list_id', -1) 181 | if l:list_id != -1 && !s:SurfaceLocationList(l:list_id) 182 | let l:path = lsc#file#normalize(bufname(winbufnr(l:window_id))) 183 | let l:items = lsc#diagnostics#forFile(l:path).ListItems() 184 | call s:CreateLocationList(l:window_id, l:items) 185 | endif 186 | lopen 187 | endfunction 188 | 189 | " If the LSC maintained location list exists in the location list stack, switch 190 | " to it and return true, otherwise return false. 191 | function! s:SurfaceLocationList(list_id) abort 192 | let l:list_info = getloclist(0, {'nr': 0, 'id': a:list_id}) 193 | let l:nr = get(l:list_info, 'nr', -1) 194 | if l:nr <= 0 | return v:false | endif 195 | 196 | let l:diff = getloclist(0, {'nr': 0}).nr - l:nr 197 | if l:diff == 0 198 | " already there 199 | elseif l:diff > 0 200 | execute 'lolder '.string(l:diff) 201 | else 202 | execute 'lnewer '.string(abs(l:diff)) 203 | endif 204 | return v:true 205 | endfunction 206 | 207 | " Returns the total number of diagnostics in all files. 208 | " 209 | " If the number grows very large returns instead a String like `'500+'` 210 | function! lsc#diagnostics#count() abort 211 | let l:total = 0 212 | for l:diagnostics in values(s:file_diagnostics) 213 | let l:total += len(l:diagnostics.lsp_diagnostics) 214 | if l:total > 500 215 | return string(l:total).'+' 216 | endif 217 | endfor 218 | return l:total 219 | endfunction 220 | 221 | " Finds all diagnostics and populates the quickfix list. 222 | function! lsc#diagnostics#showInQuickFix() abort 223 | call setqflist([], ' ', { 224 | \ 'items': s:AllDiagnostics(), 225 | \ 'title': 'LSC Diagnostics', 226 | \ 'context': {'client': 'LSC'} 227 | \}) 228 | copen 229 | endfunction 230 | 231 | function! s:UpdateQuickFix(...) abort 232 | unlet s:quickfix_debounce 233 | let l:current = getqflist({'context': 1, 'idx': 1, 'items': 1}) 234 | let l:context = get(l:current, 'context', 0) 235 | if type(l:context) != type({}) || 236 | \ !has_key(l:context, 'client') || 237 | \ l:context.client !=# 'LSC' 238 | return 239 | endif 240 | let l:new_list = {'items': s:AllDiagnostics()} 241 | if len(l:new_list.items) > 0 && 242 | \ l:current.idx > 0 && 243 | \ len(l:current.items) >= l:current.idx 244 | let l:prev_item = l:current.items[l:current.idx - 1] 245 | let l:new_list.idx = s:FindNearest(l:prev_item, l:new_list.items) 246 | endif 247 | call setqflist([], 'r', l:new_list) 248 | endfunction 249 | 250 | function! s:FindNearest(prev, items) abort 251 | let l:idx = 1 252 | for l:item in a:items 253 | if lsc#util#compareQuickFixItems(l:item, a:prev) >= 0 254 | return l:idx 255 | endif 256 | let l:idx += 1 257 | endfor 258 | return l:idx - 1 259 | endfunction 260 | 261 | function! s:AllDiagnostics() abort 262 | let l:all_diagnostics = [] 263 | let l:files = keys(s:file_diagnostics) 264 | if exists('s:highest_used_diagnostic') 265 | call filter(l:files, funcref('IsUsed', [s:highest_used_diagnostic])) 266 | elseif len(l:files) > 500 267 | let l:files = s:First500(l:files) 268 | endif 269 | call sort(l:files, funcref('lsc#file#compare')) 270 | for l:file_path in l:files 271 | let l:diagnostics = s:file_diagnostics[l:file_path] 272 | call extend(l:all_diagnostics, l:diagnostics.ListItems()) 273 | if len(l:all_diagnostics) >= 500 274 | let s:highest_used_diagnostic = l:file_path 275 | break 276 | endif 277 | endfor 278 | return l:all_diagnostics 279 | endfunction 280 | function! s:IsUsed(highest_used, idx, to_check) abort 281 | return lsc#file#compare(a:highest_used, a:to_check) >= 0 282 | endfunction 283 | function! s:First500(file_list) abort 284 | if !exists('*Rand') 285 | if exists('*rand') 286 | function! s:Rand(max) abort 287 | return rand() % a:max 288 | endfunction 289 | elseif has('nvim-0.4.0') 290 | function! s:Rand(max) abort 291 | return luaeval('math.random(0,'.string(a:max - 1).')') 292 | endfunction 293 | else 294 | call lsc#message#error('Missing support for rand().' 295 | \.' :LSClientAllDiagnostics may be inconsistent when there' 296 | \.' are more than 500 files with diagnostics.') 297 | return a:file_list[:500] 298 | endif 299 | endif 300 | let l:result = [] 301 | let l:search_in = a:file_list 302 | while len(l:result) != 500 303 | let l:pivot = l:search_in[s:Rand(len(l:search_in))] 304 | let l:accept = [] 305 | let l:reject = [] 306 | for l:file in l:search_in 307 | if lsc#file#compare(l:pivot, l:file) < 0 308 | call add(l:reject, l:file) 309 | else 310 | call add(l:accept, l:file) 311 | endif 312 | endfor 313 | let l:need = 500 - len(l:result) 314 | if len(l:accept) > l:need 315 | let l:search_in = l:accept 316 | else 317 | call extend(l:result, l:accept) 318 | let l:search_in = l:reject 319 | endif 320 | endwhile 321 | return l:result 322 | endfunction 323 | 324 | " Clear the LSC controlled location list for the current window. 325 | function! lsc#diagnostics#clear() abort 326 | if !empty(w:lsc_diagnostics.lsp_diagnostics) 327 | call s:UpdateLocationList(win_getid(), []) 328 | endif 329 | unlet w:lsc_diagnostics 330 | endfunction 331 | 332 | " Finds the first diagnostic which is under the cursor on the current line. If 333 | " no diagnostic is directly under the cursor returns the last seen diagnostic 334 | " on this line. 335 | function! lsc#diagnostics#underCursor() abort 336 | let l:file_diagnostics = lsc#diagnostics#forFile(lsc#file#fullPath()).ByLine() 337 | let l:line = line('.') 338 | if !has_key(l:file_diagnostics, l:line) 339 | if l:line != line('$') | return {} | endif 340 | " Find a diagnostic reported after the end of the file 341 | for l:diagnostic_line in keys(l:file_diagnostics) 342 | if l:diagnostic_line > l:line 343 | return l:file_diagnostics[l:diagnostic_line][0] 344 | endif 345 | endfor 346 | return {} 347 | endif 348 | let l:diagnostics = l:file_diagnostics[l:line] 349 | let l:col = col('.') 350 | let l:closest_diagnostic = {} 351 | let l:closest_distance = -1 352 | let l:closest_is_within = v:false 353 | for l:diagnostic in l:file_diagnostics[l:line] 354 | let l:range = l:diagnostic.range 355 | let l:is_within = l:range.start.character < l:col && 356 | \ (l:range.end.line >= l:line || l:range.end.character > l:col) 357 | if l:closest_is_within && !l:is_within 358 | continue 359 | endif 360 | let l:distance = abs(l:range.start.character - l:col) 361 | if l:closest_distance < 0 || l:distance < l:closest_distance 362 | let l:closest_diagnostic = l:diagnostic 363 | let l:closest_distance = l:distance 364 | let l:closest_is_within = l:is_within 365 | endif 366 | endfor 367 | return l:closest_diagnostic 368 | endfunction 369 | 370 | " Returns the original LSP representation of diagnostics on a zero-indexed line. 371 | function! lsc#diagnostics#forLine(file, line) abort 372 | let l:result = [] 373 | for l:diagnostic in lsc#diagnostics#forFile(a:file).lsp_diagnostics 374 | if l:diagnostic.range.start.line <= a:line && 375 | \ l:diagnostic.range.end.line >= a:line 376 | call add(l:result, l:diagnostic) 377 | endif 378 | endfor 379 | return l:result 380 | endfunction 381 | 382 | function! lsc#diagnostics#echoForLine() abort 383 | let l:file_diagnostics = lsc#diagnostics#forFile(lsc#file#fullPath()).ByLine() 384 | let l:line = line('.') 385 | if !has_key(l:file_diagnostics, l:line) 386 | echo 'No diagnostics' 387 | return 388 | endif 389 | let l:diagnostics = l:file_diagnostics[l:line] 390 | for l:diagnostic in l:diagnostics 391 | let l:label = '['.l:diagnostic.severity.']' 392 | if stridx(l:diagnostic.message, "\n") >= 0 393 | echo l:label 394 | echo l:diagnostic.message 395 | else 396 | echo l:label.': '.l:diagnostic.message 397 | endif 398 | endfor 399 | endfunction 400 | 401 | function! s:Diagnostics(file_path, lsp_diagnostics) abort 402 | return { 403 | \ 'lsp_diagnostics': a:lsp_diagnostics, 404 | \ 'Highlights': funcref('DiagnosticsHighlights'), 405 | \ 'ListItems': funcref('DiagnosticsListItems', [a:file_path]), 406 | \ 'ByLine': funcref('DiagnosticsByLine'), 407 | \} 408 | endfunction 409 | function! s:DiagnosticsHighlights() abort dict 410 | if !has_key(l:self, '_highlights') 411 | let l:self._highlights = [] 412 | for l:diagnostic in l:self.lsp_diagnostics 413 | call add(l:self._highlights, { 414 | \ 'group': s:SeverityGroup(l:diagnostic.severity), 415 | \ 'severity': l:diagnostic.severity, 416 | \ 'ranges': lsc#convert#rangeToHighlights(l:diagnostic.range), 417 | \}) 418 | endfor 419 | endif 420 | return l:self._highlights 421 | endfunction 422 | function! s:DiagnosticsListItems(file_path) abort dict 423 | if !has_key(l:self, '_list_items') 424 | let l:self._list_items = [] 425 | let l:bufnr = lsc#file#bufnr(a:file_path) 426 | if l:bufnr == -1 427 | let l:file_ref = {'filename': fnamemodify(a:file_path, ':.')} 428 | else 429 | let l:file_ref = {'bufnr': l:bufnr} 430 | endif 431 | for l:diagnostic in l:self.lsp_diagnostics 432 | let l:item = { 433 | \ 'lnum': l:diagnostic.range.start.line + 1, 434 | \ 'col': l:diagnostic.range.start.character + 1, 435 | \ 'text': s:DiagnosticMessage(l:diagnostic), 436 | \ 'type': s:SeverityType(l:diagnostic.severity) 437 | \} 438 | call extend(l:item, l:file_ref) 439 | call add(l:self._list_items, l:item) 440 | endfor 441 | call sort(l:self._list_items, 'lsc#util#compareQuickFixItems') 442 | endif 443 | return l:self._list_items 444 | endfunction 445 | function! s:DiagnosticsByLine() abort dict 446 | if !has_key(l:self, '_by_line') 447 | let l:self._by_line = {} 448 | for l:diagnostic in l:self.lsp_diagnostics 449 | let l:start_line = string(l:diagnostic.range.start.line + 1) 450 | if !has_key(l:self._by_line, l:start_line) 451 | let l:line = [] 452 | let l:self._by_line[l:start_line] = l:line 453 | else 454 | let l:line = l:self._by_line[l:start_line] 455 | endif 456 | let l:simple = { 457 | \ 'message': s:DiagnosticMessage(l:diagnostic), 458 | \ 'range': l:diagnostic.range, 459 | \ 'severity': s:SeverityLabel(l:diagnostic.severity), 460 | \} 461 | call add(l:line, l:simple) 462 | endfor 463 | for l:line in values(l:self._by_line) 464 | call sort(l:line, function('CompareRanges')) 465 | endfor 466 | endif 467 | return l:self._by_line 468 | endfunction 469 | 470 | function! s:EmptyDiagnostics() abort 471 | if !exists('s:empty_diagnostics') 472 | let s:empty_diagnostics = { 473 | \ 'lsp_diagnostics': [], 474 | \ 'Highlights': {->[]}, 475 | \ 'ListItems': {->[]}, 476 | \ 'ByLine': {->{}}, 477 | \} 478 | endif 479 | return s:empty_diagnostics 480 | endfunction 481 | 482 | " Compare the ranges of 2 diagnostics that start on the same line 483 | function! s:CompareRanges(d1, d2) abort 484 | if a:d1.range.start.character != a:d2.range.start.character 485 | return a:d1.range.start.character - a:d2.range.start.character 486 | endif 487 | if a:d1.range.end.line != a:d2.range.end.line 488 | return a:d1.range.end.line - a:d2.range.end.line 489 | endif 490 | return a:d1.range.end.character - a:d2.range.end.character 491 | endfunction 492 | -------------------------------------------------------------------------------- /autoload/lsc/diff.vim: -------------------------------------------------------------------------------- 1 | " Computes a simplistic diff between [old] and [new]. 2 | " 3 | " Returns a dict with keys `range`, `rangeLength`, and `text` matching the LSP 4 | " definition of `TextDocumentContentChangeEvent`. 5 | " 6 | " Finds a single change between the common prefix, and common postfix. 7 | function! lsc#diff#compute(old, new) abort 8 | let [l:start_line, l:start_char] = s:FirstDifference(a:old, a:new) 9 | let [l:end_line, l:end_char] = 10 | \ s:LastDifference(a:old[l:start_line : ], 11 | \ a:new[l:start_line : ], l:start_char) 12 | 13 | let l:text = 14 | \ s:ExtractText(a:new, l:start_line, l:start_char, l:end_line, l:end_char) 15 | let l:length = 16 | \ s:Length(a:old, l:start_line, l:start_char, l:end_line, l:end_char) 17 | 18 | let l:adj_end_line = len(a:old) + l:end_line 19 | let l:adj_end_char = 20 | \ l:end_line == 0 ? 0 : strchars(a:old[l:end_line]) + l:end_char + 1 21 | 22 | let l:result = { 'range': { 23 | \ 'start': {'line': l:start_line, 'character': l:start_char}, 24 | \ 'end': {'line': l:adj_end_line, 'character': l:adj_end_char}}, 25 | \ 'text': l:text, 26 | \ 'rangeLength': l:length, 27 | \} 28 | 29 | return l:result 30 | endfunction 31 | 32 | let s:has_lua = has('lua') || has('nvim-0.4.0') 33 | " neovim, and recent vim use 1-index in lua 34 | " older vim without patch-8.2.1066 lists use 0-index 35 | let s:lua_array_start_index = has('nvim-0.4.0') || has('patch-8.2.1066') 36 | 37 | if s:has_lua && !exists('s:lua') 38 | function! s:DefLua() abort 39 | lua <= l:line_count 85 | return [l:line_count - 1, strchars(a:old[l:line_count - 1])] 86 | endif 87 | let l:old_line = a:old[l:i] 88 | let l:new_line = a:new[l:i] 89 | let l:length = min([strchars(l:old_line), strchars(l:new_line)]) 90 | let l:j = 0 91 | while l:j < l:length 92 | if strgetchar(l:old_line, l:j) != strgetchar(l:new_line, l:j) 93 | break 94 | endif 95 | let l:j += 1 96 | endwhile 97 | return [l:i, l:j] 98 | endfunction 99 | 100 | function! s:LastDifference(old, new, start_char) abort 101 | let l:line_count = min([len(a:old), len(a:new)]) 102 | if l:line_count == 0 | return [0, 0] | endif 103 | if s:has_lua 104 | let l:eval = has('nvim') ? 'vim.api.nvim_eval' : 'vim.eval' 105 | let l:i = float2nr(luaeval('lsc_last_difference(' 106 | \.l:eval.'("a:old"),'.l:eval.'("a:new"),'.l:eval.'("has(\"nvim\")"))')) 107 | else 108 | for l:i in range(-1, -1 * l:line_count, -1) 109 | if a:old[l:i] !=# a:new[l:i] | break | endif 110 | endfor 111 | endif 112 | if l:i <= -1 * l:line_count 113 | let l:i = -1 * l:line_count 114 | let l:old_line = strcharpart(a:old[l:i], a:start_char) 115 | let l:new_line = strcharpart(a:new[l:i], a:start_char) 116 | else 117 | let l:old_line = a:old[l:i] 118 | let l:new_line = a:new[l:i] 119 | endif 120 | let l:old_line_length = strchars(l:old_line) 121 | let l:new_line_length = strchars(l:new_line) 122 | let l:length = min([l:old_line_length, l:new_line_length]) 123 | let l:j = -1 124 | while l:j >= -1 * l:length 125 | if strgetchar(l:old_line, l:old_line_length + l:j) != 126 | \ strgetchar(l:new_line, l:new_line_length + l:j) 127 | break 128 | endif 129 | let l:j -= 1 130 | endwhile 131 | return [l:i, l:j] 132 | endfunction 133 | 134 | function! s:ExtractText(lines, start_line, start_char, end_line, end_char) abort 135 | if a:start_line == len(a:lines) + a:end_line 136 | if a:end_line == 0 | return '' | endif 137 | let l:line = a:lines[a:start_line] 138 | let l:length = strchars(l:line) + a:end_char - a:start_char + 1 139 | return strcharpart(l:line, a:start_char, l:length) 140 | endif 141 | let l:result = strcharpart(a:lines[a:start_line], a:start_char)."\n" 142 | for l:line in a:lines[a:start_line + 1:a:end_line - 1] 143 | let l:result .= l:line."\n" 144 | endfor 145 | if a:end_line != 0 146 | let l:line = a:lines[a:end_line] 147 | let l:length = strchars(l:line) + a:end_char + 1 148 | let l:result .= strcharpart(l:line, 0, l:length) 149 | endif 150 | return l:result 151 | endfunction 152 | 153 | function! s:Length(lines, start_line, start_char, end_line, end_char) 154 | \ abort 155 | let l:adj_end_line = len(a:lines) + a:end_line 156 | if l:adj_end_line >= len(a:lines) 157 | let l:adj_end_char = a:end_char - 1 158 | else 159 | let l:adj_end_char = strchars(a:lines[l:adj_end_line]) + a:end_char 160 | endif 161 | if a:start_line == l:adj_end_line 162 | return l:adj_end_char - a:start_char + 1 163 | endif 164 | let l:result = strchars(a:lines[a:start_line]) - a:start_char + 1 165 | for l:line in range(a:start_line + 1, l:adj_end_line - 1) 166 | let l:result += strchars(a:lines[l:line]) + 1 167 | endfor 168 | let l:result += l:adj_end_char + 1 169 | return l:result 170 | endfunction 171 | -------------------------------------------------------------------------------- /autoload/lsc/edit.vim: -------------------------------------------------------------------------------- 1 | function! lsc#edit#findCodeActions(...) abort 2 | if a:0 > 0 3 | let l:ActionFilter = a:1 4 | else 5 | let l:ActionFilter = function('ActionMenu') 6 | endif 7 | call lsc#file#flushChanges() 8 | let l:params = lsc#params#documentRange() 9 | let l:params.context = {'diagnostics': 10 | \ lsc#diagnostics#forLine(lsc#file#fullPath(), line('.') - 1)} 11 | call lsc#server#userCall('textDocument/codeAction', l:params, 12 | \ lsc#util#gateResult('CodeActions', 13 | \ function('SelectAction', [l:ActionFilter]))) 14 | endfunction 15 | 16 | function! s:SelectAction(ActionFilter, result) abort 17 | if type(a:result) != type([]) || len(a:result) == 0 18 | call lsc#message#show('No actions available') 19 | return 20 | endif 21 | call a:ActionFilter(a:result, function('ExecuteCommand')) 22 | endfunction 23 | 24 | function! s:ExecuteCommand(choice) abort 25 | if has_key(a:choice, 'edit') && type(a:choice.edit) == type({}) 26 | call lsc#edit#apply(a:choice.edit) 27 | elseif has_key(a:choice, 'command') 28 | let l:command = type(a:choice.command) == type('') ? 29 | \ a:choice : a:choice.command 30 | call lsc#server#userCall('workspace/executeCommand', 31 | \ {'command': l:command.command, 32 | \ 'arguments': l:command.arguments}, 33 | \ {_->0}) 34 | endif 35 | endfunction 36 | 37 | " Returns a function which can filter actions against a patter and select when 38 | " exactly 1 matches or show a menu for the matching actions. 39 | function! lsc#edit#filterActions(...) abort 40 | if a:0 >= 1 41 | return function('FilteredActionMenu', [a:1]) 42 | else 43 | return function('ActionMenu') 44 | endif 45 | endfunction 46 | 47 | function! s:FilteredActionMenu(filter, actions, OnSelected) abort 48 | call filter(a:actions, {idx, val -> val.title =~ a:filter}) 49 | if empty(a:actions) 50 | call lsc#message#show('No actions available matching '.a:filter) 51 | return v:false 52 | endif 53 | if len(a:actions) == 1 54 | call a:OnSelected(a:actions[0]) 55 | else 56 | call s:ActionMenu(a:actions, a:OnSelected) 57 | endif 58 | endfunction 59 | 60 | function! s:ActionMenu(actions, OnSelected) abort 61 | if has_key(g:, 'LSC_action_menu') 62 | call g:LSC_action_menu(a:actions, a:OnSelected) 63 | return 64 | endif 65 | let l:choices = ['Choose an action:'] 66 | for l:index in range(len(a:actions)) 67 | call add(l:choices, string(l:index + 1).' - '.a:actions[l:index]['title']) 68 | endfor 69 | let l:choice = inputlist(l:choices) 70 | if l:choice > 0 71 | call a:OnSelected(a:actions[l:choice - 1]) 72 | endif 73 | endfunction 74 | 75 | function! lsc#edit#rename(...) abort 76 | call lsc#file#flushChanges() 77 | if a:0 >= 1 78 | let l:new_name = a:1 79 | else 80 | let l:new_name = input('Enter a new name: ') 81 | endif 82 | if l:new_name =~# '\v^\s*$' 83 | echo "\n" 84 | call lsc#message#error('Name can not be blank') 85 | return 86 | endif 87 | let l:params = lsc#params#documentPosition() 88 | let l:params.newName = l:new_name 89 | call lsc#server#userCall('textDocument/rename', l:params, 90 | \ lsc#util#gateResult('Rename', function('lsc#edit#apply'))) 91 | endfunction 92 | 93 | " Applies a workspace edit and returns `v:true` if it was successful. 94 | function! lsc#edit#apply(workspace_edit) abort 95 | if !get(g:, 'lsc_enable_apply_edit', v:true) | return v:false | endif 96 | if !has_key(a:workspace_edit, 'changes') 97 | \ && !has_key(a:workspace_edit, 'documentChanges') 98 | return v:false 99 | endif 100 | let l:view = winsaveview() 101 | let l:alternate=@# 102 | let l:old_buffer = bufnr('%') 103 | let l:old_paste = &paste 104 | let l:old_selection = &selection 105 | let l:old_virtualedit = &virtualedit 106 | set paste 107 | set selection=exclusive 108 | set virtualedit=onemore 109 | 110 | 111 | if (!has_key(a:workspace_edit, 'documentChanges')) 112 | let l:changes = a:workspace_edit.changes 113 | else 114 | let l:changes = {} 115 | for l:textDocumentEdit in a:workspace_edit.documentChanges 116 | let l:uri = l:textDocumentEdit.textDocument.uri 117 | let l:changes[l:uri] = l:textDocumentEdit.edits 118 | endfor 119 | endif 120 | 121 | try 122 | keepjumps keepalt call s:ApplyAll(l:changes) 123 | finally 124 | if len(l:alternate) > 0 | let @#=l:alternate | endif 125 | if l:old_buffer != bufnr('%') | execute 'buffer' l:old_buffer | endif 126 | let &paste = l:old_paste 127 | let &selection = l:old_selection 128 | let &virtualedit = l:old_virtualedit 129 | call winrestview(l:view) 130 | endtry 131 | return v:true 132 | endfunction 133 | 134 | function! s:ApplyAll(changes) abort 135 | for [l:uri, l:edits] in items(a:changes) 136 | let l:file_path = lsc#uri#documentPath(l:uri) 137 | let l:bufnr = lsc#file#bufnr(l:file_path) 138 | if l:bufnr == -1 139 | execute 'edit '.l:file_path 140 | else 141 | execute 'b '.string(l:bufnr) 142 | endif 143 | call sort(l:edits, 'CompareEdits') 144 | let l:foldenable = &foldenable 145 | let l:cmd = 'set nofoldenable' 146 | for l:idx in range(0, len(l:edits) - 1) 147 | let l:cmd .= ' | silent execute "normal! ' 148 | let l:cmd .= s:Apply(l:edits[l:idx]) 149 | let l:cmd .= '\=l:edits['.string(l:idx).'].newText\"' 150 | endfor 151 | if l:foldenable 152 | let l:cmd .= ' | set foldenable' 153 | endif 154 | execute l:cmd 155 | if !&hidden | update | endif 156 | call lsc#file#onChange(l:file_path) 157 | endfor 158 | endfunction 159 | 160 | " Find the normal mode commands to prepare for inserting the text in [edit]. 161 | " 162 | " For inserts, moves the cursor and uses an `a` or `i` to append or insert. 163 | " For replacements, selects the text with `v` and then `c` to change. 164 | function! s:Apply(edit) abort 165 | if s:IsEmptyRange(a:edit.range) 166 | if a:edit.range.start.character >= len(getline(a:edit.range.start.line + 1)) 167 | let l:insert = 'a' 168 | else 169 | let l:insert = 'i' 170 | endif 171 | return printf('%s%s', 172 | \ s:GoToChar(a:edit.range.start), 173 | \ l:insert, 174 | \) 175 | else 176 | return printf('%sv%sc', 177 | \ s:GoToChar(a:edit.range.start), 178 | \ s:GoToChar(a:edit.range.end), 179 | \) 180 | endif 181 | endfunction 182 | 183 | function! s:IsEmptyRange(range) abort 184 | return a:range.start.line == a:range.end.line && 185 | \ a:range.start.character == a:range.end.character 186 | endfunction 187 | 188 | " Find the normal mode commands to go to [pos] 189 | function! s:GoToChar(pos) abort 190 | " In case the position is beyond the end of the buffer, we assume the range 191 | " goes till the end of the buffer. We change pos to last line/last 192 | " character. 193 | if a:pos.line > line('$') - 1 194 | let a:pos.line = line('$') - 1 195 | let a:pos.character = strchars(getline('$')) 196 | endif 197 | let l:cmd = '' 198 | let l:cmd .= printf('%dG', a:pos.line + 1) 199 | if a:pos.character == 0 200 | let l:cmd .= '0' 201 | else 202 | let l:cmd .= printf('0%dl', a:pos.character) 203 | endif 204 | return l:cmd 205 | endfunction 206 | 207 | " Orders edits such that those later in the document appear earlier, and inserts 208 | " at a given index always appear after an edit that starts at that index. 209 | " Assumes that edits have non-overlapping ranges. 210 | function! s:CompareEdits(e1, e2) abort 211 | if a:e1.range.start.line != a:e2.range.start.line 212 | return a:e2.range.start.line - a:e1.range.start.line 213 | endif 214 | if a:e1.range.start.character != a:e2.range.start.character 215 | return a:e2.range.start.character - a:e1.range.start.character 216 | endif 217 | return !s:IsEmptyRange(a:e1.range) ? -1 218 | \ : s:IsEmptyRange(a:e2.range) ? 0 : 1 219 | endfunction 220 | -------------------------------------------------------------------------------- /autoload/lsc/file.vim: -------------------------------------------------------------------------------- 1 | if !exists('s:initialized') 2 | let s:initialized = v:true 3 | " file path -> file version 4 | let s:file_versions = {} 5 | " file path -> file content 6 | let s:file_content = {} 7 | " file path -> flush timer 8 | let s:flush_timers = {} 9 | " full file path -> buffer name 10 | let s:normalized_paths = {} 11 | endif 12 | 13 | " Send a 'didOpen' message for all open buffers with a tracked file type for a 14 | " running server. 15 | function! lsc#file#trackAll(server) abort 16 | for l:buffer in getbufinfo({'bufloaded': v:true}) 17 | if !getbufvar(l:buffer.bufnr, '&modifiable') | continue | endif 18 | if l:buffer.name =~# '\vfugitive:///' | continue | endif 19 | let l:filetype = getbufvar(l:buffer.bufnr, '&filetype') 20 | if index(a:server.filetypes, l:filetype) < 0 | continue | endif 21 | call lsc#file#track(a:server, l:buffer, l:filetype) 22 | endfor 23 | endfunction 24 | 25 | function! lsc#file#track(server, buffer, filetype) abort 26 | let l:file_path = lsc#file#normalize(a:buffer.name) 27 | call s:DidOpen(a:server, a:buffer.bufnr, l:file_path, a:filetype) 28 | endfunction 29 | 30 | " Run language servers for this filetype if they aren't already running and 31 | " flush file changes. 32 | function! lsc#file#onOpen() abort 33 | let l:file_path = lsc#file#fullPath() 34 | if has_key(s:file_versions, l:file_path) 35 | call lsc#file#flushChanges() 36 | else 37 | let l:bufnr = bufnr('%') 38 | for l:server in lsc#server#forFileType(&filetype) 39 | if !get(l:server.config, 'enabled', v:true) | continue | endif 40 | if l:server.status ==# 'running' 41 | call s:DidOpen(l:server, l:bufnr, l:file_path, &filetype) 42 | else 43 | call lsc#server#start(l:server) 44 | endif 45 | endfor 46 | endif 47 | endfunction 48 | 49 | function! lsc#file#onClose(full_path, filetype) abort 50 | if has_key(s:file_versions, a:full_path) 51 | unlet s:file_versions[a:full_path] 52 | endif 53 | if has_key(s:file_content, a:full_path) 54 | unlet s:file_content[a:full_path] 55 | endif 56 | if !lsc#server#filetypeActive(a:filetype) | return | endif 57 | let l:params = {'textDocument': {'uri': lsc#uri#documentUri(a:full_path)}} 58 | for l:server in lsc#server#forFileType(a:filetype) 59 | call l:server.notify('textDocument/didClose', l:params) 60 | endfor 61 | endfunction 62 | 63 | " Send a `textDocument/didSave` notification if the server may be interested. 64 | function! lsc#file#onWrite(full_path, filetype) abort 65 | let l:params = {'textDocument': {'uri': lsc#uri#documentUri(a:full_path)}} 66 | for l:server in lsc#server#forFileType(a:filetype) 67 | if !l:server.capabilities.textDocumentSync.sendDidSave | continue | endif 68 | call l:server.notify('textDocument/didSave', l:params) 69 | endfor 70 | endfunction 71 | 72 | " Flushes changes for the current buffer. 73 | function! lsc#file#flushChanges() abort 74 | call s:FlushIfChanged(lsc#file#fullPath(), &filetype) 75 | endfunction 76 | 77 | " Send the 'didOpen' message for a file. 78 | function! s:DidOpen(server, bufnr, file_path, filetype) abort 79 | let l:buffer_content = has_key(s:file_content, a:file_path) 80 | \ ? s:file_content[a:file_path] 81 | \ : getbufline(a:bufnr, 1, '$') 82 | let l:version = has_key(s:file_versions, a:file_path) 83 | \ ? s:file_versions[a:file_path] 84 | \ : 1 85 | let l:params = {'textDocument': 86 | \ {'uri': lsc#uri#documentUri(a:file_path), 87 | \ 'version': l:version, 88 | \ 'text': join(l:buffer_content, "\n")."\n", 89 | \ 'languageId': a:server.languageId[a:filetype], 90 | \ } 91 | \ } 92 | if a:server.notify('textDocument/didOpen', l:params) 93 | let s:file_versions[a:file_path] = l:version 94 | if get(g:, 'lsc_enable_incremental_sync', v:true) 95 | \ && a:server.capabilities.textDocumentSync.incremental 96 | let s:file_content[a:file_path] = l:buffer_content 97 | endif 98 | doautocmd User LSCOnChangesFlushed 99 | endif 100 | endfunction 101 | 102 | " Mark all files of type `filetype` as untracked. 103 | function! lsc#file#clean(filetype) abort 104 | for l:buffer in getbufinfo({'bufloaded': v:true}) 105 | if getbufvar(l:buffer.bufnr, '&filetype') != a:filetype | continue | endif 106 | if has_key(s:file_versions, l:buffer.name) 107 | unlet s:file_versions[l:buffer.name] 108 | if has_key(s:file_content, l:buffer.name) 109 | unlet s:file_content[l:buffer.name] 110 | endif 111 | endif 112 | endfor 113 | endfunction 114 | 115 | function! lsc#file#onChange(...) abort 116 | if a:0 >= 1 117 | let l:file_path = a:1 118 | let l:filetype = getbufvar(lsc#file#bufnr(l:file_path), '&filetype') 119 | else 120 | let l:file_path = lsc#file#fullPath() 121 | let l:filetype = &filetype 122 | endif 123 | if has_key(s:flush_timers, l:file_path) 124 | call timer_stop(s:flush_timers[l:file_path]) 125 | endif 126 | let s:flush_timers[l:file_path] = 127 | \ timer_start(get(g:, 'lsc_change_debounce_time', 500), 128 | \ {_->s:FlushIfChanged(file_path, filetype)}, 129 | \ {'repeat': 1}) 130 | endfunction 131 | 132 | " Flushes only if `onChange` had previously been called for the file and those 133 | " changes aren't flushed yet, and the file is tracked by at least one server. 134 | function! s:FlushIfChanged(file_path, filetype) abort 135 | " Buffer may not have any pending changes to flush. 136 | if !has_key(s:flush_timers, a:file_path) | return | endif 137 | " Buffer may not be tracked with a `didOpen` call by any server yet. 138 | if !has_key(s:file_versions, a:file_path) | return | endif 139 | let s:file_versions[a:file_path] += 1 140 | if has_key(s:flush_timers, a:file_path) 141 | call timer_stop(s:flush_timers[a:file_path]) 142 | unlet s:flush_timers[a:file_path] 143 | endif 144 | let l:document_params = {'textDocument': 145 | \ {'uri': lsc#uri#documentUri(a:file_path), 146 | \ 'version': s:file_versions[a:file_path], 147 | \ }, 148 | \ } 149 | let l:current_content = getbufline(lsc#file#bufnr(a:file_path), 1, '$') 150 | for l:server in lsc#server#forFileType(a:filetype) 151 | if l:server.status !=# 'running' | continue | endif 152 | if l:server.capabilities.textDocumentSync.incremental 153 | if !exists('l:incremental_params') 154 | let l:old_content = s:file_content[a:file_path] 155 | let l:change = lsc#diff#compute(l:old_content, l:current_content) 156 | let s:file_content[a:file_path] = l:current_content 157 | let l:incremental_params = copy(l:document_params) 158 | let l:incremental_params.contentChanges = [l:change] 159 | endif 160 | let l:params = l:incremental_params 161 | else 162 | if !exists('l:full_params') 163 | let l:full_params = copy(l:document_params) 164 | let l:change = {'text': join(l:current_content, "\n")."\n"} 165 | let l:full_params.contentChanges = [l:change] 166 | endif 167 | let l:params = l:full_params 168 | endif 169 | call l:server.notify('textDocument/didChange', l:params) 170 | endfor 171 | doautocmd User LSCOnChangesFlushed 172 | endfunction 173 | 174 | function! lsc#file#version() abort 175 | return get(s:file_versions, lsc#file#fullPath(), '') 176 | endfunction 177 | 178 | " The full path to the current buffer. 179 | " 180 | " The association between a buffer and full path may change if the file has not 181 | " been written yet - this makes a best-effort attempt to get a full path anyway. 182 | " In most cases if the working directory doesn't change this isn't harmful. 183 | " 184 | " Paths which do need to be manually normalized are stored so that the full path 185 | " can be associated back to a buffer with `lsc#file#bufnr()`. 186 | function! lsc#file#fullPath() abort 187 | let l:full_path = expand('%:p') 188 | if l:full_path ==# expand('%') 189 | " Path could not be expanded due to pointing to a non-existent directory 190 | let l:full_path = lsc#file#normalize(getbufinfo('%')[0].name) 191 | elseif has('win32') 192 | let l:full_path = s:os_normalize(l:full_path) 193 | endif 194 | return l:full_path 195 | endfunction 196 | 197 | " Like `bufnr()` but handles the case where a relative path was normalized 198 | " against cwd. 199 | function! lsc#file#bufnr(full_path) abort 200 | let l:bufnr = bufnr(a:full_path) 201 | if l:bufnr == -1 && has_key(s:normalized_paths, a:full_path) 202 | let l:bufnr = bufnr(s:normalized_paths[a:full_path]) 203 | endif 204 | return l:bufnr 205 | endfunction 206 | 207 | " Normalize `original_path` for OS separators and relative paths, and store the 208 | " mapping. 209 | " 210 | " The return value is always a full path, even if vim won't expand it with `:p` 211 | " because it is in a non-existent directory. The original path is stored, keyed 212 | " by the normalized path, so that it can be retrieved by `lsc#file#bufnr`. 213 | function! lsc#file#normalize(original_path) abort 214 | let l:full_path = a:original_path 215 | if l:full_path !~# '^/\|\%([c-zC-Z]:[/\\]\)' 216 | let l:full_path = getcwd().'/'.l:full_path 217 | endif 218 | let l:full_path = s:os_normalize(l:full_path) 219 | let s:normalized_paths[l:full_path] = a:original_path 220 | return l:full_path 221 | endfunction 222 | 223 | function! lsc#file#compare(file_1, file_2) abort 224 | if a:file_1 == a:file_2 | return 0 | endif 225 | let l:cwd = '^'.s:os_normalize(getcwd()) 226 | let l:file_1_in_cwd = a:file_1 =~# l:cwd 227 | let l:file_2_in_cwd = a:file_2 =~# l:cwd 228 | if l:file_1_in_cwd && !l:file_2_in_cwd | return -1 | endif 229 | if l:file_2_in_cwd && !l:file_1_in_cwd | return 1 | endif 230 | return a:file_1 > a:file_2 ? 1 : -1 231 | endfunction 232 | 233 | " `getcwd` with OS path normalization. 234 | function! lsc#file#cwd() abort 235 | return s:os_normalize(getcwd()) 236 | endfunction 237 | 238 | function! s:os_normalize(path) abort 239 | if has('win32') | return substitute(a:path, '\\', '/', 'g') | endif 240 | return a:path 241 | endfunction 242 | -------------------------------------------------------------------------------- /autoload/lsc/highlights.vim: -------------------------------------------------------------------------------- 1 | " Refresh highlight matches on windows open for [bufnr]. 2 | function! lsc#highlights#updateDisplayed(bufnr) abort 3 | for l:window_id in win_findbuf(a:bufnr) 4 | call win_execute(l:window_id, 'call lsc#highlights#update()') 5 | endfor 6 | endfunction 7 | 8 | " Refresh highlight matches in the current window. 9 | function! lsc#highlights#update() abort 10 | if !get(g:, 'lsc_enable_diagnostics', v:true) | return | endif 11 | if !get(g:, 'lsc_diagnostic_highlights', v:true) | return | endif 12 | if s:CurrentWindowIsFresh() | return | endif 13 | call lsc#highlights#clear() 14 | if &diff | return | endif 15 | for l:highlight in lsc#diagnostics#forFile(lsc#file#fullPath()).Highlights() 16 | let l:priority = -1 * l:highlight.severity 17 | let l:group = l:highlight.group 18 | if l:highlight.ranges[0][0] > line('$') 19 | " Diagnostic starts after end of file 20 | let l:match = matchadd(l:group, '\%'.line('$').'l$', l:priority) 21 | elseif len(l:highlight.ranges) == 1 && 22 | \ l:highlight.ranges[0][1] > len(getline(l:highlight.ranges[0][0])) 23 | " Diagnostic starts after end of line 24 | let l:line_range = '\%'.l:highlight.ranges[0][0].'l$' 25 | let l:match = matchadd(l:group, l:line_range, l:priority) 26 | else 27 | let l:match = matchaddpos(l:group, l:highlight.ranges, l:priority) 28 | endif 29 | call add(w:lsc_diagnostic_matches, l:match) 30 | endfor 31 | call s:MarkCurrentWindowFresh() 32 | endfunction 33 | 34 | " Remove all highlighted matches in the current window. 35 | function! lsc#highlights#clear() abort 36 | if exists('w:lsc_diagnostic_matches') 37 | for l:current_match in w:lsc_diagnostic_matches 38 | silent! call matchdelete(l:current_match) 39 | endfor 40 | endif 41 | let w:lsc_diagnostic_matches = [] 42 | if exists('w:lsc_highlights_source') 43 | unlet w:lsc_highlights_source 44 | endif 45 | endfunction 46 | 47 | " Whether the diagnostic highlights for the current window are up to date. 48 | function! s:CurrentWindowIsFresh() abort 49 | if !exists('w:lsc_diagnostics') | return v:true | endif 50 | if !exists('w:lsc_highlights_source') | return v:false | endif 51 | return w:lsc_highlights_source is w:lsc_diagnostics 52 | endfunction 53 | 54 | function! s:MarkCurrentWindowFresh() abort 55 | let w:lsc_highlight_source = w:lsc_diagnostics 56 | endfunction 57 | -------------------------------------------------------------------------------- /autoload/lsc/message.vim: -------------------------------------------------------------------------------- 1 | function! lsc#message#show(message, ...) abort 2 | call s:Echo('echo', a:message, get(a:, 1, 'Log')) 3 | endfunction 4 | 5 | function! lsc#message#showRequest(message, actions) abort 6 | let l:options = [a:message] 7 | for l:index in range(len(a:actions)) 8 | let l:title = get(a:actions, l:index)['title'] 9 | call add(l:options, (l:index + 1) . ' - ' . l:title) 10 | endfor 11 | let l:result = inputlist(l:options) 12 | if l:result <= 0 || l:result - 1 > len(a:actions) 13 | return v:null 14 | else 15 | return get(a:actions, l:result - 1) 16 | endif 17 | endfunction 18 | 19 | function! lsc#message#log(message, type) abort 20 | call s:Echo('echom', a:message, a:type) 21 | endfunction 22 | 23 | function! lsc#message#error(message) abort 24 | call s:Echo('echom', a:message, 'Error') 25 | endfunction 26 | 27 | function! s:Echo(echo_cmd, message, level) abort 28 | let [l:level, l:hl_group] = s:Level(a:level) 29 | exec 'echohl '.l:hl_group 30 | exec a:echo_cmd.' "[lsc:'.l:level.'] ".a:message' 31 | echohl None 32 | endfunction 33 | 34 | function! s:Level(level) abort 35 | if type(a:level) == type(0) 36 | if a:level == 1 37 | return ['Error', 'lscDiagnosticError'] 38 | elseif a:level == 2 39 | return ['Warning', 'lscDiagnosticWarning'] 40 | elseif a:level == 3 41 | return ['Info', 'lscDiagnosticInfo'] 42 | endif 43 | return ['Log', 'None'] " Level 4 or unmatched 44 | endif 45 | if a:level ==# 'Error' 46 | return ['Error', 'lscDiagnosticError'] 47 | elseif a:level ==# 'Warning' 48 | return ['Warning', 'lscDiagnosticWarning'] 49 | elseif a:level ==# 'Info' 50 | return ['Info', 'lscDiagnosticInfo'] 51 | endif 52 | return ['Log', 'None'] " 'Log' or unmatched 53 | endfunction 54 | -------------------------------------------------------------------------------- /autoload/lsc/params.vim: -------------------------------------------------------------------------------- 1 | function! lsc#params#textDocument() abort 2 | return {'textDocument': {'uri': lsc#uri#documentUri()}} 3 | endfunction 4 | 5 | function! lsc#params#documentPosition() abort 6 | return { 'textDocument': {'uri': lsc#uri#documentUri()}, 7 | \ 'position': {'line': line('.') - 1, 'character': col('.') - 1} 8 | \ } 9 | endfunction 10 | 11 | function! lsc#params#documentRange() abort 12 | return { 'textDocument': {'uri': lsc#uri#documentUri()}, 13 | \ 'range': { 14 | \ 'start': {'line': line('.') - 1, 'character': col('.') - 1}, 15 | \ 'end': {'line': line('.') - 1, 'character': col('.')}}, 16 | \ } 17 | endfunction 18 | -------------------------------------------------------------------------------- /autoload/lsc/protocol.vim: -------------------------------------------------------------------------------- 1 | if !exists('s:initialized') 2 | let s:log_size = 10 3 | let s:initialized = v:true 4 | endif 5 | 6 | function! lsc#protocol#open(command, on_message, on_err, on_exit) abort 7 | let l:c = { 8 | \ '_call_id': 0, 9 | \ '_in': [], 10 | \ '_out': [], 11 | \ '_buffer': [], 12 | \ '_on_message': lsc#util#async('message handler', a:on_message), 13 | \ '_callbacks': {}, 14 | \} 15 | function! l:c.request(method, params, callback, options) abort 16 | let l:self._call_id += 1 17 | let l:message = s:Format(a:method, a:params, l:self._call_id) 18 | let l:self._callbacks[l:self._call_id] = get(a:options, 'sync', v:false) 19 | \ ? [a:callback] 20 | \ : [lsc#util#async('request callback for '.a:method, a:callback)] 21 | call l:self._send(l:message) 22 | endfunction 23 | function! l:c.notify(method, params) abort 24 | let l:message = s:Format(a:method, a:params, v:null) 25 | call l:self._send(l:message) 26 | endfunction 27 | function! l:c.respond(id, result) abort 28 | call l:self._send({'id': a:id, 'result': a:result}) 29 | endfunction 30 | function! l:c._send(message) abort 31 | call lsc#util#shift(l:self._in, s:log_size, a:message) 32 | call l:self._channel.send(s:Encode(a:message)) 33 | endfunction 34 | function! l:c._receive(message) abort 35 | call add(l:self._buffer, a:message) 36 | if has_key(l:self, '_consume') | return | endif 37 | if s:Consume(l:self) 38 | let l:self._consume = timer_start(0, 39 | \ function('HandleTimer', [l:self])) 40 | endif 41 | endfunction 42 | let l:channel = lsc#channel#open(a:command, l:c._receive, a:on_err, a:on_exit) 43 | if type(l:channel) == type(v:null) 44 | return v:null 45 | endif 46 | let l:c._channel = l:channel 47 | return l:c 48 | endfunction 49 | 50 | function! s:HandleTimer(server, ...) abort 51 | if s:Consume(a:server) 52 | let a:server._consume = timer_start(0, 53 | \ function('HandleTimer', [a:server])) 54 | else 55 | unlet a:server._consume 56 | endif 57 | endfunction 58 | 59 | function! s:Format(method, params, id) abort 60 | let l:message = {'method': a:method} 61 | if type(a:params) != type(v:null) | let l:message['params'] = a:params | endif 62 | if type(a:id) != type(v:null) | let l:message['id'] = a:id | endif 63 | return l:message 64 | endfunction 65 | 66 | " Prepend the JSON RPC headers and serialize to JSON. 67 | function! s:Encode(message) abort 68 | let a:message['jsonrpc'] = '2.0' 69 | let l:encoded = json_encode(a:message) 70 | let l:length = len(l:encoded) 71 | return 'Content-Length: '.l:length."\r\n\r\n".l:encoded 72 | endfunction 73 | 74 | " Reads from the buffer for [server] and processes a message, if one is 75 | " available. 76 | " 77 | " Returns true if there are more messages to consume in the buffer. 78 | function! s:Consume(server) abort 79 | let l:buffer = a:server._buffer 80 | let l:message = l:buffer[0] 81 | let l:end_of_header = stridx(l:message, "\r\n\r\n") 82 | if l:end_of_header < 0 83 | return s:Incomplete(l:buffer) 84 | endif 85 | let l:headers = split(l:message[:l:end_of_header - 1], "\r\n") 86 | let l:message_start = l:end_of_header + len("\r\n\r\n") 87 | let l:message_end = l:message_start + s:ContentLength(l:headers) 88 | if len(l:message) < l:message_end 89 | return s:Incomplete(l:buffer) 90 | endif 91 | if len(l:message) == l:message_end 92 | let l:payload = l:message[l:message_start :] 93 | call remove(l:buffer, 0) 94 | else 95 | let l:payload = l:message[l:message_start : l:message_end-1] 96 | let l:buffer[0] = l:message[l:message_end :] 97 | endif 98 | try 99 | if len(l:payload) > 0 100 | let l:content = json_decode(l:payload) 101 | if type(l:content) != type({}) 102 | unlet l:content 103 | throw 1 104 | endif 105 | endif 106 | catch 107 | call lsc#message#error('Could not decode message: ['.l:payload.']') 108 | endtry 109 | if exists('l:content') 110 | call lsc#util#shift(a:server._out, s:log_size, deepcopy(l:content)) 111 | call s:Dispatch(l:content, a:server._on_message, a:server._callbacks) 112 | endif 113 | return !empty(l:buffer) 114 | endfunction 115 | 116 | function! s:Incomplete(buffer) abort 117 | if len(a:buffer) == 1 | return v:false | endif 118 | " Merge 2 messages 119 | let l:first = remove(a:buffer, 0) 120 | let l:second = remove(a:buffer, 0) 121 | call insert(a:buffer, l:first.l:second) 122 | return v:true 123 | endfunction 124 | 125 | " Finds the header with 'Content-Length' and returns the integer value 126 | function! s:ContentLength(headers) abort 127 | for l:header in a:headers 128 | if l:header =~? '^Content-Length' 129 | let l:parts = split(l:header, ':') 130 | let l:length = l:parts[1] 131 | if l:length[0] ==# ' ' | let l:length = l:length[1:] | endif 132 | return l:length + 0 133 | endif 134 | endfor 135 | return -1 136 | endfunction 137 | 138 | function! s:Dispatch(message, OnMessage, callbacks, ...) abort 139 | if has_key(a:message, 'method') 140 | let l:method = a:message.method 141 | let l:params = has_key(a:message, 'params') ? a:message.params : v:null 142 | let l:id = has_key(a:message, 'id') ? a:message.id : v:null 143 | call a:OnMessage(l:method, l:params, l:id) 144 | elseif has_key(a:message, 'error') 145 | let l:error = a:message.error 146 | let l:message = has_key(l:error, 'message') ? 147 | \ l:error.message : 148 | \ string(l:error) 149 | call lsc#message#error(l:message) 150 | elseif has_key(a:message, 'id') 151 | let l:call_id = a:message['id'] 152 | if has_key(a:callbacks, l:call_id) 153 | let l:Callback = a:callbacks[l:call_id][0] 154 | unlet a:callbacks[l:call_id] 155 | call l:Callback(get(a:message, 'result', v:null)) 156 | endif 157 | else 158 | call lsc#message#error('Unknown message type: '.string(a:message)) 159 | endif 160 | endfunction 161 | -------------------------------------------------------------------------------- /autoload/lsc/reference.vim: -------------------------------------------------------------------------------- 1 | let s:popup_id = 0 2 | 3 | function! lsc#reference#goToDefinition(mods, issplit) abort 4 | call lsc#file#flushChanges() 5 | call lsc#server#userCall('textDocument/definition', 6 | \ lsc#params#documentPosition(), 7 | \ lsc#util#gateResult('GoToDefinition', 8 | \ function('GoToDefinition', [a:mods, a:issplit]))) 9 | endfunction 10 | 11 | function! s:GoToDefinition(mods, issplit, result) abort 12 | if type(a:result) == type(v:null) || 13 | \ (type(a:result) == type([]) && len(a:result) == 0) 14 | call lsc#message#error('No definition found') 15 | return 16 | endif 17 | if type(a:result) == type([]) 18 | let l:location = a:result[0] 19 | else 20 | let l:location = a:result 21 | endif 22 | let l:file = lsc#uri#documentPath(l:location.uri) 23 | let l:line = l:location.range.start.line + 1 24 | let l:character = l:location.range.start.character + 1 25 | let l:dotag = &tagstack && exists('*gettagstack') && exists('*settagstack') 26 | if l:dotag 27 | let l:from = [bufnr('%'), line('.'), col('.'), 0] 28 | let l:tagname = expand('') 29 | let l:stack = gettagstack() 30 | if l:stack.curidx > 1 31 | let l:stack.items = l:stack.items[0:l:stack.curidx-2] 32 | else 33 | let l:stack.items = [] 34 | endif 35 | let l:stack.items += [{'from': l:from, 'tagname': l:tagname}] 36 | let l:stack.curidx = len(l:stack.items) 37 | call settagstack(win_getid(), l:stack) 38 | endif 39 | call s:goTo(l:file, l:line, l:character, a:mods, a:issplit) 40 | if l:dotag 41 | let l:curidx = gettagstack().curidx + 1 42 | call settagstack(win_getid(), {'curidx': l:curidx}) 43 | endif 44 | endfunction 45 | 46 | function! lsc#reference#findReferences() abort 47 | call lsc#file#flushChanges() 48 | let l:params = lsc#params#documentPosition() 49 | let l:params.context = {'includeDeclaration': v:true} 50 | call lsc#server#userCall('textDocument/references', l:params, 51 | \ function('setQuickFixLocations', ['references'])) 52 | endfunction 53 | 54 | function! lsc#reference#findImplementations() abort 55 | call lsc#file#flushChanges() 56 | call lsc#server#userCall('textDocument/implementation', 57 | \ lsc#params#documentPosition(), 58 | \ function('setQuickFixLocations', ['implementations'])) 59 | endfunction 60 | 61 | function! s:setQuickFixLocations(label, results) abort 62 | if empty(a:results) 63 | call lsc#message#show('No '.a:label.' found') 64 | return 65 | endif 66 | call map(a:results, {_, ref -> s:QuickFixItem(ref)}) 67 | call sort(a:results, 'lsc#util#compareQuickFixItems') 68 | call setqflist(a:results) 69 | copen 70 | endfunction 71 | 72 | " Convert an LSP Location to a item suitable for the vim quickfix list. 73 | " 74 | " Both representations are dictionaries. 75 | " 76 | " Location: 77 | " 'uri': file:// URI 78 | " 'range': {'start': {'line', 'character'}, 'end': {'line', 'character'}} 79 | " 80 | " QuickFix Item: (as used) 81 | " 'filename': file path if file is not open 82 | " 'lnum': line number 83 | " 'col': column number 84 | " 'text': The content of the referenced line 85 | " 86 | " LSP line and column are zero-based, vim is one-based. 87 | function! s:QuickFixItem(location) abort 88 | let l:item = {'lnum': a:location.range.start.line + 1, 89 | \ 'col': a:location.range.start.character + 1} 90 | let l:file_path = lsc#uri#documentPath(a:location.uri) 91 | let l:item.filename = fnamemodify(l:file_path, ':.') 92 | let l:bufnr = lsc#file#bufnr(l:file_path) 93 | if l:bufnr != -1 && bufloaded(l:bufnr) 94 | let l:item.text = getbufline(l:bufnr, l:item.lnum)[0] 95 | else 96 | let l:item.text = readfile(l:file_path, '', l:item.lnum)[l:item.lnum - 1] 97 | endif 98 | return l:item 99 | endfunction 100 | 101 | function! s:goTo(file, line, character, mods, issplit) abort 102 | let l:prev_buf = bufnr('%') 103 | if a:issplit || a:file !=# lsc#file#fullPath() 104 | let l:cmd = 'edit' 105 | if a:issplit 106 | let l:cmd = lsc#file#bufnr(a:file) == -1 ? 'split' : 'sbuffer' 107 | endif 108 | let l:relative_path = fnamemodify(a:file, ':~:.') 109 | exec a:mods l:cmd fnameescape(l:relative_path) 110 | endif 111 | if l:prev_buf != bufnr('%') 112 | " switching buffers already left a jump 113 | " Set curswant manually to work around vim bug 114 | call cursor([a:line, a:character, 0, virtcol([a:line, a:character])]) 115 | redraw 116 | else 117 | " Move with 'G' to ensure a jump is left 118 | exec 'normal! '.a:line.'G' 119 | " Set curswant manually to work around vim bug 120 | call cursor([0, a:character, 0, virtcol([a:line, a:character])]) 121 | endif 122 | endfunction 123 | 124 | function! lsc#reference#hover() abort 125 | call lsc#file#flushChanges() 126 | let l:params = lsc#params#documentPosition() 127 | call lsc#server#userCall('textDocument/hover', l:params, 128 | \ function('showHover', [s:hasOpenHover()])) 129 | endfunction 130 | 131 | function! s:hasOpenHover() abort 132 | if s:popup_id == 0 | return v:false | endif 133 | if !exists('*nvim_win_get_config') && !exists('*popup_getoptions') 134 | return v:false 135 | endif 136 | if has('nvim') 137 | return nvim_win_is_valid(s:popup_id) 138 | endif 139 | return len(popup_getoptions(s:popup_id)) > 0 140 | endfunction 141 | 142 | function! s:showHover(force_preview, result) abort 143 | if empty(a:result) || empty(a:result.contents) 144 | echom 'No hover information' 145 | return 146 | endif 147 | let l:contents = a:result.contents 148 | if type(l:contents) != type([]) 149 | let l:contents = [l:contents] 150 | endif 151 | let l:lines = [] 152 | let l:filetype = 'markdown' 153 | for l:item in l:contents 154 | if type(l:item) == type({}) 155 | let l:lines += split(l:item.value, "\n") 156 | if has_key(l:item, 'language') 157 | let l:filetype = l:item.language 158 | elseif has_key(l:item, 'kind') 159 | let l:filetype = l:item.kind ==# 'markdown' ? 'markdown' : 'text' 160 | endif 161 | else 162 | let l:lines += split(l:item, "\n") 163 | endif 164 | endfor 165 | let b:lsc_last_hover = l:lines 166 | if get(g:, 'lsc_hover_popup', v:true) 167 | \ && (exists('*popup_atcursor') || exists('*nvim_open_win')) 168 | call s:closeHoverPopup() 169 | if (a:force_preview) 170 | call lsc#util#displayAsPreview(l:lines, l:filetype, 171 | \ function('lsc#util#noop')) 172 | else 173 | call s:openHoverPopup(l:lines, l:filetype) 174 | endif 175 | else 176 | call lsc#util#displayAsPreview(l:lines, l:filetype, 177 | \ function('lsc#util#noop')) 178 | endif 179 | endfunction 180 | 181 | function! s:openHoverPopup(lines, filetype) abort 182 | if len(a:lines) == 0 | return | endif 183 | if has('nvim') 184 | let l:buf = nvim_create_buf(v:false, v:true) 185 | call nvim_buf_set_option(l:buf, 'synmaxcol', 0) 186 | if g:lsc_enable_popup_syntax 187 | call nvim_buf_set_option(l:buf, 'filetype', a:filetype) 188 | endif 189 | " Note, the +2s below will be used for padding around the hover text. 190 | let l:height = len(a:lines) + 2 191 | let l:width = 1 192 | " The maximum width of the floating window should not exceed 95% of the 193 | " screen width. 194 | let l:max_width = float2nr(&columns * 0.95) 195 | 196 | " Need to figure out the longest line and base the popup width on that. 197 | " Also increase the floating window 'height' if any lines are going to wrap. 198 | for l:val in a:lines 199 | let l:val_width = strdisplaywidth(l:val) + 2 200 | if l:val_width > l:max_width 201 | let l:height = l:height + (l:val_width / l:max_width) 202 | let l:val_width = l:max_width 203 | endif 204 | let l:width = l:val_width > l:width ? l:val_width : l:width 205 | endfor 206 | 207 | " Prefer an upward floating window, but if there is no space fallback to 208 | " a downward floating window. 209 | let l:current_position = getpos('.') 210 | let l:top_line_number = line('w0') 211 | if l:current_position[1] - l:top_line_number >= l:height 212 | " There is space to display the floating window above the current cursor 213 | " line. 214 | let l:vertical_alignment = 'S' 215 | let l:row = 0 216 | else 217 | " No space above, so we will float downward instead. 218 | let l:vertical_alignment = 'N' 219 | let l:row = 1 220 | " Truncate the float height so that the popup always floats below and 221 | " never overflows into and above the cursor line. 222 | let l:lines_above_cursor = l:current_position[1] - l:top_line_number 223 | if l:height > winheight(0) + 2 - l:lines_above_cursor 224 | let l:height = winheight(0) - l:lines_above_cursor 225 | endif 226 | endif 227 | 228 | let l:opts = { 229 | \ 'relative': 'cursor', 230 | \ 'anchor': l:vertical_alignment . 'W', 231 | \ 'row': l:row, 232 | \ 'col': 1, 233 | \ 'width': l:width, 234 | \ 'height': l:height, 235 | \ 'style': 'minimal', 236 | \ } 237 | let s:popup_id = nvim_open_win(l:buf, v:false, l:opts) 238 | call nvim_win_set_option(s:popup_id, 'colorcolumn', '') 239 | " Add padding to the left and right of each text line. 240 | call map(a:lines, {_, val -> ' ' . val . ' '}) 241 | call nvim_buf_set_lines(winbufnr(s:popup_id), 1, -1, v:false, a:lines) 242 | call nvim_buf_set_option(l:buf, 'modifiable', v:false) 243 | " Close the floating window upon a cursor move. 244 | " vint: -ProhibitAutocmdWithNoGroup 245 | " https://github.com/Kuniwak/vint/issues/285 246 | autocmd CursorMoved ++once call s:closeHoverPopup() 247 | " vint: +ProhibitAutocmdWithNoGroup 248 | " Also close the floating window when focussed into with the escape key. 249 | call nvim_buf_set_keymap(l:buf, 'n', '', ':close', {}) 250 | else 251 | let s:popup_id = popup_atcursor(a:lines, { 252 | \ 'padding': [1, 1, 1, 1], 253 | \ 'border': [0, 0, 0, 0], 254 | \ 'moved': 'any', 255 | \ }) 256 | if g:lsc_enable_popup_syntax 257 | call setbufvar(winbufnr(s:popup_id), '&filetype', a:filetype) 258 | endif 259 | end 260 | endfunction 261 | 262 | function! s:closeHoverPopup() abort 263 | if has('nvim') 264 | if win_id2win(s:popup_id) > 0 && nvim_win_is_valid(s:popup_id) 265 | call nvim_win_close(s:popup_id, v:true) 266 | endif 267 | else 268 | call popup_close(s:popup_id) 269 | end 270 | let s:popup_id = 0 271 | endfunction 272 | 273 | " Request a list of symbols in the current document and populate the quickfix 274 | " list. 275 | function! lsc#reference#documentSymbols() abort 276 | call lsc#file#flushChanges() 277 | call lsc#server#userCall('textDocument/documentSymbol', 278 | \ lsc#params#textDocument(), 279 | \ function('setQuickFixSymbols')) 280 | endfunction 281 | 282 | function! s:setQuickFixSymbols(results) abort 283 | if empty(a:results) 284 | call lsc#message#show('No symbols found') 285 | return 286 | endif 287 | 288 | call map(a:results, {_, symbol -> lsc#convert#quickFixSymbol(symbol)}) 289 | call sort(a:results, 'lsc#util#compareQuickFixItems') 290 | call setqflist(a:results) 291 | copen 292 | endfunction 293 | 294 | 295 | " If the server supports `textDocument/documentHighlight` and they are enabled, 296 | " use the active highlights to move the cursor to the next or previous referene 297 | " in the same document to the symbol under the cursor. 298 | function! lsc#reference#findNext(direction) abort 299 | if exists('w:lsc_references') 300 | let l:idx = lsc#cursor#isInReference(w:lsc_references) 301 | if l:idx != -1 && 302 | \ l:idx + a:direction >= 0 && 303 | \ l:idx + a:direction < len(w:lsc_references) 304 | let l:target = w:lsc_references[l:idx + a:direction].ranges[0][0:1] 305 | endif 306 | endif 307 | if !exists('l:target') 308 | return 309 | endif 310 | " Move with 'G' to ensure a jump is left 311 | exec 'normal! '.l:target[0].'G'.l:target[1].'|' 312 | endfunction 313 | -------------------------------------------------------------------------------- /autoload/lsc/search.vim: -------------------------------------------------------------------------------- 1 | function! lsc#search#workspaceSymbol(...) abort 2 | if a:0 >= 1 3 | let l:query = a:1 4 | else 5 | let l:query = input('Search Workspace For: ') 6 | endif 7 | call lsc#server#userCall('workspace/symbol', {'query': l:query}, 8 | \ function('setQuickFixSymbols')) 9 | endfunction 10 | 11 | function! s:setQuickFixSymbols(results) abort 12 | if type(a:results) != type([]) || len(a:results) == 0 13 | call lsc#message#show('No symbols found') 14 | return 15 | endif 16 | 17 | call map(a:results, {_, symbol -> lsc#convert#quickFixSymbol(symbol)}) 18 | call sort(a:results, 'lsc#util#compareQuickFixItems') 19 | call setqflist(a:results) 20 | copen 21 | endfunction 22 | -------------------------------------------------------------------------------- /autoload/lsc/server.vim: -------------------------------------------------------------------------------- 1 | if !exists('s:initialized') 2 | " server name -> server info. 3 | " 4 | " Server name defaults to the command string. 5 | " 6 | " Info contains: 7 | " - status. Possible statuses are: 8 | " [disabled, not started, 9 | " starting, running, restarting, 10 | " exiting, exited, unexpected exit, failed] 11 | " - capabilities. Configuration for client/server interaction. 12 | " - filetypes. List of filetypes handled by this server. 13 | " - logs. The last 100 logs from `window/logMessage`. 14 | " - config. Config dict. Contains: 15 | " - name: Same as the key into `s:servers` 16 | " - command: Executable 17 | " - enabled: (optional) Whether the server should be started. 18 | " - message_hooks: (optional) Functions call to override params 19 | " - workspace_config: (optional) Arbitrary data to send as 20 | " `workspace/didChangeConfiguration` settings on startup. 21 | let s:servers = {} 22 | let s:initialized = v:true 23 | endif 24 | 25 | function! lsc#server#start(server) abort 26 | call s:Start(a:server) 27 | endfunction 28 | 29 | function! lsc#server#status(filetype) abort 30 | return s:servers[g:lsc_servers_by_filetype[a:filetype]].status 31 | endfunction 32 | 33 | function! lsc#server#servers() abort 34 | return s:servers 35 | endfunction 36 | 37 | " Returns a list of the servers for the current filetype. 38 | " 39 | " For now there will only ever be 1 or no values in this list. 40 | function! lsc#server#current() abort 41 | return lsc#server#forFileType(&filetype) 42 | endfunction 43 | 44 | " Returns a list of the servers for [filetype]. 45 | " 46 | " For now there will only ever be 1 or no values in this list. 47 | function! lsc#server#forFileType(filetype) abort 48 | if !has_key(g:lsc_servers_by_filetype, a:filetype) | return [] | endif 49 | return [s:servers[g:lsc_servers_by_filetype[a:filetype]]] 50 | endfunction 51 | 52 | " Wait for all running servers to shut down with a 5 second timeout. 53 | function! lsc#server#exit() abort 54 | let l:exit_start = reltime() 55 | let l:pending = [] 56 | for l:server in values(s:servers) 57 | if s:Kill(l:server, 'exiting', 58 | \ funcref('OnExit', [l:server.config.name, l:pending])) 59 | call add(l:pending, l:server.config.name) 60 | endif 61 | endfor 62 | let l:reported = [] 63 | while len(l:pending) > 0 && reltimefloat(reltime(l:exit_start)) <= 5.0 64 | if reltimefloat(reltime(l:exit_start)) >= 1.0 && l:pending != l:reported 65 | echo 'Waiting for language server exit: '.join(l:pending, ', ') 66 | let l:reported = copy(l:pending) 67 | endif 68 | sleep 100m 69 | endwhile 70 | return len(l:pending) == 0 71 | endfunction 72 | 73 | function! s:OnExit(server_name, pending) abort 74 | call remove(a:pending, index(a:pending, a:server_name)) 75 | endfunction 76 | 77 | " Request a 'shutdown' then 'exit'. 78 | " 79 | " Calls `OnExit` after the exit is requested. Returns `v:false` if no request 80 | " was made because the server is not currently running. 81 | function! s:Kill(server, status, OnExit) abort 82 | return a:server.request('shutdown', v:null, 83 | \ funcref('HandleShutdownResponse', [a:server, a:status, a:OnExit]), 84 | \ {'sync': v:true}) 85 | endfunction 86 | 87 | function! s:HandleShutdownResponse(server, status, OnExit, result) abort 88 | let a:server.status = a:status 89 | if has_key(a:server, '_channel') 90 | " An early exit still could have remove the channel. 91 | " The status has been updated so `a:server.notify` would bail 92 | call a:server._channel.notify('exit', v:null) 93 | endif 94 | if a:OnExit != v:null | call a:OnExit() | endif 95 | endfunction 96 | 97 | function! lsc#server#restart() abort 98 | let l:server_name = g:lsc_servers_by_filetype[&filetype] 99 | let l:server = s:servers[l:server_name] 100 | let l:old_status = l:server.status 101 | if l:old_status ==# 'starting' || l:old_status ==# 'running' 102 | call s:Kill(l:server, 'restarting', v:null) 103 | else 104 | call s:Start(l:server) 105 | endif 106 | endfunction 107 | 108 | " A server call explicitly initiated by the user for the current buffer. 109 | " 110 | " Expects the call to succeed and shows an error if it does not. 111 | function! lsc#server#userCall(method, params, callback) abort 112 | " TODO handle multiple servers 113 | let l:server = lsc#server#forFileType(&filetype)[0] 114 | let l:result = l:server.request(a:method, a:params, a:callback) 115 | if !l:result 116 | call lsc#message#error('Failed to call '.a:method) 117 | call lsc#message#error('Server status: '.lsc#server#status(&filetype)) 118 | endif 119 | endfunction 120 | 121 | " Start `server` if it isn't already running. 122 | function! s:Start(server) abort 123 | if has_key(a:server, '_channel') 124 | " Server is already running 125 | return 126 | endif 127 | let l:command = a:server.config.command 128 | let a:server.status = 'starting' 129 | let a:server._channel = lsc#protocol#open(l:command, 130 | \ function('Dispatch', [a:server]), 131 | \ a:server.on_err, a:server.on_exit) 132 | if type(a:server._channel) == type(v:null) 133 | let a:server.status = 'failed' 134 | return 135 | endif 136 | if exists('g:lsc_trace_level') && 137 | \ index(['off', 'messages', 'verbose'], g:lsc_trace_level) >= 0 138 | let l:trace_level = g:lsc_trace_level 139 | else 140 | let l:trace_level = 'off' 141 | endif 142 | let l:params = {'processId': getpid(), 143 | \ 'clientInfo': {'name': 'vim-lsc'}, 144 | \ 'rootUri': lsc#uri#documentUri(lsc#file#cwd()), 145 | \ 'capabilities': s:ClientCapabilities(), 146 | \ 'trace': l:trace_level 147 | \} 148 | call a:server._initialize(l:params, funcref('OnInitialize', [a:server])) 149 | endfunction 150 | 151 | function! s:OnInitialize(server, init_result) abort 152 | let a:server.status = 'running' 153 | call a:server.notify('initialized', {}) 154 | if type(a:init_result) == type({}) && has_key(a:init_result, 'capabilities') 155 | let a:server.capabilities = 156 | \ lsc#capabilities#normalize(a:init_result.capabilities) 157 | endif 158 | if has_key(a:server.config, 'workspace_config') 159 | call a:server.notify('workspace/didChangeConfiguration', { 160 | \ 'settings': a:server.config.workspace_config 161 | \}) 162 | endif 163 | call lsc#file#trackAll(a:server) 164 | endfunction 165 | 166 | " Missing value means no support 167 | function! s:ClientCapabilities() abort 168 | let l:applyEdit = v:false 169 | if !exists('g:lsc_enable_apply_edit') || g:lsc_enable_apply_edit 170 | let l:applyEdit = v:true 171 | endif 172 | return { 173 | \ 'workspace': { 174 | \ 'applyEdit': l:applyEdit, 175 | \ 'configuration': v:true, 176 | \ }, 177 | \ 'textDocument': { 178 | \ 'synchronization': { 179 | \ 'willSave': v:false, 180 | \ 'willSaveWaitUntil': v:false, 181 | \ 'didSave': v:false, 182 | \ }, 183 | \ 'completion': { 184 | \ 'completionItem': { 185 | \ 'snippetSupport': g:lsc_enable_snippet_support, 186 | \ 'deprecatedSupport': v:true, 187 | \ 'tagSupport': { 188 | \ 'valueSet': [1], 189 | \ }, 190 | \ }, 191 | \ }, 192 | \ 'definition': {'dynamicRegistration': v:false}, 193 | \ 'codeAction': { 194 | \ 'codeActionLiteralSupport': { 195 | \ 'codeActionKind': {'valueSet': ['quickfix', 'refactor', 'source']} 196 | \ } 197 | \ }, 198 | \ 'hover': {'contentFormat': ['plaintext', 'markdown']}, 199 | \ 'signatureHelp': {'dynamicRegistration': v:false}, 200 | \ 'publishDiagnostics': { 201 | \ 'relatedInformation': v:false, 202 | \ 'versionSupport': v:false, 203 | \ 'codeDescriptionSupport': v:false, 204 | \ 'dataSupport': v:false, 205 | \ }, 206 | \ } 207 | \} 208 | endfunction 209 | 210 | function! lsc#server#filetypeActive(filetype) abort 211 | let l:server = s:servers[g:lsc_servers_by_filetype[a:filetype]] 212 | return get(l:server.config, 'enabled', v:true) 213 | endfunction 214 | 215 | function! lsc#server#disable() abort 216 | if !has_key(g:lsc_servers_by_filetype, &filetype) 217 | return v:false 218 | endif 219 | let l:server = s:servers[g:lsc_servers_by_filetype[&filetype]] 220 | let l:server.config.enabled = v:false 221 | call s:Kill(l:server, 'disabled', v:null) 222 | endfunction 223 | 224 | function! lsc#server#enable() abort 225 | if !has_key(g:lsc_servers_by_filetype, &filetype) 226 | return v:false 227 | endif 228 | let l:server = s:servers[g:lsc_servers_by_filetype[&filetype]] 229 | let l:server.config.enabled = v:true 230 | call s:Start(l:server) 231 | endfunction 232 | 233 | function! lsc#server#register(filetype, config) abort 234 | let l:languageId = a:filetype 235 | if type(a:config) == type('') 236 | let l:config = {'command': a:config, 'name': a:config} 237 | elseif type(a:config) == type([]) 238 | let l:config = {'command': a:config, 'name': string(a:config)} 239 | else 240 | if type(a:config) != type({}) 241 | throw 'Server configuration must be an executable or a dict' 242 | endif 243 | let l:config = a:config 244 | if !has_key(l:config, 'command') 245 | throw 'Server configuration must have a "command" key' 246 | endif 247 | if !has_key(l:config, 'name') 248 | let l:config.name = string(l:config.command) 249 | endif 250 | if has_key(l:config, 'languageId') 251 | let l:languageId = l:config.languageId 252 | endif 253 | endif 254 | let g:lsc_servers_by_filetype[a:filetype] = l:config.name 255 | if has_key(s:servers, l:config.name) 256 | let l:server = s:servers[l:config.name] 257 | call add(l:server.filetypes, a:filetype) 258 | let l:server.languageId[a:filetype] = l:languageId 259 | return l:server 260 | endif 261 | let l:initial_status = 'not started' 262 | if !get(l:config, 'enabled', v:true) 263 | let l:initial_status = 'disabled' 264 | endif 265 | let l:server = { 266 | \ 'status': l:initial_status, 267 | \ 'logs': [], 268 | \ 'filetypes': [a:filetype], 269 | \ 'languageId': {}, 270 | \ 'config': l:config, 271 | \ 'capabilities': lsc#capabilities#defaults() 272 | \} 273 | let l:server.languageId[a:filetype] = l:languageId 274 | function! l:server.request(method, params, callback, ...) abort 275 | if l:self.status !=# 'running' | return v:false | endif 276 | let l:params = lsc#config#messageHook(l:self, a:method, a:params) 277 | if l:params is lsc#config#skip() | return v:false | endif 278 | let l:Callback = lsc#config#responseHook(l:self, a:method, a:callback) 279 | let l:options = a:0 > 0 ? a:1 : {} 280 | call l:self._channel.request(a:method, l:params, l:Callback, l:options) 281 | return v:true 282 | endfunction 283 | function! l:server.notify(method, params) abort 284 | if l:self.status !=# 'running' | return v:false | endif 285 | let l:params = lsc#config#messageHook(l:self, a:method, a:params) 286 | if l:params is lsc#config#skip() | return v:false | endif 287 | call l:self._channel.notify(a:method, l:params) 288 | return v:true 289 | endfunction 290 | function! l:server.respond(id, result) abort 291 | call l:self._channel.respond(a:id, a:result) 292 | endfunction 293 | function! l:server._initialize(params, callback) abort 294 | let l:params = lsc#config#messageHook(l:self, 'initialize', a:params) 295 | call l:self._channel.request('initialize', l:params, a:callback, {}) 296 | endfunction 297 | function! l:server.on_err(message) abort 298 | if get(l:self.config, 'suppress_stderr', v:false) | return | endif 299 | call lsc#message#error('StdErr from '.l:self.config.name.': '.a:message) 300 | endfunction 301 | function! l:server.on_exit() abort 302 | unlet l:self._channel 303 | let l:old_status = l:self.status 304 | if l:old_status ==# 'starting' 305 | let l:self.status= 'failed' 306 | let l:message = 'Failed to initialize server "'.l:self.config.name.'".' 307 | if l:self.config.name != string(l:self.config.command) 308 | let l:message .= ' Failing command is: '.string(l:self.config.command) 309 | endif 310 | call lsc#message#error(l:message) 311 | elseif l:old_status ==# 'exiting' 312 | let l:self.status= 'exited' 313 | elseif l:old_status ==# 'running' 314 | let l:self.status = 'unexpected exit' 315 | call lsc#message#error('Command exited unexpectedly: '.l:self.config.name) 316 | endif 317 | for l:filetype in l:self.filetypes 318 | call lsc#complete#clean(l:filetype) 319 | call lsc#diagnostics#clean(l:filetype) 320 | call lsc#file#clean(l:filetype) 321 | call lsc#cursor#clean() 322 | endfor 323 | if l:old_status ==# 'restarting' 324 | call s:Start(l:self) 325 | endif 326 | endfunction 327 | function! l:server.find_config(item) abort 328 | if !has_key(l:self.config, 'workspace_config') | return v:null | endif 329 | if !has_key(a:item, 'section') || empty(a:item.section) 330 | return l:self.config.workspace_config 331 | endif 332 | let l:config = l:self.config.workspace_config 333 | for l:part in split(a:item.section, '\.') 334 | if !has_key(l:config, l:part) 335 | return v:null 336 | else 337 | let l:config = l:config[l:part] 338 | endif 339 | endfor 340 | return l:config 341 | endfunction 342 | let s:servers[l:config.name] = l:server 343 | return l:server 344 | endfunction 345 | 346 | function! s:Dispatch(server, method, params, id) abort 347 | if a:method ==? 'textDocument/publishDiagnostics' 348 | let l:file_path = lsc#uri#documentPath(a:params['uri']) 349 | call lsc#diagnostics#setForFile(l:file_path, a:params['diagnostics']) 350 | elseif a:method ==? 'window/showMessage' 351 | call lsc#message#show(a:params['message'], a:params['type']) 352 | elseif a:method ==? 'window/showMessageRequest' 353 | let l:response = 354 | \ lsc#message#showRequest(a:params['message'], a:params['actions']) 355 | call a:server.respond(a:id, l:response) 356 | elseif a:method ==? 'window/logMessage' 357 | if lsc#config#shouldEcho(a:server, a:params.type) 358 | call lsc#message#log(a:params.message, a:params.type) 359 | endif 360 | call lsc#util#shift(a:server.logs, 100, 361 | \ {'message': a:params.message, 'type': a:params.type}) 362 | elseif a:method ==? 'window/progress' 363 | if has_key(a:params, 'message') 364 | let l:full = a:params['title'] . a:params['message'] 365 | call lsc#message#show('Progress ' . l:full) 366 | elseif has_key(a:params, 'done') 367 | call lsc#message#show('Finished ' . a:params['title']) 368 | else 369 | call lsc#message#show('Starting ' . a:params['title']) 370 | endif 371 | elseif a:method ==? 'workspace/applyEdit' 372 | let l:applied = lsc#edit#apply(a:params.edit) 373 | let l:response = {'applied': l:applied} 374 | call a:server.respond(a:id, l:response) 375 | elseif a:method ==? 'workspace/configuration' 376 | let l:items = a:params.items 377 | let l:response = map(l:items, {_, item -> a:server.find_config(item)}) 378 | call a:server.respond(a:id, l:response) 379 | elseif a:method =~? '\v^\$' 380 | call lsc#config#handleNotification(a:server, a:method, a:params) 381 | endif 382 | endfunction 383 | -------------------------------------------------------------------------------- /autoload/lsc/signaturehelp.vim: -------------------------------------------------------------------------------- 1 | if !exists('s:initialized') 2 | let s:current_parameter = '' 3 | let s:initialized = v:true 4 | endif 5 | 6 | function! lsc#signaturehelp#getSignatureHelp() abort 7 | call lsc#file#flushChanges() 8 | let l:params = lsc#params#documentPosition() 9 | " TODO handle multiple servers 10 | let l:server = lsc#server#forFileType(&filetype)[0] 11 | call l:server.request('textDocument/signatureHelp', l:params, 12 | \ lsc#util#gateResult('SignatureHelp', function('ShowHelp'))) 13 | endfunction 14 | 15 | function! s:HighlightCurrentParameter() abort 16 | execute 'match lscCurrentParameter /\V' . s:current_parameter . '/' 17 | endfunction 18 | 19 | function! s:ShowHelp(signatureHelp) abort 20 | if empty(a:signatureHelp) 21 | call lsc#message#show('No signature help available') 22 | return 23 | endif 24 | let l:signatures = [] 25 | if has_key(a:signatureHelp, 'signatures') 26 | if type(a:signatureHelp.signatures) == type([]) 27 | let l:signatures = a:signatureHelp.signatures 28 | endif 29 | endif 30 | 31 | if len(l:signatures) == 0 32 | return 33 | endif 34 | 35 | let l:active_signature = 0 36 | if has_key(a:signatureHelp, 'activeSignature') 37 | let l:active_signature = a:signatureHelp.activeSignature 38 | if l:active_signature >= len(l:signatures) 39 | let l:active_signature = 0 40 | endif 41 | endif 42 | 43 | let l:signature = get(l:signatures, l:active_signature) 44 | 45 | if !has_key(l:signature, 'label') 46 | return 47 | endif 48 | 49 | if !has_key(l:signature, 'parameters') 50 | call lsc#util#displayAsPreview([l:signature.label], &filetype, 51 | \ function('HighlightCurrentParameter')) 52 | return 53 | endif 54 | 55 | if has_key(a:signatureHelp, 'activeParameter') 56 | let l:active_parameter = a:signatureHelp.activeParameter 57 | if l:active_parameter < len(l:signature.parameters) 58 | \ && has_key(l:signature.parameters[l:active_parameter], 'label') 59 | let s:current_parameter = l:signature.parameters[l:active_parameter].label 60 | endif 61 | endif 62 | 63 | call lsc#util#displayAsPreview([l:signature.label], &filetype, 64 | \ function('HighlightCurrentParameter')) 65 | 66 | endfunction 67 | -------------------------------------------------------------------------------- /autoload/lsc/uri.vim: -------------------------------------------------------------------------------- 1 | function! lsc#uri#documentUri(...) abort 2 | if a:0 >= 1 3 | let l:file_path = a:1 4 | else 5 | let l:file_path = lsc#file#fullPath() 6 | endif 7 | return s:filePrefix().s:EncodePath(l:file_path) 8 | endfunction 9 | 10 | function! lsc#uri#documentPath(uri) abort 11 | return s:DecodePath(substitute(a:uri, '^'.s:filePrefix(), '', 'v')) 12 | endfunction 13 | 14 | function! s:EncodePath(value) abort 15 | " shamelessly taken from Mr. T. Pope and adapted: 16 | " (https://github.com/tpope/vim-unimpaired/blob/master/plugin/unimpaired.vim#L461) 17 | " This follows the VIM License over at https://github.com/vim/vim/blob/master/LICENSE 18 | return substitute(iconv(a:value, 'latin-1', 'utf-8'), 19 | \ '[^a-zA-Z0-9-_.~/]', 20 | \ '\=s:EncodeChar(submatch(0))', 'g') 21 | endfunction 22 | 23 | function! s:EncodeChar(char) abort 24 | let l:charcode = char2nr(a:char) 25 | return printf('%%%02x', l:charcode) 26 | endfunction 27 | 28 | function! s:DecodePath(value) abort 29 | " shamelessly taken from Mr. T. Pope and adapted: 30 | " (https://github.com/tpope/vim-unimpaired/blob/master/plugin/unimpaired.vim#L466) 31 | " This follows the VIM License over at https://github.com/vim/vim/blob/master/LICENSE 32 | return iconv( 33 | \ substitute( 34 | \ a:value, 35 | \ '%\(\x\x\)', 36 | \ '\=nr2char("0x".submatch(1))','g'), 37 | \ 'utf-8', 38 | \ 'latin1') 39 | endfunction 40 | 41 | function! s:filePrefix(...) abort 42 | if has('win32') 43 | return 'file:///' 44 | else 45 | return 'file://' 46 | endif 47 | endfunction 48 | -------------------------------------------------------------------------------- /autoload/lsc/util.vim: -------------------------------------------------------------------------------- 1 | if !exists('s:initialized') 2 | let s:callback_gates = {} 3 | let s:au_group_id = 0 4 | let s:initialized = v:true 5 | endif 6 | 7 | " Run `command` in all windows, keeping old open window. 8 | function! lsc#util#winDo(command) abort 9 | let l:current_window = winnr() 10 | execute 'keepjumps noautocmd windo '.a:command 11 | execute 'keepjumps noautocmd '.l:current_window.'wincmd w' 12 | endfunction 13 | 14 | " Compare two quickfix or location list items. 15 | " 16 | " Items are compared with priority order: 17 | " filename > line > column > type (severity) > text 18 | " 19 | " filenames within cwd are always considered to have a lower sort than others. 20 | function! lsc#util#compareQuickFixItems(i1, i2) abort 21 | let l:file_1 = s:QuickFixFilename(a:i1) 22 | let l:file_2 = s:QuickFixFilename(a:i2) 23 | if l:file_1 != l:file_2 24 | return lsc#file#compare(l:file_1, l:file_2) 25 | endif 26 | if a:i1.lnum != a:i2.lnum | return a:i1.lnum - a:i2.lnum | endif 27 | if a:i1.col != a:i2.col | return a:i1.col - a:i2.col | endif 28 | if has_key(a:i1, 'type') && has_key(a:i2, 'type') && a:i1.type != a:i2.type 29 | " Reverse order so high severity is ordered first 30 | return s:QuickFixSeverity(a:i2.type) - s:QuickFixSeverity(a:i1.type) 31 | endif 32 | return a:i1.text == a:i2.text ? 0 : a:i1.text > a:i2.text ? 1 : -1 33 | endfunction 34 | 35 | function! s:QuickFixSeverity(type) abort 36 | if a:type ==# 'E' | return 1 37 | elseif a:type ==# 'W' | return 2 38 | elseif a:type ==# 'I' | return 3 39 | elseif a:type ==# 'H' | return 4 40 | else | return 5 41 | endif 42 | endfunction 43 | 44 | function! s:QuickFixFilename(item) abort 45 | if has_key(a:item, 'filename') 46 | return a:item.filename 47 | endif 48 | return lsc#file#normalize(bufname(a:item.bufnr)) 49 | endfunction 50 | 51 | " Populate a buffer with [lines] and show it as a preview window. 52 | " 53 | " If the __lsc_preview__ buffer was already showing, reuse it's window, 54 | " otherwise split a window with a max height of `&previewheight`. 55 | " After the content of the content of the preview window is set, 56 | " `function` is called (the buffer is still the preview). 57 | function! lsc#util#displayAsPreview(lines, filetype, function) abort 58 | let l:view = winsaveview() 59 | let l:alternate=@# 60 | call s:createOrJumpToPreview(s:countDisplayLines(a:lines, &previewheight)) 61 | setlocal modifiable 62 | setlocal noreadonly 63 | %d 64 | call setline(1, a:lines) 65 | let &filetype = a:filetype 66 | call a:function() 67 | setlocal nomodifiable 68 | setlocal readonly 69 | wincmd p 70 | call winrestview(l:view) 71 | let @#=l:alternate 72 | endfunction 73 | 74 | " Approximates the number of lines it will take to display some text assuming an 75 | " 80 character line wrap. Only counts up to `max`. 76 | function! s:countDisplayLines(lines, max) abort 77 | let l:count = 0 78 | for l:line in a:lines 79 | if len(l:line) <= 80 80 | let l:count += 1 81 | else 82 | let l:count += float2nr(ceil(len(l:line) / 80.0)) 83 | endif 84 | if l:count > a:max | return a:max | endif 85 | endfor 86 | return l:count 87 | endfunction 88 | 89 | function! s:createOrJumpToPreview(want_height) abort 90 | let l:windows = range(1, winnr('$')) 91 | call filter(l:windows, {_, win -> getwinvar(win, "&previewwindow") == 1}) 92 | if len(l:windows) > 0 93 | execute string(l:windows[0]).' wincmd W' 94 | edit __lsc_preview__ 95 | if winheight(l:windows[0]) < a:want_height 96 | execute 'resize '.a:want_height 97 | endif 98 | else 99 | if exists('g:lsc_preview_split_direction') 100 | let l:direction = g:lsc_preview_split_direction 101 | else 102 | let l:direction = '' 103 | endif 104 | execute l:direction.' '.string(a:want_height).'split __lsc_preview__' 105 | if exists('#User#LSCShowPreview') 106 | doautocmd User LSCShowPreview 107 | endif 108 | endif 109 | set previewwindow 110 | set winfixheight 111 | setlocal bufhidden=hide 112 | setlocal nobuflisted 113 | setlocal buftype=nofile 114 | setlocal noswapfile 115 | endfunction 116 | 117 | " Adds [value] to the [list] and removes the earliest entry if it would make the 118 | " list longer than [max_length] 119 | function! lsc#util#shift(list, max_length, value) abort 120 | call add(a:list, a:value) 121 | if len(a:list) > a:max_length | call remove(a:list, 0) | endif 122 | endfunction 123 | 124 | function! lsc#util#gateResult(name, callback, ...) abort 125 | if !has_key(s:callback_gates, a:name) 126 | let s:callback_gates[a:name] = 0 127 | else 128 | let s:callback_gates[a:name] += 1 129 | endif 130 | let l:gate = s:callback_gates[a:name] 131 | let l:old_pos = getcurpos() 132 | if a:0 >= 1 && type(a:1) == type({_->_}) 133 | let l:OnSkip = a:1 134 | else 135 | let l:OnSkip = v:false 136 | endif 137 | return function('Gated', 138 | \ [a:name, l:gate, l:old_pos, a:callback, l:OnSkip]) 139 | endfunction 140 | 141 | function! s:Gated(name, gate, old_pos, on_call, on_skip, ...) abort 142 | if s:callback_gates[a:name] != a:gate || 143 | \ a:old_pos != getcurpos() 144 | if type(a:on_skip) == type({_->_}) 145 | call call(a:on_skip, a:000) 146 | endif 147 | else 148 | call call(a:on_call, a:000) 149 | endif 150 | endfunction 151 | 152 | function! lsc#util#noop() abort 153 | endfunction 154 | 155 | function! lsc#util#async(name, Callback) abort 156 | return funcref('Async', [a:name, a:Callback]) 157 | endfunction 158 | 159 | function! s:Async(name, Callback, ...) abort 160 | call timer_start(0, funcref('IgnoreArgs', [a:name, a:Callback, a:000])) 161 | endfunction 162 | 163 | function! s:IgnoreArgs(name, Callback, args, ...) abort 164 | try 165 | call call(a:Callback, a:args) 166 | catch 167 | call lsc#message#error('Error from '.a:name.': '.string(v:exception)) 168 | let g:lsc_last_error = v:exception 169 | let g:lsc_last_throwpoint = v:throwpoint 170 | let g:lsc_last_error_callback = [a:Callback, a:args] 171 | endtry 172 | endfunction 173 | -------------------------------------------------------------------------------- /plugin/lsc.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_lsc') 2 | finish 3 | endif 4 | let g:loaded_lsc = 1 5 | let g:_lsc_is_exiting = v:false 6 | 7 | if !exists('g:lsc_servers_by_filetype') 8 | " filetype -> server name 9 | let g:lsc_servers_by_filetype = {} 10 | endif 11 | if !exists('g:lsc_enable_autocomplete') 12 | let g:lsc_enable_autocomplete = v:true 13 | endif 14 | if !exists('g:lsc_auto_completeopt') 15 | let g:lsc_auto_completeopt = v:true 16 | endif 17 | if !exists('g:lsc_enable_snippet_support') 18 | let g:lsc_enable_snippet_support = v:false 19 | endif 20 | if !exists('g:lsc_enable_popup_syntax') 21 | let g:lsc_enable_popup_syntax = v:true 22 | endif 23 | 24 | command! LSClientGoToDefinitionSplit 25 | \ call lsc#reference#goToDefinition(, 1) 26 | command! LSClientGoToDefinition 27 | \ call lsc#reference#goToDefinition(, 0) 28 | command! LSClientFindReferences call lsc#reference#findReferences() 29 | command! LSClientNextReference call lsc#reference#findNext(1) 30 | command! LSClientPreviousReference call lsc#reference#findNext(-1) 31 | command! LSClientFindImplementations call lsc#reference#findImplementations() 32 | command! -nargs=? LSClientShowHover call lsc#reference#hover() 33 | command! LSClientDocumentSymbol call lsc#reference#documentSymbols() 34 | command! -nargs=? LSClientWorkspaceSymbol 35 | \ call lsc#search#workspaceSymbol() 36 | command! -nargs=? LSClientFindCodeActions 37 | \ call lsc#edit#findCodeActions(lsc#edit#filterActions()) 38 | command! LSClientAllDiagnostics call lsc#diagnostics#showInQuickFix() 39 | command! LSClientWindowDiagnostics call lsc#diagnostics#showLocationList() 40 | command! LSClientLineDiagnostics call lsc#diagnostics#echoForLine() 41 | command! LSClientSignatureHelp call lsc#signaturehelp#getSignatureHelp() 42 | command! LSClientRestartServer call IfEnabled('lsc#server#restart') 43 | command! LSClientDisable call lsc#server#disable() 44 | command! LSClientEnable call lsc#server#enable() 45 | command! LSClientDisableDiagnosticHighlights call DisableHighlights() 46 | command! LSClientEnableDiagnosticHighlights call EnableHighlights() 47 | 48 | if !exists('g:lsc_enable_apply_edit') || g:lsc_enable_apply_edit 49 | command! -nargs=? LSClientRename call lsc#edit#rename() 50 | endif 51 | 52 | 53 | " Returns the status of the language server for the current filetype or empty 54 | " string if it is not configured. 55 | function! LSCServerStatus() abort 56 | if !has_key(g:lsc_servers_by_filetype, &filetype) | return '' | endif 57 | return lsc#server#status(&filetype) 58 | endfunction 59 | 60 | " RegisterLanguageServer 61 | " 62 | " Registers a command as the server to start the first time a file with type 63 | " filetype is seen. As long as the server is running it won't be restarted on 64 | " subsequent appearances of this file type. If the server exits it will be 65 | " restarted the next time a window or tab is entered with this file type. 66 | function! RegisterLanguageServer(filetype, config) abort 67 | let l:server = lsc#server#register(a:filetype, a:config) 68 | if !get(l:server.config, 'enabled', v:true) | return | endif 69 | let l:buffers = s:BuffersOfType(a:filetype) 70 | if empty(l:buffers) | return | endif 71 | if l:server.status ==# 'running' 72 | for l:buffer in l:buffers 73 | call lsc#file#track(l:server, l:buffer, a:filetype) 74 | endfor 75 | else 76 | call lsc#server#start(l:server) 77 | endif 78 | endfunction 79 | 80 | function! s:BuffersOfType(filetype) abort 81 | let l:buffers = [] 82 | for l:buffer in getbufinfo({'bufloaded': v:true}) 83 | if getbufvar(l:buffer.bufnr, '&filetype') == a:filetype && 84 | \ getbufvar(l:buffer.bufnr, '&modifiable') && 85 | \ l:buffer.name !~# '\v^fugitive:///' 86 | call add(l:buffers, l:buffer) 87 | endif 88 | endfor 89 | return l:buffers 90 | endfunction 91 | 92 | function! s:DisableHighlights() abort 93 | let g:lsc_enable_highlights = v:false 94 | call lsc#util#winDo('call lsc#highlights#clear()') 95 | endfunction 96 | 97 | function! s:EnableHighlights() abort 98 | let g:lsc_enable_highlights = v:true 99 | call lsc#util#winDo('call lsc#highlights#update()') 100 | endfunction 101 | 102 | augroup LSC 103 | autocmd! 104 | " Some state which is logically owned by a buffer is attached to the window in 105 | " practice and needs to be manage manually: 106 | " 107 | " 1. Diagnostic highlights 108 | " 2. Diagnostic location list 109 | " 110 | " The `BufEnter` event indicates most times when the buffer <-> window 111 | " relationship can change. There are some exceptions where this event is not 112 | " fired such as `:split` and `:lopen` so `WinEnter` is used as a fallback with 113 | " a block to ensure it only happens once. 114 | autocmd BufEnter * call LSCEnsureCurrentWindowState() 115 | autocmd WinEnter * call timer_start(1, function('OnWinEnter')) 116 | 117 | " Window local state is only correctly maintained for the current tab. 118 | autocmd TabEnter * call lsc#util#winDo('call LSCEnsureCurrentWindowState()') 119 | 120 | autocmd BufNewFile,BufReadPost * call OnOpen() 121 | autocmd TextChanged,TextChangedI,CompleteDone * 122 | \ call IfEnabled('lsc#file#onChange') 123 | autocmd BufLeave * call IfEnabled('lsc#file#flushChanges') 124 | autocmd BufUnload * call OnClose() 125 | autocmd BufWritePost * call OnWrite() 126 | 127 | autocmd CursorMoved * call IfEnabled('lsc#cursor#onMove') 128 | autocmd WinEnter * call IfEnabled('lsc#cursor#onWinEnter') 129 | autocmd WinLeave,InsertEnter * call IfEnabled('lsc#cursor#clean') 130 | autocmd User LSCOnChangesFlushed 131 | \ call IfEnabled('lsc#cursor#onChangesFlushed') 132 | 133 | autocmd TextChangedI * call IfEnabled('lsc#complete#textChanged') 134 | autocmd InsertCharPre * call IfEnabled('lsc#complete#insertCharPre') 135 | 136 | autocmd VimLeave * call lsc#server#exit() 137 | if exists('##ExitPre') 138 | autocmd ExitPre * let g:_lsc_is_exiting = v:true 139 | endif 140 | augroup END 141 | 142 | " Set window local state only if this is a brand new window which has not 143 | " already been initialized for LSC. 144 | " 145 | " This function must be called on a delay since critical values like 146 | " `expand('%')` and `&filetype` are not correctly set when the event fires. The 147 | " delay means that in the cases where `BufWinEnter` actually runs this will run 148 | " later and do nothing. 149 | function! s:OnWinEnter(timer) abort 150 | if exists('w:lsc_window_initialized') 151 | return 152 | endif 153 | call LSCEnsureCurrentWindowState() 154 | endfunction 155 | 156 | " Update or clear state local to the current window. 157 | function! LSCEnsureCurrentWindowState() abort 158 | let w:lsc_window_initialized = v:true 159 | if !has_key(g:lsc_servers_by_filetype, &filetype) 160 | if exists('w:lsc_diagnostic_matches') 161 | call lsc#highlights#clear() 162 | endif 163 | if exists('w:lsc_diagnostics') 164 | call lsc#diagnostics#clear() 165 | endif 166 | if exists('w:lsc_reference_matches') 167 | call lsc#cursor#clean() 168 | endif 169 | return 170 | endif 171 | call lsc#diagnostics#updateCurrentWindow() 172 | call lsc#highlights#update() 173 | call lsc#cursor#onWinEnter() 174 | endfunction 175 | 176 | " Run `function` if LSC is enabled for the current filetype. 177 | " 178 | " This should only be used for the autocommands which are known to only fire for 179 | " the current buffer where '&filetype' can be trusted. 180 | function! s:IfEnabled(function, ...) abort 181 | if !has_key(g:lsc_servers_by_filetype, &filetype) | return | endif 182 | if !&modifiable | return | endif 183 | if !lsc#server#filetypeActive(&filetype) | return | endif 184 | call call(a:function, a:000) 185 | endfunction 186 | 187 | function! s:OnOpen() abort 188 | if !has_key(g:lsc_servers_by_filetype, &filetype) | return | endif 189 | if expand('%') =~# '\vfugitive:///' | return | endif 190 | call lsc#config#mapKeys() 191 | if !&modifiable | return | endif 192 | if !lsc#server#filetypeActive(&filetype) | return | endif 193 | call lsc#file#onOpen() 194 | endfunction 195 | 196 | function! s:OnClose() abort 197 | if g:_lsc_is_exiting | return | endif 198 | let l:filetype = getbufvar(str2nr(expand('')), '&filetype') 199 | if !has_key(g:lsc_servers_by_filetype, l:filetype) | return | endif 200 | let l:full_path = lsc#file#normalize(expand(':p')) 201 | call lsc#file#onClose(l:full_path, l:filetype) 202 | endfunction 203 | 204 | function! s:OnWrite() abort 205 | let l:filetype = getbufvar(str2nr(expand('')), '&filetype') 206 | if !has_key(g:lsc_servers_by_filetype, l:filetype) | return | endif 207 | if !lsc#server#filetypeActive(l:filetype) | return | endif 208 | let l:full_path = expand(':p') 209 | call lsc#file#onWrite(l:full_path, l:filetype) 210 | endfunction 211 | 212 | " Highlight groups {{{2 213 | if !hlexists('lscDiagnosticError') 214 | highlight link lscDiagnosticError Error 215 | endif 216 | if !hlexists('lscDiagnosticWarning') 217 | highlight link lscDiagnosticWarning SpellBad 218 | endif 219 | if !hlexists('lscDiagnosticInfo') 220 | highlight link lscDiagnosticInfo SpellCap 221 | endif 222 | if !hlexists('lscDiagnosticHint') 223 | highlight link lscDiagnosticHint SpellCap 224 | endif 225 | if !hlexists('lscReference') 226 | highlight link lscReference CursorColumn 227 | endif 228 | if !hlexists('lscCurrentParameter') 229 | highlight link lscCurrentParameter CursorColumn 230 | endif 231 | -------------------------------------------------------------------------------- /test/diff_test.vim: -------------------------------------------------------------------------------- 1 | scriptencoding utf-8 2 | 3 | function! TestDiff() abort 4 | " First and last lines swapped 5 | call s:TestDiff( 6 | \ [0,0,2,3], 11, "baz\nb╵r\nfoo", 7 | \ "foo\nb╵r\nbaz", 8 | \ "baz\nb╵r\nfoo" 9 | \ ) 10 | 11 | " First line changed 12 | call s:TestDiff( 13 | \ [0,1,0,2], 1, 'n', 14 | \ "foo\nbar\nbaz", 15 | \ "fno\nbar\nbaz" 16 | \ ) 17 | 18 | " Middle line changed 19 | call s:TestDiff( 20 | \ [1,0,1,3], 3, 'new', 21 | \ "foo\nb╵r\nbaz", 22 | \ "foo\nnew\nbaz" 23 | \ ) 24 | 25 | " Last line changed 26 | call s:TestDiff( 27 | \ [2,1,2,2], 1, 'n', 28 | \ "foo\nbar\nbaz", 29 | \ "foo\nbar\nbnz" 30 | \ ) 31 | 32 | " Middle characters changed 33 | call s:TestDiff( 34 | \ [1,1,1,2], 1, 'x', 35 | \ "foo\nb╵r\nbaz", 36 | \ "foo\nbxr\nbaz" 37 | \ ) 38 | 39 | " End of line changed 40 | call s:TestDiff( 41 | \ [1,1,1,3], 2, 'y', 42 | \ "foo\nb╵r\nbaz", 43 | \ "foo\nby\nbaz" 44 | \ ) 45 | 46 | " End of file changed 47 | call s:TestDiff( 48 | \ [2,1,2,3], 2, 'y', 49 | \ "foo\nb╵r\nbaz", 50 | \ "foo\nb╵r\nby") 51 | 52 | " Characters inserted 53 | call s:TestDiff( 54 | \ [1,1,1,1], 0, 'e', 55 | \ "foo\nb╵r\nbaz", 56 | \ "foo\nbe╵r\nbaz" 57 | \ ) 58 | 59 | " Characters inserted at beginning 60 | call s:TestDiff( 61 | \ [0,0,0,0], 0, 'a', 62 | \ "foo\nb╵r\nbaz", 63 | \ "afoo\nb╵r\nbaz" 64 | \ ) 65 | 66 | " Line inserted 67 | call s:TestDiff( 68 | \ [1,0,1,0], 0, "more\n", 69 | \ "foo\nb╵r\nbaz", 70 | \ "foo\nmore\nb╵r\nbaz" 71 | \ ) 72 | 73 | " Line inserted at end 74 | " It's important this appears to *prefix* the newline 75 | call s:TestDiff( 76 | \ [2,3,2,3], 0, "\nanother", 77 | \ "foo\nb╵r\nbaz", 78 | \ "foo\nb╵r\nbaz\nanother" 79 | \ ) 80 | 81 | " Line inserted at beginning 82 | call s:TestDiff( 83 | \ [0,0,0,0], 0, "line\n", 84 | \ "foo\nb╵r\nbaz", 85 | \ "line\nfoo\nb╵r\nbaz" 86 | \ ) 87 | 88 | " Line inserted at beginning with same leading characters 89 | call s:TestDiff( 90 | \ [0,3,0,3], 0, "line\n// ", 91 | \ "// foo\n// b╵r\n// baz", 92 | \ "// line\n// foo\n// b╵r\n// baz" 93 | \ ) 94 | 95 | " Change spanning lines 96 | call s:TestDiff( 97 | \ [0,2,2,1], 7, "r\nmany\nlines\nsp", 98 | \ "foo\nb╵r\nbaz", 99 | \ "for\nmany\nlines\nspaz" 100 | \ ) 101 | 102 | " Delete within a line 103 | call s:TestDiff( 104 | \ [1,1,1,2], 1, '', 105 | \ "ab\ncde\nfghi", 106 | \ "ab\nce\nfghi" 107 | \ ) 108 | 109 | " Delete across a line 110 | call s:TestDiff( 111 | \ [1,1,2,1], 4, '', 112 | \ "foo\nb╵r\nqux", 113 | \ "foo\nbux", 114 | \ ) 115 | 116 | " Delete entire line 117 | call s:TestDiff( 118 | \ [1,0,2,0], 4, '', 119 | \ "foo\nb╵r\nqux", 120 | \ "foo\nqux", 121 | \ ) 122 | 123 | " Delete multiple lines 124 | call s:TestDiff( 125 | \ [1,0,3,0], 8, '', 126 | \ "foo\nb╵r\nbaz\nqux", 127 | \ "foo\nqux", 128 | \ ) 129 | 130 | " Delete with repeated substring 131 | call s:TestDiff( 132 | \ [0, 4, 0, 6], 2, '', 133 | \ 'ABABAB', 134 | \ 'ABAB') 135 | 136 | " Delete at beginning 137 | call s:TestDiff( 138 | \ [0, 0, 0, 1], 1, '', 139 | \ "foo\nb╵r\nbaz", 140 | \ "oo\nb╵r\nbaz") 141 | 142 | " Delete line at beginning 143 | call s:TestDiff( 144 | \ [0, 0, 1, 0], 4, '', 145 | \ "foo\nb╵r\nbaz", 146 | \ "b╵r\nbaz") 147 | 148 | " Delete line at beginning with same leading characters 149 | call s:TestDiff( 150 | \ [0, 3, 1, 3], 7, '', 151 | \ "// foo\n// b╵r\n// baz", 152 | \ "// b╵r\n// baz") 153 | 154 | " Delete lines at beginning with same leading characters 155 | call s:TestDiff( 156 | \ [0, 3, 2, 3], 14, '', 157 | \ "// foo\n// b╵r\n// baz", 158 | \ '// baz') 159 | 160 | " Delete at end 161 | call s:TestDiff( 162 | \ [2, 2, 2, 3], 1, '', 163 | \ "foo\nb╵r\nbaz", 164 | \ "foo\nb╵r\nba") 165 | 166 | " Delete lines at end 167 | call s:TestDiff( 168 | \ [0, 3, 2, 3], 8, '', 169 | \ "foo\nb╵r\nbaz", 170 | \ 'foo') 171 | 172 | " Handles multiple blank lines 173 | call s:TestDiff( 174 | \ [5,1,5,2], 1, 'x', 175 | \ "\n\n\n\nfoo\nb╵r\nbaz", 176 | \ "\n\n\n\nfoo\nbxr\nbaz" 177 | \ ) 178 | 179 | " File becomes empty 180 | call s:TestDiff( 181 | \ [0,0,1,0], 5, '', 182 | \ 'line', 183 | \ '') 184 | 185 | " File Starts empty 186 | call s:TestDiff( 187 | \ [0,0,0,0], 0, "line\n", 188 | \ '', 189 | \ 'line') 190 | 191 | " File is identical 192 | " Would be better to not send a change, but an arbitrary empty change is OK 193 | call s:TestDiff( 194 | \ [2,3,2,3], 0, '', 195 | \ "foo\nbar\nbaz", 196 | \ "foo\nbar\nbaz") 197 | 198 | " Starts and ends empty 199 | call s:TestDiff( 200 | \ [0,0,0,0], 0, '', 201 | \ '', 202 | \ '') 203 | endfunction 204 | 205 | function! s:TestDiff(range, length, text, old, new) abort 206 | let l:start = {'line': a:range[0], 'character': a:range[1]} 207 | let l:end = {'line': a:range[2], 'character': a:range[3]} 208 | let l:old = empty(a:old) ? [] : split(a:old, "\n", v:true) 209 | let l:new = empty(a:new) ? [] : split(a:new, "\n", v:true) 210 | let l:result = lsc#diff#compute(l:old, l:new) 211 | call assert_equal({'start': l:start}, {'start': l:result.range.start}) 212 | call assert_equal({'end': l:end}, {'end': l:result.range.end}) 213 | call assert_equal({'length': a:length}, {'length': l:result.rangeLength}) 214 | call assert_equal({'text': a:text}, {'text': l:result.text}) 215 | endfunction 216 | 217 | function! s:RunTest(test) 218 | let v:errors = [] 219 | silent! call lsc#diff#not_a_function() 220 | 221 | call function(a:test)() 222 | 223 | if len(v:errors) > 0 224 | for l:error in v:errors 225 | echoerr l:error 226 | endfor 227 | else 228 | echom 'No errors in: '.a:test 229 | endif 230 | endfunction 231 | 232 | function! s:RunTests(...) 233 | for l:test in a:000 234 | call s:RunTest(l:test) 235 | endfor 236 | endfunction 237 | 238 | messages clear 239 | call s:RunTests('TestDiff') 240 | -------------------------------------------------------------------------------- /test/initialization_input: -------------------------------------------------------------------------------- 1 | Content-Length: 310 2 | 3 | {"method":"initialize","jsonrpc":"2.0","id":1,"params":{"rootUri":"file:///some/path","capabilities":{"workspace":{},"textDocument":{"completion":{"snippetSupport":false},"synchronization":{"willSaveWaitUntil":false,"willSave":false,"didSave":false},"definition":{"dynamicRegistration":false}}},"trace":"off"}} 4 | -------------------------------------------------------------------------------- /test/integration/.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /test/integration/bin/stub_server.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:_test/stub_lsp.dart'; 4 | import 'package:lsp/lsp.dart'; 5 | import 'package:json_rpc_2/json_rpc_2.dart'; 6 | 7 | void main() async { 8 | final server = StubServer(Peer(lspChannel(stdin, stdout))); 9 | await server.initialized; 10 | await server.peer.done; 11 | } 12 | -------------------------------------------------------------------------------- /test/integration/lib/stub_lsp.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:json_rpc_2/json_rpc_2.dart'; 4 | 5 | class StubServer { 6 | final Peer peer; 7 | 8 | StubServer(this.peer, {Map capabilities = const {}}) { 9 | peer 10 | ..registerMethod('initialize', (_) { 11 | return {'capabilities': capabilities}; 12 | }) 13 | ..registerMethod('initialized', (_) { 14 | _initialized.complete(); 15 | }) 16 | ..registerMethod('workspace/didChangeConfiguration', (_) {}) 17 | ..registerMethod('textDocument/didClose', (_) {}) 18 | ..registerMethod('textDocument/didOpen', _didOpen.add) 19 | ..registerMethod('textDocument/didChange', _didChange.add) 20 | ..registerMethod('textDocument/didSave', _didSave.add) 21 | ..registerMethod('shutdown', (_) {}) 22 | ..registerMethod('exit', (_) { 23 | peer.close(); 24 | }); 25 | } 26 | Stream get didOpen => _didOpen.stream; 27 | final _didOpen = StreamController(); 28 | 29 | Stream get didChange => _didChange.stream; 30 | final _didChange = StreamController(); 31 | 32 | Stream get didSave => _didSave.stream; 33 | final _didSave = StreamController(); 34 | 35 | Future get initialized { 36 | peer.listen(); 37 | return _initialized.future; 38 | } 39 | 40 | final _initialized = Completer(); 41 | } 42 | -------------------------------------------------------------------------------- /test/integration/lib/test_bed.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:json_rpc_2/json_rpc_2.dart'; 4 | import 'package:lsp/lsp.dart' show lspChannel; 5 | import 'package:test/test.dart'; 6 | import 'package:test_descriptor/test_descriptor.dart' as d; 7 | 8 | import 'vim_remote.dart'; 9 | 10 | class TestBed { 11 | final Vim vim; 12 | final Stream clients; 13 | 14 | TestBed._(this.vim, this.clients); 15 | 16 | static Future setup( 17 | {Future Function(Vim) beforeRegister, String config = ''}) async { 18 | final serverSocket = await ServerSocket.bind('localhost', 0); 19 | 20 | final clients = serverSocket 21 | .map((socket) => Peer(lspChannel(socket, socket), 22 | onUnhandledError: (error, stack) => 23 | fail('Unhandled server error: $error'))) 24 | .asBroadcastStream(); 25 | final vim = await Vim.start(workingDirectory: d.sandbox); 26 | await beforeRegister?.call(vim); 27 | await vim.expr('RegisterLanguageServer("text", {' 28 | '"command":"localhost:${serverSocket.port}",' 29 | '"name":"Test Server",' 30 | '"enabled":v:false,' 31 | '$config' 32 | '})'); 33 | 34 | addTearDown(() async { 35 | await vim.quit(); 36 | print(await d.file(vim.name).io.readAsString()); 37 | await serverSocket.close(); 38 | }); 39 | return TestBed._(vim, clients); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/integration/lib/vim_remote.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:isolate'; 3 | import 'dart:math'; 4 | 5 | import 'package:path/path.dart' as p; 6 | 7 | class Vim { 8 | /// The `--servername` argument. 9 | final String name; 10 | final Process _process; 11 | final String _workingDirectory; 12 | 13 | static Future start({String workingDirectory}) async { 14 | final version = (await Process.run('vim', ['--version'])).stdout as String; 15 | assert(version.contains('+clientserver')); 16 | final name = 'DARTVIM-${Random().nextInt(4294967296)}'; 17 | final process = await Process.start('vim', 18 | ['--servername', name, '-u', await _vimrcPath, '-U', 'NONE', '-V$name'], 19 | workingDirectory: workingDirectory, 20 | mode: ProcessStartMode.detachedWithStdio); 21 | while (!await _isRunning(name)) { 22 | await Future.delayed(const Duration(milliseconds: 100)); 23 | } 24 | return Vim._(name, process, workingDirectory); 25 | } 26 | 27 | Vim._(this.name, this._process, this._workingDirectory); 28 | 29 | /// Sends `:qall!` and waits for the process to exit. 30 | Future quit() { 31 | _process.stdin.writeln(':qall!'); 32 | return _process.stdout.drain(); 33 | } 34 | 35 | /// Send [keys] as if they were press in the vim window. 36 | /// 37 | /// Use vim syntax for special keys, for instance '' for enter or '' 38 | /// for escape. 39 | Future sendKeys(String keys) async { 40 | await Process.run('vim', [..._serverNameArg, '--remote-send', keys]); 41 | await Future.delayed(const Duration(milliseconds: 10)); 42 | } 43 | 44 | /// Evaluate [expression] as a vim expression. 45 | Future expr(String expression) async { 46 | final result = await Process.run( 47 | 'vim', [..._serverNameArg, '--remote-expr', expression]); 48 | final stdout = result.stdout as String; 49 | return stdout.endsWith('\n') 50 | ? stdout.substring(0, stdout.length - 1) 51 | : stdout; 52 | } 53 | 54 | Future edit(String fileName) async { 55 | final result = await Process.run( 56 | 'vim', [..._serverNameArg, '--remote', fileName], 57 | workingDirectory: _workingDirectory); 58 | final exitCode = await result.exitCode; 59 | assert(exitCode == 0); 60 | final openFile = await expr('expand(\'%\')'); 61 | assert(openFile == fileName); 62 | } 63 | 64 | /// Returns the last [count] messages from `:messages`. 65 | Future> messages(int count) async { 66 | await sendKeys(':redir => vim_remote_messages'); 67 | await sendKeys(':${count}messages'); 68 | await sendKeys(':redir END'); 69 | final output = await expr('vim_remote_messages'); 70 | return output.split('\n').skip(2).toList(); 71 | } 72 | 73 | /// The full content of the currently active buffer. 74 | Future get currentBufferContent async => expr(r'getline(1, "$")'); 75 | 76 | Iterable get _serverNameArg => ['--servername', name]; 77 | } 78 | 79 | Future _isRunning(String name) async { 80 | final result = await Process.run('vim', ['--serverlist']); 81 | final serverList = (result.stdout as String).split('\n'); 82 | return serverList.contains(name); 83 | } 84 | 85 | Future get _vimrcPath async { 86 | final packageUriDir = p.dirname(p.fromUri(await Isolate.resolvePackageUri( 87 | Uri(scheme: 'package', path: '_test/_test')))); 88 | // Assume pub layout 89 | final packageRoot = p.dirname(packageUriDir); 90 | return p.join(packageRoot, 'vimrc'); 91 | } 92 | -------------------------------------------------------------------------------- /test/integration/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: _test 2 | publish_to: none 3 | 4 | environment: 5 | sdk: ">=2.7.0 <3.0.0" 6 | 7 | dependencies: 8 | async: ^2.4.0 9 | lsp: ^0.1.1 10 | path: ^1.7.0 11 | test: ^1.13.0 12 | test_descriptor: ^2.0.0 13 | -------------------------------------------------------------------------------- /test/integration/test/complete_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:_test/stub_lsp.dart'; 4 | import 'package:_test/test_bed.dart'; 5 | import 'package:_test/vim_remote.dart'; 6 | import 'package:json_rpc_2/json_rpc_2.dart'; 7 | import 'package:lsp/lsp.dart' show CompletionItem; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | TestBed testBed; 12 | Peer client; 13 | 14 | setUpAll(() async { 15 | testBed = await TestBed.setup(); 16 | }); 17 | 18 | setUp(() async { 19 | final nextClient = testBed.clients.first; 20 | await testBed.vim.edit('foo.txt'); 21 | await testBed.vim.sendKeys(':LSClientEnable'); 22 | client = await nextClient; 23 | }); 24 | 25 | tearDown(() async { 26 | await testBed.vim.sendKeys(''); 27 | await testBed.vim.sendKeys(':LSClientDisable'); 28 | await testBed.vim.sendKeys(':%bwipeout!'); 29 | await client.done; 30 | client = null; 31 | }); 32 | 33 | test('autocomplete on trigger', () async { 34 | final server = StubServer(client, capabilities: { 35 | 'completionProvider': { 36 | 'triggerCharacters': ['.'] 37 | }, 38 | }); 39 | server.peer.registerMethod('textDocument/completion', (Parameters params) { 40 | return [ 41 | CompletionItem((b) => b..label = 'abcd'), 42 | CompletionItem((b) => b..label = 'foo') 43 | ]; 44 | }); 45 | await server.initialized; 46 | await testBed.vim.sendKeys('ifoo.'); 47 | await testBed.vim.waitForPopUpMenu(); 48 | await testBed.vim.sendKeys('a'); 49 | expect(await testBed.vim.expr('getline(1)'), 'foo.abcd'); 50 | }); 51 | 52 | test('filter out trigger characters', () async { 53 | await testBed.vim 54 | .sendKeys(':let g:lsc_block_complete_triggers = ["{"]'); 55 | final server = StubServer(client, capabilities: { 56 | 'completionProvider': { 57 | 'triggerCharacters': ['.', '{'] 58 | }, 59 | }); 60 | var completeWasCalled = false; 61 | server.peer.registerMethod('textDocument/completion', (Parameters _) { 62 | // Thrown exception would be caught by JSON RPC. 63 | completeWasCalled = true; 64 | }); 65 | await server.initialized; 66 | await testBed.vim.sendKeys('i{'); 67 | await Future.delayed(const Duration(seconds: 1)); 68 | expect(completeWasCalled, false); 69 | }); 70 | 71 | test('autocomplete on 3 word characters', () async { 72 | final server = StubServer(client, capabilities: { 73 | 'completionProvider': {'triggerCharacters': []}, 74 | }); 75 | server.peer.registerMethod('textDocument/completion', (Parameters params) { 76 | return [ 77 | CompletionItem((b) => b..label = 'foobar'), 78 | CompletionItem((b) => b..label = 'fooother') 79 | ]; 80 | }); 81 | await server.initialized; 82 | await testBed.vim.sendKeys('ifoo'); 83 | await testBed.vim.waitForPopUpMenu(); 84 | await testBed.vim.sendKeys('b'); 85 | expect(await testBed.vim.expr('getline(1)'), 'foobar'); 86 | }); 87 | 88 | test('manual completion', () async { 89 | final server = StubServer(client, capabilities: { 90 | 'completionProvider': {'triggerCharacters': []}, 91 | }); 92 | server.peer.registerMethod('textDocument/completion', (Parameters params) { 93 | return [ 94 | CompletionItem((b) => b..label = 'foobar'), 95 | CompletionItem((b) => b..label = 'fooother') 96 | ]; 97 | }); 98 | await server.initialized; 99 | await testBed.vim.sendKeys('if'); 100 | await testBed.vim.waitForPopUpMenu(); 101 | await testBed.vim.sendKeys(''); 102 | expect(await testBed.vim.expr('getline(1)'), 'foobar'); 103 | }); 104 | } 105 | 106 | extension PopUp on Vim { 107 | Future waitForPopUpMenu() async { 108 | final until = DateTime.now().add(const Duration(seconds: 5)); 109 | while (await this.expr('pumvisible()') != '1') { 110 | await Future.delayed(const Duration(milliseconds: 50)); 111 | if (DateTime.now().isAfter(until)) { 112 | throw StateError('Pop up menu is not visible'); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/integration/test/did_open_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:async/async.dart'; 2 | import 'package:_test/stub_lsp.dart'; 3 | import 'package:_test/test_bed.dart'; 4 | import 'package:json_rpc_2/json_rpc_2.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('Registering a new file type', () { 9 | TestBed testBed; 10 | Peer client; 11 | 12 | setUpAll(() async { 13 | testBed = await TestBed.setup(); 14 | }); 15 | 16 | setUp(() async { 17 | final nextClient = testBed.clients.first; 18 | await testBed.vim.edit('foo.txt'); 19 | await testBed.vim.sendKeys(':LSClientEnable'); 20 | client = await nextClient; 21 | }); 22 | 23 | tearDown(() async { 24 | await testBed.vim.sendKeys(':LSClientDisable'); 25 | await testBed.vim.sendKeys(':%bwipeout!'); 26 | await client.done; 27 | client = null; 28 | }); 29 | 30 | test('sends didOpen for already open files', () async { 31 | final server = StubServer(client); 32 | final opens = StreamQueue(server.didOpen); 33 | 34 | await server.initialized; 35 | await opens.next; 36 | 37 | await testBed.vim.edit('foo.py'); 38 | await testBed.vim.expr('RegisterLanguageServer("python", "Test Server")'); 39 | final nextOpen = await opens.next; 40 | expect(nextOpen['textDocument']['uri'].asString, endsWith('foo.py')); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/integration/test/did_save_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:_test/stub_lsp.dart'; 4 | import 'package:_test/test_bed.dart'; 5 | import 'package:json_rpc_2/json_rpc_2.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | TestBed testBed; 10 | Peer client; 11 | 12 | setUpAll(() async { 13 | testBed = await TestBed.setup(); 14 | }); 15 | 16 | setUp(() async { 17 | final nextClient = testBed.clients.first; 18 | await testBed.vim.edit('foo.txt'); 19 | await testBed.vim.sendKeys(':LSClientEnable'); 20 | client = await nextClient; 21 | }); 22 | 23 | tearDown(() async { 24 | await testBed.vim.sendKeys(':LSClientDisable'); 25 | await testBed.vim.sendKeys(':%bwipeout!'); 26 | await client.done; 27 | client = null; 28 | }); 29 | 30 | Future testNoDidSave(Map capabilities) async { 31 | final server = StubServer(client, capabilities: capabilities) 32 | ..didSave.listen((_) { 33 | fail('Unexpected didSave'); 34 | }); 35 | 36 | await server.initialized; 37 | 38 | await server.didOpen.first; 39 | 40 | await testBed.vim.sendKeys(':w'); 41 | 42 | await testBed.vim.sendKeys('iHello'); 43 | await await server.didChange.first; 44 | } 45 | 46 | test('TextDocumentSyncKind instead of TextDocumentSyncOptions', () async { 47 | await testNoDidSave({'textDocumentSync': 1}); 48 | }); 49 | 50 | test('omitted sync key', () async { 51 | await testNoDidSave({ 52 | 'textDocumentSync': {'openClose': true, 'change': 1} 53 | }); 54 | }); 55 | 56 | test('include sync key', () async { 57 | final server = StubServer(client, capabilities: { 58 | 'textDocumentSync': {'openClose': true, 'save': {}} 59 | }); 60 | 61 | await server.initialized; 62 | 63 | await server.didOpen.first; 64 | 65 | await testBed.vim.sendKeys(':w'); 66 | 67 | await server.didSave.first; 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /test/integration/test/disabled_server_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:_test/stub_lsp.dart'; 2 | import 'package:_test/test_bed.dart'; 3 | import 'package:json_rpc_2/json_rpc_2.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('initially disabled', () { 8 | TestBed testBed; 9 | Peer client; 10 | 11 | setUpAll(() async { 12 | testBed = await TestBed.setup(beforeRegister: (vim) async { 13 | await vim.edit('foo.txt'); 14 | }); 15 | }); 16 | 17 | tearDown(() async { 18 | await testBed.vim.sendKeys(':LSClientDisable'); 19 | await testBed.vim.sendKeys(':%bwipeout!'); 20 | await client?.done; 21 | }); 22 | 23 | test('waits to start until explicitly enabled', () async { 24 | expect(await testBed.vim.expr('lsc#server#status(\'text\')'), 'disabled'); 25 | final nextClient = testBed.clients.first; 26 | await testBed.vim.sendKeys(':LSClientEnable'); 27 | client = await nextClient; 28 | final server = StubServer(client); 29 | await server.initialized; 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/integration/test/early_exit_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:_test/vim_remote.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | Vim vim; 8 | setUpAll(() async { 9 | vim = await Vim.start(); 10 | await vim.expr('RegisterLanguageServer("text", {' 11 | '"command":"false",' 12 | '})'); 13 | }); 14 | 15 | tearDownAll(() async { 16 | await vim.quit(); 17 | final log = File(vim.name); 18 | print(await log.readAsString()); 19 | await log.delete(); 20 | }); 21 | 22 | test('reports a failure to start', () async { 23 | await vim.edit('foo.txt'); 24 | final messages = await vim.messages(1); 25 | expect(messages, ['[lsc:Error] Failed to initialize server "\'false\'".']); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /test/integration/test/edit_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:_test/stub_lsp.dart'; 5 | import 'package:_test/test_bed.dart'; 6 | import 'package:test/test.dart'; 7 | import 'package:json_rpc_2/json_rpc_2.dart'; 8 | import 'package:lsp/lsp.dart' show WorkspaceEdit, TextEdit, Range, Position; 9 | 10 | void main() { 11 | TestBed testBed; 12 | Peer client; 13 | 14 | setUpAll(() async { 15 | testBed = await TestBed.setup(); 16 | }); 17 | 18 | setUp(() async { 19 | final nextClient = testBed.clients.first; 20 | await testBed.vim.edit('foo.txt'); 21 | await testBed.vim.sendKeys(':LSClientEnable'); 22 | client = await nextClient; 23 | }); 24 | 25 | tearDown(() async { 26 | await testBed.vim.sendKeys(':LSClientDisable'); 27 | await testBed.vim.sendKeys(':%bwipeout!'); 28 | await client.done; 29 | client = null; 30 | }); 31 | 32 | test('edit of entire file', () async { 33 | final renameDone = Completer(); 34 | final server = StubServer(client); 35 | server.peer.registerMethod('textDocument/rename', (Parameters params) { 36 | renameDone.complete(); 37 | final uri = params['textDocument']['uri'].asString; 38 | return WorkspaceEdit((b) => b 39 | ..changes = { 40 | uri: [ 41 | TextEdit((b) => b 42 | ..newText = 'bar\nbar\n' 43 | ..range = Range((b) => b 44 | ..start = Position((b) => b 45 | ..line = 0 46 | ..character = 0) 47 | ..end = Position((b) => b 48 | ..line = 2 49 | ..character = 0))) 50 | ] 51 | }); 52 | }); 53 | await server.initialized; 54 | await testBed.vim.sendKeys('ifoofoo'); 55 | await testBed.vim.sendKeys(':LSClientRename \'bar\''); 56 | await renameDone; 57 | await Future.delayed(const Duration(milliseconds: 100)); 58 | expect(await testBed.vim.expr(r'getline(1, "$")'), 'bar\nbar\n'); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/integration/test/highlight_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:_test/stub_lsp.dart'; 4 | import 'package:_test/test_bed.dart'; 5 | import 'package:json_rpc_2/json_rpc_2.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | TestBed testBed; 10 | Peer client; 11 | 12 | setUpAll(() async { 13 | testBed = await TestBed.setup(); 14 | }); 15 | 16 | setUp(() async { 17 | final nextClient = testBed.clients.first; 18 | await testBed.vim.edit('foo.txt'); 19 | await testBed.vim.sendKeys(':LSClientEnable'); 20 | client = await nextClient; 21 | }); 22 | 23 | tearDown(() async { 24 | await testBed.vim.sendKeys(':LSClientDisable'); 25 | await testBed.vim.sendKeys(':%bwipeout!'); 26 | await client.done; 27 | client = null; 28 | }); 29 | 30 | test('requests highlights with bool capability', () async { 31 | final server = StubServer(client, capabilities: { 32 | 'documentHighlightProvider': true, 33 | }); 34 | final callMade = Completer(); 35 | server.peer.registerMethod('textDocument/documentHighlight', 36 | (Parameters params) { 37 | callMade.complete(); 38 | return []; 39 | }); 40 | await server.initialized; 41 | expect(callMade.future, completes); 42 | }); 43 | 44 | test('requests highlights with map capability', () async { 45 | final server = StubServer(client, capabilities: { 46 | 'documentHighlightProvider': true, 47 | }); 48 | final callMade = Completer(); 49 | server.peer.registerMethod('textDocument/documentHighlight', 50 | (Parameters params) { 51 | callMade.complete(); 52 | return []; 53 | }); 54 | await server.initialized; 55 | expect(callMade.future, completes); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /test/integration/test/stderr_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:_test/vim_remote.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | Vim vim; 8 | setUp(() async { 9 | vim = await Vim.start(); 10 | }); 11 | 12 | tearDown(() async { 13 | await vim.quit(); 14 | final log = File(vim.name); 15 | print(await log.readAsString()); 16 | await log.delete(); 17 | }); 18 | 19 | test('emits stderr by default', () async { 20 | await vim.expr('RegisterLanguageServer("text", {' 21 | '"command":["sh", "-c", "echo messagestderr >&2"],' 22 | '"name":"some server"' 23 | '})'); 24 | await vim.edit('foo.txt'); 25 | while (await vim.expr('lsc#server#status(\'text\')') != 'failed') { 26 | await Future.delayed(const Duration(milliseconds: 50)); 27 | } 28 | final messages = await vim.messages(2); 29 | expect(messages, [ 30 | '[lsc:Error] StdErr from some server: messagestderr', 31 | '[lsc:Error] Failed to initialize server "some server". ' 32 | 'Failing command is: [\'sh\', \'-c\', \'echo messagestderr >&2\']' 33 | ]); 34 | }); 35 | 36 | test('suppresses stderr', () async { 37 | await vim.expr('RegisterLanguageServer("text", {' 38 | '"command":["sh", "-c", "echo messagestderr >&2"],' 39 | '"name":"some server",' 40 | '"suppress_stderr": v:true,' 41 | '})'); 42 | await vim.edit('foo.txt'); 43 | while (await vim.expr('lsc#server#status(\'text\')') != 'failed') { 44 | await Future.delayed(const Duration(milliseconds: 50)); 45 | } 46 | final messages = await vim.messages(2); 47 | expect(messages, [ 48 | matches('"foo.txt" .*--No lines in buffer--'), 49 | '[lsc:Error] Failed to initialize server "some server". ' 50 | 'Failing command is: [\'sh\', \'-c\', \'echo messagestderr >&2\']' 51 | ]); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /test/integration/test/vim_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:_test/vim_remote.dart'; 4 | import 'package:path/path.dart' as p; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group(Vim, () { 9 | Vim vim; 10 | setUpAll(() async { 11 | vim = await Vim.start(); 12 | final serverPath = p.absolute('bin', 'stub_server.dart'); 13 | await vim.expr('RegisterLanguageServer("text","dart $serverPath")'); 14 | }); 15 | 16 | tearDownAll(() async { 17 | await vim.quit(); 18 | final log = File(vim.name); 19 | print(await log.readAsString()); 20 | await log.delete(); 21 | }); 22 | 23 | test('evaluates expressions', () async { 24 | final result = await vim.expr('version'); 25 | expect(int.tryParse(result), isNotNull); 26 | }); 27 | 28 | test('sends keys', () async { 29 | await vim.sendKeys('iHello there!'); 30 | expect(await vim.currentBufferContent, 'Hello there!'); 31 | }); 32 | 33 | test('loads plugin', () async { 34 | final result = await vim.expr('exists(\':LSClientGoToDefinition\')'); 35 | expect(result, '2'); 36 | }); 37 | 38 | test('opens files, has filetype detection', () async { 39 | await vim.edit('foo.txt'); 40 | expect(await vim.expr('&ft'), 'text'); 41 | }); 42 | 43 | group('open file', () { 44 | setUpAll(() async { 45 | await vim.edit('foo.txt'); 46 | }); 47 | 48 | tearDownAll(() async { 49 | await vim.sendKeys(':%bd!'); 50 | }); 51 | 52 | test('sets filetype', () async { 53 | expect(await vim.expr('&ft'), 'text'); 54 | }); 55 | 56 | test('starts language server', () async { 57 | while (await vim.expr('lsc#server#status(\'text\')') != 'running') { 58 | await Future.delayed(const Duration(milliseconds: 50)); 59 | } 60 | }); 61 | }); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /test/integration/test/workspace_configuration_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:_test/stub_lsp.dart'; 4 | import 'package:_test/test_bed.dart'; 5 | import 'package:json_rpc_2/json_rpc_2.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | TestBed testBed; 10 | Peer client; 11 | 12 | setUpAll(() async { 13 | testBed = await TestBed.setup( 14 | config: 15 | '"workspace_config":{"foo":{"baz":"bar"},"other":"something"},'); 16 | }); 17 | 18 | setUp(() async { 19 | final nextClient = testBed.clients.first; 20 | await testBed.vim.edit('foo.txt'); 21 | await testBed.vim.sendKeys(':LSClientEnable'); 22 | client = await nextClient; 23 | }); 24 | 25 | tearDown(() async { 26 | await testBed.vim.sendKeys(':LSClientDisable'); 27 | await testBed.vim.sendKeys(':%bwipeout!'); 28 | await client.done; 29 | client = null; 30 | }); 31 | 32 | test('can send workspace configuration', () async { 33 | final server = StubServer(client); 34 | await server.initialized; 35 | 36 | final response = await server.peer.sendRequest('workspace/configuration', { 37 | 'items': [{}] 38 | }); 39 | expect(response, [ 40 | { 41 | 'foo': {'baz': 'bar'}, 42 | 'other': 'something', 43 | } 44 | ]); 45 | }); 46 | 47 | test('can send multiple configurations', () async { 48 | final server = StubServer(client); 49 | await server.initialized; 50 | 51 | final response = await server.peer.sendRequest('workspace/configuration', { 52 | 'items': [ 53 | {'section': 'foo'}, 54 | {'section': 'other'} 55 | ] 56 | }); 57 | expect(response, [ 58 | {'baz': 'bar'}, 59 | 'something' 60 | ]); 61 | }); 62 | 63 | test('can send nested config with dotted keys', () async { 64 | final server = StubServer(client); 65 | await server.initialized; 66 | 67 | final response = await server.peer.sendRequest('workspace/configuration', { 68 | 'items': [ 69 | {'section': 'foo.baz'}, 70 | ] 71 | }); 72 | expect(response, ['bar']); 73 | }); 74 | 75 | test('handles missing keys', () async { 76 | final server = StubServer(client); 77 | await server.initialized; 78 | 79 | final response = await client.sendRequest('workspace/configuration', { 80 | 'items': [ 81 | {'section': 'foo.missing'}, 82 | {'section': 'missing'} 83 | ] 84 | }); 85 | expect(response, [null, null]); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /test/integration/vimrc: -------------------------------------------------------------------------------- 1 | set nocompatible 2 | 3 | let s:test_dir = expand(':p:h') 4 | let s:plugin = fnamemodify(s:test_dir, ':h:h') 5 | exe 'set rtp+='.s:plugin 6 | exe 'set rtp+='.s:plugin.'/after' 7 | 8 | let g:lsc_auto_map = v:true 9 | 10 | set noswapfile 11 | 12 | filetype plugin indent on 13 | -------------------------------------------------------------------------------- /test/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | vim --version 4 | 5 | pushd test/integration 6 | 7 | pub get 8 | 9 | pub run test 10 | -------------------------------------------------------------------------------- /test/uri_test.vim: -------------------------------------------------------------------------------- 1 | function! TestUriEncode() abort 2 | call assert_equal('file://foo/bar%20baz', lsc#uri#documentUri('foo/bar baz')) 3 | call assert_equal('file://foo/bar%23baz', lsc#uri#documentUri('foo/bar#baz')) 4 | endfunction 5 | 6 | function! TestUriDecode() abort 7 | call assert_equal('foo/bar baz', lsc#uri#documentPath('file://foo/bar%20baz')) 8 | call assert_equal('foo/bar#baz', lsc#uri#documentPath('file://foo/bar%23baz')) 9 | endfunction 10 | 11 | function! s:RunTest(test) 12 | let v:errors = [] 13 | silent! call lsc#uri#not_a_function() 14 | 15 | call function(a:test)() 16 | 17 | if len(v:errors) > 0 18 | for l:error in v:errors 19 | echoerr l:error 20 | endfor 21 | else 22 | echom 'No errors in: '.a:test 23 | endif 24 | endfunction 25 | 26 | function! s:RunTests(...) 27 | for l:test in a:000 28 | call s:RunTest(l:test) 29 | endfor 30 | endfunction 31 | 32 | call s:RunTests('TestUriEncode', 'TestUriDecode') 33 | --------------------------------------------------------------------------------