From: martin f. krafft Date: Tue, 8 Apr 2025 15:04:08 +0000 (+0200) Subject: Squashed '.vim/bundle/vim-lsp/' content from commit 04428c92 X-Git-Url: https://git.madduck.net/etc/vim.git/commitdiff_plain/a39f715c13be3352193ffd9c5b7536b8786eff64 Squashed '.vim/bundle/vim-lsp/' content from commit 04428c92 git-subtree-dir: .vim/bundle/vim-lsp git-subtree-split: 04428c920002ac7cfacbecacb070a8af57b455d0 --- a39f715c13be3352193ffd9c5b7536b8786eff64 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..934e94f1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto +*.vimspec linguist-language=vim +.themisrc linguist-language=vim diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..dc90e5a1 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/linux_neovim.yml b/.github/workflows/linux_neovim.yml new file mode 100644 index 00000000..1ffb9f96 --- /dev/null +++ b/.github/workflows/linux_neovim.yml @@ -0,0 +1,88 @@ +name: linux_neovim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + VIM_LSP_GO_VERSION: '1.17' + VIM_LSP_GOPLS_VERSION: '0.7.3' + VIM_LSP_GOPLS_CACHE_VER: 1 + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + name: [neovim-v04-x64, neovim-v05-x64, neovim-nightly-x64] + include: + - name: neovim-v04-x64 + os: ubuntu-latest + neovim_version: v0.4.4 + allow_failure: false + - name: neovim-v05-x64 + os: ubuntu-latest + neovim_version: v0.5.1 + allow_failure: false + - name: neovim-nightly-x64 + os: ubuntu-latest + neovim_version: nightly + allow_failure: true + runs-on: ${{matrix.os}} + continue-on-error: ${{matrix.allow_failure}} + steps: + - name: Install dependencies + shell: bash + run: | + # https://github.com/Zettlr/Zettlr/issues/3517 + sudo apt-get install libfuse2 + - uses: actions/checkout@v4 + - name: Download neovim + shell: bash + run: | + mkdir -p ~/nvim/bin + curl -L https://github.com/neovim/neovim/releases/download/${{matrix.neovim_version}}/nvim.appimage -o ~/nvim/bin/nvim + chmod u+x ~/nvim/bin/nvim + continue-on-error: ${{matrix.allow_failure}} + - name: Cache gopls + id: cache-gopls + uses: actions/cache@v4 + with: + path: bin/gopls + key: ${{ runner.os }}-${{ env.VIM_LSP_GO_VERSION }}-${{ env.VIM_LSP_GOPLS_VERSION }}-${{ env.VIM_LSP_GOPLS_CACHE_VER }}-gopls + - name: Install Go for gopls + if: steps.cache-gopls.outputs.cache-hit != 'true' + uses: actions/setup-go@v5 + with: + go-version: ${{ env.VIM_LSP_GO_VERSION }} + - name: Install gopls + if: steps.cache-gopls.outputs.cache-hit != 'true' + shell: bash + run: | + go install golang.org/x/tools/gopls@v${{ env.VIM_LSP_GOPLS_VERSION }} + gopls version + mkdir bin + mv "$(which gopls)" ./bin/ + env: + GO111MODULE: 'on' + - name: Download test runner + uses: actions/checkout@v4 + with: + repository: thinca/vim-themis + path: ./vim-themis + ref: v1.5.5 + - name: Run tests + shell: bash + run: | + export PATH=~/nvim/bin:$PATH + export PATH=./vim-themis/bin:$PATH + export PATH=./bin:$PATH + export THEMIS_VIM=nvim + nvim --version + themis + continue-on-error: ${{matrix.allow_failure}} diff --git a/.github/workflows/linux_vim.yml b/.github/workflows/linux_vim.yml new file mode 100644 index 00000000..abead965 --- /dev/null +++ b/.github/workflows/linux_vim.yml @@ -0,0 +1,88 @@ +name: linux_vim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + VIM_LSP_GO_VERSION: '1.17' + VIM_LSP_GOPLS_VERSION: '0.7.3' + VIM_LSP_GOPLS_CACHE_VER: 1 + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + name: [vim-v90-x64, vim-v82-x64, vim-v81-x64] + include: + - name: vim-v90-x64 + os: ubuntu-latest + vim_version: 9.0.0250 + glibc_version: 2.15 + - name: vim-v82-x64 + os: ubuntu-latest + vim_version: 8.2.0813 + glibc_version: 2.15 + - name: vim-v81-x64 + os: ubuntu-latest + vim_version: 8.1.2414 + glibc_version: 2.15 + runs-on: ${{matrix.os}} + steps: + - name: Install dependencies + shell: bash + run: | + # https://github.com/Zettlr/Zettlr/issues/3517 + sudo apt-get install libfuse2 + - uses: actions/checkout@v4 + - name: Download vim + shell: bash + run: | + mkdir -p ~/vim/bin + curl -L https://github.com/vim/vim-appimage/releases/download/v${{matrix.vim_version}}/GVim-v${{matrix.vim_version}}.glibc${{matrix.glibc_version}}-x86_64.AppImage -o ~/vim/bin/vim + chmod u+x ~/vim/bin/vim + - name: Cache gopls + id: cache-gopls + uses: actions/cache@v4 + with: + path: bin/gopls + key: ${{ runner.os }}-${{ env.VIM_LSP_GO_VERSION }}-${{ env.VIM_LSP_GOPLS_VERSION }}-${{ env.VIM_LSP_GOPLS_CACHE_VER }}-gopls + - name: Install Go for gopls + if: steps.cache-gopls.outputs.cache-hit != 'true' + uses: actions/setup-go@v5 + with: + go-version: ${{ env.VIM_LSP_GO_VERSION }} + - name: Install gopls + if: steps.cache-gopls.outputs.cache-hit != 'true' + shell: bash + run: | + go install golang.org/x/tools/gopls@v${{ env.VIM_LSP_GOPLS_VERSION }} + gopls version + mkdir bin + mv "$(which gopls)" ./bin/ + env: + GO111MODULE: 'on' + - name: Download test runner + uses: actions/checkout@v4 + with: + repository: thinca/vim-themis + path: ./vim-themis + ref: v1.5.5 + - name: Run tests + shell: bash + run: | + export PATH=~/vim/bin:$PATH + export PATH=./vim-themis/bin:$PATH + export PATH=./bin:$PATH + export THEMIS_VIM=vim + # https://github.com/project-slippi/Ishiiruka/issues/323 + # It was needed to detect the actual path of `libgmodule` via `ldconfig -p | grep libg`. + LD_PRELOAD=/lib/x86_64-linux-gnu/libgmodule-2.0.so vim --version + LD_PRELOAD=/lib/x86_64-linux-gnu/libgmodule-2.0.so themis + diff --git a/.github/workflows/mac_neovim.yml b/.github/workflows/mac_neovim.yml new file mode 100644 index 00000000..c06dee23 --- /dev/null +++ b/.github/workflows/mac_neovim.yml @@ -0,0 +1,76 @@ +name: mac_neovim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + VIM_LSP_GO_VERSION: '1.17' + VIM_LSP_GOPLS_VERSION: '0.7.3' + VIM_LSP_GOPLS_CACHE_VER: 1 + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [macos-latest] + name: [neovim-v04-x64, neovim-v05-x64, neovim-nightly-x64] + include: + - name: neovim-v04-x64 + os: macos-latest + neovim_version: v0.4.4 + allow_failure: false + - name: neovim-v05-x64 + os: macos-latest + neovim_version: v0.5.1 + allow_failure: false + - name: neovim-nightly-x64 + os: macos-latest + neovim_version: nightly + allow_failure: true + runs-on: ${{matrix.os}} + continue-on-error: ${{matrix.allow_failure}} + steps: + - uses: actions/checkout@v4 + - name: Download neovim + shell: bash + run: curl -L https://github.com/neovim/neovim/releases/download/${{matrix.neovim_version}}/nvim-macos.tar.gz -o ~/nvim.tar.gz + continue-on-error: ${{matrix.allow_failure}} + - name: Extract neovim + shell: bash + run: tar xzf ~/nvim.tar.gz -C ~/ + continue-on-error: ${{matrix.allow_failure}} + - name: Download test runner + uses: actions/checkout@v4 + with: + repository: thinca/vim-themis + path: ./vim-themis + ref: v1.5.5 + - name: Install Go for gopls + uses: actions/setup-go@v5 + with: + go-version: ${{ env.VIM_LSP_GO_VERSION }} + - name: Install gopls + shell: bash + run: | + go install golang.org/x/tools/gopls@v${{ env.VIM_LSP_GOPLS_VERSION }} + gopls version + mkdir bin + mv "$(which gopls)" ./bin/ + env: + GO111MODULE: 'on' + - name: Run tests + shell: bash + run: | + export PATH=~/nvim-osx64/bin:$PATH + export PATH=./vim-themis/bin:$PATH + export PATH=./bin:$PATH + export THEMIS_VIM=nvim + nvim --version + themis + continue-on-error: ${{matrix.allow_failure}} diff --git a/.github/workflows/mac_vim.yml b/.github/workflows/mac_vim.yml new file mode 100644 index 00000000..1da6efbd --- /dev/null +++ b/.github/workflows/mac_vim.yml @@ -0,0 +1,50 @@ +name: mac_vim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + VIM_LSP_GO_VERSION: '1.17' + VIM_LSP_GOPLS_VERSION: '0.7.3' + VIM_LSP_GOPLS_CACHE_VER: 1 + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Install MacVim + shell: bash + run: brew install macvim + - name: Download test runner + uses: actions/checkout@v4 + with: + repository: thinca/vim-themis + path: ./vim-themis + ref: v1.5.5 + - name: Install Go for gopls + uses: actions/setup-go@v5 + with: + go-version: ${{ env.VIM_LSP_GO_VERSION }} + - name: Install gopls + shell: bash + run: | + go install golang.org/x/tools/gopls@v${{ env.VIM_LSP_GOPLS_VERSION }} + gopls version + mkdir bin + mv "$(which gopls)" ./bin/ + env: + GO111MODULE: 'on' + - name: Run tests + shell: bash + run: | + export PATH=./vim-themis/bin:$PATH + export PATH=./bin:$PATH + export THEMIS_VIM=vim + vim --version + themis diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml new file mode 100644 index 00000000..48f5b082 --- /dev/null +++ b/.github/workflows/reviewdog.yml @@ -0,0 +1,22 @@ +name: reviewdog + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + vimlint: + name: runner / vint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: vint + uses: reviewdog/action-vint@v1 + with: + github_token: ${{ secrets.github_token }} + level: error + reporter: github-pr-review diff --git a/.github/workflows/windows_neovim.yml b/.github/workflows/windows_neovim.yml new file mode 100644 index 00000000..e0a3e60a --- /dev/null +++ b/.github/workflows/windows_neovim.yml @@ -0,0 +1,87 @@ +name: windows_neovim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + VIM_LSP_GO_VERSION: '1.17' + VIM_LSP_GOPLS_VERSION: '0.7.3' + VIM_LSP_GOPLS_CACHE_VER: 1 + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [windows-latest] + name: [neovim-v04-x64, neovim-v05-x64, neovim-nightly-x64] + include: + - name: neovim-v04-x64 + os: windows-latest + neovim_version: v0.4.4 + neovim_arch: win64 + allow_failure: false + - name: neovim-v05-x64 + os: windows-latest + neovim_version: v0.5.1 + neovim_arch: win64 + allow_failure: false + - name: neovim-nightly-x64 + os: windows-latest + neovim_version: nightly + neovim_arch: win64 + allow_failure: true + runs-on: ${{matrix.os}} + continue-on-error: ${{matrix.allow_failure}} + steps: + - uses: actions/checkout@v4 + - name: Download neovim + shell: PowerShell + run: Invoke-WebRequest -Uri https://github.com/neovim/neovim/releases/download/${{matrix.neovim_version}}/nvim-${{matrix.neovim_arch}}.zip -OutFile neovim.zip + continue-on-error: ${{matrix.allow_failure}} + - name: Extract neovim + shell: PowerShell + run: Expand-Archive -Path neovim.zip -DestinationPath $env:USERPROFILE + continue-on-error: ${{matrix.allow_failure}} + - name: Cache gopls + id: cache-gopls + uses: actions/cache@v4 + with: + path: bin/gopls + key: ${{ runner.os }}-${{ env.VIM_LSP_GO_VERSION }}-${{ env.VIM_LSP_GOPLS_VERSION }}-${{ env.VIM_LSP_GOPLS_CACHE_VER }}-gopls + - name: Install Go for gopls + if: steps.cache-gopls.outputs.cache-hit != 'true' + uses: actions/setup-go@v5 + with: + go-version: ${{ env.VIM_LSP_GO_VERSION }} + - name: Install gopls + if: steps.cache-gopls.outputs.cache-hit != 'true' + shell: bash + run: | + go install golang.org/x/tools/gopls@v${{ env.VIM_LSP_GOPLS_VERSION }} + gopls version + mkdir bin + mv "$(which gopls)" ./bin/ + env: + GO111MODULE: 'on' + - name: Download test runner + uses: actions/checkout@v4 + with: + repository: thinca/vim-themis + path: ./vim-themis + ref: v1.5.5 + - name: Run tests + shell: cmd + run: | + SET PATH=%USERPROFILE%\Neovim\bin;%PATH%; + SET PATH=.\vim-themis\bin;%PATH%; + SET PATH=.\bin;%PATH%; + SET THEMIS_VIM=nvim + nvim --version + themis + continue-on-error: ${{matrix.allow_failure}} diff --git a/.github/workflows/windows_vim.yml b/.github/workflows/windows_vim.yml new file mode 100644 index 00000000..b92b7419 --- /dev/null +++ b/.github/workflows/windows_vim.yml @@ -0,0 +1,87 @@ +name: windows_vim + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + VIM_LSP_GO_VERSION: '1.17' + VIM_LSP_GOPLS_VERSION: '0.7.3' + VIM_LSP_GOPLS_CACHE_VER: 1 + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [windows-latest] + name: [vim-v90-x64, vim-v82-x64, vim-v81-x64, vim-v80-x64] + include: + - name: vim-v90-x64 + os: windows-latest + vim_version: 9.0.0000 + vim_arch: x64 + vim_ver_path: vim90 + - name: vim-v82-x64 + os: windows-latest + vim_version: 8.2.0813 + vim_arch: x64 + vim_ver_path: vim82 + - name: vim-v81-x64 + os: windows-latest + vim_version: 8.1.2414 + vim_arch: x64 + vim_ver_path: vim81 + - name: vim-v80-x64 + os: windows-latest + vim_version: 8.0.1567 + vim_arch: x64 + vim_ver_path: vim80 + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v4 + - name: Download vim + shell: PowerShell + run: Invoke-WebRequest -Uri https://github.com/vim/vim-win32-installer/releases/download/v${{matrix.vim_version}}/gvim_${{matrix.vim_version}}_${{matrix.vim_arch}}.zip -OutFile vim.zip + - name: Extract vim + shell: PowerShell + run: Expand-Archive -Path vim.zip -DestinationPath $env:USERPROFILE + - name: Cache gopls + id: cache-gopls + uses: actions/cache@v4 + with: + path: bin/gopls + key: ${{ runner.os }}-${{ env.VIM_LSP_GO_VERSION }}-${{ env.VIM_LSP_GOPLS_VERSION }}-${{ env.VIM_LSP_GOPLS_CACHE_VER }}-gopls + - name: Install Go for gopls + if: steps.cache-gopls.outputs.cache-hit != 'true' + uses: actions/setup-go@v5 + with: + go-version: ${{ env.VIM_LSP_GO_VERSION }} + - name: Install gopls + if: steps.cache-gopls.outputs.cache-hit != 'true' + shell: bash + run: | + go install golang.org/x/tools/gopls@v${{ env.VIM_LSP_GOPLS_VERSION }} + gopls version + mkdir bin + mv "$(which gopls)" ./bin/ + env: + GO111MODULE: 'on' + - name: Download test runner + uses: actions/checkout@v4 + with: + repository: thinca/vim-themis + path: ./vim-themis + ref: v1.5.5 + - name: Run tests + shell: cmd + run: | + SET PATH=%USERPROFILE%\vim\${{matrix.vim_ver_path}};%PATH%; + SET PATH=.\vim-themis\bin;%PATH%; + SET PATH=.\bin;%PATH%; + vim --version + themis diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6e92f57d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tags diff --git a/.vintrc.yaml b/.vintrc.yaml new file mode 100644 index 00000000..c96daa07 --- /dev/null +++ b/.vintrc.yaml @@ -0,0 +1,10 @@ +cmdargs: + severity: style_problem + +policies: + ProhibitUnusedVariable: + enabled: false + ProhibitImplicitScopeVariable: + enabled: true + ProhibitNoAbortFunction: + enabled: true diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..bf181978 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Prabir Shrestha + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE-THIRD-PARTY b/LICENSE-THIRD-PARTY new file mode 100644 index 00000000..8c97603c --- /dev/null +++ b/LICENSE-THIRD-PARTY @@ -0,0 +1,47 @@ +The vim-lsp source code bundle part of third party's code following which carry +their own copyright notices and license terms: + +* vim-lsc - https://github.com/natebosch/vim-lsc + * autoload/lsc/diff.vim + * autoload/lsc/cursor.vim + ==================================================================== + + Copyright 2017 vim-lsc authors + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +* vim-yggdrasil - https://github.com/m-pilia/vim-yggdrasil + * autoload/utils/tree.vim + ==================================================================== + +Copyright 2019 Martino Pilia + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..f7d16352 --- /dev/null +++ b/README.md @@ -0,0 +1,247 @@ +# vim-lsp [![Gitter](https://badges.gitter.im/vimlsp/community.svg)](https://gitter.im/vimlsp/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +Async [Language Server Protocol](https://github.com/Microsoft/language-server-protocol) plugin for vim8 and neovim. + +# Installing + +Install [vim-plug](https://github.com/junegunn/vim-plug) and then: + +```viml +Plug 'prabirshrestha/vim-lsp' +``` + +__Performance__ + +Certain bottlenecks in Vim script have been implemented in lua. If you would like to take advantage of these performance gains +use vim compiled with lua or neovim v0.4.0+ + +## Registering servers + +```viml +if executable('pylsp') + " pip install python-lsp-server + au User lsp_setup call lsp#register_server({ + \ 'name': 'pylsp', + \ 'cmd': {server_info->['pylsp']}, + \ 'allowlist': ['python'], + \ }) +endif + +function! s:on_lsp_buffer_enabled() abort + setlocal omnifunc=lsp#complete + setlocal signcolumn=yes + if exists('+tagfunc') | setlocal tagfunc=lsp#tagfunc | endif + nmap gd (lsp-definition) + nmap gs (lsp-document-symbol-search) + nmap gS (lsp-workspace-symbol-search) + nmap gr (lsp-references) + nmap gi (lsp-implementation) + nmap gt (lsp-type-definition) + nmap rn (lsp-rename) + nmap [g (lsp-previous-diagnostic) + nmap ]g (lsp-next-diagnostic) + nmap K (lsp-hover) + nnoremap lsp#scroll(+4) + nnoremap lsp#scroll(-4) + + let g:lsp_format_sync_timeout = 1000 + autocmd! BufWritePre *.rs,*.go call execute('LspDocumentFormatSync') + + " refer to doc to add more commands +endfunction + +augroup lsp_install + au! + " call s:on_lsp_buffer_enabled only for languages that has the server registered. + autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled() +augroup END +``` + +Refer to [vim-lsp-settings](https://github.com/mattn/vim-lsp-settings) on how to easily setup language servers using vim-lsp automatically. + +```viml +Plug 'prabirshrestha/vim-lsp' +Plug 'mattn/vim-lsp-settings' +``` + +## auto-complete + +Refer to docs on configuring omnifunc or [asyncomplete.vim](https://github.com/prabirshrestha/asyncomplete.vim). + +## Snippets +vim-lsp does not support snippets by default. If you want snippet integration, you will first have to install a third-party snippet plugin and a plugin that integrates it in vim-lsp. +At the moment, you have following options: +1. [vim-vsnip](https://github.com/hrsh7th/vim-vsnip) together with [vim-vsnip-integ](https://github.com/hrsh7th/vim-vsnip-integ) +2. [UltiSnips](https://github.com/SirVer/ultisnips) together with [vim-lsp-ultisnips](https://github.com/thomasfaingnaert/vim-lsp-ultisnips) +3. [neosnippet.vim](https://github.com/Shougo/neosnippet.vim) together with [vim-lsp-neosnippet](https://github.com/thomasfaingnaert/vim-lsp-neosnippet) + +For more information, refer to the readme and documentation of the respective plugins. + +## Folding + +You can let the language server automatically handle folding for you. To enable this, you have to set `'foldmethod'`, `'foldexpr'` and (optionally) `'foldtext'`: + +```vim +set foldmethod=expr + \ foldexpr=lsp#ui#vim#folding#foldexpr() + \ foldtext=lsp#ui#vim#folding#foldtext() +``` + +If you would like to disable folding globally, you can add this to your configuration: + +```vim +let g:lsp_fold_enabled = 0 +``` + +Also see `:h vim-lsp-folding`. + +## Semantic highlighting +vim-lsp supports the unofficial extension to the LSP protocol for semantic highlighting (https://github.com/microsoft/vscode-languageserver-node/pull/367). +This feature requires Neovim highlights, or Vim with the `textprop` feature enabled. +You will also need to link language server semantic scopes to Vim highlight groups. +Refer to `:h vim-lsp-semantic` for more info. + +## Supported commands + +**Note:** +* Some servers may only support partial commands. +* While it is possible to register multiple servers for the same filetype, some commands will pick only the first server that supports it. For example, it doesn't make sense for rename and format commands to be sent to multiple servers. + +| Command | Description| +|--|--| +|`:LspAddTreeCallHierarchyIncoming`| Find incoming call hierarchy for the symbol under cursor, but add the result to the current list | +|`:LspCallHierarchyIncoming`| Find incoming call hierarchy for the symbol under cursor | +|`:LspCallHierarchyOutgoing`| Find outgoing call hierarchy for the symbol under cursor | +|`:LspCodeAction`| Gets a list of possible commands that can be applied to a file so it can be fixed (quick fix) | +|`:LspCodeLens`| Gets a list of possible commands that can be executed on the current document | +|`:LspDeclaration`| Go to the declaration of the word under the cursor, and open in the current window | +|`:LspDefinition`| Go to the definition of the word under the cursor, and open in the current window | +|`:LspDocumentDiagnostics`| Get current document diagnostics information | +|`:LspDocumentFormat`| Format entire document | +|`:LspDocumentRangeFormat`| Format document selection | +|`:LspDocumentSymbol`| Show document symbols | +|`:LspHover`| Show hover information | +|`:LspImplementation` | Show implementation of interface in the current window | +|`:LspNextDiagnostic`| jump to next diagnostic (all of error, warning, information, hint) | +|`:LspNextError`| jump to next error | +|`:LspNextReference`| jump to next reference to the symbol under cursor | +|`:LspNextWarning`| jump to next warning | +|`:LspPeekDeclaration`| Go to the declaration of the word under the cursor, but open in preview window | +|`:LspPeekDefinition`| Go to the definition of the word under the cursor, but open in preview window | +|`:LspPeekImplementation`| Go to the implementation of an interface, but open in preview window | +|`:LspPeekTypeDefinition`| Go to the type definition of the word under the cursor, but open in preview window | +|`:LspPreviousDiagnostic`| jump to previous diagnostic (all of error, warning, information, hint) | +|`:LspPreviousError`| jump to previous error | +|`:LspPreviousReference`| jump to previous reference to the symbol under cursor | +|`:LspPreviousWarning`| jump to previous warning | +|`:LspReferences`| Find references | +|`:LspRename`| Rename symbol | +|`:LspStatus` | Show the status of the language server | +|`:LspTypeDefinition`| Go to the type definition of the word under the cursor, and open in the current window | +|`:LspTypeHierarchy`| View type hierarchy of the symbol under the cursor | +|`:LspWorkspaceSymbol`| Search/Show workspace symbol | + +### Diagnostics + +Document diagnostics (e.g. warnings, errors) are enabled by default, but if you +preferred to turn them off and use other plugins instead (like +[Neomake](https://github.com/neomake/neomake) or +[ALE](https://github.com/w0rp/ale), set `g:lsp_diagnostics_enabled` to +`0`: + +```viml +let g:lsp_diagnostics_enabled = 0 " disable diagnostics support +``` + +### Highlight references + +Highlight references to the symbol under the cursor (enabled by default). +You can disable it by adding + +```viml +let g:lsp_document_highlight_enabled = 0 +``` + +to your configuration. + +To change the style of the highlighting, you can set or link the `lspReference` +highlight group, e.g.: + +```viml +highlight lspReference ctermfg=red guifg=red ctermbg=green guibg=green +``` + +## Debugging + +In order to enable file logging set `g:lsp_log_file`. + +```vim +let g:lsp_log_verbose = 1 +let g:lsp_log_file = expand('~/vim-lsp.log') + +" for asyncomplete.vim log +let g:asyncomplete_log_file = expand('~/asyncomplete.log') +``` + +You can get detailed status on your servers using `:CheckHealth` with a plugin on vim: + +```vim +if !has('nvim') | Plug 'rhysd/vim-healthcheck' | endif +CheckHealth +``` + +## Tests + +[vim-themis](https://github.com/thinca/vim-themis) is used for testing. To run +integration tests [gopls](https://github.com/golang/tools/tree/master/gopls) +executable must be in path. + +## Maintainers + +- [Prabir Shrestha](https://github.com/prabirshrestha) (author, maintainer) +- [mattn](https://github.com/mattn) (maintainer) +- [hrsh7th](https://github.com/hrsh7th) (maintainer) +- [Thomas Faingnaert](https://github.com/thomasfaingnaert) (maintainer) +- [rhysd](https://github.com/rhysd) (maintainer) + +## Backers + +Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/vim-lsp#backer)] + + + + + + + + + + + + + + + + + + +## Sponsors + +Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor](https://opencollective.com/vim-lsp#sponsor)] + + + + + + + + + + + + + + + + + diff --git a/autoload/health/lsp.vim b/autoload/health/lsp.vim new file mode 100644 index 00000000..726eb780 --- /dev/null +++ b/autoload/health/lsp.vim @@ -0,0 +1,88 @@ +function! s:BuildConfigBlock(section, info) abort + let l:block = get(a:info, a:section, '') + if !empty(l:block) + return printf("### %s\n%s\n", a:section, l:block) + endif + return '' +endf + + +function! health#lsp#check() abort + call s:report_start('server status') + let l:server_status = lsp#collect_server_status() + + let l:has_printed = v:false + for l:k in sort(keys(l:server_status)) + let l:report = l:server_status[l:k] + + let l:status_msg = printf('%s: %s', l:k, l:report.status) + if l:report.status == 'running' + call s:report_ok(l:status_msg) + elseif l:report.status == 'failed' + call health#report_error(l:status_msg, 'See :help g:lsp_log_verbose to debug server failure.') + else + call s:report_warn(l:status_msg) + endif + let l:has_printed = v:true + endfor + + if !l:has_printed + call s:report_warn('no servers connected') + endif + + for l:k in sort(keys(l:server_status)) + call s:report_start(printf('server configuration: %s', l:k)) + let l:report = l:server_status[l:k] + + let l:msg = "\t\n" + let l:msg .= s:BuildConfigBlock('allowlist', l:report.info) + let l:msg .= s:BuildConfigBlock('blocklist', l:report.info) + let l:cfg = get(l:report.info, 'workspace_config', '') + if !empty(l:cfg) + if get(g:, 'loaded_scriptease', 0) + let l:cfg = scriptease#dump(l:cfg, {'width': &columns-1}) + else + let l:cfg = json_encode(l:cfg) + " Add some whitespace to make it readable. + let l:cfg = substitute(l:cfg, '[,{(\[]', "&\n\t", 'g') + let l:cfg = substitute(l:cfg, '":', '& ', 'g') + let l:cfg = substitute(l:cfg, '\v[})\]]+', "\n&", 'g') + let l:cfg = substitute(l:cfg, '\n\s*\n', "\n", 'g') + endif + let l:msg .= printf("### workspace_config\n```json\n%s\n```", l:cfg) + endif + call health#report_info(l:msg) + endfor + + call s:report_start('Performance') + if lsp#utils#has_lua() && g:lsp_use_lua + call s:report_ok('Using lua for faster performance.') + else + call s:report_warn('Missing requirements to enable lua for faster performance.') + endif + +endf + +function! s:report_start(report) abort + if has('nvim-0.10') + call v:lua.vim.health.start(a:report) + else + call health#report_start(a:report) + endif +endf + +function! s:report_warn(report) abort + if has('nvim-0.10') + call v:lua.vim.health.warn(a:report) + else + call health#report_warn(a:report) + endif +endf + +function! s:report_ok(report) abort + if has('nvim-0.10') + call v:lua.vim.health.ok(a:report) + else + call health#report_ok(a:report) + endif +endf diff --git a/autoload/lsp.vim b/autoload/lsp.vim new file mode 100644 index 00000000..dbb9d77b --- /dev/null +++ b/autoload/lsp.vim @@ -0,0 +1,1364 @@ +let s:enabled = 0 +let s:already_setup = 0 +let s:Stream = lsp#callbag#makeSubject() +" workspace_folders = { 'uri': { uri, name } } +let s:servers = {} " { lsp_id, server_info, workspace_folders, init_callbacks, init_result, buffers: { path: { changed_tick } } +let s:last_command_id = 0 +let s:notification_callbacks = [] " { name, callback } + +" This hold previous content for each language servers to make +" DidChangeTextDocumentParams. The key is buffer numbers: +" { +" 1: { +" "golsp": [ "first-line", "next-line", ... ], +" "bingo": [ "first-line", "next-line", ... ] +" }, +" 2: { +" "pylsp": [ "first-line", "next-line", ... ] +" } +" } +let s:file_content = {} + +" do nothing, place it here only to avoid the message +augroup _lsp_silent_ + autocmd! + autocmd User lsp_setup silent + autocmd User lsp_register_server silent + autocmd User lsp_unregister_server silent + autocmd User lsp_server_init silent + autocmd User lsp_server_exit silent + autocmd User lsp_complete_done silent + autocmd User lsp_float_opened silent + autocmd User lsp_float_closed silent + autocmd User lsp_float_focused silent + autocmd User lsp_buffer_enabled silent + autocmd User lsp_diagnostics_updated silent + autocmd User lsp_progress_updated silent +augroup END + +function! lsp#log_verbose(...) abort + if g:lsp_log_verbose + call call(function('lsp#log'), a:000) + endif +endfunction + +function! lsp#log(...) abort + if !empty(g:lsp_log_file) + call writefile([strftime('%c') . ':' . json_encode(a:000)], g:lsp_log_file, 'a') + endif +endfunction + +function! lsp#enable() abort + if s:enabled + return + endif + if !s:already_setup + doautocmd User lsp_setup + let s:already_setup = 1 + endif + let s:enabled = 1 + if g:lsp_signature_help_enabled + call lsp#ui#vim#signature_help#setup() + endif + call lsp#ui#vim#completion#_setup() + call lsp#internal#document_highlight#_enable() + call lsp#internal#diagnostics#_enable() + call lsp#internal#document_code_action#signs#_enable() + call lsp#internal#semantic#_enable() + call lsp#internal#show_message_request#_enable() + call lsp#internal#show_message#_enable() + call lsp#internal#work_done_progress#_enable() + call lsp#internal#completion#documentation#_enable() + call lsp#internal#inlay_hints#_enable() + call s:register_events() +endfunction + +function! lsp#disable() abort + if !s:enabled + return + endif + call lsp#ui#vim#signature_help#_disable() + call lsp#ui#vim#completion#_disable() + call lsp#internal#document_highlight#_disable() + call lsp#internal#diagnostics#_disable() + call lsp#internal#document_code_action#signs#_disable() + call lsp#internal#semantic#_disable() + call lsp#internal#show_message_request#_disable() + call lsp#internal#show_message#_disable() + call lsp#internal#work_done_progress#_disable() + call lsp#internal#completion#documentation#_disable() + call s:unregister_events() + let s:enabled = 0 +endfunction + +function! lsp#get_server_names() abort + return keys(s:servers) +endfunction + +function! lsp#is_valid_server_name(name) abort + return has_key(s:servers, a:name) +endfunction + +function! lsp#get_server_info(server_name) abort + return get(get(s:servers, a:server_name, {}), 'server_info', {}) +endfunction + +function! lsp#get_server_root_uri(server_name) abort + return get(s:servers[a:server_name]['server_info'], '_root_uri_resolved', '') +endfunction + +function! lsp#get_server_capabilities(server_name) abort + let l:server = s:servers[a:server_name] + return has_key(l:server, 'init_result') ? l:server['init_result']['result']['capabilities'] : {} +endfunction + +function! s:server_status(server_name) abort + if !has_key(s:servers, a:server_name) + return 'unknown server' + endif + let l:server = s:servers[a:server_name] + if has_key(l:server, 'exited') + return 'exited' + endif + if has_key(l:server, 'init_callbacks') + return 'starting' + endif + if has_key(l:server, 'failed') + return 'failed' + endif + if has_key(l:server, 'init_result') + return 'running' + endif + return 'not running' +endfunction + +function! lsp#is_server_running(name) abort + if !has_key(s:servers, a:name) + return 0 + endif + + let l:server = s:servers[a:name] + + return has_key(l:server, 'init_result') + \ && !has_key(l:server, 'exited') + \ && !has_key(l:server, 'init_callbacks') + \ && !has_key(l:server, 'failed') +endfunction + +" Returns the current status of all servers (if called with no arguments) or +" the given server (if given an argument). Can be one of "unknown server", +" "exited", "starting", "failed", "running", "not running" +function! lsp#get_server_status(...) abort + if a:0 == 0 + let l:strs = map(keys(s:servers), {k, v -> v . ': ' . s:server_status(v)}) + return join(l:strs, "\n") + else + return s:server_status(a:1) + endif +endfunction + +let s:color_map = { +\ 'exited': 'Error', +\ 'starting': 'MoreMsg', +\ 'failed': 'WarningMsg', +\ 'running': 'Keyword', +\ 'not running': 'Comment' +\} + +" Collect the current status of all servers +function! lsp#collect_server_status() abort + let l:results = {} + for l:k in keys(s:servers) + let l:status = s:server_status(l:k) + " Copy to prevent callers from corrupting our config. + let l:info = deepcopy(s:servers[l:k].server_info) + let l:results[l:k] = { + \ 'status': l:status, + \ 'info': l:info, + \ } + endfor + return l:results +endfunction + +" Print the current status of all servers +function! lsp#print_server_status() abort + for l:k in sort(keys(s:servers)) + let l:status = s:server_status(l:k) + echon l:k . ': ' + exec 'echohl' s:color_map[l:status] + echon l:status + echohl None + echo '' + if &verbose + let l:cfg = { 'workspace_config': s:servers[l:k].server_info.workspace_config } + if get(g:, 'loaded_scriptease', 0) + call scriptease#pp_command(0, -1, l:cfg) + else + echo json_encode(l:cfg) + endif + echo '' + endif + endfor +endfunction + +" @params {server_info} = { +" 'name': 'go-langserver', " required, must be unique +" 'allowlist': ['go'], " optional, array of filetypes to allow, * for all filetypes +" 'blocklist': [], " optional, array of filetypes to block, * for all filetypes, +" 'cmd': {server_info->['go-langserver]} " function that takes server_info and returns array of cmd and args, return empty if you don't want to start the server +" } +function! lsp#register_server(server_info) abort + let l:server_name = a:server_info['name'] + if has_key(s:servers, l:server_name) + call lsp#log('lsp#register_server', 'server already registered', l:server_name) + endif + " NOTE: workspace_folders is dict for faster lookup instead of array + let s:servers[l:server_name] = { + \ 'server_info': a:server_info, + \ 'lsp_id': 0, + \ 'buffers': {}, + \ 'workspace_folders': {}, + \ } + call lsp#log('lsp#register_server', 'server registered', l:server_name) + doautocmd User lsp_register_server +endfunction + +" +" lsp#register_command +" +" @param {command_name} = string +" @param {callback} = funcref +" +function! lsp#register_command(command_name, callback) abort + call lsp#ui#vim#execute_command#_register(a:command_name, a:callback) +endfunction + +function! lsp#register_notifications(name, callback) abort + call add(s:notification_callbacks, { 'name': a:name, 'callback': a:callback }) +endfunction + +function! lsp#unregister_notifications(name) abort + " TODO +endfunction + +function! lsp#stop_server(server_name) abort + if has_key(s:servers, a:server_name) && s:servers[a:server_name]['lsp_id'] > 0 + call lsp#client#stop(s:servers[a:server_name]['lsp_id']) + endif +endfunction + +function! s:register_events() abort + augroup lsp + autocmd! + autocmd BufNewFile * call s:on_text_document_did_open() + autocmd BufReadPost * call s:on_text_document_did_open() + autocmd BufWritePost * call s:on_text_document_did_save() + autocmd BufWinLeave * call s:on_text_document_did_close() + autocmd BufWipeout * call s:on_buf_wipeout(expand('')) + autocmd InsertLeave * call s:on_text_document_did_change() + autocmd TextChanged * call s:on_text_document_did_change() + if exists('##TextChangedP') + autocmd TextChangedP * call s:on_text_document_did_change() + endif + if g:lsp_untitled_buffer_enabled + autocmd FileType * call s:on_filetype_changed(bufnr(expand(''))) + endif + augroup END + + for l:bufnr in range(1, bufnr('$')) + if bufexists(l:bufnr) && bufloaded(l:bufnr) + call s:on_text_document_did_open(l:bufnr) + endif + endfor +endfunction + +function! s:unregister_events() abort + augroup lsp + autocmd! + augroup END + doautocmd User lsp_unregister_server +endfunction + +function! s:on_filetype_changed(buf) abort + call s:on_buf_wipeout(a:buf) + " TODO: stop unused servers + call s:on_text_document_did_open() +endfunction + +function! s:on_text_document_did_open(...) abort + let l:buf = a:0 > 0 ? a:1 : bufnr('%') + if getbufvar(l:buf, '&buftype') ==# 'terminal' | return | endif + if getcmdwintype() !=# '' | return | endif + call lsp#log('s:on_text_document_did_open()', l:buf, &filetype, getcwd(), lsp#utils#get_buffer_uri(l:buf)) + + " Some language server notify diagnostics to the buffer that has not been loaded yet. + " This diagnostics was stored `autoload/lsp/internal/diagnostics/state.vim` but not highlighted. + " So we should refresh highlights when buffer opened. + call lsp#internal#diagnostics#state#_force_notify_buffer(l:buf) + + for l:server_name in lsp#get_allowed_servers(l:buf) + call s:ensure_flush(l:buf, l:server_name, function('s:fire_lsp_buffer_enabled', [l:server_name, l:buf])) + endfor +endfunction + +function! lsp#activate() abort + call s:on_text_document_did_open() +endfunction + +function! s:on_text_document_did_save() abort + let l:buf = bufnr('%') + if getbufvar(l:buf, '&buftype') ==# 'terminal' | return | endif + call lsp#log('s:on_text_document_did_save()', l:buf) + for l:server_name in lsp#get_allowed_servers(l:buf) + if g:lsp_text_document_did_save_delay >= 0 + " We delay the callback by one loop iteration as calls to ensure_flush + " can introduce mmap'd file locks that linger on Windows and collide + " with the second lang server call preventing saves (see #455) + call s:ensure_flush(l:buf, l:server_name, {result->timer_start(g:lsp_text_document_did_save_delay, {timer->s:call_did_save(l:buf, l:server_name, result, function('s:Noop'))})}) + else + call s:ensure_flush(l:buf, l:server_name, {result->s:call_did_save(l:buf, l:server_name, result, function('s:Noop'))}) + endif + endfor +endfunction + +function! s:on_text_document_did_change() abort + let l:buf = bufnr('%') + if getbufvar(l:buf, '&buftype') ==# 'terminal' | return | endif + call lsp#log('s:on_text_document_did_change()', l:buf) + call s:add_didchange_queue(l:buf) +endfunction + +function! s:call_did_save(buf, server_name, result, cb) abort + if lsp#client#is_error(a:result['response']) + return + endif + + let l:server = s:servers[a:server_name] + let l:path = lsp#utils#get_buffer_uri(a:buf) + + let [l:supports_did_save, l:did_save_options] = lsp#capabilities#get_text_document_save_registration_options(a:server_name) + if !l:supports_did_save + let l:msg = s:new_rpc_success('---> ignoring textDocument/didSave. not supported by server', { 'server_name': a:server_name, 'path': l:path }) + call lsp#log(l:msg) + call a:cb(l:msg) + return + endif + + call s:update_file_content(a:buf, a:server_name, lsp#utils#buffer#_get_lines(a:buf)) + + let l:buffers = l:server['buffers'] + let l:buffer_info = l:buffers[l:path] + + let l:params = { + \ 'textDocument': s:get_text_document_identifier(a:buf), + \ } + + if l:did_save_options['includeText'] + let l:params['text'] = s:get_text_document_text(a:buf, a:server_name) + endif + call s:send_notification(a:server_name, { + \ 'method': 'textDocument/didSave', + \ 'params': l:params, + \ }) + + let l:msg = s:new_rpc_success('textDocument/didSave sent', { 'server_name': a:server_name, 'path': l:path }) + call lsp#log(l:msg) + call a:cb(l:msg) +endfunction + +function! s:on_text_document_did_close() abort + let l:buf = bufnr('%') + if getbufvar(l:buf, '&buftype') ==# 'terminal' | return | endif + call lsp#log('s:on_text_document_did_close()', l:buf) +endfunction + +function! s:get_last_file_content(buf, server_name) abort + if has_key(s:file_content, a:buf) && has_key(s:file_content[a:buf], a:server_name) + return s:file_content[a:buf][a:server_name] + endif + return [] +endfunction + +function! s:update_file_content(buf, server_name, new) abort + if !has_key(s:file_content, a:buf) + let s:file_content[a:buf] = {} + endif + call lsp#log('s:update_file_content()', a:buf) + let s:file_content[a:buf][a:server_name] = a:new +endfunction + +function! s:on_buf_wipeout(buf) abort + if has_key(s:file_content, a:buf) + call remove(s:file_content, a:buf) + endif +endfunction + +function! s:ensure_flush_all(buf, server_names) abort + for l:server_name in a:server_names + call s:ensure_flush(a:buf, l:server_name, function('s:Noop')) + endfor +endfunction + +function! s:fire_lsp_buffer_enabled(server_name, buf, ...) abort + if a:buf == bufnr('%') + doautocmd User lsp_buffer_enabled + else + " Not using ++once in autocmd for compatibility of VIM8.0 + let l:cmd = printf('autocmd BufEnter doautocmd User lsp_buffer_enabled', a:buf) + exec printf('augroup _lsp_fire_buffer_enabled | exec "%s | autocmd! _lsp_fire_buffer_enabled BufEnter " | augroup END', l:cmd) + endif +endfunction + +function! s:Noop(...) abort +endfunction + +function! s:is_step_error(s) abort + return lsp#client#is_error(a:s.result[0]['response']) +endfunction + +function! s:throw_step_error(s) abort + call a:s.callback(a:s.result[0]) +endfunction + +function! s:new_rpc_success(message, data) abort + return { + \ 'response': { + \ 'message': a:message, + \ 'data': extend({ '__data__': 'vim-lsp'}, a:data), + \ } + \ } +endfunction + +function! s:new_rpc_error(message, data) abort + return { + \ 'response': { + \ 'error': { + \ 'code': 0, + \ 'message': a:message, + \ 'data': extend({ '__error__': 'vim-lsp'}, a:data), + \ }, + \ } + \ } +endfunction + +function! s:ensure_flush(buf, server_name, cb) abort + call lsp#utils#step#start([ + \ {s->s:ensure_start(a:buf, a:server_name, s.callback)}, + \ {s->s:is_step_error(s) ? s:throw_step_error(s) : s:ensure_init(a:buf, a:server_name, s.callback)}, + \ {s->s:is_step_error(s) ? s:throw_step_error(s) : s:ensure_conf(a:buf, a:server_name, s.callback)}, + \ {s->s:is_step_error(s) ? s:throw_step_error(s) : s:ensure_open(a:buf, a:server_name, s.callback)}, + \ {s->s:is_step_error(s) ? s:throw_step_error(s) : s:ensure_changed(a:buf, a:server_name, s.callback)}, + \ {s->a:cb(s.result[0])} + \ ]) +endfunction + +function! s:ensure_start(buf, server_name, cb) abort + let l:path = lsp#utils#get_buffer_path(a:buf) + + if lsp#utils#is_remote_uri(l:path) || !has_key(s:servers, a:server_name) + let l:msg = s:new_rpc_error('ignoring start server due to remote uri', { 'server_name': a:server_name, 'uri': l:path}) + call lsp#log(l:msg) + call a:cb(l:msg) + return + endif + + let l:server = s:servers[a:server_name] + let l:server_info = l:server['server_info'] + if l:server['lsp_id'] > 0 + let l:msg = s:new_rpc_success('server already started', { 'server_name': a:server_name }) + call lsp#log_verbose(l:msg) + call a:cb(l:msg) + return + endif + + if has_key(l:server_info, 'tcp') + let l:tcp = l:server_info['tcp'](l:server_info) + let l:lsp_id = lsp#client#start({ + \ 'tcp': l:tcp, + \ 'on_stderr': function('s:on_stderr', [a:server_name]), + \ 'on_exit': function('s:on_exit', [a:server_name]), + \ 'on_notification': function('s:on_notification', [a:server_name]), + \ 'on_request': function('s:on_request', [a:server_name]), + \ }) + elseif has_key(l:server_info, 'cmd') + let l:cmd_type = type(l:server_info['cmd']) + if l:cmd_type == v:t_list + let l:cmd = l:server_info['cmd'] + else + let l:cmd = l:server_info['cmd'](l:server_info) + endif + + if empty(l:cmd) + let l:msg = s:new_rpc_error('ignore server start since cmd is empty', { 'server_name': a:server_name }) + call lsp#log(l:msg) + call a:cb(l:msg) + return + endif + + call lsp#log('Starting server', a:server_name, l:cmd) + let l:opts = { + \ 'cmd': l:cmd, + \ 'on_stderr': function('s:on_stderr', [a:server_name]), + \ 'on_exit': function('s:on_exit', [a:server_name]), + \ 'on_notification': function('s:on_notification', [a:server_name]), + \ 'on_request': function('s:on_request', [a:server_name]), + \ } + if has_key(l:server_info, 'env') + let l:opts.env = l:server_info.env + endif + let l:lsp_id = lsp#client#start(l:opts) + endif + + if l:lsp_id > 0 + let l:server['lsp_id'] = l:lsp_id + let l:msg = s:new_rpc_success('started lsp server successfully', { 'server_name': a:server_name, 'lsp_id': l:lsp_id }) + call lsp#log(l:msg) + call a:cb(l:msg) + else + let l:msg = s:new_rpc_error('failed to start server', { 'server_name': a:server_name, 'cmd': l:cmd }) + call lsp#log(l:msg) + call a:cb(l:msg) + endif +endfunction + +function! lsp#default_get_supported_capabilities(server_info) abort + " Sorted alphabetically + return { + \ 'textDocument': { + \ 'callHierarchy': { + \ 'dynamicRegistration': v:false, + \ }, + \ 'codeAction': { + \ 'dynamicRegistration': v:false, + \ 'codeActionLiteralSupport': { + \ 'codeActionKind': { + \ 'valueSet': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'], + \ } + \ }, + \ 'isPreferredSupport': v:true, + \ 'disabledSupport': v:true, + \ }, + \ 'codeLens': { + \ 'dynamicRegistration': v:false, + \ }, + \ 'completion': { + \ 'dynamicRegistration': v:false, + \ 'completionItem': { + \ 'documentationFormat': ['markdown', 'plaintext'], + \ 'snippetSupport': v:false, + \ 'resolveSupport': { + \ 'properties': ['additionalTextEdits'] + \ } + \ }, + \ 'completionItemKind': { + \ 'valueSet': lsp#omni#get_completion_item_kinds() + \ } + \ }, + \ 'declaration': { + \ 'dynamicRegistration': v:false, + \ 'linkSupport' : v:true + \ }, + \ 'definition': { + \ 'dynamicRegistration': v:false, + \ 'linkSupport' : v:true + \ }, + \ 'documentHighlight': { + \ 'dynamicRegistration': v:false, + \ }, + \ 'documentSymbol': { + \ 'dynamicRegistration': v:false, + \ 'symbolKind': { + \ 'valueSet': lsp#ui#vim#utils#get_symbol_kinds() + \ }, + \ 'hierarchicalDocumentSymbolSupport': v:false, + \ 'labelSupport': v:false + \ }, + \ 'foldingRange': { + \ 'dynamicRegistration': v:false, + \ 'lineFoldingOnly': v:true, + \ 'rangeLimit': 5000, + \ }, + \ 'formatting': { + \ 'dynamicRegistration': v:false, + \ }, + \ 'hover': { + \ 'dynamicRegistration': v:false, + \ 'contentFormat': ['markdown', 'plaintext'], + \ }, + \ 'inlayHint': { + \ 'dynamicRegistration': v:false, + \ }, + \ 'implementation': { + \ 'dynamicRegistration': v:false, + \ 'linkSupport' : v:true + \ }, + \ 'publishDiagnostics': { + \ 'relatedInformation': v:true, + \ }, + \ 'rangeFormatting': { + \ 'dynamicRegistration': v:false, + \ }, + \ 'references': { + \ 'dynamicRegistration': v:false, + \ }, + \ 'rename': { + \ 'dynamicRegistration': v:false, + \ 'prepareSupport': v:true, + \ 'prepareSupportDefaultBehavior': 1 + \ }, + \ 'semanticTokens': { + \ 'dynamicRegistration': v:false, + \ 'requests': { + \ 'range': v:false, + \ 'full': lsp#internal#semantic#is_enabled() + \ ? {'delta': v:true} + \ : v:false + \ + \ }, + \ 'tokenTypes': [ + \ 'type', 'class', 'enum', 'interface', 'struct', + \ 'typeParameter', 'parameter', 'variable', 'property', + \ 'enumMember', 'event', 'function', 'method', 'macro', + \ 'keyword', 'modifier', 'comment', 'string', 'number', + \ 'regexp', 'operator' + \ ], + \ 'tokenModifiers': [], + \ 'formats': ['relative'], + \ 'overlappingTokenSupport': v:false, + \ 'multilineTokenSupport': v:false, + \ 'serverCancelSupport': v:false + \ }, + \ 'signatureHelp': { + \ 'dynamicRegistration': v:false, + \ }, + \ 'synchronization': { + \ 'didSave': v:true, + \ 'dynamicRegistration': v:false, + \ 'willSave': v:false, + \ 'willSaveWaitUntil': v:false, + \ }, + \ 'typeDefinition': { + \ 'dynamicRegistration': v:false, + \ 'linkSupport' : v:true + \ }, + \ 'typeHierarchy': { + \ 'dynamicRegistration': v:false + \ }, + \ }, + \ 'window': { + \ 'workDoneProgress': g:lsp_work_done_progress_enabled ? v:true : v:false, + \ }, + \ 'workspace': { + \ 'applyEdit': v:true, + \ 'configuration': v:true, + \ 'symbol': { + \ 'dynamicRegistration': v:false, + \ }, + \ 'workspaceFolders': g:lsp_experimental_workspace_folders ? v:true : v:false, + \ }, + \ } +endfunction + +function! s:ensure_init(buf, server_name, cb) abort + let l:server = s:servers[a:server_name] + + if has_key(l:server, 'init_result') + let l:msg = s:new_rpc_success('lsp server already initialized', { 'server_name': a:server_name, 'init_result': l:server['init_result'] }) + call lsp#log_verbose(l:msg) + call a:cb(l:msg) + return + endif + + if has_key(l:server, 'init_callbacks') + " waiting for initialize response + call add(l:server['init_callbacks'], a:cb) + let l:msg = s:new_rpc_success('waiting for lsp server to initialize', { 'server_name': a:server_name }) + call lsp#log(l:msg) + return + endif + + " server has already started, but not initialized + + let l:server_info = l:server['server_info'] + let l:root_uri = has_key(l:server_info, 'root_uri') ? l:server_info['root_uri'](l:server_info) : '' + if empty(l:root_uri) + let l:msg = s:new_rpc_error('ignore initialization lsp server due to empty root_uri', { 'server_name': a:server_name, 'lsp_id': l:server['lsp_id'] }) + call lsp#log(l:msg) + let l:root_uri = lsp#utils#get_default_root_uri() + endif + let l:server['server_info']['_root_uri_resolved'] = l:root_uri + let l:server['workspace_folders'][l:root_uri] = { 'name': l:root_uri, 'uri': l:root_uri } + + if has_key(l:server_info, 'capabilities') + let l:capabilities = l:server_info['capabilities'] + else + let l:capabilities = call(g:lsp_get_supported_capabilities[0], [l:server_info]) + endif + + let l:request = { + \ 'method': 'initialize', + \ 'params': { + \ 'processId': getpid(), + \ 'clientInfo': { 'name': 'vim-lsp' }, + \ 'capabilities': l:capabilities, + \ 'rootUri': l:root_uri, + \ 'rootPath': lsp#utils#uri_to_path(l:root_uri), + \ 'trace': 'off', + \ }, + \ } + + let l:workspace_capabilities = get(l:capabilities, 'workspace', {}) + if get(l:workspace_capabilities, 'workspaceFolders', v:false) + " TODO: extract folder name for l:root_uri + let l:server_info['workspaceFolders'] = [ + \ { 'uri': l:root_uri, 'name': l:root_uri } + \ ] + let l:request['params']['workspaceFolders'] = l:server_info['workspaceFolders'] + endif + + if has_key(l:server_info, 'initialization_options') + let l:request.params['initializationOptions'] = l:server_info['initialization_options'] + endif + + let l:server['init_callbacks'] = [a:cb] + + call s:send_request(a:server_name, l:request) +endfunction + +function! s:ensure_conf(buf, server_name, cb) abort + let l:server = s:servers[a:server_name] + let l:server_info = l:server['server_info'] + if has_key(l:server_info, 'workspace_config') && !get(l:server_info, '_workspace_config_sent', v:false) + let l:server_info['_workspace_config_sent'] = v:true + call s:send_notification(a:server_name, { + \ 'method': 'workspace/didChangeConfiguration', + \ 'params': { + \ 'settings': lsp#utils#workspace_config#get(a:server_name), + \ } + \ }) + endif + let l:msg = s:new_rpc_success('configuration sent', { 'server_name': a:server_name }) + call lsp#log_verbose(l:msg) + call a:cb(l:msg) +endfunction + +function! s:text_changes(buf, server_name) abort + let l:sync_kind = lsp#capabilities#get_text_document_change_sync_kind(a:server_name) + " When syncKind is None, return null for contentChanges. + if l:sync_kind == 0 + return v:null + endif + + " When syncKind is Incremental and previous content is saved. + if l:sync_kind == 2 && has_key(s:file_content, a:buf) && has_key(s:file_content[a:buf], a:server_name) + " compute diff + let l:old_content = s:get_last_file_content(a:buf, a:server_name) + let l:new_content = lsp#utils#buffer#_get_lines(a:buf) + let l:changes = lsp#utils#diff#compute(l:old_content, l:new_content) + if empty(l:changes.text) && l:changes.rangeLength ==# 0 + return [] + endif + call s:update_file_content(a:buf, a:server_name, l:new_content) + return [l:changes] + endif + + let l:new_content = lsp#utils#buffer#_get_lines(a:buf) + let l:changes = {'text': join(l:new_content, "\n")} + call s:update_file_content(a:buf, a:server_name, l:new_content) + return [l:changes] +endfunction + +function! s:ensure_changed(buf, server_name, cb) abort + let l:server = s:servers[a:server_name] + let l:path = lsp#utils#get_buffer_uri(a:buf) + + let l:buffers = l:server['buffers'] + if !has_key(l:buffers, l:path) + let l:msg = s:new_rpc_success('file is not managed', { 'server_name': a:server_name, 'path': l:path }) + call lsp#log(l:msg) + call a:cb(l:msg) + return + endif + let l:buffer_info = l:buffers[l:path] + + let l:changed_tick = getbufvar(a:buf, 'changedtick') + + if l:buffer_info['changed_tick'] == l:changed_tick + let l:msg = s:new_rpc_success('not dirty', { 'server_name': a:server_name, 'path': l:path }) + call lsp#log_verbose(l:msg) + call a:cb(l:msg) + return + endif + + let l:buffer_info['changed_tick'] = l:changed_tick + let l:buffer_info['version'] = l:buffer_info['version'] + 1 + + call s:send_notification(a:server_name, { + \ 'method': 'textDocument/didChange', + \ 'params': { + \ 'textDocument': s:get_versioned_text_document_identifier(a:buf, l:buffer_info), + \ 'contentChanges': s:text_changes(a:buf, a:server_name), + \ } + \ }) + call lsp#ui#vim#folding#send_request(a:server_name, a:buf, 0) + + let l:msg = s:new_rpc_success('textDocument/didChange sent', { 'server_name': a:server_name, 'path': l:path }) + call lsp#log(l:msg) + call a:cb(l:msg) +endfunction + +function! s:ensure_open(buf, server_name, cb) abort + let l:server = s:servers[a:server_name] + let l:path = lsp#utils#get_buffer_uri(a:buf) + + if empty(l:path) + let l:msg = s:new_rpc_success('ignore open since not a valid uri', { 'server_name': a:server_name, 'path': l:path }) + call lsp#log(l:msg) + call a:cb(l:msg) + return + endif + + let l:buffers = l:server['buffers'] + + if has_key(l:buffers, l:path) + let l:msg = s:new_rpc_success('already opened', { 'server_name': a:server_name, 'path': l:path }) + call lsp#log_verbose(l:msg) + call a:cb(l:msg) + return + endif + + if lsp#capabilities#has_workspace_folders_change_notifications(a:server_name) + call s:workspace_add_folder(a:server_name) + endif + + call s:update_file_content(a:buf, a:server_name, lsp#utils#buffer#_get_lines(a:buf)) + + let l:buffer_info = { 'changed_tick': getbufvar(a:buf, 'changedtick'), 'version': 1, 'uri': l:path } + let l:buffers[l:path] = l:buffer_info + + call s:send_notification(a:server_name, { + \ 'method': 'textDocument/didOpen', + \ 'params': { + \ 'textDocument': s:get_text_document(a:buf, a:server_name, l:buffer_info) + \ }, + \ }) + + call lsp#ui#vim#folding#send_request(a:server_name, a:buf, 0) + + let l:msg = s:new_rpc_success('textDocument/open sent', { 'server_name': a:server_name, 'path': l:path, 'filetype': getbufvar(a:buf, '&filetype') }) + call lsp#log(l:msg) + call a:cb(l:msg) +endfunction + +function! s:workspace_add_folder(server_name) abort + if !g:lsp_experimental_workspace_folders | return | endif + let l:server = s:servers[a:server_name] + let l:server_info = l:server['server_info'] + let l:root_uri = has_key(l:server_info, 'root_uri') ? l:server_info['root_uri'](l:server_info) : lsp#utils#get_default_root_uri() + if !has_key(l:server['workspace_folders'], l:root_uri) + let l:workspace_folder = { 'name': l:root_uri, 'uri': l:root_uri } + call lsp#log('adding workspace folder', a:server_name, l:workspace_folder) + call s:send_notification(a:server_name, { + \ 'method': 'workspace/didChangeWorkspaceFolders', + \ 'params': { + \ 'added': [l:workspace_folder], + \ } + \ }) + let l:server['workspace_folders'][l:root_uri] = l:workspace_folder + endif +endfunction + +function! s:send_request(server_name, data) abort + let l:lsp_id = s:servers[a:server_name]['lsp_id'] + let l:data = copy(a:data) + if has_key(l:data, 'on_notification') + let l:data['on_notification'] = '---funcref---' + endif + call lsp#log_verbose('--->', l:lsp_id, a:server_name, l:data) + return lsp#client#send_request(l:lsp_id, a:data) +endfunction + +function! s:send_notification(server_name, data) abort + let l:lsp_id = s:servers[a:server_name]['lsp_id'] + let l:data = copy(a:data) + if has_key(l:data, 'on_notification') + let l:data['on_notification'] = '---funcref---' + endif + call lsp#log_verbose('--->', l:lsp_id, a:server_name, l:data) + call lsp#client#send_notification(l:lsp_id, a:data) +endfunction + +function! s:send_response(server_name, data) abort + let l:lsp_id = s:servers[a:server_name]['lsp_id'] + let l:data = copy(a:data) + call lsp#log_verbose('--->', l:lsp_id, a:server_name, l:data) + call lsp#client#send_response(l:lsp_id, a:data) +endfunction + +function! s:on_stderr(server_name, id, data, event) abort + call lsp#log_verbose('<---(stderr)', a:id, a:server_name, a:data) +endfunction + +function! s:on_exit(server_name, id, data, event) abort + call lsp#log('s:on_exit', a:id, a:server_name, 'exited', a:data) + if has_key(s:servers, a:server_name) + let l:server = s:servers[a:server_name] + let l:server['lsp_id'] = 0 + let l:server['buffers'] = {} + let l:server['exited'] = 1 + if has_key(l:server, 'init_result') + unlet l:server['init_result'] + endif + let l:server['workspace_folders'] = {} + call lsp#stream(1, { 'server': '$vimlsp', + \ 'response': { 'method': '$/vimlsp/lsp_server_exit', 'params': { 'server': a:server_name } } }) + doautocmd User lsp_server_exit + endif +endfunction + +function! s:on_notification(server_name, id, data, event) abort + call lsp#log_verbose('<---', a:id, a:server_name, a:data) + let l:response = a:data['response'] + let l:server = s:servers[a:server_name] + let l:server_info = l:server['server_info'] + + let l:stream_data = { 'server': a:server_name, 'response': l:response } + if has_key(a:data, 'request') + let l:stream_data['request'] = a:data['request'] + endif + call lsp#stream(1, l:stream_data) " notify stream before callbacks + + if !lsp#client#is_server_instantiated_notification(a:data) + let l:request = a:data['request'] + let l:method = l:request['method'] + if l:method ==# 'initialize' + call s:handle_initialize(a:server_name, a:data) + endif + endif + + for l:callback_info in s:notification_callbacks + call l:callback_info.callback(a:server_name, a:data) + endfor +endfunction + +function! s:on_request(server_name, id, request) abort + call lsp#log_verbose('<---', 's:on_request', a:id, a:request) + + let l:stream_data = { 'server': a:server_name, 'request': a:request } + call lsp#stream(1, l:stream_data) " notify stream before callbacks + + if a:request['method'] ==# 'workspace/applyEdit' + call lsp#utils#workspace_edit#apply_workspace_edit(a:request['params']['edit']) + call s:send_response(a:server_name, { 'id': a:request['id'], 'result': { 'applied': v:true } }) + elseif a:request['method'] ==# 'workspace/configuration' + let l:config = lsp#utils#workspace_config#get(a:server_name) + let l:response_items = map(a:request['params']['items'], { key, val -> lsp#utils#workspace_config#projection(l:config, val) }) + call s:send_response(a:server_name, { 'id': a:request['id'], 'result': l:response_items }) + elseif a:request['method'] ==# 'workspace/workspaceFolders' + let l:server_info = s:servers[a:server_name]['server_info'] + if has_key(l:server_info, 'workspaceFolders') + call s:send_response(a:server_name, { 'id': a:request['id'], 'result': l:server_info['workspaceFolders']}) + endif + elseif a:request['method'] ==# 'window/workDoneProgress/create' + call s:send_response(a:server_name, { 'id': a:request['id'], 'result': v:null}) + else + " TODO: for now comment this out until we figure out a better solution. + " We need to comment this out so that others outside of vim-lsp can + " hook into the stream and provide their own response. + " " Error returned according to json-rpc specification. + " call s:send_response(a:server_name, { 'id': a:request['id'], 'error': { 'code': -32601, 'message': 'Method not found' } }) + endif +endfunction + +function! s:handle_initialize(server_name, data) abort + let l:response = a:data['response'] + let l:server = s:servers[a:server_name] + + if has_key(l:server, 'exited') + unlet l:server['exited'] + endif + + let l:init_callbacks = l:server['init_callbacks'] + unlet l:server['init_callbacks'] + + if !lsp#client#is_error(l:response) + let l:server['init_result'] = l:response + " Delete cache of trigger chars + if has_key(b:, 'lsp_signature_help_trigger_character') + unlet b:lsp_signature_help_trigger_character + endif + else + let l:server['failed'] = l:response['error'] + call lsp#utils#error('Failed to initialize ' . a:server_name . ' with error ' . l:response['error']['code'] . ': ' . l:response['error']['message']) + endif + + call s:send_notification(a:server_name, { 'method': 'initialized', 'params': {} }) + + for l:Init_callback in l:init_callbacks + call l:Init_callback(a:data) + endfor + + doautocmd User lsp_server_init +endfunction + +function! lsp#get_whitelisted_servers(...) abort + return call(function('lsp#get_allowed_servers'), a:000) +endfunction + +" call lsp#get_allowed_servers() +" call lsp#get_allowed_servers(bufnr('%')) +" call lsp#get_allowed_servers('typescript') +function! lsp#get_allowed_servers(...) abort + if a:0 == 0 + let l:buffer_filetype = &filetype + else + if type(a:1) == type('') + let l:buffer_filetype = a:1 + else + let l:buffer_filetype = getbufvar(a:1, '&filetype') + endif + endif + + " TODO: cache active servers per buffer + let l:active_servers = [] + + for l:server_name in keys(s:servers) + let l:server_info = s:servers[l:server_name]['server_info'] + let l:blocked = 0 + + if has_key(l:server_info, 'blocklist') + let l:blocklistkey = 'blocklist' + else + let l:blocklistkey = 'blacklist' + endif + if has_key(l:server_info, l:blocklistkey) + for l:filetype in l:server_info[l:blocklistkey] + if l:filetype ==? l:buffer_filetype || l:filetype ==# '*' + let l:blocked = 1 + break + endif + endfor + endif + + if l:blocked + continue + endif + + if has_key(l:server_info, 'allowlist') + let l:allowlistkey = 'allowlist' + else + let l:allowlistkey = 'whitelist' + endif + if has_key(l:server_info, l:allowlistkey) + for l:filetype in l:server_info[l:allowlistkey] + if l:filetype ==? l:buffer_filetype || l:filetype ==# '*' + let l:active_servers += [l:server_name] + break + endif + endfor + endif + endfor + + return l:active_servers +endfunction + +function! s:get_text_document_text(buf, server_name) abort + return join(s:get_last_file_content(a:buf, a:server_name), "\n") +endfunction + +function! s:get_text_document(buf, server_name, buffer_info) abort + let l:server = s:servers[a:server_name] + let l:server_info = l:server['server_info'] + let l:language_id = has_key(l:server_info, 'languageId') ? l:server_info['languageId'](l:server_info) : getbufvar(a:buf, '&filetype') + return { + \ 'uri': lsp#utils#get_buffer_uri(a:buf), + \ 'languageId': l:language_id, + \ 'version': a:buffer_info['version'], + \ 'text': s:get_text_document_text(a:buf, a:server_name), + \ } +endfunction + +function! lsp#get_text_document_identifier(...) abort + let l:buf = a:0 > 0 ? a:1 : bufnr('%') + return { 'uri': lsp#utils#get_buffer_uri(l:buf) } +endfunction + +function! lsp#get_position(...) abort + let l:line = line('.') + let l:char = lsp#utils#to_char('%', l:line, col('.')) + return { 'line': l:line - 1, 'character': l:char } +endfunction + +function! s:get_text_document_identifier(buf) abort + return { 'uri': lsp#utils#get_buffer_uri(a:buf) } +endfunction + +function! s:get_versioned_text_document_identifier(buf, buffer_info) abort + return { + \ 'uri': lsp#utils#get_buffer_uri(a:buf), + \ 'version': a:buffer_info['version'], + \ } +endfunction + +" lsp#stream {{{ +" +" example 1: +" +" function! s:on_textDocumentDiagnostics(x) abort +" echom 'Diagnostics for ' . a:x['server'] . ' ' . json_encode(a:x['response']) +" endfunction +" +" au User lsp_setup call lsp#callbag#pipe( +" \ lsp#stream(), +" \ lsp#callbag#filter({x-> has_key(x, 'response') && !has_key(x['response'], 'error') && get(x['response'], 'method', '') == 'textDocument/publishDiagnostics'}), +" \ lsp#callbag#subscribe({ 'next':{x->s:on_textDocumentDiagnostics(x)} }), +" \ ) +" +" example 2: +" call lsp#stream(1, { 'command': 'DocumentFormat' }) +function! lsp#stream(...) abort + if a:0 == 0 + return lsp#callbag#share(s:Stream) + else + call s:Stream(a:1, a:2) + endif +endfunction +" }}} + +" lsp#request {{{ +function! lsp#request(server_name, request) abort + let l:ctx = { + \ 'server_name': a:server_name, + \ 'request': copy(a:request), + \ 'request_id': 0, + \ 'done': 0, + \ 'cancelled': 0, + \ } + return lsp#callbag#create(function('s:request_create', [l:ctx])) +endfunction + +function! s:request_create(ctx, next, error, complete) abort + let a:ctx['next'] = a:next + let a:ctx['error'] = a:error + let a:ctx['complete'] = a:complete + let a:ctx['bufnr'] = get(a:ctx['request'], 'bufnr', bufnr('%')) + let a:ctx['request']['on_notification'] = function('s:request_on_notification', [a:ctx]) + call lsp#utils#step#start([ + \ {s->s:ensure_flush(a:ctx['bufnr'], a:ctx['server_name'], s.callback)}, + \ {s->s:is_step_error(s) ? s:request_error(a:ctx, s.result[0]) : s:request_send(a:ctx) }, + \ ]) + return function('s:request_cancel', [a:ctx]) +endfunction + +function! s:request_send(ctx) abort + if a:ctx['cancelled'] | return | endif " caller already unsubscribed so don't bother sending request + let a:ctx['request_id'] = s:send_request(a:ctx['server_name'], a:ctx['request']) +endfunction + +function! s:request_error(ctx, error) abort + if a:ctx['cancelled'] | return | endif " caller already unsubscribed so don't bother notifying + let a:ctx['done'] = 1 + call a:ctx['error'](a:error) +endfunction + +function! s:request_on_notification(ctx, id, data, event) abort + if a:ctx['cancelled'] | return | endif " caller already unsubscribed so don't bother notifying + let a:ctx['done'] = 1 + call a:ctx['next'](extend({ 'server_name': a:ctx['server_name'] }, a:data)) + call a:ctx['complete']() +endfunction + +function! s:request_cancel(ctx) abort + if a:ctx['cancelled'] | return | endif + let a:ctx['cancelled'] = 1 + if a:ctx['request_id'] <= 0 || a:ctx['done'] | return | endif " we have not made the request yet or request is complete, so nothing to cancel + if lsp#get_server_status(a:ctx['server_name']) !=# 'running' | return | endif " if server is not running we cant send the request + " send the actual cancel request + let a:ctx['dispose'] = lsp#callbag#pipe( + \ lsp#notification(a:ctx['server_name'], { + \ 'method': '$/cancelRequest', + \ 'params': { 'id': a:ctx['request_id'] }, + \ }), + \ lsp#callbag#subscribe({ + \ 'error':{e->s:send_request_dispose(a:ctx)}, + \ 'complete':{->s:send_request_dispose(a:ctx)}, + \ }) + \) +endfunction + +function! lsp#send_request(server_name, request) abort + let l:ctx = { + \ 'server_name': a:server_name, + \ 'request': copy(a:request), + \ 'cb': has_key(a:request, 'on_notification') ? a:request['on_notification'] : function('s:Noop'), + \ } + let l:ctx['dispose'] = lsp#callbag#pipe( + \ lsp#request(a:server_name, a:request), + \ lsp#callbag#subscribe({ + \ 'next':{d->l:ctx['cb'](d)}, + \ 'error':{e->s:send_request_error(l:ctx, e)}, + \ 'complete':{->s:send_request_dispose(l:ctx)}, + \ }) + \) +endfunction + +function! s:send_request_dispose(ctx) abort + " dispose function may not have been created so check before calling + if has_key(a:ctx, 'dispose') + call a:ctx['dispose']() + endif +endfunction + +function! s:send_request_error(ctx, error) abort + call a:ctx['cb'](a:error) + call s:send_request_dispose(a:ctx) +endfunction +" }}} + +" lsp#notification {{{ +function! lsp#notification(server_name, request) abort + return lsp#callbag#lazy(function('s:send_notification', [a:server_name, a:request])) +endfunction +" }}} + +" omnicompletion +function! lsp#complete(...) abort + return call('lsp#omni#complete', a:000) +endfunction + +function! lsp#tagfunc(...) abort + return call('lsp#tag#tagfunc', a:000) +endfunction + +let s:didchange_queue = [] +let s:didchange_timer = -1 + +function! s:add_didchange_queue(buf) abort + if g:lsp_use_event_queue == 0 + for l:server_name in lsp#get_allowed_servers(a:buf) + call s:ensure_flush(a:buf, l:server_name, function('s:Noop')) + endfor + return + endif + if index(s:didchange_queue, a:buf) == -1 + call add(s:didchange_queue, a:buf) + endif + call lsp#log('s:send_didchange_queue() will be triggered') + call timer_stop(s:didchange_timer) + let l:lazy = &updatetime > 1000 ? &updatetime : 1000 + let s:didchange_timer = timer_start(l:lazy, function('s:send_didchange_queue')) +endfunction + +function! s:send_didchange_queue(...) abort + call lsp#log('s:send_event_queue()') + for l:buf in s:didchange_queue + if !bufexists(l:buf) + continue + endif + for l:server_name in lsp#get_allowed_servers(l:buf) + call s:ensure_flush(l:buf, l:server_name, function('s:Noop')) + endfor + endfor + let s:didchange_queue = [] +endfunction + +function! lsp#enable_diagnostics_for_buffer(...) abort + let l:bufnr = a:0 > 0 ? a:1 : bufnr('%') + call lsp#internal#diagnostics#state#_enable_for_buffer(l:bufnr) +endfunction + +function! lsp#disable_diagnostics_for_buffer(...) abort + let l:bufnr = a:0 > 0 ? a:1 : bufnr('%') + call lsp#internal#diagnostics#state#_disable_for_buffer(l:bufnr) +endfunction + +" Return dict with diagnostic counts for current buffer +" { 'error': 1, 'warning': 0, 'information': 0, 'hint': 0 } +function! lsp#get_buffer_diagnostics_counts() abort + return lsp#internal#diagnostics#state#_get_diagnostics_count_for_buffer(bufnr('%')) +endfunction + +" Return first error line or v:null if there are no errors +function! lsp#get_buffer_first_error_line() abort + return lsp#internal#diagnostics#first_line#get_first_error_line({'bufnr': bufnr('%')}) +endfunction + +" Return UI list with window/workDoneProgress +" The list is most recently update order. +" [{ 'server': 'clangd', 'token': 'backgroundIndexProgress', 'title': 'indexing', 'messages': '50/100', 'percentage': 50 }, +" { 'server': 'rust-analyzer', 'token': 'rustAnalyzer/indexing', 'title': 'indexing', 'messages': '9/262 (std)', 'percentage': 3 }] +" 'percentage': 0 - 100 or not exist +function! lsp#get_progress() abort + return lsp#internal#work_done_progress#get_progress() +endfunction + +function! lsp#document_hover_preview_winid() abort + return lsp#internal#document_hover#under_cursor#getpreviewwinid() +endfunction + +" +" Scroll vim-lsp related windows. +" +" NOTE: This method can be used to mapping. +" +function! lsp#scroll(count) abort + let l:ctx = {} + function! l:ctx.callback(count) abort + let l:Window = vital#lsp#import('VS.Vim.Window') + for l:winid in l:Window.find({ winid -> l:Window.is_floating(winid) }) + call l:Window.scroll(l:winid, l:Window.info(l:winid).topline + a:count) + endfor + endfunction + call timer_start(0, { -> l:ctx.callback(a:count) }) + return "\" +endfunction + +function! s:merge_dict(dict_old, dict_new) abort + for l:key in keys(a:dict_new) + if has_key(a:dict_old, l:key) && type(a:dict_old[l:key]) == v:t_dict && type(a:dict_new[l:key]) == v:t_dict + call s:merge_dict(a:dict_old[l:key], a:dict_new[l:key]) + else + let a:dict_old[l:key] = a:dict_new[l:key] + endif + endfor +endfunction + +function! lsp#update_workspace_config(server_name, workspace_config) abort + let l:server = s:servers[a:server_name] + let l:server_info = l:server['server_info'] + if has_key(l:server_info, 'workspace_config') + if type(l:server_info['workspace_config']) == v:t_func + call lsp#utils#error('''workspace_config'' is a function, so + \ lsp#update_workspace_config() can not be used. Either + \ replace function with a dictionary, or adjust the value + \ generated by the function as necessary.') + return + endif + call s:merge_dict(l:server_info['workspace_config'], a:workspace_config) + else + let l:server_info['workspace_config'] = a:workspace_config + endif + let l:server_info['_workspace_config_sent'] = v:false + call s:ensure_conf(bufnr('%'), a:server_name, function('s:Noop')) +endfunction + +function! lsp#server_complete(lead, line, pos) abort + return filter(sort(keys(s:servers)), 'stridx(v:val, a:lead)==0 && has_key(s:servers[v:val], "init_result")') +endfunction + +function! lsp#server_complete_running(lead, line, pos) abort + let l:all_servers = sort(keys(s:servers)) + return filter(l:all_servers, {idx, name -> + \ stridx(name, a:lead) == 0 && lsp#is_server_running(name) + \ }) +endfunction + +function! lsp#_new_command() abort + let s:last_command_id += 1 + call lsp#stream(1, { 'command': 1 }) + return s:last_command_id +endfunction + +function! lsp#_last_command() abort + return s:last_command_id +endfunction diff --git a/autoload/lsp/callbag.vim b/autoload/lsp/callbag.vim new file mode 100644 index 00000000..f08c4e2b --- /dev/null +++ b/autoload/lsp/callbag.vim @@ -0,0 +1,1700 @@ +" https://github.com/prabirshrestha/callbag.vim#82f96a7d97342fbf0286e6578b65a60f2bc1ce33 +" :CallbagEmbed path=autoload/lsp/callbag.vim namespace=lsp#callbag + +let s:undefined_token = '__callbag_undefined__' +let s:str_type = type('') + +function! lsp#callbag#undefined() abort + return s:undefined_token +endfunction + +function! lsp#callbag#isUndefined(d) abort + return type(a:d) == s:str_type && a:d ==# s:undefined_token +endfunction + +function! s:noop(...) abort +endfunction + +function! s:createArrayWithSize(size, defaultValue) abort + let l:i = 0 + let l:array = [] + while l:i < a:size + call add(l:array, a:defaultValue) + let l:i = l:i + 1 + endwhile + return l:array +endfunction + +" pipe() {{{ +function! lsp#callbag#pipe(...) abort + let l:Res = a:1 + let l:i = 1 + while l:i < a:0 + let l:Res = a:000[l:i](l:Res) + let l:i = l:i + 1 + endwhile + return l:Res +endfunction +" }}} + +" operate() {{{ +function! lsp#callbag#operate(...) abort + let l:data = { 'cbs': a:000 } + return function('s:operateFactory', [l:data]) +endfunction + +function! s:operateFactory(data, src) abort + let l:Res = a:src + let l:n = len(a:data['cbs']) + let l:i = 0 + while l:i < l:n + let l:Res = a:data['cbs'][l:i](l:Res) + let l:i = l:i + 1 + endwhile + return l:Res +endfunction +" }}} + +" makeSubject() {{{ +function! lsp#callbag#makeSubject() abort + let l:data = { 'sinks': [] } + return function('s:makeSubjectFactory', [l:data]) +endfunction + +function! s:makeSubjectFactory(data, t, d) abort + if a:t == 0 + let l:Sink = a:d + call add(a:data['sinks'], l:Sink) + call l:Sink(0, function('s:makeSubjectSinkCallback', [a:data, l:Sink])) + else + let l:zinkz = copy(a:data['sinks']) + let l:i = 0 + let l:n = len(l:zinkz) + while l:i < l:n + let l:Sink = l:zinkz[l:i] + let l:j = -1 + let l:found = 0 + for l:Item in a:data['sinks'] + let l:j += 1 + if l:Item == l:Sink + let l:found = 1 + break + endif + endfor + + if l:found + call l:Sink(a:t, a:d) + endif + let l:i += 1 + endwhile + endif +endfunction + +function! s:makeSubjectSinkCallback(data, Sink, t, d) abort + if a:t == 2 + let l:i = -1 + let l:found = 0 + for l:Item in a:data['sinks'] + let l:i += 1 + if l:Item == a:Sink + let l:found = 1 + break + endif + endfor + if l:found + call remove(a:data['sinks'], l:i) + endif + endif +endfunction +" }}} + +" create() {{{ +function! lsp#callbag#create(...) abort + let l:data = {} + if a:0 > 0 + let l:data['prod'] = a:1 + endif + return function('s:createProd', [l:data]) +endfunction + +function! s:createProd(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + if !has_key(a:data, 'prod') || type(a:data['prod']) != type(function('s:noop')) + call a:sink(0, function('s:noop')) + call a:sink(2, lsp#callbag#undefined()) + return + endif + let a:data['end'] = 0 + call a:sink(0, function('s:createSinkCallback', [a:data])) + if a:data['end'] | return | endif + let a:data['clean'] = a:data['prod'](function('s:createNext', [a:data]), function('s:createError', [a:data]), function('s:createComplete', [a:data])) +endfunction + +function! s:createSinkCallback(data, t, ...) abort + if !a:data['end'] + let a:data['end'] = (a:t == 2) + if a:data['end'] && has_key(a:data, 'clean') && type(a:data['clean']) == type(function('s:noop')) + call a:data['clean']() + endif + endif +endfunction + +function! s:createNext(data, d) abort + if !a:data['end'] | call a:data['sink'](1, a:d) | endif +endfunction + +function! s:createError(data, e) abort + if !a:data['end'] && !lsp#callbag#isUndefined(a:e) + let a:data['end'] = 1 + call a:data['sink'](2, a:e) + endif +endfunction + +function! s:createComplete(data) abort + if !a:data['end'] + let a:data['end'] = 1 + call a:data['sink'](2, lsp#callbag#undefined()) + endif +endfunction +" }}} + +" lazy() {{{ +function! lsp#callbag#lazy(F) abort + let l:data = { 'F': a:F } + return function('s:lazyFactory', [l:data]) +endfunction + +function! s:lazyFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['unsubed'] = 0 + call a:data['sink'](0, function('s:lazySinkCallback', [a:data])) + call a:data['sink'](1, a:data['F']()) + if !a:data['unsubed'] | call a:data['sink'](2, lsp#callbag#undefined()) | endif +endfunction + +function! s:lazySinkCallback(data, t, d) abort + if a:t == 2 | let a:data['unsubed'] = 1 | endif +endfunction +" }}} + +" empty() {{{ +function! lsp#callbag#empty() abort + let l:data = {} + return function('s:emptyStart', [l:data]) +endfunction + +function! s:emptyStart(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['disposed'] = 0 + call a:sink(0, function('s:emptySinkCallback', [a:data])) + if a:data['disposed'] | return | endif + call a:sink(2, lsp#callbag#undefined()) +endfunction + +function! s:emptySinkCallback(data, t, ...) abort + if a:t != 2 | return | endif + let a:data['disposed'] = 1 +endfunction + +function! s:empty_sink_callback(data, t, ...) abort + if a:t == 2 | call timer_stop(a:data['timer']) | endif +endfunction +" }}} + +" never() {{{ +function! lsp#callbag#never() abort + return function('s:never') +endfunction + +function! s:never(start, sink) abort + if a:start != 0 | return | endif + call a:sink(0, function('s:noop')) +endfunction +" }}} + +" forEach() {{{ +function! lsp#callbag#forEach(operation) abort + let l:data = { 'operation': a:operation } + return function('s:forEachOperation', [l:data]) +endfunction + +function! s:forEachOperation(data, source) abort + return a:source(0, function('s:forEachOperationSource', [a:data])) +endfunction + +function! s:forEachOperationSource(data, t, d) abort + if a:t == 0 | let a:data['talkback'] = a:d | endif + if a:t == 1 | call a:data['operation'](a:d) | endif + if (a:t == 1 || a:t == 0) | call a:data['talkback'](1, lsp#callbag#undefined()) | endif +endfunction +" }}} + +" tap() {{{ +function! lsp#callbag#tap(...) abort + let l:data = {} + if a:0 > 0 && type(a:1) == type({}) " a:1 { next, error, complete } + if has_key(a:1, 'next') | let l:data['next'] = a:1['next'] | endif + if has_key(a:1, 'error') | let l:data['error'] = a:1['error'] | endif + if has_key(a:1, 'complete') | let l:data['complete'] = a:1['complete'] | endif + else " a:1 = next, a:2 = error, a:3 = complete + if a:0 >= 1 | let l:data['next'] = a:1 | endif + if a:0 >= 2 | let l:data['error'] = a:2 | endif + if a:0 >= 3 | let l:data['complete'] = a:3 | endif + endif + return function('s:tapFactory', [l:data]) +endfunction + +function! s:tapFactory(data, source) abort + let a:data['source'] = a:source + return function('s:tapSouceFactory', [a:data]) +endfunction + +function! s:tapSouceFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + call a:data['source'](0, function('s:tapSourceCallback', [a:data])) +endfunction + +function! s:tapSourceCallback(data, t, d) abort + if a:t == 1 && has_key(a:data, 'next') | call a:data['next'](a:d) | endif + if a:t == 2 && lsp#callbag#isUndefined(a:d) && has_key(a:data, 'complete') | call a:data['complete']() | endif + if a:t == 2 && !lsp#callbag#isUndefined(a:d) && has_key(a:data, 'error') | call a:data['error'](a:d) | endif + call a:data['sink'](a:t, a:d) +endfunction +" }}} + +" interval() {{{ +function! lsp#callbag#interval(period) abort + let l:data = { 'period': a:period } + return function('s:intervalPeriod', [l:data]) +endfunction + +function! s:intervalPeriod(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['i'] = 0 + let a:data['sink'] = a:sink + let a:data['timer'] = timer_start(a:data['period'], function('s:interval_callback', [a:data]), { 'repeat': -1 }) + call a:sink(0, function('s:interval_sink_callback', [a:data])) +endfunction + +function! s:interval_callback(data, ...) abort + let l:i = a:data['i'] + let a:data['i'] = a:data['i'] + 1 + call a:data['sink'](1, l:i) +endfunction + +function! s:interval_sink_callback(data, t, ...) abort + if a:t == 2 | call timer_stop(a:data['timer']) | endif +endfunction +" }}} + +" delay() {{{ +function! lsp#callbag#delay(period) abort + let l:data = { 'period': a:period } + return function('s:delayPeriod', [l:data]) +endfunction + +function! s:delayPeriod(data, source) abort + let a:data['source'] = a:source + return function('s:delayFactory', [a:data]) +endfunction + +function! s:delayFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + call a:data['source'](0, function('s:delaySourceCallback', [a:data])) +endfunction + +function! s:delaySourceCallback(data, t, d) abort + if a:t != 1 + call a:data['sink'](a:t, a:d) + return + endif + let a:data['d'] = a:d + call timer_start(a:data['period'], function('s:delayTimerCallback', [a:data])) +endfunction + +function! s:delayTimerCallback(data, ...) abort + call a:data['sink'](1, a:data['d']) +endfunction +" }}} + +" take() {{{ +function! lsp#callbag#take(max) abort + let l:data = { 'max': a:max } + return function('s:takeMax', [l:data]) +endfunction + +function! s:takeMax(data, source) abort + let a:data['source'] = a:source + return function('s:takeMaxSource', [a:data]) +endfunction + +function! s:takeMaxSource(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['taken'] = 0 + let a:data['end'] = 0 + let a:data['sink'] = a:sink + let a:data['talkback'] = function('s:takeTalkback', [a:data]) + call a:data['source'](0, function('s:takeSourceCallback', [a:data])) +endfunction + +function! s:takeTalkback(data, t, d) abort + if a:t == 2 + let a:data['end'] = 1 + call a:data['sourceTalkback'](a:t, a:d) + elseif a:data['taken'] < a:data['max'] + call a:data['sourceTalkback'](a:t, a:d) + endif +endfunction + +function! s:takeSourceCallback(data, t, d) abort + if a:t == 0 + let a:data['sourceTalkback'] = a:d + call a:data['sink'](0, a:data['talkback']) + elseif a:t == 1 + if a:data['taken'] < a:data['max'] + let a:data['taken'] = a:data['taken'] + 1 + call a:data['sink'](a:t, a:d) + if a:data['taken'] == a:data['max'] && !a:data['end'] + let a:data['end'] = 1 + call a:data['sink'](2, lsp#callbag#undefined()) + call a:data['sourceTalkback'](2, lsp#callbag#undefined()) + endif + endif + else + call a:data['sink'](a:t, a:d) + endif +endfunction +" }}} + +" skip() {{{ +function! lsp#callbag#skip(max) abort + let l:data = { 'max': a:max } + return function('s:skipMax', [l:data]) +endfunction + +function! s:skipMax(data, source) abort + let a:data['source'] = a:source + return function('s:skipMaxSource', [a:data]) +endfunction + +function! s:skipMaxSource(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['skipped'] = 0 + call a:data['source'](0, function('s:skipSouceCallback', [a:data])) +endfunction + +function! s:skipSouceCallback(data, t, d) abort + if a:t == 0 + let a:data['talkback'] = a:d + call a:data['sink'](a:t, a:d) + elseif a:t == 1 + if a:data['skipped'] < a:data['max'] + let a:data['skipped'] = a:data['skipped'] + 1 + call a:data['talkback'](1, lsp#callbag#undefined()) + else + call a:data['sink'](a:t, a:d) + endif + else + call a:data['sink'](a:t, a:d) + endif +endfunction +" }}} + +" map() {{{ +function! lsp#callbag#map(F) abort + let l:data = { 'f': a:F } + return function('s:mapF', [l:data]) +endfunction + +function! s:mapF(data, source) abort + let a:data['source'] = a:source + return function('s:mapFSource', [a:data]) +endfunction + +function! s:mapFSource(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + call a:data['source'](0, function('s:mapFSourceCallback', [a:data])) +endfunction + +function! s:mapFSourceCallback(data, t, d) abort + call a:data['sink'](a:t, a:t == 1 ? a:data['f'](a:d) : a:d) +endfunction +" }}} + +" filter() {{{ +function! lsp#callbag#filter(condition) abort + let l:data = { 'condition': a:condition } + return function('s:filterCondition', [l:data]) +endfunction + +function! s:filterCondition(data, source) abort + let a:data['source'] = a:source + return function('s:filterConditionSource', [a:data]) +endfunction + +function! s:filterConditionSource(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + call a:data['source'](0, function('s:filterSourceCallback', [a:data])) +endfunction + +function! s:filterSourceCallback(data, t, d) abort + if a:t == 0 + let a:data['talkback'] = a:d + call a:data['sink'](a:t, a:d) + elseif a:t == 1 + if a:data['condition'](a:d) + call a:data['sink'](a:t, a:d) + else + call a:data['talkback'](1, lsp#callbag#undefined()) + endif + else + call a:data['sink'](a:t, a:d) + endif +endfunction +" }}} + +" fromEvent() {{{ +let s:event_prefix_index = 0 +function! lsp#callbag#fromEvent(events, ...) abort + let l:data = { 'events': a:events } + if a:0 > 0 + let l:data['augroup'] = a:1 + else + let l:data['augroup'] = '__callbag_fromEvent_prefix_' . s:event_prefix_index . '__' + let s:event_prefix_index = s:event_prefix_index + 1 + endif + return function('s:fromEventFactory', [l:data]) +endfunction + +let s:event_handler_index = 0 +let s:event_handlers_data = {} +function! s:fromEventFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['disposed'] = 0 + let a:data['handler'] = function('s:fromEventHandlerCallback', [a:data]) + let a:data['handler_index'] = s:event_handler_index + let s:event_handler_index = s:event_handler_index + 1 + call a:sink(0, function('s:fromEventSinkHandler', [a:data])) + + if a:data['disposed'] | return | endif + let s:event_handlers_data[a:data['handler_index']] = a:data + + execute 'augroup ' . a:data['augroup'] + execute 'autocmd!' + let l:events = type(a:data['events']) == type('') ? [a:data['events']] : a:data['events'] + for l:event in l:events + let l:exec = 'call s:notify_event_handler(' . a:data['handler_index'] . ')' + if type(l:event) == type('') + execute 'au ' . l:event . ' * ' . l:exec + else + execute 'au ' . join(l:event, ' ') .' ' . l:exec + endif + endfor + execute 'augroup end' +endfunction + +function! s:fromEventHandlerCallback(data) abort + " send v:event if it exists + call a:data['sink'](1, lsp#callbag#undefined()) +endfunction + +function! s:fromEventSinkHandler(data, t, ...) abort + if a:t != 2 | return | endif + let a:data['disposed'] = 1 + execute 'augroup ' a:data['augroup'] + autocmd! + execute 'augroup end' + if has_key(s:event_handlers_data, a:data['handler_index']) + call remove(s:event_handlers_data, a:data['handler_index']) + endif +endfunction + +function! s:notify_event_handler(index) abort + let l:data = s:event_handlers_data[a:index] + call l:data['handler']() +endfunction +" }}} + +" fromPromise() {{{ +function! lsp#callbag#fromPromise(promise) abort + let l:data = { 'promise': a:promise } + return function('s:fromPromiseFactory', [l:data]) +endfunction + +function! s:fromPromiseFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['ended'] = 0 + call a:data['promise'].then( + \ function('s:fromPromiseOnFulfilledCallback', [a:data]), + \ function('s:fromPromiseOnRejectedCallback', [a:data]), + \ ) + call a:sink(0, function('s:fromPromiseSinkCallback', [a:data])) +endfunction + +function! s:fromPromiseOnFulfilledCallback(data, ...) abort + if a:data['ended'] | return | endif + call a:data['sink'](1, a:0 > 0 ? a:1 : lsp#callbag#undefined()) + if a:data['ended'] | return | endif + call a:data['sink'](2, lsp#callbag#undefined()) +endfunction + +function! s:fromPromiseOnRejectedCallback(data, err) abort + if a:data['ended'] | return | endif + call a:data['sink'](2, a:err) +endfunction + +function! s:fromPromiseSinkCallback(data, t, ...) abort + if a:t == 2 | let a:data['ended'] = 1 | endif +endfunction +" }}} + +" debounceTime() {{{ +function! lsp#callbag#debounceTime(duration) abort + let l:data = { 'duration': a:duration } + return function('s:debounceTimeDuration', [l:data]) +endfunction + +function! s:debounceTimeDuration(data, source) abort + let a:data['source'] = a:source + return function('s:debounceTimeDurationSource', [a:data]) +endfunction + +function! s:debounceTimeDurationSource(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + call a:data['source'](0, function('s:debounceTimeSourceCallback', [a:data])) +endfunction + +function! s:debounceTimeSourceCallback(data, t, d) abort + if has_key(a:data, 'timer') | call timer_stop(a:data['timer']) | endif + if a:t == 1 + let a:data['timer'] = timer_start(a:data['duration'], function('s:debounceTimeTimerCallback', [a:data, a:d])) + else + call a:data['sink'](a:t, a:d) + endif +endfunction + +function! s:debounceTimeTimerCallback(data, d, ...) abort + call a:data['sink'](1, a:d) +endfunction +" }}} + +" subscribe() {{{ +function! lsp#callbag#subscribe(...) abort + let l:data = {} + if a:0 > 0 && type(a:1) == type({}) " a:1 { next, error, complete } + if has_key(a:1, 'next') | let l:data['next'] = a:1['next'] | endif + if has_key(a:1, 'error') | let l:data['error'] = a:1['error'] | endif + if has_key(a:1, 'complete') | let l:data['complete'] = a:1['complete'] | endif + else " a:1 = next, a:2 = error, a:3 = complete + if a:0 >= 1 | let l:data['next'] = a:1 | endif + if a:0 >= 2 | let l:data['error'] = a:2 | endif + if a:0 >= 3 | let l:data['complete'] = a:3 | endif + endif + return function('s:subscribeListener', [l:data]) +endfunction + +function! s:subscribeListener(data, source) abort + call a:source(0, function('s:subscribeSourceCallback', [a:data])) + return function('s:subscribeDispose', [a:data]) +endfunction + +function! s:subscribeSourceCallback(data, t, d) abort + if a:t == 0 | let a:data['talkback'] = a:d | endif + if a:t == 1 && has_key(a:data, 'next') | call a:data['next'](a:d) | endif + if a:t == 1 || a:t == 0 | call a:data['talkback'](1, lsp#callbag#undefined()) | endif + if a:t == 2 && lsp#callbag#isUndefined(a:d) && has_key(a:data, 'complete') | call a:data['complete']() | endif + if a:t == 2 && !lsp#callbag#isUndefined(a:d) && has_key(a:data, 'error') | call a:data['error'](a:d) | endif +endfunction + +function! s:subscribeDispose(data, ...) abort + if has_key(a:data, 'talkback') | call a:data['talkback'](2, lsp#callbag#undefined()) | endif +endfunction +" }}} + +" toList() {{{ +function! lsp#callbag#toList() abort + let l:data = { 'done': 0, 'items': [], 'unsubscribed': 0 } + return function('s:toListFactory', [l:data]) +endfunction + +function! s:toListFactory(data, source) abort + let a:data['unsubscribe'] = lsp#callbag#subscribe( + \ function('s:toListOnNext', [a:data]), + \ function('s:toListOnError', [a:data]), + \ function('s:toListOnComplete', [a:data]) + \ )(a:source) + if a:data['done'] | call s:toListUnsubscribe(a:data) | endif + return { + \ 'unsubscribe': function('s:toListUnsubscribe', [a:data]), + \ 'wait': function('s:toListWait', [a:data]) + \ } +endfunction + +function! s:toListUnsubscribe(data) abort + if !has_key(a:data, 'unsubscribe') | return | endif + if !a:data['unsubscribed'] + call a:data['unsubscribe']() + let a:data['unsubscribed'] = 1 + if !a:data['done'] + let a:data['done'] = 1 + try + throw 'lsp#callbag toList() is already unsubscribed.' + catch + let a:data['error'] = v:exception . ' ' . v:throwpoint + endtry + endif + endif +endfunction + +function! s:toListOnNext(data, item) abort + call add(a:data['items'], a:item) +endfunction + +function! s:toListOnError(data, error) abort + let a:data['done'] = 1 + let a:data['error'] = a:error + call s:toListUnsubscribe(a:data) +endfunction + +function! s:toListOnComplete(data) abort + let a:data['done'] = 1 + call s:toListUnsubscribe(a:data) +endfunction + +function! s:toListWait(data, ...) abort + if a:data['done'] + if has_key(a:data, 'error') + throw a:data['error'] + else + return a:data['items'] + endif + else + let l:opt = a:0 > 0 ? copy(a:1) : {} + let l:opt['timedout'] = 0 + let l:opt['sleep'] = get(l:opt, 'sleep', 1) + let l:opt['timeout'] = get(l:opt, 'timeout', -1) + + if l:opt['timeout'] > -1 + let l:opt['timer'] = timer_start(l:opt['timeout'], function('s:toListTimeoutCallback', [l:opt])) + endif + + while !a:data['done'] && !l:opt['timedout'] + exec 'sleep ' . l:opt['sleep'] . 'm' + endwhile + + if has_key(l:opt, 'timer') + silent! call timer_stop(l:opt['timer']) + endif + + if l:opt['timedout'] + throw 'lsp#callbag toList().wait() timedout.' + endif + + if has_key(a:data, 'error') + throw a:data['error'] + else + return a:data['items'] + endif + endif +endfunction + +function! s:toListTimeoutCallback(opt, ...) abort + let a:opt['timedout'] = 1 +endfunction +" }}} + +" throwError() {{{ +function! lsp#callbag#throwError(error) abort + let l:data = { 'error': a:error } + return function('s:throwErrorFactory', [l:data]) +endfunction + +function! s:throwErrorFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['disposed'] = 0 + call a:sink(0, function('s:throwErrorSinkCallback', [a:data])) + if a:data['disposed'] | return | endif + call a:sink(2, a:data['error']) +endfunction + +function! s:throwErrorSinkCallback(data, t, ...) abort + if a:t != 2 | return | endif + let a:data['disposed'] = 1 +endfunction +" }}} + +" of() {{{ +function! lsp#callbag#of(...) abort + let l:data = { 'values': a:000 } + return function('s:listFactory', [l:data]) +endfunction +" }}} + +" fromList() {{{ +function! lsp#callbag#fromList(list) abort + let l:data = { 'values': a:list } + return function('s:listFactory', [l:data]) +endfunction + +function! s:listFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['disposed'] = 0 + call a:sink(0, function('s:listSinkCallback', [a:data])) + let l:i = 0 + let l:n = len(a:data['values']) + while l:i < l:n + if a:data['disposed'] | break | endif + call a:sink(1, a:data['values'][l:i]) + let l:i += 1 + endwhile + if a:data['disposed'] | return | endif + call a:sink(2, lsp#callbag#undefined()) +endfunction + + +function! s:listSinkCallback(data, t, ...) abort + if a:t != 2 | return | endif + let a:data['disposed'] = 1 +endfunction +" }}} + +" merge() {{{ +function! lsp#callbag#merge(...) abort + let l:data = { 'sources': a:000 } + return function('s:mergeFactory', [l:data]) +endfunction + +function! s:mergeFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['n'] = len(a:data['sources']) + let a:data['sourceTalkbacks'] = [] + let a:data['startCount'] = 0 + let a:data['endCount'] = 0 + let a:data['ended'] = 0 + let a:data['talkback'] = function('s:mergeTalkbackCallback', [a:data]) + let l:i = 0 + while l:i < a:data['n'] + if a:data['ended'] | return | endif + call a:data['sources'][l:i](0, function('s:mergeSourceCallback', [a:data, l:i])) + let l:i += 1 + endwhile +endfunction + +function! s:mergeTalkbackCallback(data, t, d) abort + if a:t == 2 | let a:data['ended'] = 1 | endif + let l:i = 0 + while l:i < a:data['n'] + if l:i < len(a:data['sourceTalkbacks']) && a:data['sourceTalkbacks'][l:i] != 0 + call a:data['sourceTalkbacks'][l:i](a:t, a:d) + endif + let l:i += 1 + endwhile +endfunction + +function! s:mergeSourceCallback(data, i, t, d) abort + if a:t == 0 + call insert(a:data['sourceTalkbacks'], a:d, a:i) + let a:data['startCount'] += 1 + if a:data['startCount'] == 1 | call a:data['sink'](0, a:data['talkback']) | endif + elseif a:t == 2 && !lsp#callbag#isUndefined(a:d) + let a:data['ended'] = 1 + let l:j = 0 + while l:j < a:data['n'] + if l:j != a:i && l:j < len(a:data['sourceTalkbacks']) && a:data['sourceTalkbacks'][l:j] != 0 + call a:data['sourceTalkbacks'][l:j](2, lsp#callbag#undefined()) + endif + let l:j += 1 + endwhile + call a:data['sink'](2, a:d) + elseif a:t == 2 + let a:data['sourceTalkbacks'][a:i] = 0 + let a:data['endCount'] += 1 + if a:data['endCount'] == a:data['n'] | call a:data['sink'](2, lsp#callbag#undefined()) | endif + else + call a:data['sink'](a:t, a:d) + endif +endfunction +" }}} + +" concat() {{{ +function! lsp#callbag#concat(...) abort + let l:data = { 'sources': a:000 } + return function('s:concatFactory', [l:data]) +endfunction + +let s:concatUniqueToken = '__callback__concat_unique_token__' +function! s:concatFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['n'] = len(a:data['sources']) + if a:data['n'] == 0 + call a:data['sink'](0, function('s:noop')) + call a:data['sink'](2, lsp#callbag#undefined()) + return + endif + let a:data['i'] = 0 + let a:data['lastPull'] = s:concatUniqueToken + let a:data['talkback'] = function('s:concatTalkbackCallback', [a:data]) + let a:data['next'] = function('s:concatNext', [a:data]) + call a:data['next']() +endfunction + +function! s:concatTalkbackCallback(data, t, d) abort + if a:t == 1 | let a:data['lastPull'] = a:d | endif + call a:data['sourceTalkback'](a:t, a:d) +endfunction + +function! s:concatNext(data) abort + if a:data['i'] == a:data['n'] + call a:data['sink'](2, lsp#callbag#undefined()) + return + endif + call a:data['sources'][a:data['i']](0, function('s:concatSourceCallback', [a:data])) +endfunction + +function! s:concatSourceCallback(data, t, d) abort + if a:t == 0 + let a:data['sourceTalkback'] = a:d + if a:data['i'] == 0 + call a:data['sink'](0, a:data['talkback']) + elseif (a:data['lastPull']) != s:concatUniqueToken + call a:data['sourceTalkback'](1, a:data['lastPull']) + endif + elseif a:t == 2 && a:d != lsp#callbag#undefined() + call a:data['sink'](2, a:d) + elseif a:t == 2 + let a:data['i'] = a:data['i'] + 1 + call a:data['next']() + else + call a:data['sink'](a:t, a:d) + endif +endfunction +" }}} + +" combine() {{{ +function! lsp#callbag#combine(...) abort + let l:data = { 'sources': a:000 } + return function('s:combineFactory', [l:data]) +endfunction + +let s:combineEmptyToken = '__callback__combine_empty_token__' +function! s:combineFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['n'] = len(a:data['sources']) + if a:data['n'] == 0 + call a:data['sink'](0, function('s:noop')) + call a:data['sink'](1, []) + call a:data['sink'](2, lsp#callbag#undefined()) + return + endif + let a:data['Ns'] = a:data['n'] " start counter + let a:data['Nd'] = a:data['n'] " data counter + let a:data['Ne'] = a:data['n'] " end counter + let a:data['vals'] = s:createArrayWithSize(a:data['n'], lsp#callbag#undefined()) + let a:data['sourceTalkbacks'] = s:createArrayWithSize(a:data['n'], lsp#callbag#undefined()) + let a:data['talkback'] = function('s:combineTalkbackCallback', [a:data]) + let l:i = 0 + for l:Source in a:data['sources'] + let a:data['vals'][l:i] = s:combineEmptyToken + call l:Source(0, function('s:combineSourceCallback', [a:data, l:i])) + let l:i = l:i + 1 + endfor +endfunction + +function! s:combineTalkbackCallback(data, t, d) abort + if a:t == 0 | return | endif + let l:i = 0 + while l:i < a:data['n'] + call a:data['sourceTalkbacks'][l:i](a:t, a:d) + let l:i = l:i + 1 + endwhile +endfunction + +function! s:combineSourceCallback(data, i, t, d) abort + if a:t == 0 + let a:data['sourceTalkbacks'][a:i] = a:d + let a:data['Ns'] = a:data['Ns'] - 1 + if a:data['Ns'] == 0 | call a:data['sink'](0, a:data['talkback']) | endif + elseif a:t == 1 + if a:data['Nd'] <= 0 + let l:_Nd = 0 + else + if a:data['vals'][a:i] == s:combineEmptyToken + let a:data['Nd'] = a:data['Nd'] - 1 + endif + let l:_Nd = a:data['Nd'] + endif + let a:data['vals'][a:i] = a:d + if l:_Nd == 0 + let l:arr = s:createArrayWithSize(a:data['n'], lsp#callbag#undefined()) + let l:j = 0 + while l:j < a:data['n'] + let l:arr[l:j] = a:data['vals'][l:j] + let l:j = l:j + 1 + endwhile + call a:data['sink'](1, l:arr) + endif + elseif a:t == 2 + let a:data['Ne'] = a:data['Ne'] - 1 + if a:data['Ne'] == 0 + call a:data['sink'](2, lsp#callbag#undefined()) + endif + else + call a:data['sink'](a:t, a:d) + endif +endfunction +" }}} + +" distinctUntilChanged {{{ +function! s:distinctUntilChangedDefaultCompare(a, b) abort + return a:a == a:b +endfunction + +function! lsp#callbag#distinctUntilChanged(...) abort + let l:data = { 'compare': a:0 == 0 ? function('s:distinctUntilChangedDefaultCompare') : a:1 } + return function('s:distinctUntilChangedSourceFactory', [l:data]) +endfunction + +function! s:distinctUntilChangedSourceFactory(data, source) abort + let a:data['source'] = a:source + return function('s:distinctUntilChangedSinkFactory', [a:data]) +endfunction + +function! s:distinctUntilChangedSinkFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['inited'] = 0 + call a:data['source'](0, function('s:distinctUntilChangedSourceCallback', [a:data])) +endfunction + +function! s:distinctUntilChangedSourceCallback(data, t, d) abort + if a:t == 0 | let a:data['talkback'] = a:d | endif + if a:t != 1 + call a:data['sink'](a:t, a:d) + return + endif + + if a:data['inited'] && has_key(a:data, 'prev') && a:data['compare'](a:data['prev'], a:d) + call a:data['talkback'](1, lsp#callbag#undefined()) + return + endif + + let a:data['inited'] = 1 + let a:data['prev'] = a:d + call a:data['sink'](1, a:d) +endfunction +" }}} + +" takeUntil() {{{ +function! lsp#callbag#takeUntil(notfier) abort + let l:data = { 'notifier': a:notfier } + return function('s:takeUntilNotifier', [l:data]) +endfunction + +function! s:takeUntilNotifier(data, source) abort + let a:data['source'] = a:source + return function('s:takeUntilFactory', [a:data]) +endfunction + +let s:takeUntilUniqueToken = '__callback__take_until_unique_token__' + +function! s:takeUntilFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['inited'] = 1 + let a:data['sourceTalkback'] = 0 + let a:data['notiferTalkback'] = 0 + let a:data['done'] = s:takeUntilUniqueToken + call a:data['source'](0, function('s:takeUntilSourceCallback', [a:data])) +endfunction + +function! s:takeUntilSourceCallback(data, t, d) abort + if a:t == 0 + let a:data['sourceTalkback'] = a:d + call a:data['notifier'](0, function('s:takeUntilNotifierCallback', [a:data])) + let a:data['inited'] = 1 + call a:data['sink'](0, function('s:takeUntilSinkCallback', [a:data])) + if a:data['done'] != s:takeUntilUniqueToken | call a:data['sink'](2, a:data['done']) | endif + return + endif + if a:t == 2 + call a:data['notifierTalkback'](2, lsp#callbag#undefined()) + endif + if a:data['done'] == s:takeUntilUniqueToken + call a:data['sink'](a:t, a:d) + endif +endfunction + +function! s:takeUntilNotifierCallback(data, t, d) abort + if a:t == 0 + let a:data['notifierTalkback'] = a:d + call a:data['notifierTalkback'](1, lsp#callbag#undefined()) + return + endif + if a:t == 1 + let a:data['done'] = 0 + call a:data['notifierTalkback'](2, lsp#callbag#undefined()) + call a:data['sourceTalkback'](2, lsp#callbag#undefined()) + if a:data['inited'] | call a:data['sink'](2, lsp#callbag#undefined()) | endif + return + endif + if a:t ==2 + let a:data['notifierTalkback'] = 0 + let a:data['done'] = a:d + if a:d != 0 + call a:data['sourceTalkback'](2, lsp#callbag#undefined()) + if a:data['inited'] | call a:data['sink'](a:t, a:d) | endif + endif + endif +endfunction + +function! s:takeUntilSinkCallback(data, t, d) abort + if a:data['done'] != s:takeUntilUniqueToken | return | endif + if a:t == 2 && has_key(a:data, 'notifierTalkback') && a:data['notifierTalkback'] != 0 | call a:data['notifierTalkback'](2, lsp#callbag#undefined()) | endif + call a:data['sourceTalkback'](a:t, a:d) +endfunction +" }}} + +" takeWhile() {{{ +function! lsp#callbag#takeWhile(predicate) abort + let l:data = { 'predicate': a:predicate } + return function('s:takeWhileFactory', [l:data]) +endfunction + +function! s:takeWhileFactory(data, source) abort + let a:data['source'] = a:source + return function('s:takeWhileSourceFactory', [a:data]) +endfunction + +function! s:takeWhileSourceFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + call a:data['source'](0, function('s:takeWhileSourceCallback', [a:data])) +endfunction + +function! s:takeWhileSourceCallback(data, t, d) abort + if a:t == 0 + let a:data['sourceTalkback'] = a:d + endif + + if a:t == 1 && !a:data['predicate'](a:d) + call a:data['sourceTalkback'](2, lsp#callbag#undefined()) + call a:data['sink'](2, lsp#callbag#undefined()) + return + endif + + call a:data['sink'](a:t, a:d) +endfunction +" }}} + +" group() {{{ +function! lsp#callbag#group(n) abort + let l:data = { 'n': a:n } + return function('s:groupN', [l:data]) +endfunction + +function! s:groupN(data, source) abort + let a:data['source'] = a:source + return function('s:groupFactory', [a:data]) +endfunction + +function! s:groupFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['chunk'] = [] + call a:data['source'](0, function('s:groupSourceCallback', [a:data])) +endfunction + +function! s:groupSourceCallback(data, t, d) abort + if a:t == 0 | let a:data['talkback'] = a:d | endif + if a:t == 1 + call add(a:data['chunk'], a:d) + if len(a:data['chunk']) == a:data['n'] + call a:data['sink'](a:t, remove(a:data['chunk'], 0, a:data['n'] - 1)) + endif + call a:data['talkback'](1, lsp#callbag#undefined()) + else + if a:t == 2 && len(a:data['chunk']) > 0 + call a:data['sink'](1, remove(a:data['chunk'], 0, len(a:data['chunk']) - 1)) + else + call a:data['sink'](a:t, a:d) + endif + endif +endfunction +" }}} + +" flatten() {{{ +function! lsp#callbag#flatten() abort + return function('s:flattenSource') +endfunction + +function! s:flattenSource(source) abort + let l:data = { 'source': a:source } + return function('s:flattenFactory', [l:data]) +endfunction + +function! s:flattenFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['outerEnded'] = 0 + let a:data['outerTalkback'] = 0 + let a:data['innerTalkback'] = 0 + let a:data['talkback'] = function('s:flattenTalkbackCallback', [a:data]) + call a:data['source'](0, function('s:flattenSourceCallback', [a:data])) +endfunction + +function! s:flattenTalkbackCallback(data, t, d) abort + if a:t == 1 + if a:data['innerTalkback'] != 0 + call a:data['innerTalkback'](1, a:d) + else + call a:data['outerTalkback'](1, a:d) + endif + endif + if a:t == 2 + if a:data['innerTalkback'] != 0 | call a:data['innerTalkback'](2, lsp#callbag#undefined()) | endif + call a:data['outerTalkback'](2, lsp#callbag#undefined()) + endif +endfunction + +function! s:flattenSourceCallback(data, t, d) abort + if a:t == 0 + let a:data['outerTalkback'] = a:d + call a:data['sink'](0, a:data['talkback']) + elseif a:t == 1 + let l:InnerSource = a:d + if a:data['innerTalkback'] != 0 | call a:data['innerTalkback'](2, lsp#callbag#undefined()) | endif + call l:InnerSource(0, function('s:flattenInnerSourceCallback', [a:data])) + elseif a:t == 2 && !lsp#callbag#isUndefined(a:d) + if a:data['innerTalkback'] != 0 | call a:data['innerTalkback'](2, lsp#callbag#undefined()) | endif + call a:data['outerTalkback'](1, a:d) + elseif a:t == 2 + if a:data['innerTalkback'] == 0 + call a:data['sink'](2, lsp#callbag#undefined()) + else + let a:data['outerEnded'] = 1 + endif + endif +endfunction + +function! s:flattenInnerSourceCallback(data, t, d) abort + if a:t == 0 + let a:data['innerTalkback'] = a:d + call a:data['innerTalkback'](1, lsp#callbag#undefined()) + elseif a:t == 1 + call a:data['sink'](1, a:d) + elseif a:t == 2 && !lsp#callbag#isUndefined(a:d) + call a:data['outerTalkback'](2, lsp#callbag#undefined()) + call a:data['sink'](2, a:d) + elseif a:t == 2 + if a:data['outerEnded'] != 0 + call a:data['sink'](2, lsp#callbag#undefined()) + else + let a:data['innerTalkback'] = 0 + call a:data['outerTalkback'](1, lsp#callbag#undefined()) + endif + endif +endfunction +" }}} + +" flatMap() {{{ +function! lsp#callbag#flatMap(F) abort + return lsp#callbag#operate( + \ lsp#callbag#map(a:F), + \ lsp#callbag#flatten(), + \ ) +endfunction +" }}} + +" scan() {{{ +function! lsp#callbag#scan(reducer, seed) abort + let l:data = { 'reducer': a:reducer, 'seed': a:seed } + return function('s:scanSource', [l:data]) +endfunction + +function! s:scanSource(data, source) abort + let a:data['source'] = a:source + return function('s:scanFactory', [a:data]) +endfunction + +function! s:scanFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['acc'] = a:data['seed'] + call a:data['source'](0, function('s:scanSourceCallback', [a:data])) +endfunction + +function! s:scanSourceCallback(data, t, d) abort + if a:t == 1 + let a:data['acc'] = a:data['reducer'](a:data['acc'], a:d) + call a:data['sink'](1, a:data['acc']) + else + call a:data['sink'](a:t, a:d) + endif +endfunction +" }}} + +" reduce() {{{ +function! lsp#callbag#reduce(reducer, seed) abort + let l:data = { 'reducer': a:reducer, 'seed': a:seed } + return function('s:reduceSource', [l:data]) +endfunction + +function! s:reduceSource(data, source) abort + let a:data['source'] = a:source + return function('s:reduceFactory', [a:data]) +endfunction + +function! s:reduceFactory(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + let a:data['acc'] = a:data['seed'] + call a:data['source'](0, function('s:reduceSourceCallback', [a:data])) +endfunction + +function! s:reduceSourceCallback(data, t, d) abort + if a:t == 1 + let a:data['acc'] = a:data['reducer'](a:data['acc'], a:d) + elseif a:t == 2 && lsp#callbag#isUndefined(a:d) + call a:data['sink'](1, a:data['acc']) + call a:data['sink'](2, lsp#callbag#undefined()) + else + call a:data['sink'](a:t, a:d) + endif +endfunction +" }}} + +" switchMap() {{{ +function! lsp#callbag#switchMap(makeSource, ...) abort + let l:data = { 'makeSource': a:makeSource } + if a:0 == 1 + let l:data['combineResults'] = a:1 + else + let l:data['combineResults'] = function('s:switchMapDefaultCombineResults') + endif + return function('s:switchMapSourceCallback', [l:data]) +endfunction + +function! s:switchMapDefaultCombineResults(a, b) abort + return a:b +endfunction + +function! s:switchMapSourceCallback(data, inputSource) abort + let a:data['inputSource'] = a:inputSource + return function('s:switchMapFactory', [a:data]) +endfunction + +function! s:switchMapFactory(data, start, outputSink) abort + if a:start != 0 | return | endif + let a:data['outputSink'] = a:outputSink + let a:data['sourceEnded'] = 0 + call a:data['inputSource'](0, function('s:switchMapInputSourceCallback', [a:data])) +endfunction + +function! s:switchMapInputSourceCallback(data, t, d) abort + if a:t == 0 | call a:data['outputSink'](a:t, a:d) | endif + if a:t == 1 + if has_key(a:data, 'currSourceTalkback') + call a:data['currSourceTalkback'](2, lsp#callbag#undefined()) + call remove(a:data, 'currSourceTalkback') + endif + let l:CurrSource = a:data['makeSource'](a:d) + call l:CurrSource(0, function('s:switchMapCurrSourceCallback', [a:data, a:t, a:d])) + endif + if a:t == 2 + let a:data['sourceEnded'] = 1 + if !has_key(a:data, 'currSourceTalkback') | call a:data['outputSink'](a:t, a:d) | endif + endif +endfunction + +function! s:switchMapCurrSourceCallback(data, t, d, currT, currD) abort + if a:currT == 0 | let a:data['currSourceTalkback'] = a:currD | endif + if a:currT == 1 | call a:data['outputSink'](a:t, a:data['combineResults'](a:d, a:currD)) | endif + if (a:currT == 0 || a:currT == 1) && has_key(a:data, 'currSourceTalkback') + call a:data['currSourceTalkback'](1, lsp#callbag#undefined()) + endif + if a:currT == 2 + call remove(a:data, 'currSourceTalkback') + if a:data['sourceEnded'] | call a:data['outputSink'](a:currT, a:currD) | endif + endif +endfunction +" }}} + +" {{{ +function! lsp#callbag#share(source) abort + let l:data = { 'source': a:source, 'sinks': [] } + return function('s:shareFactory', [l:data]) +endfunction + +function! s:shareFactory(data, start, sink) abort + if a:start != 0 | return | endif + call add(a:data['sinks'], a:sink) + + let a:data['talkback'] = function('s:shareTalkbackCallback', [a:data, a:sink]) + + if len(a:data['sinks']) == 1 + call a:data['source'](0, function('s:shareSourceCallback', [a:data, a:sink])) + return + endif + + call a:sink(0, a:data['talkback']) +endfunction + +function! s:shareTalkbackCallback(data, sink, t, d) abort + if a:t == 2 + let l:i = 0 + let l:found = 0 + while l:i < len(a:data['sinks']) + if a:data['sinks'][l:i] == a:sink + let l:found = 1 + break + endif + let l:i += 1 + endwhile + + if l:found + call remove(a:data['sinks'], l:i) + endif + + if empty(a:data['sinks']) + call a:data['sourceTalkback'](2, lsp#callbag#undefined()) + endif + else + call a:data['sourceTalkback'](a:t, a:d) + endif +endfunction + +function! s:shareSourceCallback(data, sink, t, d) abort + if a:t == 0 + let a:data['sourceTalkback'] = a:d + call a:sink(0, a:data['talkback']) + else + for l:S in a:data['sinks'] + call l:S(a:t, a:d) + endfor + endif + if a:t == 2 + let a:data['sinks'] = [] + endif +endfunction +" }}} + +" materialize() {{{ +function! lsp#callbag#materialize() abort + let l:data = {} + return function('s:materializeF', [l:data]) +endfunction + +function! s:materializeF(data, source) abort + let a:data['source'] = a:source + return function('s:materializeFSource', [a:data]) +endfunction + +function! s:materializeFSource(data, start, sink) abort + if a:start != 0 | return | endif + let a:data['sink'] = a:sink + call a:data['source'](0, function('s:materializeFSourceCallback', [a:data])) +endfunction + +function! s:materializeFSourceCallback(data, t, d) abort + if a:t == 1 + call a:data['sink'](1, lsp#callbag#createNextNotification(a:d)) + elseif a:t == 2 + call a:data['sink'](1, lsp#callbag#isUndefined(a:d) + \ ? lsp#callbag#createCompleteNotification() + \ : lsp#callbag#createErrorNotification(a:d)) + call a:data['sink'](2, lsp#callbag#undefined()) + else + call a:data['sink'](a:t, a:d) + endif +endfunction +" }}} + +" Notifications {{{ +function! lsp#callbag#createNextNotification(d) abort + return { 'kind': 'N', 'value': a:d } +endfunction + +function! lsp#callbag#createCompleteNotification() abort + return { 'kind': 'C' } +endfunction + +function! lsp#callbag#createErrorNotification(d) abort + return { 'kind': 'E', 'error': a:d } +endfunction + +function! lsp#callbag#isNextNotification(d) abort + return a:d['kind'] ==# 'N' +endfunction + +function! lsp#callbag#isCompleteNotification(d) abort + return a:d['kind'] ==# 'C' +endfunction + +function! lsp#callbag#isErrorNotification(d) abort + return a:d['kind'] ==# 'E' +endfunction +" }}} + +" spawn {{{ +" let s:Stdin = lsp#callbag#makeSubject() +" call lsp#callbag#spawn(['bash', '-c', 'read i; echo $i'], { +" \ 'stdin': s:Stdin, +" \ 'stdout': 0, +" \ 'stderr': 0, +" \ 'exit': 0, +" \ 'start': 0, " when job starts before subscribing to stdin +" \ 'ready': 0, " when job starts and after subscribing to stdin +" \ 'pid': 0, +" \ 'failOnNonZeroExitCode': 1, +" \ 'failOnStdinError': 1, +" \ 'normalize': 'raw' | 'string' | 'array', (defaults to raw), +" \ 'env': {}, +" \ }) +" call s:Stdin(1, 'hi') +" call s:Stdin(2, lsp#callbag#undefined()) " requried to close stdin +function! lsp#callbag#spawn(cmd, ...) abort + let l:data = { 'cmd': a:cmd, 'opt': a:0 > 0 ? copy(a:000[0]) : {} } + return lsp#callbag#create(function('s:spawnCreate', [l:data])) +endfunction + +function! s:spawnCreate(data, next, error, complete) abort + let a:data['next'] = a:next + let a:data['error'] = a:error + let a:data['complete'] = a:complete + let a:data['state'] = {} + let a:data['dispose'] = 0 + let a:data['exit'] = 0 + let a:data['close'] = 0 + + let l:normalize = get(a:data['opt'], 'normalize', 'raw') + + if has('nvim') + let a:data['jobopt'] = { + \ 'on_exit': function('s:spawnNeovimOnExit', [a:data]), + \ } + if l:normalize ==# 'string' + let a:data['normalize'] = function('s:spawnNormalizeNeovimString') + else + let a:data['normalize'] = function('s:spawnNormalizeRaw') + endif + if get(a:data['opt'], 'stdout', 0) | let a:data['jobopt']['on_stdout'] = function('s:spawnNeovimOnStdout', [a:data]) | endif + if get(a:data['opt'], 'stderr', 0) | let a:data['jobopt']['on_stderr'] = function('s:spawnNeovimOnStderr', [a:data]) | endif + if has_key(a:data['opt'], 'env') | let a:data['jobopt']['env'] = a:data['opt']['env'] | endif + let a:data['jobid'] = jobstart(a:data['cmd'], a:data['jobopt']) + else + let a:data['jobopt'] = { + \ 'exit_cb': function('s:spawnVimExitCb', [a:data]), + \ 'close_cb': function('s:spawnVimCloseCb', [a:data]), + \ } + if get(a:data['opt'], 'stdout', 0) | let a:data['jobopt']['out_cb'] = function('s:spawnVimOutCb', [a:data]) | endif + if get(a:data['opt'], 'stderr', 0) | let a:data['jobopt']['err_cb'] = function('s:spawnVimErrCb', [a:data]) | endif + if has_key(a:data['opt'], 'env') | let a:data['jobopt']['env'] = a:data['opt']['env'] | endif + if l:normalize ==# 'array' + let a:data['normalize'] = function('s:spawnNormalizeVimArray') + else + let a:data['normalize'] = function('s:spawnNormalizeRaw') + endif + if has('patch-8.1.350') | let a:data['jobopt']['noblock'] = 1 | endif + let a:data['stdinBuffer'] = '' + let a:data['job'] = job_start(a:data['cmd'], a:data['jobopt']) + let a:data['jobchannel'] = job_getchannel(a:data['job']) + let a:data['jobid'] = ch_info(a:data['jobchannel'])['id'] + endif + + if a:data['jobid'] < 0 | return | endif " jobstart failed. on_exit will notify with error + + if get(a:data['opt'], 'pid', 0) + if has('nvim') + let a:data['pid'] = jobpid(a:data['jobid']) + let l:startdata['pid'] = a:data['pid'] + else + let l:jobinfo = job_info(a:data['job']) + if type(l:jobinfo) == type({}) && has_key(l:jobinfo, 'process') + let a:data['pid'] = l:jobinfo['process'] + let l:startdata['pid'] = a:data['pid'] + endif + endif + endif + + if get(a:data['opt'], 'start', 0) + let l:startdata = { 'id': a:data['jobid'], 'state': a:data['state'] } + call a:data['next']({ 'event': 'start', 'data': l:startdata }) + endif + + if has_key(a:data['opt'], 'stdin') + let a:data['stdinDispose'] = lsp#callbag#pipe( + \ a:data['opt']['stdin'], + \ lsp#callbag#subscribe({ + \ 'next': (has('nvim') ? function('s:spawnNeovimStdinNext', [a:data]) : function('s:spawnVimStdinNext', [a:data])), + \ 'error': (has('nvim') ? function('s:spawnNeovimStdinError', [a:data]) : function('s:spawnVimStdinError', [a:data])), + \ 'complete': (has('nvim') ? function('s:spawnNeovimStdinComplete', [a:data]) : function('s:spawnVimStdinComplete', [a:data])), + \ }), + \ ) + endif + + if get(a:data['opt'], 'ready', 0) + let l:readydata = { 'id': a:data['jobid'], 'state': a:data['state'] } + if has_key(a:data, 'pid') | let l:readydata['pid'] = a:data['pid'] | endif + call a:data['next']({ 'event': 'ready', 'data': l:readydata }) + endif + + return function('s:spawnDispose', [a:data]) +endfunction + +function! s:spawnJobStop(data) abort + if has('nvim') + try + call jobstop(a:data['jobid']) + catch /^Vim\%((\a\+)\)\=:E900/ + " NOTE: + " Vim does not raise exception even the job has already closed so fail + " silently for 'E900: Invalid job id' exception + endtry + else + call job_stop(a:data['job']) + endif +endfunction + +function! s:spawnDispose(data) abort + let a:data['dispose'] = 1 + call s:spawnJobStop(a:data) +endfunction + +function! s:spawnNeovimStdinNext(data, x) abort + call jobsend(a:data['jobid'], a:x) +endfunction + +function! s:spawnVimStdinNext(data, x) abort + " Ref: https://groups.google.com/d/topic/vim_dev/UNNulkqb60k/discussion + let a:data['stdinBuffer'] .= a:x + call s:spawnVimStdinNextFlushBuffer(a:data) +endfunction + +function! s:spawnVimStdinNextFlushBuffer(data) abort + " https://github.com/vim/vim/issues/2548 + " https://github.com/natebosch/vim-lsc/issues/67#issuecomment-357469091 + sleep 1m + if len(a:data['stdinBuffer']) <= 4096 + call ch_sendraw(a:data['jobchannel'], a:data['stdinBuffer']) + let a:data['stdinBuffer'] = '' + else + let l:to_send = a:data['stdinBuffer'][:4095] + let a:data['stdinBuffer'] = a:data['stdinBuffer'][4096:] + call ch_sendraw(a:data['jobchannel'], l:to_send) + call timer_start(1, function('s:spawnVimStdinNextFlushBuffer', [a:data])) + endif +endfunction + +function! s:spawnNeovimStdinError(data, x) abort + let a:data['stdinError'] = a:x + if get(a:data['opt'], 'failOnStdinError', 1) | call s:spawnJobStop(a:data) | endif +endfunction + +function! s:spawnVimStdinError(data, x) abort + let a:data['stdinError'] = a:x + if get(a:data['opt'], 'failOnStdinError', 1) | call s:spawnJobStop(a:data) | endif +endfunction + +function! s:spawnNeovimStdinComplete(data) abort + call chanclose(a:data['jobid'], 'stdin') +endfunction + +function! s:spawnVimStdinComplete(data) abort + " There is no easy way to know when ch_sendraw() finishes writing data + " on a non-blocking channels -- has('patch-8.1.889') -- and because of + " this, we cannot safely call ch_close_in(). + while len(a:data['stdinBuffer']) != 0 + sleep 1m + endwhile + call ch_close_in(a:data['jobchannel']) +endfunction + +function! s:spawnNormalizeRaw(data) abort + return a:data +endfunction + +function! s:spawnNormalizeNeovimString(data) abort + " convert array to string since neovim uses array split by \n by default + return join(a:data, "\n") +endfunction + +function! s:spawnNormalizeVimArray(data) abort + " convert string to array since vim uses string by default. + return split(a:data, "\n", 1) +endfunction + +function! s:spawnNeovimOnStdout(data, id, d, event) abort + call a:data['next']({ 'event': 'stdout', 'data': a:data['normalize'](a:d), 'state': a:data['state'] }) +endfunction + +function! s:spawnNeovimOnStderr(data, id, d, event) abort + call a:data['next']({ 'event': 'stderr', 'data': a:data['normalize'](a:d), 'state': a:data['state'] }) +endfunction + +function! s:spawnNeovimOnExit(data, id, d, event) abort + let a:data['exit'] = 1 + let a:data['close'] = 1 + let a:data['exitcode'] = a:d + call s:spawnNotifyExit(a:data) +endfunction + +function! s:spawnVimOutCb(data, id, d, ...) abort + call a:data['next']({ 'event': 'stdout', 'data': a:data['normalize'](a:d), 'state': a:data['state'] }) +endfunction + +function! s:spawnVimErrCb(data, id, d, ...) abort + call a:data['next']({ 'event': 'stderr', 'data': a:data['normalize'](a:d), 'state': a:data['state'] }) +endfunction + +function! s:spawnVimExitCb(data, id, d) abort + let a:data['exit'] = 1 + let a:data['exitcode'] = a:d + " for more info refer to :h job-start + " job may exit before we read the output and output may be lost. + " in unix this happens because closing the write end of a pipe + " causes the read end to get EOF. + " close and exit has race condition, so wait for both to complete + if a:data['close'] && a:data['exit'] + call s:spawnNotifyExit(a:data) + endif +endfunction + +function! s:spawnVimCloseCb(data, id) abort + let a:data['close'] = 1 + if a:data['close'] && a:data['exit'] + call s:spawnNotifyExit(a:data) + endif +endfunction + +function! s:spawnNotifyExit(data) abort + if a:data['dispose'] | return | end + if has_key(a:data, 'stdinDispose') | call a:data['stdinDispose']() | endif + if get(a:data['opt'], 'failOnStdinError', 1) && has_key(a:data, 'stdinError') + call a:data['error'](a:data['stdinError']) + return + endif + if get(a:data['opt'], 'exit', 0) + call a:data['next']({ 'event': 'exit', 'data': a:data['exitcode'], 'state': a:data['state'] }) + endif + if get(a:data['opt'], 'failOnNonZeroExitCode', 1) && a:data['exitcode'] != 0 + call a:data['error']('Spawn for job ' . a:data['jobid'] .' failed with exit code ' . a:data['exitcode'] . '. ') + else + call a:data['complete']() + endif +endfunction +" }}} + +" vim:ts=4:sw=4:ai:foldmethod=marker:foldlevel=0: diff --git a/autoload/lsp/capabilities.vim b/autoload/lsp/capabilities.vim new file mode 100644 index 00000000..37453826 --- /dev/null +++ b/autoload/lsp/capabilities.vim @@ -0,0 +1,202 @@ +function! s:has_provider(server_name, ...) abort + let l:value = lsp#get_server_capabilities(a:server_name) + for l:provider in a:000 + if empty(l:value) || type(l:value) != type({}) || !has_key(l:value, l:provider) + return 0 + endif + let l:value = l:value[l:provider] + endfor + return (type(l:value) == type(v:true) && l:value == v:true) || type(l:value) == type({}) +endfunction + +function! lsp#capabilities#has_declaration_provider(server_name) abort + return s:has_provider(a:server_name, 'declarationProvider') +endfunction + +function! lsp#capabilities#has_definition_provider(server_name) abort + return s:has_provider(a:server_name, 'definitionProvider') +endfunction + +function! lsp#capabilities#has_references_provider(server_name) abort + return s:has_provider(a:server_name, 'referencesProvider') +endfunction + +function! lsp#capabilities#has_hover_provider(server_name) abort + return s:has_provider(a:server_name, 'hoverProvider') +endfunction + +function! lsp#capabilities#has_rename_provider(server_name) abort + return s:has_provider(a:server_name, 'renameProvider') +endfunction + +function! lsp#capabilities#has_rename_prepare_provider(server_name) abort + return s:has_provider(a:server_name, 'renameProvider', 'prepareProvider') +endfunction + +function! lsp#capabilities#has_workspace_folders_change_notifications(server_name) abort + let l:capabilities = lsp#get_server_capabilities(a:server_name) + if type(l:capabilities) == type({}) && !empty(l:capabilities) + let l:workspace = get(l:capabilities, 'workspace', {}) + if type(l:workspace) == type({}) && !empty(l:workspace) + let l:workspace_folders = get(l:workspace, 'workspaceFolders', {}) + if type(l:workspace_folders) == type({}) && !empty(l:workspace_folders) + if get(l:workspace_folders, 'supported', v:false) && get(l:workspace_folders, 'changeNotifications', '') ==# 'workspace/didChangeWorkspaceFolders' + return v:true + endif + endif + endif + endif + return v:false +endfunction + +function! lsp#capabilities#has_document_formatting_provider(server_name) abort + return s:has_provider(a:server_name, 'documentFormattingProvider') +endfunction + +function! lsp#capabilities#has_document_range_formatting_provider(server_name) abort + return s:has_provider(a:server_name, 'documentRangeFormattingProvider') +endfunction + +function! lsp#capabilities#has_document_symbol_provider(server_name) abort + return s:has_provider(a:server_name, 'documentSymbolProvider') +endfunction + +function! lsp#capabilities#has_workspace_symbol_provider(server_name) abort + return s:has_provider(a:server_name, 'workspaceSymbolProvider') +endfunction + +function! lsp#capabilities#has_implementation_provider(server_name) abort + return s:has_provider(a:server_name, 'implementationProvider') +endfunction + +function! lsp#capabilities#has_code_action_provider(server_name) abort + let l:capabilities = lsp#get_server_capabilities(a:server_name) + if !empty(l:capabilities) && has_key(l:capabilities, 'codeActionProvider') + if type(l:capabilities['codeActionProvider']) == type({}) + if has_key(l:capabilities['codeActionProvider'], 'codeActionKinds') && type(l:capabilities['codeActionProvider']['codeActionKinds']) == type([]) + return len(l:capabilities['codeActionProvider']['codeActionKinds']) != 0 + endif + endif + endif + return s:has_provider(a:server_name, 'codeActionProvider') +endfunction + +function! lsp#capabilities#has_code_lens_provider(server_name) abort + let l:capabilities = lsp#get_server_capabilities(a:server_name) + if !empty(l:capabilities) && has_key(l:capabilities, 'codeLensProvider') + return 1 + endif + return 0 +endfunction + +function! lsp#capabilities#has_type_definition_provider(server_name) abort + return s:has_provider(a:server_name, 'typeDefinitionProvider') +endfunction + +function! lsp#capabilities#has_type_hierarchy_provider(server_name) abort + return s:has_provider(a:server_name, 'typeHierarchyProvider') +endfunction + +function! lsp#capabilities#has_document_highlight_provider(server_name) abort + return s:has_provider(a:server_name, 'documentHighlightProvider') +endfunction + +function! lsp#capabilities#has_folding_range_provider(server_name) abort + return s:has_provider(a:server_name, 'foldingRangeProvider') +endfunction + +function! lsp#capabilities#has_call_hierarchy_provider(server_name) abort + return s:has_provider(a:server_name, 'callHierarchyProvider') +endfunction + +function! lsp#capabilities#has_semantic_tokens(server_name) abort + return s:has_provider(a:server_name, 'semanticTokensProvider') +endfunction + +" [supports_did_save (boolean), { 'includeText': boolean }] +function! lsp#capabilities#get_text_document_save_registration_options(server_name) abort + let l:capabilities = lsp#get_server_capabilities(a:server_name) + if !empty(l:capabilities) && has_key(l:capabilities, 'textDocumentSync') + if type(l:capabilities['textDocumentSync']) == type({}) + let l:save_options = get(l:capabilities['textDocumentSync'], 'save', 0) + if type(l:save_options) == type({}) + return [1, {'includeText': get(l:save_options, 'includeText', 0)}] + else + return [l:save_options ? 1 : 0, {'includeText': 0 }] + endif + else + return [1, { 'includeText': 0 }] + endif + endif + return [0, { 'includeText': 0 }] +endfunction + +" supports_did_change (boolean) +function! lsp#capabilities#get_text_document_change_sync_kind(server_name) abort + let l:capabilities = lsp#get_server_capabilities(a:server_name) + if !empty(l:capabilities) && has_key(l:capabilities, 'textDocumentSync') + if type(l:capabilities['textDocumentSync']) == type({}) + if has_key(l:capabilities['textDocumentSync'], 'change') && type(l:capabilities['textDocumentSync']['change']) == type(1) + let l:val = l:capabilities['textDocumentSync']['change'] + return l:val >= 0 && l:val <= 2 ? l:val : 1 + else + return 1 + endif + elseif type(l:capabilities['textDocumentSync']) == type(1) + return l:capabilities['textDocumentSync'] + else + return 1 + endif + endif + return 1 +endfunction + +function! lsp#capabilities#has_signature_help_provider(server_name) abort + let l:capabilities = lsp#get_server_capabilities(a:server_name) + if !empty(l:capabilities) && has_key(l:capabilities, 'signatureHelpProvider') + return 1 + endif + return 0 +endfunction + +function! lsp#capabilities#get_signature_help_trigger_characters(server_name) abort + let l:capabilities = lsp#get_server_capabilities(a:server_name) + if !empty(l:capabilities) && has_key(l:capabilities, 'signatureHelpProvider') + let l:trigger_chars = [] + if type(l:capabilities['signatureHelpProvider']) == type({}) + if has_key(l:capabilities['signatureHelpProvider'], 'triggerCharacters') + let l:trigger_chars = l:capabilities['signatureHelpProvider']['triggerCharacters'] + endif + " If retriggerChars exist, just treat them like triggerChars. + if has_key(l:capabilities['signatureHelpProvider'], 'retriggerCharacters') + let l:trigger_chars += l:capabilities['signatureHelpProvider']['retriggerCharacters'] + endif + return l:trigger_chars + endif + endif + return [] +endfunction + +function! lsp#capabilities#get_code_action_kinds(server_name) abort + let l:capabilities = lsp#get_server_capabilities(a:server_name) + if !empty(l:capabilities) && has_key(l:capabilities, 'codeActionProvider') + if type(l:capabilities['codeActionProvider']) == type({}) + if has_key(l:capabilities['codeActionProvider'], 'codeActionKinds') && type(l:capabilities['codeActionProvider']['codeActionKinds']) == type([]) + return l:capabilities['codeActionProvider']['codeActionKinds'] + endif + endif + endif + return [] +endfunction + +function! lsp#capabilities#has_completion_provider(server_name) abort + return s:has_provider(a:server_name, 'completionProvider') +endfunction + +function! lsp#capabilities#has_completion_resolve_provider(server_name) abort + return s:has_provider(a:server_name, 'completionProvider', 'resolveProvider') +endfunction + +function! lsp#capabilities#has_inlay_hint_provider(server_name) abort + return s:has_provider(a:server_name, 'inlayHintProvider') +endfunction diff --git a/autoload/lsp/client.vim b/autoload/lsp/client.vim new file mode 100644 index 00000000..af2aec73 --- /dev/null +++ b/autoload/lsp/client.vim @@ -0,0 +1,492 @@ +let s:save_cpo = &cpoptions +set cpoptions&vim + +let s:clients = {} " { client_id: ctx } + +" Vars used by native lsp +let s:jobidseq = 0 + +function! s:create_context(client_id, opts) abort + if a:client_id <= 0 + return {} + endif + + let l:ctx = { + \ 'opts': a:opts, + \ 'buffer': '', + \ 'content-length': -1, + \ 'requests': {}, + \ 'request_sequence': 0, + \ 'on_notifications': {}, + \ } + + let s:clients[a:client_id] = l:ctx + + return l:ctx +endfunction + +function! s:dispose_context(client_id) abort + if a:client_id > 0 + if has_key(s:clients, a:client_id) + unlet s:clients[a:client_id] + endif + endif +endfunction + +function! s:on_stdout(id, data, event) abort + let l:ctx = get(s:clients, a:id, {}) + + if empty(l:ctx) + return + endif + + let l:ctx['buffer'] .= a:data + + while 1 + if l:ctx['content-length'] < 0 + " wait for all headers to arrive + let l:header_end_index = stridx(l:ctx['buffer'], "\r\n\r\n") + if l:header_end_index < 0 + " no headers found + return + endif + let l:headers = l:ctx['buffer'][:l:header_end_index - 1] + let l:ctx['content-length'] = s:get_content_length(l:headers) + if l:ctx['content-length'] < 0 + " invalid content-length + call lsp#log('on_stdout', a:id, 'invalid content-length') + call s:lsp_stop(a:id) + return + endif + let l:ctx['buffer'] = l:ctx['buffer'][l:header_end_index + 4:] " 4 = len(\r\n\r\n) + endif + + if len(l:ctx['buffer']) < l:ctx['content-length'] + " incomplete message, wait for next buffer to arrive + return + endif + + " we have full message + let l:response_str = l:ctx['buffer'][:l:ctx['content-length'] - 1] + let l:ctx['content-length'] = -1 + + try + let l:response = json_decode(l:response_str) + catch + call lsp#log('s:on_stdout json_decode failed', v:exception) + endtry + + let l:ctx['buffer'] = l:ctx['buffer'][len(l:response_str):] + + if exists('l:response') + " call appropriate callbacks + let l:on_notification_data = { 'response': l:response } + if has_key(l:response, 'method') && has_key(l:response, 'id') + " it is a request from a server + let l:request = l:response + if has_key(l:ctx['opts'], 'on_request') + call l:ctx['opts']['on_request'](a:id, l:request) + endif + elseif has_key(l:response, 'id') + " it is a request->response + if !(type(l:response['id']) == type(0) || type(l:response['id']) == type('')) + " response['id'] can be number | string | null based on the spec + call lsp#log('invalid response id. ignoring message', l:response) + continue + endif + if has_key(l:ctx['requests'], l:response['id']) + let l:on_notification_data['request'] = l:ctx['requests'][l:response['id']] + endif + if has_key(l:ctx['opts'], 'on_notification') + " call client's on_notification first + try + call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification') + catch + call lsp#log('s:on_stdout client option on_notification() error', v:exception, v:throwpoint) + endtry + endif + if has_key(l:ctx['on_notifications'], l:response['id']) + " call lsp#client#send({ 'on_notification }) second + try + call l:ctx['on_notifications'][l:response['id']](a:id, l:on_notification_data, 'on_notification') + catch + call lsp#log('s:on_stdout client request on_notification() error', v:exception, v:throwpoint) + endtry + unlet l:ctx['on_notifications'][l:response['id']] + endif + if has_key(l:ctx['requests'], l:response['id']) + unlet l:ctx['requests'][l:response['id']] + else + call lsp#log('cannot find the request corresponding to response: ', l:response) + endif + else + " it is a notification + if has_key(l:ctx['opts'], 'on_notification') + try + call l:ctx['opts']['on_notification'](a:id, l:on_notification_data, 'on_notification') + catch + call lsp#log('s:on_stdout on_notification() error', v:exception, v:throwpoint) + endtry + endif + endif + endif + + if empty(l:response_str) + " buffer is empty, wait for next message to arrive + return + endif + endwhile +endfunction + +function! s:get_content_length(headers) abort + for l:header in split(a:headers, "\r\n") + let l:kvp = split(l:header, ':') + if len(l:kvp) == 2 + if l:kvp[0] =~? '^Content-Length' + return str2nr(l:kvp[1], 10) + endif + endif + endfor + return -1 +endfunction + +function! s:on_stderr(id, data, event) abort + let l:ctx = get(s:clients, a:id, {}) + if empty(l:ctx) + return + endif + if has_key(l:ctx['opts'], 'on_stderr') + try + call l:ctx['opts']['on_stderr'](a:id, a:data, a:event) + catch + call lsp#log('s:on_stderr exception', v:exception, v:throwpoint) + echom v:exception + endtry + endif +endfunction + +function! s:on_exit(id, status, event) abort + let l:ctx = get(s:clients, a:id, {}) + if empty(l:ctx) + return + endif + if has_key(l:ctx['opts'], 'on_exit') + try + call l:ctx['opts']['on_exit'](a:id, a:status, a:event) + catch + call lsp#log('s:on_exit exception', v:exception, v:throwpoint) + echom v:exception + endtry + endif + call s:dispose_context(a:id) +endfunction + +function! s:lsp_start(opts) abort + let l:opts = { + \ 'on_stdout': function('s:on_stdout'), + \ 'on_stderr': function('s:on_stderr'), + \ 'on_exit': function('s:on_exit'), + \ 'normalize': 'string' + \ } + if has_key(a:opts, 'env') + let l:opts.env = a:opts.env + endif + + if has_key(a:opts, 'cmd') + let l:client_id = lsp#utils#job#start(a:opts.cmd, l:opts) + elseif has_key(a:opts, 'tcp') + let l:client_id = lsp#utils#job#connect(a:opts.tcp, l:opts) + else + return -1 + endif + + let l:ctx = s:create_context(l:client_id, a:opts) + let l:ctx['id'] = l:client_id + + return l:client_id +endfunction + +function! s:lsp_stop(id) abort + call lsp#utils#job#stop(a:id) +endfunction + +let s:send_type_request = 1 +let s:send_type_notification = 2 +let s:send_type_response = 3 +function! s:lsp_send(id, opts, type) abort " opts = { id?, method?, result?, params?, on_notification } + let l:ctx = get(s:clients, a:id, {}) + if empty(l:ctx) | return -1 | endif + + let l:request = { 'jsonrpc': '2.0' } + + if (a:type == s:send_type_request) + let l:ctx['request_sequence'] = l:ctx['request_sequence'] + 1 + let l:request['id'] = l:ctx['request_sequence'] + let l:ctx['requests'][l:request['id']] = l:request + if has_key(a:opts, 'on_notification') + let l:ctx['on_notifications'][l:request['id']] = a:opts['on_notification'] + endif + endif + + if has_key(a:opts, 'id') + let l:request['id'] = a:opts['id'] + endif + if has_key(a:opts, 'method') + let l:request['method'] = a:opts['method'] + endif + if has_key(a:opts, 'params') + let l:request['params'] = a:opts['params'] + endif + if has_key(a:opts, 'result') + let l:request['result'] = a:opts['result'] + endif + if has_key(a:opts, 'error') + let l:request['error'] = a:opts['error'] + endif + + let l:json = json_encode(l:request) + let l:payload = 'Content-Length: ' . len(l:json) . "\r\n\r\n" . l:json + + call lsp#utils#job#send(a:id, l:payload) + + if (a:type == s:send_type_request) + let l:id = l:request['id'] + if get(a:opts, 'sync', 0) !=# 0 + let l:timeout = get(a:opts, 'sync_timeout', -1) + if lsp#utils#_wait(l:timeout, {-> !has_key(l:ctx['requests'], l:request['id'])}, 1) == -1 + throw 'lsp#client: timeout' + endif + endif + return l:id + else + return 0 + endif +endfunction + +function! s:lsp_get_last_request_id(id) abort + return s:clients[a:id]['request_sequence'] +endfunction + +function! s:lsp_is_error(obj_or_response) abort + let l:vt = type(a:obj_or_response) + if l:vt == type('') + return len(a:obj_or_response) > 0 + elseif l:vt == type({}) + return has_key(a:obj_or_response, 'error') + endif + return 0 +endfunction + + +function! s:is_server_instantiated_notification(notification) abort + return !has_key(a:notification, 'request') +endfunction + +function! s:native_out_cb(cbctx, channel, response) abort + if !has_key(a:cbctx, 'ctx') | return | endif + let l:ctx = a:cbctx['ctx'] + if has_key(a:response, 'method') && has_key(a:response, 'id') + " it is a request from a server + let l:request = a:response + if has_key(l:ctx['opts'], 'on_request') + call l:ctx['opts']['on_request'](l:ctx['id'], l:request) + endif + elseif !has_key(a:response, 'id') && has_key(l:ctx['opts'], 'on_notification') + " it is a notification + let l:on_notification_data = { 'response': a:response } + try + call l:ctx['opts']['on_notification'](l:ctx['id'], l:on_notification_data, 'on_notification') + catch + call lsp#log('s:native_notification_callback on_notification() error', v:exception, v:throwpoint) + endtry + endif +endfunction + +function! s:native_err_cb(cbctx, channel, response) abort + if !has_key(a:cbctx, 'ctx') | return | endif + let l:ctx = a:cbctx['ctx'] + if has_key(l:ctx['opts'], 'on_stderr') + try + call l:ctx['opts']['on_stderr'](l:ctx['id'], a:response, 'stderr') + catch + call lsp#log('s:on_stderr exception', v:exception, v:throwpoint) + echom v:exception + endtry + endif +endfunction + +function! s:native_exit_cb(cbctx, channel, response) abort + if !has_key(a:cbctx, 'ctx') | return | endif + let l:ctx = a:cbctx['ctx'] + if has_key(l:ctx['opts'], 'on_exit') + try + call l:ctx['opts']['on_exit'](l:ctx['id'], a:response, 'exit') + catch + call lsp#log('s:on_exit exception', v:exception, v:throwpoint) + echom v:exception + endtry + endif + call s:dispose_context(l:ctx['id']) +endfunction + +" public apis {{{ + +function! lsp#client#start(opts) abort + if g:lsp_use_native_client && lsp#utils#has_native_lsp_client() + if has_key(a:opts, 'cmd') + let l:cbctx = {} + let l:jobopt = { 'in_mode': 'lsp', 'out_mode': 'lsp', 'noblock': 1, + \ 'out_cb': function('s:native_out_cb', [l:cbctx]), + \ 'err_cb': function('s:native_err_cb', [l:cbctx]), + \ 'exit_cb': function('s:native_exit_cb', [l:cbctx]), + \ } + if has_key(a:opts, 'cwd') | let l:jobopt['cwd'] = a:opts['cwd'] | endif + if has_key(a:opts, 'env') | let l:jobopt['env'] = a:opts['env'] | endif + let s:jobidseq += 1 + let l:jobid = s:jobidseq " jobid == clientid + call lsp#log_verbose('using native lsp client') + let l:job = job_start(a:opts['cmd'], l:jobopt) + if job_status(l:job) !=? 'run' | return -1 | endif + let l:ctx = s:create_context(l:jobid, a:opts) + let l:ctx['id'] = l:jobid + let l:ctx['job'] = l:job + let l:ctx['channel'] = job_getchannel(l:job) + let l:cbctx['ctx'] = l:ctx + return l:jobid + elseif has_key(a:opts, 'tcp') + " add support for tcp + call lsp#log('tcp not supported when using native lsp client') + return -1 + endif + endif + return s:lsp_start(a:opts) +endfunction + +function! lsp#client#stop(client_id) abort + if g:lsp_use_native_client && lsp#utils#has_native_lsp_client() + let l:ctx = get(s:clients, a:client_id, {}) + if empty(l:ctx) | return | endif + call job_stop(l:ctx['job']) + else + return s:lsp_stop(a:client_id) + endif +endfunction + +function! lsp#client#send_request(client_id, opts) abort + if g:lsp_use_native_client && lsp#utils#has_native_lsp_client() + let l:ctx = get(s:clients, a:client_id, {}) + if empty(l:ctx) | return -1 | endif + let l:request = {} + " id shouldn't be passed to request as vim will overwrite it. refer to :h language-server-protocol + if has_key(a:opts, 'method') | let l:request['method'] = a:opts['method'] | endif + if has_key(a:opts, 'params') | let l:request['params'] = a:opts['params'] | endif + + call ch_sendexpr(l:ctx['channel'], l:request, { 'callback': function('s:on_response_native', [l:ctx, l:request]) }) + let l:ctx['requests'][l:request['id']] = l:request + if has_key(a:opts, 'on_notification') + let l:ctx['on_notifications'][l:request['id']] = a:opts['on_notification'] + endif + if get(a:opts, 'sync', 0) !=# 0 + let l:timeout = get(a:opts, 'sync_timeout', -1) + if lsp#utils#_wait(l:timeout, {-> !has_key(l:ctx['requests'], l:request['id'])}, 1) == -1 + throw 'lsp#client#send_request: timeout' + endif + endif + let l:ctx['request_sequence'] = l:request['id'] + return l:request['id'] + else + return s:lsp_send(a:client_id, a:opts, s:send_type_request) + endif +endfunction + +function! s:on_response_native(ctx, request, channel, response) abort + " request -> response + let l:on_notification_data = { 'response': a:response, 'request': a:request } + if has_key(a:ctx['opts'], 'on_notification') + " call client's on_notification first + try + call a:ctx['opts']['on_notification'](a:ctx['id'], l:on_notification_data, 'on_notification') + catch + call lsp#log('s:on_response_native client option on_notification() error', v:exception, v:throwpoint) + endtry + endif + if has_key(a:ctx['on_notifications'], a:request['id']) + " call lsp#client#send({ 'on_notification' }) second + try + call a:ctx['on_notifications'][a:request['id']](a:ctx['id'], l:on_notification_data, 'on_notification') + catch + call lsp#log('s:on_response_native client request on_notification() error', v:exception, v:throwpoint, a:request, a:response) + endtry + unlet a:ctx['on_notifications'][a:response['id']] + if has_key(a:ctx['requests'], a:response['id']) + unlet a:ctx['requests'][a:response['id']] + else + call lsp#log('cannot find the request corresponding to response: ', a:response) + endif + endif +endfunction + +function! lsp#client#send_notification(client_id, opts) abort + if g:lsp_use_native_client && lsp#utils#has_native_lsp_client() + let l:ctx = get(s:clients, a:client_id, {}) + if empty(l:ctx) | return -1 | endif + let l:request = {} + if has_key(a:opts, 'method') | let l:request['method'] = a:opts['method'] | endif + if has_key(a:opts, 'params') | let l:request['params'] = a:opts['params'] | endif + call ch_sendexpr(l:ctx['channel'], l:request) + return 0 + else + return s:lsp_send(a:client_id, a:opts, s:send_type_notification) + endif +endfunction + +function! lsp#client#send_response(client_id, opts) abort + if g:lsp_use_native_client && lsp#utils#has_native_lsp_client() + let l:ctx = get(s:clients, a:client_id, {}) + if empty(l:ctx) | return -1 | endif + let l:request = {} + if has_key(a:opts, 'id') | let l:request['id'] = a:opts['id'] | endif + if has_key(a:opts, 'result') | let l:request['result'] = a:opts['result'] | endif + if has_key(a:opts, 'error') | let l:request['error'] = a:opts['error'] | endif + try + call ch_sendexpr(l:ctx['channel'], l:request) + catch + " vim only supports id as number and fails when string, hence add a try catch: https://github.com/vim/vim/issues/14091 + call lsp#log('lsp#client#send_response error', v:exception, v:throwpoint, + \ has_key(l:request, 'id') && type(l:request['id']) != type(1)) + endtry + return 0 + else + return s:lsp_send(a:client_id, a:opts, s:send_type_response) + endif +endfunction + +function! lsp#client#get_last_request_id(client_id) abort + return s:lsp_get_last_request_id(a:client_id) +endfunction + +function! lsp#client#is_error(obj_or_response) abort + return s:lsp_is_error(a:obj_or_response) +endfunction + +function! lsp#client#error_message(obj_or_response) abort + try + return a:obj_or_response['error']['data']['message'] + catch + endtry + try + return a:obj_or_response['error']['message'] + catch + endtry + return string(a:obj_or_response) +endfunction + +function! lsp#client#is_server_instantiated_notification(notification) abort + return s:is_server_instantiated_notification(a:notification) +endfunction + +" }}} + +let &cpoptions = s:save_cpo +unlet s:save_cpo +" vim sw=4 ts=4 et diff --git a/autoload/lsp/internal/completion/documentation.vim b/autoload/lsp/internal/completion/documentation.vim new file mode 100644 index 00000000..fb3e9b5e --- /dev/null +++ b/autoload/lsp/internal/completion/documentation.vim @@ -0,0 +1,207 @@ +" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +let s:enabled = 0 + +let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown') +let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent') +let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow') +let s:Window = vital#lsp#import('VS.Vim.Window') +let s:Buffer = vital#lsp#import('VS.Vim.Buffer') + +function! lsp#internal#completion#documentation#_enable() abort + " don't even bother registering if the feature is disabled + if !g:lsp_completion_documentation_enabled | return | endif + + if !s:FloatingWindow.is_available() | return | endif + if !exists('##CompleteChanged') | return | endif + + if s:enabled | return | endif + let s:enabled = 1 + + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#merge( + \ lsp#callbag#pipe( + \ lsp#callbag#fromEvent('CompleteChanged'), + \ lsp#callbag#filter({_->g:lsp_completion_documentation_enabled}), + \ lsp#callbag#map({->copy(v:event)}), + \ lsp#callbag#debounceTime(g:lsp_completion_documentation_delay), + \ lsp#callbag#switchMap({event-> + \ lsp#callbag#pipe( + \ s:resolve_completion(event), + \ lsp#callbag#tap({managed_user_data->s:show_floating_window(event, managed_user_data)}), + \ lsp#callbag#takeUntil(lsp#callbag#fromEvent('CompleteDone')) + \ ) + \ }) + \ ), + \ lsp#callbag#pipe( + \ lsp#callbag#fromEvent('CompleteDone'), + \ lsp#callbag#tap({_->s:close_floating_window(v:false)}), + \ ) + \ ), + \ lsp#callbag#subscribe(), + \ ) +endfunction + +function! s:resolve_completion(event) abort + let l:managed_user_data = lsp#omni#get_managed_user_data_from_completed_item(a:event['completed_item']) + if empty(l:managed_user_data) + return lsp#callbag#of({}) + endif + + let l:completion_item = l:managed_user_data['completion_item'] + + if has_key(l:completion_item, 'documentation') + return lsp#callbag#of(l:managed_user_data) + elseif lsp#capabilities#has_completion_resolve_provider(l:managed_user_data['server_name']) + return lsp#callbag#pipe( + \ lsp#request(l:managed_user_data['server_name'], { + \ 'method': 'completionItem/resolve', + \ 'params': l:completion_item, + \ }), + \ lsp#callbag#map({x->{ + \ 'server_name': l:managed_user_data['server_name'], + \ 'completion_item': x['response']['result'], + \ 'complete_position': l:managed_user_data['complete_position'], + \ }}) + \ ) + else + return lsp#callbag#of(l:managed_user_data) + endif +endfunction + +function! s:show_floating_window(event, managed_user_data) abort + if empty(a:managed_user_data) || !pumvisible() + call s:close_floating_window(v:true) + return + endif + let l:completion_item = a:managed_user_data['completion_item'] + + let l:contents = [] + + " Add detail field if provided. + if type(get(l:completion_item, 'detail', v:null)) == type('') + if !empty(l:completion_item.detail) + let l:detail = s:MarkupContent.normalize({ + \ 'language': &filetype, + \ 'value': l:completion_item['detail'], + \ }, { + \ 'compact': !g:lsp_preview_fixup_conceal + \ }) + let l:contents += [l:detail] + endif + endif + + " Add documentation filed if provided. + let l:documentation = s:MarkupContent.normalize(get(l:completion_item, 'documentation', ''), { + \ 'compact': !g:lsp_preview_fixup_conceal + \ }) + if !empty(l:documentation) + let l:contents += [l:documentation] + endif + + " Ignore if contents is empty. + if empty(l:contents) + return s:close_floating_window(v:true) + endif + + " Update contents. + let l:doc_win = s:get_doc_win() + call deletebufline(l:doc_win.get_bufnr(), 1, '$') + call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(join(l:contents, "\n\n"))) + + " Calculate layout. + if g:lsp_float_max_width >= 1 + let l:maxwidth = g:lsp_float_max_width + elseif g:lsp_float_max_width == 0 + let l:maxwidth = &columns + else + let l:maxwidth = float2nr(&columns * 0.4) + endif + let l:size = l:doc_win.get_size({ + \ 'maxwidth': l:maxwidth, + \ 'maxheight': float2nr(&lines * 0.4), + \ }) + let l:margin_right = &columns - 1 - (float2nr(a:event.col) + float2nr(a:event.width) + 1 + (a:event.scrollbar ? 1 : 0)) + let l:margin_left = float2nr(a:event.col) - 3 + if l:size.width < l:margin_right + " do nothing + elseif l:margin_left <= l:margin_right + let l:size.width = l:margin_right + else + let l:size.width = l:margin_left + endif + let l:pos = s:compute_position(a:event, l:size) + if empty(l:pos) + call s:close_floating_window(v:true) + return + endif + + " Show popupmenu and apply markdown syntax. + call l:doc_win.open({ + \ 'row': l:pos[0] + 1, + \ 'col': l:pos[1] + 1, + \ 'width': l:size.width, + \ 'height': l:size.height, + \ 'border': v:true, + \ 'topline': 1, + \ }) + call s:Window.do(l:doc_win.get_winid(), { -> s:Markdown.apply() }) +endfunction + +function! s:close_floating_window(force) abort + " Ignore `CompleteDone` if it occurred by `complete()` because in this case, the popup menu will re-appear immediately. + let l:ctx = {} + function! l:ctx.callback(force) abort + if !pumvisible() || a:force + call s:get_doc_win().close() + endif + endfunction + call timer_start(1, { -> l:ctx.callback(a:force) }) +endfunction + +function! s:compute_position(event, size) abort + let l:col_if_right = a:event.col + a:event.width + 1 + (a:event.scrollbar ? 1 : 0) + let l:col_if_left = a:event.col - a:size.width - 2 + + if a:size.width >= (&columns - l:col_if_right) + let l:col = l:col_if_left + else + let l:col = l:col_if_right + endif + + if l:col <= 0 + return [] + endif + if &columns <= l:col + a:size.width + return [] + endif + + return [a:event.row, l:col] +endfunction + +function! s:get_doc_win() abort + if exists('s:doc_win') + return s:doc_win + endif + + let s:doc_win = s:FloatingWindow.new({ + \ 'on_opened': { -> execute('doautocmd User lsp_float_opened') }, + \ 'on_closed': { -> execute('doautocmd User lsp_float_closed') } + \ }) + call s:doc_win.set_var('&wrap', 1) + call s:doc_win.set_var('&conceallevel', 2) + noautocmd silent let l:bufnr = s:Buffer.create() + call s:doc_win.set_bufnr(l:bufnr) + call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile') + call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide') + call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0) + call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0) + return s:doc_win +endfunction + +function! lsp#internal#completion#documentation#_disable() abort + if !s:enabled | return | endif + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction diff --git a/autoload/lsp/internal/diagnostics.vim b/autoload/lsp/internal/diagnostics.vim new file mode 100644 index 00000000..3e2d2614 --- /dev/null +++ b/autoload/lsp/internal/diagnostics.vim @@ -0,0 +1,20 @@ +function! lsp#internal#diagnostics#_enable() abort + " don't even bother registering if the feature is disabled + if !g:lsp_diagnostics_enabled | return | endif + + call lsp#internal#diagnostics#state#_enable() " Needs to be the first one to register + call lsp#internal#diagnostics#echo#_enable() + call lsp#internal#diagnostics#highlights#_enable() + call lsp#internal#diagnostics#float#_enable() + call lsp#internal#diagnostics#signs#_enable() + call lsp#internal#diagnostics#virtual_text#_enable() +endfunction + +function! lsp#internal#diagnostics#_disable() abort + call lsp#internal#diagnostics#echo#_disable() + call lsp#internal#diagnostics#float#_disable() + call lsp#internal#diagnostics#highlights#_disable() + call lsp#internal#diagnostics#virtual_text#_disable() + call lsp#internal#diagnostics#signs#_disable() + call lsp#internal#diagnostics#state#_disable() " Needs to be the last one to unregister +endfunction diff --git a/autoload/lsp/internal/diagnostics/document_diagnostics_command.vim b/autoload/lsp/internal/diagnostics/document_diagnostics_command.vim new file mode 100644 index 00000000..cc43b948 --- /dev/null +++ b/autoload/lsp/internal/diagnostics/document_diagnostics_command.vim @@ -0,0 +1,40 @@ +" options = { +" buffers: '1' " optional string, defaults to current buffer, '*' for all buffers +" } +function! lsp#internal#diagnostics#document_diagnostics_command#do(options) abort + if !g:lsp_diagnostics_enabled + call lsp#utils#error(':LspDocumentDiagnostics g:lsp_diagnostics_enabled must be enabled') + return + endif + + let l:buffers = get(a:options, 'buffers', '') + + let l:filtered_diagnostics = {} + + if l:buffers ==# '*' + let l:filtered_diagnostics = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server() + else + let l:uri = lsp#utils#get_buffer_uri() + if !empty(l:uri) + let l:filtered_diagnostics[l:uri] = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri) + endif + endif + + let l:result = [] + for [l:uri, l:value] in items(l:filtered_diagnostics) + if lsp#internal#diagnostics#state#_is_enabled_for_buffer(bufnr(lsp#utils#uri_to_path(l:uri))) + for l:diagnostics in values(l:value) + let l:result += lsp#ui#vim#utils#diagnostics_to_loc_list({ 'response': l:diagnostics }) + endfor + endif + endfor + + if empty(l:result) + call lsp#utils#error('No diagnostics results') + return + else + call setloclist(0, l:result) + echo 'Retrieved diagnostics results' + botright lopen + endif +endfunction diff --git a/autoload/lsp/internal/diagnostics/echo.vim b/autoload/lsp/internal/diagnostics/echo.vim new file mode 100644 index 00000000..86acf63b --- /dev/null +++ b/autoload/lsp/internal/diagnostics/echo.vim @@ -0,0 +1,41 @@ +" internal state for whether it is enabled or not to avoid multiple subscriptions +let s:enabled = 0 + +function! lsp#internal#diagnostics#echo#_enable() abort + " don't even bother registering if the feature is disabled + if !g:lsp_diagnostics_echo_cursor | return | endif + + if s:enabled | return | endif + let s:enabled = 1 + + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#fromEvent(['CursorMoved']), + \ lsp#callbag#filter({_->g:lsp_diagnostics_echo_cursor}), + \ lsp#callbag#debounceTime(g:lsp_diagnostics_echo_delay), + \ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}), + \ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}), + \ lsp#callbag#filter({_->mode() is# 'n'}), + \ lsp#callbag#filter({_->getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }), + \ lsp#callbag#map({_->lsp#internal#diagnostics#under_cursor#get_diagnostic()}), + \ lsp#callbag#subscribe({x->s:echo(x)}), + \ ) +endfunction + +function! lsp#internal#diagnostics#echo#_disable() abort + if !s:enabled | return | endif + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif + let s:enabled = 0 +endfunction + +function! s:echo(diagnostic) abort + if !empty(a:diagnostic) && has_key(a:diagnostic, 'message') + call lsp#utils#echo_with_truncation('LSP: '. substitute(a:diagnostic['message'], '\n\+', ' ', 'g')) + let s:displaying_message = 1 + elseif get(s:, 'displaying_message', 0) + call lsp#utils#echo_with_truncation('') + let s:displaying_message = 0 + endif +endfunction diff --git a/autoload/lsp/internal/diagnostics/first_line.vim b/autoload/lsp/internal/diagnostics/first_line.vim new file mode 100644 index 00000000..4ac53f0a --- /dev/null +++ b/autoload/lsp/internal/diagnostics/first_line.vim @@ -0,0 +1,26 @@ +" Return first error line or v:null if there are no errors +" available. +" options = { +" 'bufnr': '', " optional +" } +function! lsp#internal#diagnostics#first_line#get_first_error_line(options) abort + let l:bufnr = get(a:options, 'bufnr', bufnr('%')) + + if !lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) + return v:null + endif + + let l:uri = lsp#utils#get_buffer_uri(l:bufnr) + let l:diagnostics_by_server = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri) + + let l:first_error_line = v:null + for l:diagnostics_response in values(l:diagnostics_by_server) + for l:item in lsp#utils#iteratable(l:diagnostics_response['params']['diagnostics']) + let l:severity = get(l:item, 'severity', 1) + if l:severity ==# 1 && (l:first_error_line ==# v:null || l:first_error_line ># l:item['range']['start']['line']) + let l:first_error_line = l:item['range']['start']['line'] + endif + endfor + endfor + return l:first_error_line ==# v:null ? v:null : l:first_error_line + 1 +endfunction diff --git a/autoload/lsp/internal/diagnostics/float.vim b/autoload/lsp/internal/diagnostics/float.vim new file mode 100644 index 00000000..f7f40a15 --- /dev/null +++ b/autoload/lsp/internal/diagnostics/float.vim @@ -0,0 +1,123 @@ +" internal state for whether it is enabled or not to avoid multiple subscriptions +let s:enabled = 0 + +let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown') +let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent') +let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow') +let s:Window = vital#lsp#import('VS.Vim.Window') +let s:Buffer = vital#lsp#import('VS.Vim.Buffer') + +function! lsp#internal#diagnostics#float#_enable() abort + " don't even bother registering if the feature is disabled + if !lsp#ui#vim#output#float_supported() | return | endif + if !g:lsp_diagnostics_float_cursor | return | endif + + if s:enabled | return | endif + let s:enabled = 1 + + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#merge( + \ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']), + \ lsp#callbag#pipe( + \ lsp#callbag#fromEvent(['InsertEnter']), + \ lsp#callbag#filter({_->!g:lsp_diagnostics_float_insert_mode_enabled}), + \ lsp#callbag#tap({_->s:hide_float()}), + \ ) + \ ), + \ lsp#callbag#filter({_->g:lsp_diagnostics_float_cursor}), + \ lsp#callbag#tap({_->s:hide_float()}), + \ lsp#callbag#debounceTime(g:lsp_diagnostics_float_delay), + \ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}), + \ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}), + \ lsp#callbag#filter({_->mode() is# 'n'}), + \ lsp#callbag#filter({_->getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }), + \ lsp#callbag#map({_->lsp#internal#diagnostics#under_cursor#get_diagnostic()}), + \ lsp#callbag#subscribe({x->s:show_float(x)}), + \ ) +endfunction + +function! lsp#internal#diagnostics#float#_disable() abort + if !s:enabled | return | endif + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif + let s:enabled = 0 +endfunction + +function! s:show_float(diagnostic) abort + let l:doc_win = s:get_doc_win() + if !empty(a:diagnostic) && has_key(a:diagnostic, 'message') + " Update contents. + call deletebufline(l:doc_win.get_bufnr(), 1, '$') + call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(a:diagnostic['message'])) + + " Compute size. + if g:lsp_float_max_width >= 1 + let l:maxwidth = g:lsp_float_max_width + elseif g:lsp_float_max_width == 0 + let l:maxwidth = &columns + else + let l:maxwidth = float2nr(&columns * 0.4) + endif + let l:size = l:doc_win.get_size({ + \ 'maxwidth': l:maxwidth, + \ 'maxheight': float2nr(&lines * 0.4), + \ }) + + " Compute position. + let l:pos = s:compute_position(l:size) + + " Open window. + call l:doc_win.open({ + \ 'row': l:pos[0], + \ 'col': l:pos[1], + \ 'width': l:size.width, + \ 'height': l:size.height, + \ 'border': v:true, + \ 'topline': 1, + \ }) + else + call s:hide_float() + endif +endfunction + +function! s:hide_float() abort + let l:doc_win = s:get_doc_win() + call l:doc_win.close() +endfunction + +function! s:get_doc_win() abort + if exists('s:doc_win') + return s:doc_win + endif + + let s:doc_win = s:FloatingWindow.new({ + \ 'on_opened': { -> execute('doautocmd User lsp_float_opened') }, + \ 'on_closed': { -> execute('doautocmd User lsp_float_closed') } + \ }) + call s:doc_win.set_var('&wrap', 1) + call s:doc_win.set_var('&conceallevel', 2) + noautocmd silent let l:bufnr = s:Buffer.create() + call s:doc_win.set_bufnr(l:bufnr) + call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile') + call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide') + call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0) + call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0) + return s:doc_win +endfunction + +function! s:compute_position(size) abort + let l:pos = screenpos(0, line('.'), col('.')) + if l:pos.row == 0 && l:pos.col == 0 + let l:pos = {'curscol': screencol(), 'row': screenrow()} + endif + let l:pos = [l:pos.row + 1, l:pos.curscol + 1] + if l:pos[0] + a:size.height > &lines + let l:pos[0] = l:pos[0] - a:size.height - 3 + endif + if l:pos[1] + a:size.width > &columns + let l:pos[1] = l:pos[1] - a:size.width - 3 + endif + return l:pos +endfunction diff --git a/autoload/lsp/internal/diagnostics/highlights.vim b/autoload/lsp/internal/diagnostics/highlights.vim new file mode 100644 index 00000000..970c606a --- /dev/null +++ b/autoload/lsp/internal/diagnostics/highlights.vim @@ -0,0 +1,211 @@ +" internal state for whether it is enabled or not to avoid multiple subscriptions +let s:enabled = 0 +let s:namespace_id = '' " will be set when enabled + +let s:severity_sign_names_mapping = { + \ 1: 'LspError', + \ 2: 'LspWarning', + \ 3: 'LspInformation', + \ 4: 'LspHint', + \ } + +if !hlexists('LspErrorHighlight') + highlight link LspErrorHighlight Error +endif + +if !hlexists('LspWarningHighlight') + highlight link LspWarningHighlight Todo +endif + +if !hlexists('LspInformationHighlight') + highlight link LspInformationHighlight Normal +endif + +if !hlexists('LspHintHighlight') + highlight link LspHintHighlight Normal +endif + +function! lsp#internal#diagnostics#highlights#_enable() abort + " don't even bother registering if the feature is disabled + if !lsp#utils#_has_highlights() | return | endif + if !g:lsp_diagnostics_highlights_enabled | return | endif + + if s:enabled | return | endif + let s:enabled = 1 + + if empty(s:namespace_id) + if has('nvim') + let s:namespace_id = nvim_create_namespace('vim_lsp_diagnostics_highlights') + else + let s:namespace_id = 'vim_lsp_diagnostics_highlights' + for l:severity in keys(s:severity_sign_names_mapping) + let l:hl_group = s:severity_sign_names_mapping[l:severity] . 'Highlight' + call prop_type_add(s:get_prop_type_name(l:severity), + \ {'highlight': l:hl_group, 'combine': v:true, 'priority': lsp#internal#textprop#priority('diagnostics_highlight') }) + endfor + endif + endif + + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#merge( + \ lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response') + \ && has_key(x['response'], 'method') && x['response']['method'] ==# '$/vimlsp/lsp_diagnostics_updated' + \ && !lsp#client#is_error(x['response'])}), + \ lsp#callbag#map({x->x['response']['params']}), + \ ), + \ lsp#callbag#pipe( + \ lsp#callbag#fromEvent(['InsertEnter', 'InsertLeave']), + \ lsp#callbag#filter({_->!g:lsp_diagnostics_highlights_insert_mode_enabled}), + \ lsp#callbag#map({_->{ 'uri': lsp#utils#get_buffer_uri() }}), + \ ), + \ ), + \ lsp#callbag#filter({_->g:lsp_diagnostics_highlights_enabled}), + \ lsp#callbag#debounceTime(g:lsp_diagnostics_highlights_delay), + \ lsp#callbag#tap({x->s:clear_highlights(x)}), + \ lsp#callbag#tap({x->s:set_highlights(x)}), + \ lsp#callbag#subscribe(), + \ ) +endfunction + +function! lsp#internal#diagnostics#highlights#_disable() abort + if !s:enabled | return | endif + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif + call s:clear_all_highlights() + let s:enabled = 0 +endfunction + +function! s:get_prop_type_name(severity) abort + return s:namespace_id . '_' . get(s:severity_sign_names_mapping, a:severity, 'LspError') +endfunction + +function! s:clear_all_highlights() abort + for l:bufnr in range(1, bufnr('$')) + if bufexists(l:bufnr) && bufloaded(l:bufnr) + if has('nvim') + call nvim_buf_clear_namespace(l:bufnr, s:namespace_id, 0, -1) + else + for l:severity in keys(s:severity_sign_names_mapping) + try + " TODO: need to check for valid range before calling prop_add + " See https://github.com/prabirshrestha/vim-lsp/pull/721 + silent! call prop_remove({ + \ 'type': s:get_prop_type_name(l:severity), + \ 'bufnr': l:bufnr, + \ 'all': v:true }) + catch + call lsp#log('diagnostics', 'clear_all_highlights', 'prop_remove', v:exception, v:throwpoint) + endtry + endfor + endif + endif + endfor +endfunction + +function! s:clear_highlights(params) abort + " TODO: optimize by looking at params + call s:clear_all_highlights() +endfunction + +function! s:set_highlights(params) abort + " TODO: optimize by looking at params + if !g:lsp_diagnostics_highlights_insert_mode_enabled + if mode()[0] ==# 'i' | return | endif + endif + + for l:bufnr in range(1, bufnr('$')) + if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr) + let l:uri = lsp#utils#get_buffer_uri(l:bufnr) + for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)) + call s:place_highlights(l:server, l:diagnostics_response, l:bufnr) + endfor + endif + endfor +endfunction + +function! s:place_highlights(server, diagnostics_response, bufnr) abort + " TODO: make diagnostics highlights same across vim and neovim + for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics']) + let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim(a:bufnr, l:item['range']['start']) + let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim(a:bufnr, l:item['range']['end']) + let l:severity = get(l:item, 'severity', 3) + let l:hl_group = get(s:severity_sign_names_mapping, l:severity, 'LspError') . 'Highlight' + if has('nvim') + for l:line in range(l:start_line, l:end_line) + if l:line == l:start_line + let l:highlight_start_col = l:start_col + else + let l:highlight_start_col = 1 + endif + + if l:line == l:end_line + let l:highlight_end_col = l:end_col + else + " neovim treats -1 as end of line, special handle it later + " when calling nvim_buf_add_higlight + let l:highlight_end_col = -1 + endif + + if l:start_line == l:end_line && l:highlight_start_col == l:highlight_end_col + " higlighting same start col and end col on same line + " doesn't work so use -1 for start col + let l:highlight_start_col -= 1 + if l:highlight_start_col <= 0 + let l:highlight_start_col = 1 + endif + endif + + call nvim_buf_add_highlight(a:bufnr, s:namespace_id, l:hl_group, + \ l:line - 1, l:highlight_start_col - 1, l:highlight_end_col == -1 ? -1 : l:highlight_end_col - 1) + endfor + else + if l:start_line == l:end_line + try + " TODO: need to check for valid range before calling prop_add + " See https://github.com/prabirshrestha/vim-lsp/pull/721 + silent! call prop_add(l:start_line, l:start_col, { + \ 'end_col': l:end_col, + \ 'bufnr': a:bufnr, + \ 'type': s:get_prop_type_name(l:severity), + \ }) + catch + call lsp#log('diagnostics', 'place_highlights', 'prop_add', v:exception, v:throwpoint) + endtry + else + for l:line in range(l:start_line, l:end_line) + if l:line == l:start_line + let l:highlight_start_col = l:start_col + else + let l:highlight_start_col = 1 + endif + + if l:line == l:end_line + let l:highlight_end_col = l:end_col + else + if has('patch-9.0.0916') + let l:highlight_end_col = strlen(getbufoneline(a:bufnr, l:line)) + 1 + else + let l:highlight_end_col = strlen(getbufline(a:bufnr, l:line)[0]) + 1 + endif + endif + + try + " TODO: need to check for valid range before calling prop_add + " See https://github.com/prabirshrestha/vim-lsp/pull/721 + silent! call prop_add(l:line, l:highlight_start_col, { + \ 'end_col': l:highlight_end_col, + \ 'bufnr': a:bufnr, + \ 'type': s:get_prop_type_name(l:severity), + \ }) + catch + call lsp#log('diagnostics', 'place_highlights', 'prop_add', v:exception, v:throwpoint) + endtry + endfor + endif + endif + endfor +endfunction diff --git a/autoload/lsp/internal/diagnostics/movement.vim b/autoload/lsp/internal/diagnostics/movement.vim new file mode 100644 index 00000000..39027f70 --- /dev/null +++ b/autoload/lsp/internal/diagnostics/movement.vim @@ -0,0 +1,199 @@ +function! s:severity_of(diagnostic) abort + return get(a:diagnostic, 'severity', 1) +endfunction + +function! lsp#internal#diagnostics#movement#_next_error(...) abort + let l:diagnostics = filter(s:get_all_buffer_diagnostics(), + \ {_, diagnostic -> s:severity_of(diagnostic) ==# 1 }) + let l:options = lsp#utils#parse_command_options(a:000) + call s:next_diagnostic(l:diagnostics, l:options) +endfunction + +function! lsp#internal#diagnostics#movement#_next_warning(...) abort + let l:diagnostics = filter(s:get_all_buffer_diagnostics(), + \ {_, diagnostic -> s:severity_of(diagnostic) ==# 2 }) + let l:options = lsp#utils#parse_command_options(a:000) + call s:next_diagnostic(l:diagnostics, l:options) +endfunction + +function! lsp#internal#diagnostics#movement#_next_diagnostics(...) abort + let l:options = lsp#utils#parse_command_options(a:000) + call s:next_diagnostic(s:get_all_buffer_diagnostics(), l:options) +endfunction + +function! s:next_diagnostic(diagnostics, options) abort + if !len(a:diagnostics) + return + endif + call sort(a:diagnostics, 's:compare_diagnostics') + + let l:wrap = 1 + if has_key(a:options, 'wrap') + let l:wrap = a:options['wrap'] + endif + + let l:view = winsaveview() + let l:next_line = 0 + let l:next_col = 0 + for l:diagnostic in a:diagnostics + let [l:line, l:col] = lsp#utils#position#lsp_to_vim('%', l:diagnostic['range']['start']) + if l:line > l:view['lnum'] + \ || (l:line == l:view['lnum'] && l:col > l:view['col'] + 1) + let l:next_line = l:line + let l:next_col = l:col - 1 + break + endif + endfor + + if l:next_line == 0 + if !l:wrap + return + endif + " Wrap to start + let [l:next_line, l:next_col] = lsp#utils#position#lsp_to_vim('%', a:diagnostics[0]['range']['start']) + let l:next_col -= 1 + endif + + let l:view['lnum'] = l:next_line + let l:view['col'] = l:next_col + let l:view['topline'] = 1 + let l:height = winheight(0) + let l:totalnum = line('$') + if l:totalnum > l:height + let l:half = l:height / 2 + if l:totalnum - l:half < l:view['lnum'] + let l:view['topline'] = l:totalnum - l:height + 1 + else + let l:view['topline'] = l:view['lnum'] - l:half + endif + endif + call winrestview(l:view) +endfunction + +function! lsp#internal#diagnostics#movement#_previous_error(...) abort + let l:diagnostics = filter(s:get_all_buffer_diagnostics(), + \ {_, diagnostic -> s:severity_of(diagnostic) ==# 1 }) + let l:options = lsp#utils#parse_command_options(a:000) + call s:previous_diagnostic(l:diagnostics, l:options) +endfunction + +function! lsp#internal#diagnostics#movement#_previous_warning(...) abort + let l:options = lsp#utils#parse_command_options(a:000) + let l:diagnostics = filter(s:get_all_buffer_diagnostics(), + \ {_, diagnostic -> s:severity_of(diagnostic) ==# 2 }) + call s:previous_diagnostic(l:diagnostics, l:options) +endfunction + +function! lsp#internal#diagnostics#movement#_previous_diagnostics(...) abort + let l:options = lsp#utils#parse_command_options(a:000) + call s:previous_diagnostic(s:get_all_buffer_diagnostics(), l:options) +endfunction + +function! s:previous_diagnostic(diagnostics, options) abort + if !len(a:diagnostics) + return + endif + call sort(a:diagnostics, 's:compare_diagnostics') + + let l:wrap = 1 + if has_key(a:options, 'wrap') + let l:wrap = a:options['wrap'] + endif + + let l:view = winsaveview() + let l:next_line = 0 + let l:next_col = 0 + let l:index = len(a:diagnostics) - 1 + while l:index >= 0 + let [l:line, l:col] = lsp#utils#position#lsp_to_vim('%', a:diagnostics[l:index]['range']['start']) + if l:line < l:view['lnum'] + \ || (l:line == l:view['lnum'] && l:col < l:view['col']) + let l:next_line = l:line + let l:next_col = l:col - 1 + break + endif + let l:index = l:index - 1 + endwhile + + if l:next_line == 0 + if !l:wrap + return + endif + " Wrap to end + let [l:next_line, l:next_col] = lsp#utils#position#lsp_to_vim('%', a:diagnostics[-1]['range']['start']) + let l:next_col -= 1 + endif + + let l:view['lnum'] = l:next_line + let l:view['col'] = l:next_col + let l:view['topline'] = 1 + let l:height = winheight(0) + let l:totalnum = line('$') + if l:totalnum > l:height + let l:half = l:height / 2 + if l:totalnum - l:half < l:view['lnum'] + let l:view['topline'] = l:totalnum - l:height + 1 + else + let l:view['topline'] = l:view['lnum'] - l:half + endif + endif + call winrestview(l:view) +endfunction + +function! s:get_diagnostics(uri) abort + if has_key(s:diagnostics, a:uri) + return [1, s:diagnostics[a:uri]] + else + if s:is_win + " vim in windows always uses upper case for drive letter, so use lowercase in case lang server uses lowercase + " https://github.com/theia-ide/typescript-language-server/issues/23 + let l:uri = substitute(a:uri, '^' . a:uri[:8], tolower(a:uri[:8]), '') + if has_key(s:diagnostics, l:uri) + return [1, s:diagnostics[l:uri]] + endif + endif + endif + return [0, {}] +endfunction + +" Get diagnostics for the current buffer URI from all servers +function! s:get_all_buffer_diagnostics(...) abort + let l:server = get(a:000, 0, '') + + let l:bufnr = bufnr('%') + let l:uri = lsp#utils#get_buffer_uri(l:bufnr) + + if !lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) + return [] + endif + + let l:diagnostics_by_server = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri) + let l:diagnostics = [] + if empty(l:server) + for l:item in values(l:diagnostics_by_server) + let l:diagnostics += lsp#utils#iteratable(l:item['params']['diagnostics']) + endfor + else + if has_key(l:diagnostics_by_server, l:server) + let l:diagnostics = lsp#utils#iteratable(l:diagnostics_by_server[l:server]['params']['diagnostics']) + endif + endif + + return l:diagnostics +endfunction + +function! s:compare_diagnostics(d1, d2) abort + let l:range1 = a:d1['range'] + let l:line1 = l:range1['start']['line'] + 1 + let l:col1 = l:range1['start']['character'] + 1 + let l:range2 = a:d2['range'] + let l:line2 = l:range2['start']['line'] + 1 + let l:col2 = l:range2['start']['character'] + 1 + + if l:line1 == l:line2 + return l:col1 == l:col2 ? 0 : l:col1 > l:col2 ? 1 : -1 + else + return l:line1 > l:line2 ? 1 : -1 + endif +endfunction +" vim sw=4 ts=4 et diff --git a/autoload/lsp/internal/diagnostics/signs.vim b/autoload/lsp/internal/diagnostics/signs.vim new file mode 100644 index 00000000..74da606c --- /dev/null +++ b/autoload/lsp/internal/diagnostics/signs.vim @@ -0,0 +1,152 @@ +" internal state for whether it is enabled or not to avoid multiple subscriptions +let s:enabled = 0 +let s:sign_group = 'vim_lsp' + +let s:severity_sign_names_mapping = { + \ 1: 'LspError', + \ 2: 'LspWarning', + \ 3: 'LspInformation', + \ 4: 'LspHint', + \ } + +if !hlexists('LspErrorText') + highlight link LspErrorText Error +endif + +if !hlexists('LspWarningText') + highlight link LspWarningText Todo +endif + +if !hlexists('LspInformationText') + highlight link LspInformationText Normal +endif + +if !hlexists('LspHintText') + highlight link LspHintText Normal +endif + +" imports +let s:Buffer = vital#lsp#import('VS.Vim.Buffer') + +function! lsp#internal#diagnostics#signs#_enable() abort + " don't even bother registering if the feature is disabled + if !lsp#utils#_has_signs() | return | endif + if !g:lsp_diagnostics_signs_enabled | return | endif + + if s:enabled | return | endif + let s:enabled = 1 + + call s:define_sign('LspError', 'E>', g:lsp_diagnostics_signs_error) + call s:define_sign('LspWarning', 'W>', g:lsp_diagnostics_signs_warning) + call s:define_sign('LspInformation', 'I>', g:lsp_diagnostics_signs_information) + call s:define_sign('LspHint', 'H>', g:lsp_diagnostics_signs_hint) + + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#merge( + \ lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response') + \ && has_key(x['response'], 'method') && x['response']['method'] ==# '$/vimlsp/lsp_diagnostics_updated' + \ && !lsp#client#is_error(x['response'])}), + \ lsp#callbag#map({x->x['response']['params']}), + \ ), + \ lsp#callbag#pipe( + \ lsp#callbag#fromEvent(['InsertEnter', 'InsertLeave']), + \ lsp#callbag#filter({_->!g:lsp_diagnostics_signs_insert_mode_enabled}), + \ lsp#callbag#map({_->{ 'uri': lsp#utils#get_buffer_uri() }}), + \ ), + \ ), + \ lsp#callbag#filter({_->g:lsp_diagnostics_signs_enabled}), + \ lsp#callbag#debounceTime(g:lsp_diagnostics_signs_delay), + \ lsp#callbag#tap({x->s:clear_signs(x)}), + \ lsp#callbag#tap({x->s:set_signs(x)}), + \ lsp#callbag#subscribe(), + \ ) +endfunction + +function! lsp#internal#diagnostics#signs#_disable() abort + if !s:enabled | return | endif + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif + call s:clear_all_signs() + call s:undefine_signs() + let s:enabled = 0 +endfunction + +" Set default sign text to handle case when user provides empty dict +function! s:define_sign(sign_name, sign_default_text, sign_options) abort + let l:options = { + \ 'text': get(a:sign_options, 'text', a:sign_default_text), + \ 'texthl': a:sign_name . 'Text', + \ 'linehl': a:sign_name . 'Line', + \ } + let l:sign_icon = get(a:sign_options, 'icon', '') + if !empty(l:sign_icon) + let l:options['icon'] = l:sign_icon + endif + call sign_define(a:sign_name, l:options) +endfunction + +function! s:undefine_signs() abort + call sign_undefine('LspError') + call sign_undefine('LspWarning') + call sign_undefine('LspInformation') + call sign_undefine('LspHint') +endfunction + +function! s:clear_all_signs() abort + call sign_unplace(s:sign_group) +endfunction + +" params => { +" server: '' " optional +" uri: '' " optional +" } +function! s:clear_signs(params) abort + " TODO: optimize by looking at params + call s:clear_all_signs() +endfunction + +" params => { +" server: '' " optional +" uri: '' " optional +" } +function! s:set_signs(params) abort + " TODO: optimize by looking at params + if !g:lsp_diagnostics_signs_insert_mode_enabled + if mode()[0] ==# 'i' | return | endif + endif + + for l:bufnr in range(1, bufnr('$')) + if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr) + let l:uri = lsp#utils#get_buffer_uri(l:bufnr) + for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)) + call s:place_signs(l:server, l:diagnostics_response, l:bufnr) + endfor + endif + endfor +endfunction + +function! s:place_signs(server, diagnostics_response, bufnr) abort + let l:linecount = s:Buffer.get_line_count(a:bufnr) + for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics']) + let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start']) + + " Some language servers report an unexpected EOF one line past the end + if l:line == l:linecount + 1 + let l:line = l:line - 1 + endif + + if has_key(l:item, 'severity') && !empty(l:item['severity']) + let l:sign_name = get(s:severity_sign_names_mapping, l:item['severity'], 'LspError') + let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map, l:sign_name, g:lsp_diagnostics_signs_priority) + let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map, + \ a:server . '_' . l:sign_name, l:sign_priority) + " pass 0 and let vim generate sign id + let l:sign_id = sign_place(0, s:sign_group, l:sign_name, a:bufnr, + \{ 'lnum': l:line, 'priority': l:sign_priority }) + endif + endfor +endfunction diff --git a/autoload/lsp/internal/diagnostics/state.vim b/autoload/lsp/internal/diagnostics/state.vim new file mode 100644 index 00000000..41ffa277 --- /dev/null +++ b/autoload/lsp/internal/diagnostics/state.vim @@ -0,0 +1,173 @@ +" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic +" +" Refer to https://github.com/microsoft/language-server-protocol/pull/1019 on normalization of urls. +" { +" 'normalized_uri': { +" 'server_name': { +" 'method': 'textDocument/publishDiagnostics', +" 'params': { +" 'uri': 'uri', " this uri is not normalized and is exactly what server returns +" 'dignostics': [ " array can never be null but can be empty +" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic +" { range, message, severity?, code?, codeDesciption?, source?, tags?, relatedInformation?, data? } +" ] +" } +" } +" } +" Note: Do not remove when buffer unloads or doesn't exist since some server +" may send diagnsotics information regardless of textDocument/didOpen. +" buffer state is removed when server exits. +" TODO: reset buffer state when server initializes. ignoring for now for perf. +let s:diagnostics_state = {} + +" internal state for whether it is enabled or not to avoid multiple subscriptions +let s:enabled = 0 + +let s:diagnostic_kinds = { + \ 1: 'error', + \ 2: 'warning', + \ 3: 'information', + \ 4: 'hint', + \ } + +function! lsp#internal#diagnostics#state#_enable() abort + " don't even bother registering if the feature is disabled + if !g:lsp_diagnostics_enabled | return | endif + + if s:enabled | return | endif + let s:enabled = 1 + + call lsp#internal#diagnostics#state#_reset() + + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#merge( + \ lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response') + \ && get(x['response'], 'method', '') ==# 'textDocument/publishDiagnostics'}), + \ lsp#callbag#tap({x->s:on_text_documentation_publish_diagnostics(x['server'], x['response'])}), + \ ), + \ lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response') + \ && get(x['response'], 'method', '') ==# '$/vimlsp/lsp_server_exit' }), + \ lsp#callbag#tap({x->s:on_exit(x['response'])}), + \ ), + \ ), + \ lsp#callbag#subscribe(), + \ ) + + call s:notify_diagnostics_update() +endfunction + +function! lsp#internal#diagnostics#state#_disable() abort + if !s:enabled | return | endif + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif + call lsp#internal#diagnostics#state#_reset() + call s:notify_diagnostics_update() + let s:enabled = 0 +endfunction + +function! lsp#internal#diagnostics#state#_reset() abort + let s:diagnostics_state = {} + let s:diagnostics_disabled_buffers = {} +endfunction + +" callers should always treat the return value as immutable +" @return { +" 'servername': response +" } +function! lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(uri) abort + return get(s:diagnostics_state, lsp#utils#normalize_uri(a:uri), {}) +endfunction + +" callers should always treat the return value as immutable. +" callers should treat uri as normalized via lsp#utils#normalize_uri +" @return { +" 'normalized_uri': { +" 'servername': response +" } +" } +function! lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server() abort + return s:diagnostics_state +endfunction + +function! s:on_text_documentation_publish_diagnostics(server, response) abort + if lsp#client#is_error(a:response) | return | endif + let l:normalized_uri = lsp#utils#normalize_uri(a:response['params']['uri']) + if !has_key(s:diagnostics_state, l:normalized_uri) + let s:diagnostics_state[l:normalized_uri] = {} + endif + let s:diagnostics_state[l:normalized_uri][a:server] = a:response + call s:notify_diagnostics_update(a:server, l:normalized_uri) +endfunction + +function! s:on_exit(response) abort + let l:server = a:response['params']['server'] + let l:notify = 0 + for [l:key, l:value] in items(s:diagnostics_state) + if has_key(l:value, l:server) + let l:notify = 1 + call remove(l:value, l:server) + endif + endfor + if l:notify | call s:notify_diagnostics_update(l:server) | endif +endfunction + +function! lsp#internal#diagnostics#state#_force_notify_buffer(buffer) abort + " TODO: optimize buffer only + call s:notify_diagnostics_update() +endfunction + +" call s:notify_diagnostics_update() +" call s:notify_diagnostics_update('server') +" call s:notify_diagnostics_update('server', 'uri') +function! s:notify_diagnostics_update(...) abort + let l:data = { 'server': '$vimlsp', 'response': { 'method': '$/vimlsp/lsp_diagnostics_updated', 'params': {} } } + " if a:0 > 0 | let l:data['response']['params']['server'] = a:1 | endif + " if a:0 > 1 | let l:data['response']['params']['uri'] = a:2 | endif + call lsp#stream(1, l:data) + doautocmd User lsp_diagnostics_updated +endfunction + +function! lsp#internal#diagnostics#state#_enable_for_buffer(bufnr) abort + if getbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) == 0 + call setbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) + call s:notify_diagnostics_update() + endif +endfunction + +function! lsp#internal#diagnostics#state#_disable_for_buffer(bufnr) abort + if getbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) != 0 + call setbufvar(a:bufnr, 'lsp_diagnostics_enabled', 0) + call s:notify_diagnostics_update() + endif +endfunction + +function! lsp#internal#diagnostics#state#_is_enabled_for_buffer(bufnr) abort + return getbufvar(a:bufnr, 'lsp_diagnostics_enabled', 1) == 1 +endfunction + +" Return dict with diagnostic counts for the specified buffer +" { 'error': 1, 'warning': 0, 'information': 0, 'hint': 0 } +function! lsp#internal#diagnostics#state#_get_diagnostics_count_for_buffer(bufnr) abort + let l:counts = { + \ 'error': 0, + \ 'warning': 0, + \ 'information': 0, + \ 'hint': 0, + \ } + if lsp#internal#diagnostics#state#_is_enabled_for_buffer(a:bufnr) + let l:uri = lsp#utils#get_buffer_uri(a:bufnr) + for [l:_, l:response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)) + for l:diagnostic in lsp#utils#iteratable(l:response['params']['diagnostics']) + let l:key = get(s:diagnostic_kinds, get(l:diagnostic, 'severity', 1) , 'error') + let l:counts[l:key] += 1 + endfor + endfor + end + return l:counts +endfunction diff --git a/autoload/lsp/internal/diagnostics/under_cursor.vim b/autoload/lsp/internal/diagnostics/under_cursor.vim new file mode 100644 index 00000000..48f8c47b --- /dev/null +++ b/autoload/lsp/internal/diagnostics/under_cursor.vim @@ -0,0 +1,48 @@ +" Returns a diagnostic object, or empty dictionary if no diagnostics are +" available. +" options = { +" 'server': '', " optional +" } +function! lsp#internal#diagnostics#under_cursor#get_diagnostic(...) abort + let l:options = get(a:000, 0, {}) + let l:server = get(l:options, 'server', '') + let l:bufnr = bufnr('%') + + if !lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) + return {} + endif + + let l:uri = lsp#utils#get_buffer_uri(l:bufnr) + + let l:diagnostics_by_server = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri) + let l:diagnostics = [] + if empty(l:server) + for l:item in values(l:diagnostics_by_server) + let l:diagnostics += lsp#utils#iteratable(l:item['params']['diagnostics']) + endfor + else + if has_key(l:diagnostics_by_server, l:server) + let l:diagnostics = lsp#utils#iteratable(l:diagnostics_by_server[l:server]['params']['diagnostics']) + endif + endif + + let l:line = line('.') + let l:col = col('.') + + let l:closest_diagnostic = {} + let l:closest_distance = -1 + + for l:diagnostic in l:diagnostics + let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim('%', l:diagnostic['range']['start']) + + if l:line == l:start_line + let l:distance = abs(l:start_col - l:col) + if l:closest_distance < 0 || l:distance < l:closest_distance + let l:closest_diagnostic = l:diagnostic + let l:closest_distance = l:distance + endif + endif + endfor + + return l:closest_diagnostic +endfunction diff --git a/autoload/lsp/internal/diagnostics/virtual_text.vim b/autoload/lsp/internal/diagnostics/virtual_text.vim new file mode 100644 index 00000000..a343ea54 --- /dev/null +++ b/autoload/lsp/internal/diagnostics/virtual_text.vim @@ -0,0 +1,195 @@ +" internal state for whether it is enabled or not to avoid multiple subscriptions +let s:enabled = 0 +let s:namespace_id = '' " will be set when enabled +let s:severity_sign_names_mapping = { + \ 1: 'LspError', + \ 2: 'LspWarning', + \ 3: 'LspInformation', + \ 4: 'LspHint', + \ } + +if !hlexists('LspErrorVirtualText') + if !hlexists('LspErrorText') + highlight link LspErrorVirtualText Error + else + highlight link LspErrorVirtualText LspErrorText + endif +endif + +if !hlexists('LspWarningVirtualText') + if !hlexists('LspWarningText') + highlight link LspWarningVirtualText Todo + else + highlight link LspWarningVirtualText LspWarningText + endif +endif + +if !hlexists('LspInformationVirtualText') + if !hlexists('LspInformationText') + highlight link LspInformationVirtualText Normal + else + highlight link LspInformationVirtualText LspInformationText + endif +endif + +if !hlexists('LspHintVirtualText') + if !hlexists('LspHintText') + highlight link LspHintVirtualText Normal + else + highlight link LspHintVirtualText LspHintText + endif +endif + +" imports +let s:Buffer = vital#lsp#import('VS.Vim.Buffer') + +function! lsp#internal#diagnostics#virtual_text#_enable() abort + " don't even bother registering if the feature is disabled + if !lsp#utils#_has_nvim_virtual_text() && !lsp#utils#_has_vim_virtual_text() | return | endif + if !g:lsp_diagnostics_virtual_text_enabled | return | endif + + if s:enabled | return | endif + let s:enabled = 1 + + if has('nvim') + if empty(s:namespace_id) + let s:namespace_id = nvim_create_namespace('vim_lsp_diagnostic_virtual_text') + endif + else + if index(prop_type_list(), 'vim_lsp_LspError_virtual_text') ==# -1 + call prop_type_add('vim_lsp_LspError_virtual_text', { 'highlight': 'LspErrorVirtualText' }) + call prop_type_add('vim_lsp_LspWarning_virtual_text', { 'highlight': 'LspWarningVirtualText' }) + call prop_type_add('vim_lsp_LspInformation_virtual_text', { 'highlight': 'LspInformationVirtualText' }) + call prop_type_add('vim_lsp_LspHint_virtual_text', { 'highlight': 'LspHintVirtualText' }) + endif + endif + + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#merge( + \ lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'server') && has_key(x, 'response') + \ && has_key(x['response'], 'method') && x['response']['method'] ==# '$/vimlsp/lsp_diagnostics_updated' + \ && !lsp#client#is_error(x['response'])}), + \ lsp#callbag#map({x->x['response']['params']}), + \ ), + \ lsp#callbag#pipe( + \ lsp#callbag#fromEvent(['InsertEnter', 'InsertLeave']), + \ lsp#callbag#filter({_->!g:lsp_diagnostics_virtual_text_insert_mode_enabled}), + \ lsp#callbag#map({_->{ 'uri': lsp#utils#get_buffer_uri() }}), + \ ), + \ ), + \ lsp#callbag#filter({_->g:lsp_diagnostics_virtual_text_enabled}), + \ lsp#callbag#debounceTime(g:lsp_diagnostics_virtual_text_delay), + \ lsp#callbag#tap({x->s:clear_virtual_text(x)}), + \ lsp#callbag#tap({x->s:set_virtual_text(x)}), + \ lsp#callbag#subscribe(), + \ ) +endfunction + +function! lsp#internal#diagnostics#virtual_text#_disable() abort + if !s:enabled | return | endif + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif + call s:clear_all_virtual_text() + let s:enabled = 0 +endfunction + +function! s:clear_all_virtual_text() abort + if has('nvim') + for l:bufnr in nvim_list_bufs() + if bufexists(l:bufnr) && bufloaded(l:bufnr) + call nvim_buf_clear_namespace(l:bufnr, s:namespace_id, 0, -1) + endif + endfor + else + let l:types = ['vim_lsp_LspError_virtual_text', 'vim_lsp_LspWarning_virtual_text', 'vim_lsp_LspInformation_virtual_text', 'vim_lsp_LspHint_virtual_text'] + for l:bufnr in map(copy(getbufinfo()), 'v:val.bufnr') + if lsp#utils#_has_prop_remove_types() + call prop_remove({'types': l:types, 'bufnr': l:bufnr, 'all': v:true}) + else + for l:type in l:types + call prop_remove({'type': l:type, 'bufnr': l:bufnr, 'all': v:true}) + endfor + endif + endfor + endif +endfunction + +" params => { +" server: '' " optional +" uri: '' " optional +" } +function! s:clear_virtual_text(params) abort + " TODO: optimize by looking at params + call s:clear_all_virtual_text() +endfunction + +" params => { +" server: '' " optional +" uri: '' " optional +" } +function! s:set_virtual_text(params) abort + " TODO: optimize by looking at params + if !g:lsp_diagnostics_virtual_text_insert_mode_enabled + if mode()[0] ==# 'i' | return | endif + endif + + if has('nvim') + for l:bufnr in nvim_list_bufs() + if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr) + let l:uri = lsp#utils#get_buffer_uri(l:bufnr) + for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)) + call s:place_virtual_text(l:server, l:diagnostics_response, l:bufnr) + endfor + endif + endfor + else + for l:bufnr in map(copy(getbufinfo()), 'v:val.bufnr') + if lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr) && bufexists(l:bufnr) && bufloaded(l:bufnr) + let l:uri = lsp#utils#get_buffer_uri(l:bufnr) + for [l:server, l:diagnostics_response] in items(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri)) + call s:place_virtual_text(l:server, l:diagnostics_response, l:bufnr) + endfor + endif + endfor + endif +endfunction + +function! s:place_virtual_text(server, diagnostics_response, bufnr) abort + let l:linecount = s:Buffer.get_line_count(a:bufnr) + for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics']) + let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start']) + let l:name = get(s:severity_sign_names_mapping, get(l:item, 'severity', 3), 'LspError') + let l:text = g:lsp_diagnostics_virtual_text_prefix . l:item['message'] + + " Some language servers report an unexpected EOF one line past the end + if l:line == l:linecount + 1 + let l:line = l:line - 1 + endif + + if has('nvim') + let l:hl_name = l:name . 'VirtualText' + " need to do -1 for virtual text + call nvim_buf_set_virtual_text(a:bufnr, s:namespace_id, l:line - 1, + \ [[l:text, l:hl_name]], {}) + else + " it's an error to add virtual text on lines that don't exist + " anymore due to async processing, just skip such diagnostics + if l:line <= l:linecount + let l:type = 'vim_lsp_' . l:name . '_virtual_text' + call prop_remove({'all': v:true, 'type': l:type, 'bufnr': a:bufnr}, l:line) + call prop_add( + \ l:line, 0, + \ { + \ 'type': l:type, 'text': l:text, 'bufnr': a:bufnr, + \ 'text_align': g:lsp_diagnostics_virtual_text_align, + \ 'text_padding_left': g:lsp_diagnostics_virtual_text_padding_left, + \ 'text_wrap': g:lsp_diagnostics_virtual_text_wrap, + \ }) + endif + endif + endfor +endfunction diff --git a/autoload/lsp/internal/document_code_action/signs.vim b/autoload/lsp/internal/document_code_action/signs.vim new file mode 100644 index 00000000..8b4ace4a --- /dev/null +++ b/autoload/lsp/internal/document_code_action/signs.vim @@ -0,0 +1,131 @@ +" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction + +" internal state for whether it is enabled or not to avoid multiple subscriptions +let s:enabled = 0 + +let s:sign_group = 'vim_lsp_document_code_action_signs' + +if !hlexists('LspCodeActionText') + highlight link LspCodeActionText Normal +endif + +function! lsp#internal#document_code_action#signs#_enable() abort + if !lsp#utils#_has_signs() | return | endif + " don't even bother registering if the feature is disabled + if !g:lsp_document_code_action_signs_enabled | return | endif + + if s:enabled | return | endif + let s:enabled = 1 + + call s:define_sign('LspCodeAction', 'A>', g:lsp_document_code_action_signs_hint) + + " Note: + " - update CodeAction signs when CusorMoved or CursorHold + " - clear signs when InsertEnter or BufLeave + " - debounce code action requests + " - automatically switch to latest code action request via switchMap() + " - cancel code action request via takeUntil() when BufLeave + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#merge( + \ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']), + \ lsp#callbag#pipe( + \ lsp#callbag#fromEvent(['InsertEnter', 'BufLeave']), + \ lsp#callbag#tap({_ -> s:clear_signs() }), + \ ) + \ ), + \ lsp#callbag#filter({_ -> g:lsp_document_code_action_signs_enabled }), + \ lsp#callbag#debounceTime(g:lsp_document_code_action_signs_delay), + \ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}), + \ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}), + \ lsp#callbag#filter({_->mode() is# 'n' && getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }), + \ lsp#callbag#switchMap({_-> + \ lsp#callbag#pipe( + \ s:send_request(), + \ lsp#callbag#materialize(), + \ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}), + \ lsp#callbag#map({x->x['value']}), + \ lsp#callbag#takeUntil( + \ lsp#callbag#fromEvent('BufLeave') + \ ) + \ ) + \ }), + \ lsp#callbag#subscribe({x->s:set_signs(x)}), + \) +endfunction + +function! lsp#internal#document_code_action#signs#_disable() abort + if !s:enabled | return | endif + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! s:send_request() abort + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)') + + if empty(l:servers) + return lsp#callbag#empty() + endif + + let l:range = lsp#utils#range#_get_current_line_range() + return lsp#callbag#pipe( + \ lsp#callbag#fromList(l:servers), + \ lsp#callbag#flatMap({server-> + \ lsp#request(server, { + \ 'method': 'textDocument/codeAction', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'range': l:range, + \ 'context': { + \ 'diagnostics': [], + \ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite'], + \ } + \ } + \ }) + \ }), + \ lsp#callbag#filter({x-> !lsp#client#is_error(x['response']) && !empty(x['response']['result'])}), + \ lsp#callbag#take(1), + \ ) +endfunction + +function! s:clear_signs() abort + call sign_unplace(s:sign_group) +endfunction + +function! s:set_signs(data) abort + call s:clear_signs() + + if lsp#client#is_error(a:data['response']) | return | endif + + if empty(a:data['response']['result']) + return + endif + + let l:bufnr = bufnr(lsp#utils#uri_to_path(a:data['request']['params']['textDocument']['uri'])) + call s:place_signs(a:data, l:bufnr) +endfunction + +" Set default sign text to handle case when user provides empty dict +function! s:define_sign(sign_name, sign_default_text, sign_options) abort + let l:options = { + \ 'text': get(a:sign_options, 'text', a:sign_default_text), + \ 'texthl': a:sign_name . 'Text', + \ 'linehl': a:sign_name . 'Line', + \ } + let l:sign_icon = get(a:sign_options, 'icon', '') + if !empty(l:sign_icon) + let l:options['icon'] = l:sign_icon + endif + call sign_define(a:sign_name, l:options) +endfunction + +function! s:place_signs(data, bufnr) abort + if !bufexists(a:bufnr) || !bufloaded(a:bufnr) + return + endif + let l:sign_priority = g:lsp_document_code_action_signs_priority + let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, a:data['request']['params']['range']['start']) + let l:sign_id = sign_place(0, s:sign_group, 'LspCodeAction', a:bufnr, + \ { 'lnum': l:line, 'priority': l:sign_priority }) +endfunction diff --git a/autoload/lsp/internal/document_formatting.vim b/autoload/lsp/internal/document_formatting.vim new file mode 100644 index 00000000..3e10fc78 --- /dev/null +++ b/autoload/lsp/internal/document_formatting.vim @@ -0,0 +1,86 @@ +" options - { +" bufnr: bufnr('%') " required +" server - 'server_name' " optional +" sync: 0 " optional, defaults to 0 (async) +" } +function! lsp#internal#document_formatting#format(options) abort + let l:mode = mode() + if l:mode =~# '[vV]' || l:mode ==# "\" + return lsp#internal#document_range_formatting#format(a:options) + endif + + if has_key(a:options, 'server') + let l:servers = [a:options['server']] + else + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_formatting_provider(v:val)') + endif + + if len(l:servers) == 0 + let l:filetype = getbufvar(a:options['bufnr'], '&filetype') + call lsp#utils#error('textDocument/formatting not supported for ' . l:filetype) + return + endif + + " TODO: ask user to select server for formatting if there are multiple servers + let l:server = l:servers[0] + + redraw | echo 'Formatting Document ...' + + call lsp#_new_command() + + let l:request = { + \ 'method': 'textDocument/formatting', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(a:options['bufnr']), + \ 'options': { + \ 'tabSize': lsp#utils#buffer#get_indent_size(a:options['bufnr']), + \ 'insertSpaces': getbufvar(a:options['bufnr'], '&expandtab') ? v:true : v:false, + \ } + \ }, + \ 'bufnr': a:options['bufnr'], + \ } + + if get(a:options, 'sync', 0) == 1 + try + let l:x = lsp#callbag#pipe( + \ lsp#request(l:server, l:request), + \ lsp#callbag#takeUntil(lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'command')}), + \ )), + \ lsp#callbag#toList(), + \ ).wait({ 'sleep': get(a:options, 'sleep', 1), 'timeout': get(a:options, 'timeout', g:lsp_format_sync_timeout) }) + call s:format_next(l:x[0]) + call s:format_complete() + catch + call s:format_error(v:exception . ' ' . v:throwpoint) + endtry + else + return lsp#callbag#pipe( + \ lsp#request(l:server, l:request), + \ lsp#callbag#takeUntil(lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'command')}), + \ )), + \ lsp#callbag#subscribe({ + \ 'next':{x->s:format_next(x)}, + \ 'error': {x->s:format_error(e)}, + \ 'complete': {->s:format_complete()}, + \ }), + \ ) + endif +endfunction + +function! s:format_next(x) abort + if lsp#client#is_error(a:x['response']) | return | endif + call lsp#utils#text_edit#apply_text_edits(a:x['request']['params']['textDocument']['uri'], get(a:x['response'], 'result', '')) +endfunction + +function! s:format_error(e) abort + call lsp#log('Formatting Document Failed', a:e) + call lsp#utils#error('Formatting Document Failed.' . (type(a:e) == type('') ? a:e : '')) +endfunction + +function! s:format_complete() abort + redraw | echo 'Formatting Document complete' +endfunction diff --git a/autoload/lsp/internal/document_highlight.vim b/autoload/lsp/internal/document_highlight.vim new file mode 100644 index 00000000..a6022e03 --- /dev/null +++ b/autoload/lsp/internal/document_highlight.vim @@ -0,0 +1,238 @@ +let s:use_vim_textprops = lsp#utils#_has_textprops() && !has('nvim') +let s:prop_id = 11 + +function! lsp#internal#document_highlight#_enable() abort + " don't event bother registering if the feature is disabled + if !g:lsp_document_highlight_enabled | return | endif + + " Highlight group for references + if !hlexists('lspReference') + highlight link lspReference CursorColumn + endif + + " Note: + " - update highlight references when CusorMoved or CursorHold + " - clear highlights when InsertEnter or BufLeave + " - debounce highlight requests + " - automatically switch to latest highlight request via switchMap() + " - cancel highlight request via takeUntil() when BufLeave + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#merge( + \ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']), + \ lsp#callbag#pipe( + \ lsp#callbag#fromEvent(['InsertEnter', 'BufLeave']), + \ lsp#callbag#tap({_ -> s:clear_highlights() }), + \ ) + \ ), + \ lsp#callbag#filter({_ -> g:lsp_document_highlight_enabled }), + \ lsp#callbag#debounceTime(g:lsp_document_highlight_delay), + \ lsp#callbag#map({_->{'bufnr': bufnr('%'), 'curpos': getcurpos()[0:2], 'changedtick': b:changedtick }}), + \ lsp#callbag#distinctUntilChanged({a,b -> a['bufnr'] == b['bufnr'] && a['curpos'] == b['curpos'] && a['changedtick'] == b['changedtick']}), + \ lsp#callbag#filter({_->mode() is# 'n' && getbufvar(bufnr('%'), '&buftype') !=# 'terminal' }), + \ lsp#callbag#switchMap({_-> + \ lsp#callbag#pipe( + \ s:send_highlight_request(), + \ lsp#callbag#materialize(), + \ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}), + \ lsp#callbag#map({x->x['value']}), + \ lsp#callbag#takeUntil( + \ lsp#callbag#fromEvent('BufLeave') + \ ) + \ ) + \ }), + \ lsp#callbag#filter({_->mode() is# 'n'}), + \ lsp#callbag#subscribe({x->s:set_highlights(x)}), + \) +endfunction + +function! lsp#internal#document_highlight#_disable() abort + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! s:send_highlight_request() abort + let l:capability = 'lsp#capabilities#has_document_highlight_provider(v:val)' + let l:servers = filter(lsp#get_allowed_servers(), l:capability) + + if empty(l:servers) + return lsp#callbag#empty() + endif + + return lsp#request(l:servers[0], { + \ 'method': 'textDocument/documentHighlight', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': lsp#get_position(), + \ }, + \ }) +endfunction + +function! s:set_highlights(data) abort + let l:bufnr = bufnr('%') + + call s:clear_highlights() + + if mode() !=# 'n' | return | endif + + if lsp#client#is_error(a:data['response']) | return | endif + + " Get references from the response + let l:reference_list = a:data['response']['result'] + if empty(l:reference_list) + return + endif + + " Convert references to vim positions + let l:position_list = [] + for l:reference in l:reference_list + call extend(l:position_list, lsp#utils#range#lsp_to_vim(l:bufnr, l:reference['range'])) + endfor + + call sort(l:position_list, function('s:compare_positions')) + + " Ignore response if the cursor is not over a reference anymore + if s:in_reference(l:position_list) == -1 | return | endif + + " Store references + if s:use_vim_textprops + let b:lsp_reference_positions = l:position_list + let b:lsp_reference_matches = [] + else + let w:lsp_reference_positions = l:position_list + let w:lsp_reference_matches = [] + endif + + " Apply highlights to the buffer + call s:init_reference_highlight(l:bufnr) + if s:use_vim_textprops + for l:position in l:position_list + try + " TODO: need to check for valid range before calling prop_add + " See https://github.com/prabirshrestha/vim-lsp/pull/721 + silent! call prop_add(l:position[0], l:position[1], { + \ 'id': s:prop_id, + \ 'bufnr': l:bufnr, + \ 'length': l:position[2], + \ 'type': 'vim-lsp-reference-highlight'}) + call add(b:lsp_reference_matches, l:position[0]) + catch + call lsp#log('document_highlight', 'set_highlights', v:exception, v:throwpoint) + endtry + endfor + else + for l:position in l:position_list + let l:match = matchaddpos('lspReference', [l:position], -5) + call add(w:lsp_reference_matches, l:match) + endfor + endif +endfunction + +function! s:clear_highlights() abort + if s:use_vim_textprops + if exists('b:lsp_reference_matches') + let l:bufnr = bufnr('%') + for l:line in b:lsp_reference_matches + silent! call prop_remove( + \ {'id': s:prop_id, + \ 'bufnr': l:bufnr, + \ 'all': v:true}, l:line) + endfor + unlet b:lsp_reference_matches + unlet b:lsp_reference_positions + endif + else + if exists('w:lsp_reference_matches') + for l:match in w:lsp_reference_matches + silent! call matchdelete(l:match) + endfor + unlet w:lsp_reference_matches + unlet w:lsp_reference_positions + endif + endif +endfunction + +" Compare two positions +function! s:compare_positions(p1, p2) abort + let l:line_1 = a:p1[0] + let l:line_2 = a:p2[0] + if l:line_1 != l:line_2 + return l:line_1 > l:line_2 ? 1 : -1 + endif + let l:col_1 = a:p1[1] + let l:col_2 = a:p2[1] + return l:col_1 - l:col_2 +endfunction + +" If the cursor is over a reference, return its index in +" the array. Otherwise, return -1. +function! s:in_reference(reference_list) abort + let l:line = line('.') + let l:column = col('.') + let l:index = 0 + for l:position in a:reference_list + if l:line == l:position[0] && + \ l:column >= l:position[1] && + \ l:column < l:position[1] + l:position[2] + return l:index + endif + let l:index += 1 + endfor + return -1 +endfunction + +function! s:init_reference_highlight(buf) abort + if s:use_vim_textprops + let l:props = { + \ 'bufnr': a:buf, + \ 'highlight': 'lspReference', + \ 'combine': v:true, + \ 'priority': lsp#internal#textprop#priority('document_highlight') + \ } + if prop_type_get('vim-lsp-reference-highlight', { 'bufnr': a:buf }) == {} + call prop_type_add('vim-lsp-reference-highlight', l:props) + endif + endif +endfunction + +" Cyclically move between references by `offset` occurrences. +function! lsp#internal#document_highlight#jump(offset) abort + if s:use_vim_textprops && !exists('b:lsp_reference_positions') || + \ !s:use_vim_textprops && !exists('w:lsp_reference_positions') + echohl WarningMsg + echom 'References not available' + echohl None + return + endif + + " Get index of reference under cursor + let l:index = s:use_vim_textprops ? s:in_reference(b:lsp_reference_positions) : s:in_reference(w:lsp_reference_positions) + if l:index < 0 + return + endif + + let l:n = s:use_vim_textprops ? len(b:lsp_reference_positions) : len(w:lsp_reference_positions) + let l:index += a:offset + + " Show a message when reaching TOP/BOTTOM of the file + if l:index < 0 + echohl WarningMsg + echom 'search hit TOP, continuing at BOTTOM' + echohl None + elseif l:index >= (s:use_vim_textprops ? len(b:lsp_reference_positions) : len(w:lsp_reference_positions)) + echohl WarningMsg + echom 'search hit BOTTOM, continuing at TOP' + echohl None + endif + + " Wrap index + if l:index < 0 || l:index >= (s:use_vim_textprops ? len(b:lsp_reference_positions) : len(w:lsp_reference_positions)) + let l:index = (l:index % l:n + l:n) % l:n + endif + + " Jump + let l:target = (s:use_vim_textprops ? b:lsp_reference_positions : w:lsp_reference_positions)[l:index][0:1] + normal! m` + call cursor(l:target[0], l:target[1]) +endfunction diff --git a/autoload/lsp/internal/document_hover/under_cursor.vim b/autoload/lsp/internal/document_hover/under_cursor.vim new file mode 100644 index 00000000..458ad6b1 --- /dev/null +++ b/autoload/lsp/internal/document_hover/under_cursor.vim @@ -0,0 +1,281 @@ +" https://microsoft.github.io/language-server-protocol/specification#textDocument_hover + +let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown') +let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent') +let s:FloatingWindow = vital#lsp#import('VS.Vim.Window.FloatingWindow') +let s:Window = vital#lsp#import('VS.Vim.Window') +let s:Buffer = vital#lsp#import('VS.Vim.Buffer') + +" options - { +" server - 'server_name' " optional +" ui - 'float' | 'preview' +" } +function! lsp#internal#document_hover#under_cursor#do(options) abort + let l:bufnr = bufnr('%') + let l:ui = get(a:options, 'ui', g:lsp_hover_ui) + if empty(l:ui) + let l:ui = s:FloatingWindow.is_available() ? 'float' : 'preview' + endif + + if l:ui ==# 'float' + let l:doc_win = s:get_doc_win() + if l:doc_win.is_visible() + if bufnr('%') ==# l:doc_win.get_bufnr() + call s:close_floating_window() + else + call l:doc_win.enter() + inoremap (lsp-float-close) :call close_floating_window() + nnoremap (lsp-float-close) :call close_floating_window() + execute('doautocmd User lsp_float_focused') + if !hasmapto('(lsp-float-close)') + imap (lsp-float-close) + nmap (lsp-float-close) + endif + endif + return + endif + endif + + if has_key(a:options, 'server') + let l:servers = [a:options['server']] + else + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_hover_provider(v:val)') + endif + + if len(l:servers) == 0 + let l:filetype = getbufvar(l:bufnr, '&filetype') + call lsp#utils#error('textDocument/hover not supported for ' . l:filetype) + return + endif + + redraw | echo 'Retrieving hover ...' + + call lsp#_new_command() + + " TODO: ask user to select server for formatting if there are multiple servers + let l:request = { + \ 'method': 'textDocument/hover', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': lsp#get_position(), + \ }, + \ } + call lsp#callbag#pipe( + \ lsp#callbag#fromList(l:servers), + \ lsp#callbag#flatMap({server-> + \ lsp#request(server, l:request) + \ }), + \ lsp#callbag#tap({x->s:show_hover(l:ui, x['server_name'], x['request'], x['response'])}), + \ lsp#callbag#takeUntil(lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'command')}), + \ )), + \ lsp#callbag#subscribe(), + \ ) +endfunction + +function! lsp#internal#document_hover#under_cursor#getpreviewwinid() abort + if exists('s:doc_win') + return s:doc_win.get_winid() + endif + return v:null +endfunction + +function! s:show_hover(ui, server_name, request, response) abort + if !has_key(a:response, 'result') || empty(a:response['result']) || + \ empty(a:response['result']['contents']) + call lsp#utils#error('No hover information found in server - ' . a:server_name) + return + endif + + echo '' + + if s:FloatingWindow.is_available() && a:ui ==? 'float' + call s:show_floating_window(a:server_name, a:request, a:response) + else + call s:show_preview_window(a:server_name, a:request, a:response) + endif +endfunction + +function! s:show_preview_window(server_name, request, response) abort + let l:contents = s:get_contents(a:response['result']['contents']) + + " Ignore if contents is empty. + if empty(l:contents) + call lsp#utils#error('Empty contents for LspHover') + return + endif + + let l:lines = lsp#utils#_split_by_eol(join(l:contents, "\n\n")) + let l:view = winsaveview() + let l:alternate=@# + silent! pclose + sp LspHoverPreview + execute 'resize '.min([len(l:lines), &previewheight]) + set previewwindow + setlocal conceallevel=2 + setlocal bufhidden=hide + setlocal nobuflisted + setlocal buftype=nofile + setlocal noswapfile + %d _ + call setline(1, l:lines) + call s:Window.do(win_getid(), {->s:Markdown.apply()}) + execute "normal \p" + call winrestview(l:view) + let @#=l:alternate +endfunction + +function! s:show_floating_window(server_name, request, response) abort + call s:close_floating_window() + + let l:contents = s:get_contents(a:response['result']['contents']) + + " Ignore if contents is empty. + if empty(l:contents) + return s:close_floating_window() + endif + + " Update contents. + let l:doc_win = s:get_doc_win() + silent! call deletebufline(l:doc_win.get_bufnr(), 1, '$') + call setbufline(l:doc_win.get_bufnr(), 1, lsp#utils#_split_by_eol(join(l:contents, "\n\n"))) + + " Calculate layout. + if g:lsp_float_max_width >= 1 + let l:maxwidth = g:lsp_float_max_width + elseif g:lsp_float_max_width == 0 + let l:maxwidth = &columns + else + let l:maxwidth = float2nr(&columns * 0.4) + endif + let l:size = l:doc_win.get_size({ + \ 'maxwidth': l:maxwidth, + \ 'maxheight': float2nr(&lines * 0.4), + \ }) + let l:pos = s:compute_position(l:size) + if empty(l:pos) + call s:close_floating_window() + return + endif + + execute printf('augroup vim_lsp_hover_close_on_move_%d', bufnr('%')) + " vint: -ProhibitAutocmdWithNoGroup + autocmd! + autocmd InsertEnter,BufLeave call s:close_floating_window() + execute printf('autocmd CursorMoved call s:close_floating_window_on_move(%s)', getcurpos()) + " vint: +ProhibitAutocmdWithNoGroup + augroup END + + " Show popupmenu and apply markdown syntax. + call l:doc_win.open({ + \ 'row': l:pos[0], + \ 'col': l:pos[1], + \ 'width': l:size.width, + \ 'height': l:size.height, + \ 'border': v:true, + \ }) + call s:Window.do(l:doc_win.get_winid(), { -> s:Markdown.apply() }) + + " Format contents to fit window + call setbufvar(l:doc_win.get_bufnr(), '&textwidth', l:size.width) + call s:Window.do(l:doc_win.get_winid(), { -> s:format_window() }) +endfunction + +function! s:format_window() abort + global/^/normal! gqgq +endfunction + +function! s:get_contents(contents) abort + if type(a:contents) == type('') + return [a:contents] + elseif type(a:contents) == type([]) + let l:result = [] + for l:content in a:contents + let l:result += s:get_contents(l:content) + endfor + return l:result + elseif type(a:contents) == type({}) + if has_key(a:contents, 'value') + if has_key(a:contents, 'kind') + if a:contents['kind'] ==? 'markdown' + let l:detail = s:MarkupContent.normalize(a:contents['value'], { + \ 'compact': !g:lsp_preview_fixup_conceal + \ }) + return [l:detail] + else + return [a:contents['value']] + endif + elseif has_key(a:contents, 'language') + let l:detail = s:MarkupContent.normalize(a:contents, { + \ 'compact': !g:lsp_preview_fixup_conceal + \ }) + return [l:detail] + else + return '' + endif + else + return '' + endif + else + return '' + endif +endfunction + +function! s:close_floating_window() abort + call s:get_doc_win().close() +endfunction + +function! s:close_floating_window_on_move(curpos) abort + if a:curpos != getcurpos() | call s:close_floating_window() | endif +endf + +function! s:on_opened() abort + inoremap (lsp-float-close) :call close_floating_window() + nnoremap (lsp-float-close) :call close_floating_window() + execute('doautocmd User lsp_float_opened') + if !hasmapto('(lsp-float-close)') + imap (lsp-float-close) + nmap (lsp-float-close) + endif +endfunction + +function! s:on_closed() abort + execute('doautocmd User lsp_float_closed') +endfunction + +function! s:get_doc_win() abort + if exists('s:doc_win') + return s:doc_win + endif + + let s:doc_win = s:FloatingWindow.new({ + \ 'on_opened': function('s:on_opened'), + \ 'on_closed': function('s:on_closed') + \ }) + call s:doc_win.set_var('&wrap', 1) + call s:doc_win.set_var('&conceallevel', 2) + call s:doc_win.set_bufnr(s:Buffer.create()) + call setbufvar(s:doc_win.get_bufnr(), '&buftype', 'nofile') + call setbufvar(s:doc_win.get_bufnr(), '&bufhidden', 'hide') + call setbufvar(s:doc_win.get_bufnr(), '&buflisted', 0) + call setbufvar(s:doc_win.get_bufnr(), '&swapfile', 0) + return s:doc_win +endfunction + +function! s:compute_position(size) abort + let l:pos = screenpos(0, line('.'), col('.')) + if l:pos.row == 0 && l:pos.col == 0 + " workaround for float position + let l:pos = {'curscol': wincol(), 'row': winline()} + endif + let l:pos = [l:pos.row + 1, l:pos.curscol + 1] + if l:pos[0] + a:size.height > &lines + let l:pos[0] = l:pos[0] - a:size.height - 3 + endif + if l:pos[1] + a:size.width > &columns + let l:pos[1] = l:pos[1] - a:size.width - 3 + endif + return l:pos +endfunction + diff --git a/autoload/lsp/internal/document_range_formatting.vim b/autoload/lsp/internal/document_range_formatting.vim new file mode 100644 index 00000000..afa26bc2 --- /dev/null +++ b/autoload/lsp/internal/document_range_formatting.vim @@ -0,0 +1,125 @@ +" options - { +" bufnr: bufnr('%') " required +" type: '' " optional: defaults to visualmode(). overridden by opfunc +" server - 'server_name' " optional +" sync: 0 " optional, defaults to 0 (async) +" } +function! lsp#internal#document_range_formatting#format(options) abort + if has_key(a:options, 'server') + let l:servers = [a:options['server']] + else + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_range_formatting_provider(v:val)') + endif + + if len(l:servers) == 0 + let l:filetype = getbufvar(a:options['bufnr'], '&filetype') + call lsp#utils#error('textDocument/rangeFormatting not supported for ' . l:filetype) + return + endif + + " TODO: ask user to select server for formatting if there are multiple servers + let l:server = l:servers[0] + + redraw | echo 'Formatting Document Range ...' + + call lsp#_new_command() + + let [l:start_lnum, l:start_col, l:end_lnum, l:end_col] = s:get_selection_pos(get(a:options, 'type', visualmode())) + let l:start_char = lsp#utils#to_char('%', l:start_lnum, l:start_col) + let l:end_char = lsp#utils#to_char('%', l:end_lnum, l:end_col) + + let l:request = { + \ 'method': 'textDocument/rangeFormatting', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(a:options['bufnr']), + \ 'range': { + \ 'start': { 'line': l:start_lnum - 1, 'character': l:start_char }, + \ 'end': { 'line': l:end_lnum - 1, 'character': l:end_char }, + \ }, + \ 'options': { + \ 'tabSize': lsp#utils#buffer#get_indent_size(a:options['bufnr']), + \ 'insertSpaces': getbufvar(a:options['bufnr'], '&expandtab') ? v:true : v:false, + \ } + \ }, + \ 'bufnr': a:options['bufnr'], + \ } + + if get(a:options, 'sync', 0) == 1 + try + let l:x = lsp#callbag#pipe( + \ lsp#request(l:server, l:request), + \ lsp#callbag#takeUntil(lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'command')}), + \ )), + \ lsp#callbag#toList(), + \ ).wait({ 'sleep': get(a:options, 'sleep', 1), 'timeout': get(a:options, 'timeout', g:lsp_format_sync_timeout) }) + call s:format_next(l:x[0]) + call s:format_complete() + catch + call s:format_error(v:exception . ' ' . v:throwpoint) + endtry + else + return lsp#callbag#pipe( + \ lsp#request(l:server, l:request), + \ lsp#callbag#takeUntil(lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'command')}), + \ )), + \ lsp#callbag#subscribe({ + \ 'next':{x->s:format_next(x)}, + \ 'error': {x->s:format_error(e)}, + \ 'complete': {->s:format_complete()}, + \ }), + \ ) + endif +endfunction + +function! s:format_next(x) abort + if lsp#client#is_error(a:x['response']) | return | endif + call lsp#utils#text_edit#apply_text_edits(a:x['request']['params']['textDocument']['uri'], a:x['response']['result']) +endfunction + +function! s:format_error(e) abort + call lsp#log('Formatting Document Range Failed', a:e) + call lsp#utils#error('Formatting Document Range Failed.' . (type(a:e) == type('') ? a:e : '')) +endfunction + +function! s:format_complete() abort + redraw | echo 'Formatting Document Range complete' +endfunction + +function! s:get_selection_pos(type) abort + " TODO: support bufnr + if a:type ==? 'v' + let l:start_pos = getpos("'<")[1:2] + let l:end_pos = getpos("'>")[1:2] + " fix end_pos column (see :h getpos() and :h 'selection') + let l:end_line = getline(l:end_pos[0]) + let l:offset = (&selection ==# 'inclusive' ? 1 : 2) + let l:end_pos[1] = len(l:end_line[:l:end_pos[1]-l:offset]) + " edge case: single character selected with selection=exclusive + if l:start_pos[0] == l:end_pos[0] && l:start_pos[1] > l:end_pos[1] + let l:end_pos[1] = l:start_pos[1] + endif + elseif a:type ==? 'line' + let l:start_pos = [line("'["), 1] + let l:end_lnum = line("']") + let l:end_pos = [line("']"), len(getline(l:end_lnum))] + elseif a:type ==? 'char' + let l:start_pos = getpos("'[")[1:2] + let l:end_pos = getpos("']")[1:2] + else + let l:start_pos = [0, 0] + let l:end_pos = [0, 0] + endif + + return l:start_pos + l:end_pos +endfunction + +function! lsp#internal#document_range_formatting#opfunc(type) abort + call lsp#internal#document_range_formatting#format({ + \ 'type': a:type, + \ 'bufnr': bufnr('%'), + \ }) +endfunction diff --git a/autoload/lsp/internal/document_symbol/search.vim b/autoload/lsp/internal/document_symbol/search.vim new file mode 100644 index 00000000..6fa4a5a1 --- /dev/null +++ b/autoload/lsp/internal/document_symbol/search.vim @@ -0,0 +1,76 @@ +" https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol +" options - { +" bufnr: bufnr('%') " optional +" server - 'server_name' " optional +" } +function! lsp#internal#document_symbol#search#do(options) abort + let l:bufnr = get(a:options, 'bufnr', bufnr('%')) + if has_key(a:options, 'server') + let l:servers = [a:options['server']] + else + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_symbol_provider(v:val)') + endif + + if len(l:servers) == 0 + let l:filetype = getbufvar(l:bufnr, '&filetype') + call lsp#utils#error('textDocument/documentSymbol not supported for ' . l:filetype) + return + endif + + redraw | echo 'Retrieving document symbols ...' + + call lsp#internal#ui#quickpick#open({ + \ 'items': [], + \ 'busy': 1, + \ 'input': '', + \ 'key': 'text', + \ 'on_accept': function('s:on_accept'), + \ 'on_close': function('s:on_close'), + \ }) + + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#fromList(l:servers), + \ lsp#callbag#flatMap({server-> + \ lsp#callbag#pipe( + \ lsp#request(server, { + \ 'method': 'textDocument/documentSymbol', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(l:bufnr), + \ }, + \ }), + \ lsp#callbag#map({x->{'server': server, 'request': x['request'], 'response': x['response']}}), + \ ) + \ }), + \ lsp#callbag#scan({acc, curr->add(acc, curr)}, []), + \ lsp#callbag#tap({x->s:update_ui_items(x)}), + \ lsp#callbag#subscribe({ + \ 'complete':{->lsp#internal#ui#quickpick#busy(0)}, + \ 'error':{e->s:on_error(e)}, + \ }), + \ ) +endfunction + +function! s:update_ui_items(x) abort + let l:items = [] + for l:i in a:x + let l:items += lsp#ui#vim#utils#symbols_to_loc_list(l:i['server'], l:i) + endfor + call lsp#internal#ui#quickpick#items(l:items) +endfunction + +function! s:on_accept(data, ...) abort + call lsp#internal#ui#quickpick#close() + call lsp#utils#location#_open_vim_list_item(a:data['items'][0], '') +endfunction + +function! s:on_close(...) abort + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! s:on_error(e) abort + call lsp#internal#ui#quickpick#close() + call lsp#log('LspDocumentSymbolSearch error', a:e) +endfunction diff --git a/autoload/lsp/internal/inlay_hints.vim b/autoload/lsp/internal/inlay_hints.vim new file mode 100644 index 00000000..2e8792c6 --- /dev/null +++ b/autoload/lsp/internal/inlay_hints.vim @@ -0,0 +1,127 @@ +let s:use_vim_textprops = lsp#utils#_has_vim_virtual_text() && !has('nvim') + +function! s:set_inlay_hints(data) abort + let l:bufnr = bufnr('%') + + call s:clear_inlay_hints() + + if mode() !=# 'n' | return | endif + + if lsp#client#is_error(a:data['response']) | return | endif + + " Get hints from the response + let l:hints = a:data['response']['result'] + if empty(l:hints) + return + endif + + let l:not_curline = s:has_inlay_hints_mode('!curline') + for l:hint in l:hints + if l:not_curline && l:hint.position.line+1 ==# line('.') + continue + endif + let l:label = '' + if type(l:hint.label) ==# v:t_list + let l:label = join(map(copy(l:hint.label), {_,v -> v.value}), '') + else + let l:label = l:hint.label + endif + let l:text = (get(l:hint, 'paddingLeft', v:false) ? ' ' : '') . l:label . (get(l:hint, 'paddingRight', v:false) ? ' ' : '') + if !has_key(l:hint, 'kind') || l:hint.kind ==# 1 + call prop_add(l:hint.position.line+1, l:hint.position.character+1, {'type': 'vim_lsp_inlay_hint_type', 'text': l:text, 'bufnr': l:bufnr}) + elseif l:hint.kind ==# 2 + call prop_add(l:hint.position.line+1, l:hint.position.character+1, {'type': 'vim_lsp_inlay_hint_parameter', 'text': l:text, 'bufnr': l:bufnr}) + endif + endfor +endfunction + +function! s:init_inlay_hints() abort + if index(prop_type_list(), 'vim_lsp_inlay_hint_type') ==# -1 + call prop_type_add('vim_lsp_inlay_hint_type', { 'highlight': 'lspInlayHintsType' }) + call prop_type_add('vim_lsp_inlay_hint_parameter', { 'highlight': 'lspInlayHintsParameter' }) + endif +endfunction + +function! lsp#internal#inlay_hints#_disable() abort + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! s:clear_inlay_hints() abort + let l:bufnr = bufnr('%') + call prop_remove({'type': 'vim_lsp_inlay_hint_type', 'bufnr': l:bufnr, 'all': v:true}) + call prop_remove({'type': 'vim_lsp_inlay_hint_parameter', 'bufnr': l:bufnr, 'all': v:true}) +endfunction + +function! s:has_inlay_hints_mode(value) abort + let l:m = get(g:, 'lsp_inlay_hints_mode', {}) + if type(l:m) != v:t_dict | return v:false | endif + if mode() ==# 'i' + let l:a = get(l:m, 'insert', []) + elseif mode() ==# 'n' + let l:a = get(l:m, 'normal', []) + else + return v:false + endif + if type(l:a) != v:t_list | return v:false | endif + return index(l:a, a:value) != -1 ? v:true : v:false +endfunction + +function! s:send_inlay_hints_request() abort + let l:capability = 'lsp#capabilities#has_inlay_hint_provider(v:val)' + let l:servers = filter(lsp#get_allowed_servers(), l:capability) + + if empty(l:servers) + return lsp#callbag#empty() + endif + + if s:has_inlay_hints_mode('curline') + let l:range = lsp#utils#range#get_range_curline() + else + let l:range = lsp#utils#range#get_range() + endif + return lsp#request(l:servers[0], { + \ 'method': 'textDocument/inlayHint', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'range': l:range, + \ }, + \ }) +endfunction + +function! lsp#internal#inlay_hints#_enable() abort + if !s:use_vim_textprops | return | endif + if !g:lsp_inlay_hints_enabled | return | endif + + if !hlexists('lspInlayHintsType') + highlight link lspInlayHintsType Label + endif + if !hlexists('lspInlayHintsParameter') + highlight link lspInlayHintsParameter Todo + endif + + call s:init_inlay_hints() + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#merge( + \ lsp#callbag#fromEvent(['CursorMoved', 'CursorHold']), + \ lsp#callbag#pipe( + \ lsp#callbag#fromEvent(['InsertEnter', 'BufLeave']), + \ lsp#callbag#tap({_ -> s:clear_inlay_hints() }), + \ ) + \ ), + \ lsp#callbag#filter({_ -> g:lsp_inlay_hints_enabled }), + \ lsp#callbag#debounceTime(g:lsp_inlay_hints_delay), + \ lsp#callbag#filter({_->getbufvar(bufnr('%'), '&buftype') !~# '^(help\|terminal\|prompt\|popup)$'}), + \ lsp#callbag#switchMap({_-> + \ lsp#callbag#pipe( + \ s:send_inlay_hints_request(), + \ lsp#callbag#materialize(), + \ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}), + \ lsp#callbag#map({x->x['value']}) + \ ) + \ }), + \ lsp#callbag#subscribe({x->s:set_inlay_hints(x)}), + \) +endfunction diff --git a/autoload/lsp/internal/semantic.vim b/autoload/lsp/internal/semantic.vim new file mode 100644 index 00000000..761b92a7 --- /dev/null +++ b/autoload/lsp/internal/semantic.vim @@ -0,0 +1,411 @@ +let s:use_vim_textprops = lsp#utils#_has_textprops() && !has('nvim') +let s:use_nvim_highlight = lsp#utils#_has_nvim_buf_highlight() +let s:textprop_cache = 'vim-lsp-semantic-cache' + +if s:use_nvim_highlight + let s:namespace_id = nvim_create_namespace('vim-lsp-semantic') +endif + +" Global functions {{{1 +function! lsp#internal#semantic#is_enabled() abort + return g:lsp_semantic_enabled && (s:use_vim_textprops || s:use_nvim_highlight) ? v:true : v:false +endfunction + +function! lsp#internal#semantic#_enable() abort + if !lsp#internal#semantic#is_enabled() | return | endif + + augroup lsp#internal#semantic + autocmd! + au User lsp_buffer_enabled call s:on_lsp_buffer_enabled() + augroup END + + let l:events = [['User', 'lsp_buffer_enabled'], 'TextChanged', 'TextChangedI'] + if exists('##TextChangedP') + call add(l:events, 'TextChangedP') + endif + let s:Dispose = lsp#callbag#pipe( + \ lsp#callbag#fromEvent(l:events), + \ lsp#callbag#filter({_->lsp#internal#semantic#is_enabled()}), + \ lsp#callbag#debounceTime(g:lsp_semantic_delay), + \ lsp#callbag#filter({_->index(['help', 'terminal', 'prompt', 'popup'], getbufvar(bufnr('%'), '&buftype')) ==# -1}), + \ lsp#callbag#filter({_->!lsp#utils#is_large_window(win_getid())}), + \ lsp#callbag#switchMap({_-> + \ lsp#callbag#pipe( + \ s:semantic_request(), + \ lsp#callbag#materialize(), + \ lsp#callbag#filter({x->lsp#callbag#isNextNotification(x)}), + \ lsp#callbag#map({x->x['value']}) + \ ) + \ }), + \ lsp#callbag#subscribe({x->s:handle_semantic_request(x)}) + \ ) +endfunction + +function! lsp#internal#semantic#_disable() abort + augroup lsp#internal#semantic + autocmd! + augroup END + + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! lsp#internal#semantic#get_legend(server) abort + if !lsp#capabilities#has_semantic_tokens(a:server) + return {'tokenTypes': [], 'tokenModifiers': []} + endif + + let l:capabilities = lsp#get_server_capabilities(a:server) + return l:capabilities['semanticTokensProvider']['legend'] +endfunction + +function! lsp#internal#semantic#get_token_types() abort + let l:capability = 'lsp#capabilities#has_semantic_tokens(v:val)' + let l:servers = filter(lsp#get_allowed_servers(), l:capability) + + if empty(l:servers) + return [] + endif + + let l:legend = lsp#internal#semantic#get_legend(l:servers[0]) + let l:token_types = l:legend['tokenTypes'] + call map(l:token_types, {_, type -> toupper(type[0]) . type[1:]}) + return l:token_types +endfunction + +function! lsp#internal#semantic#get_token_modifiers() abort + let l:capability = 'lsp#capabilities#has_semantic_tokens(v:val)' + let l:servers = filter(lsp#get_allowed_servers(), l:capability) + + if empty(l:servers) + return [] + endif + + let l:legend = lsp#internal#semantic#get_legend(l:servers[0]) + let l:token_modifiers = l:legend['tokenModifiers'] + call map(l:token_modifiers, {_, modifier -> toupper(modifier[0]) . modifier[1:]}) + return l:token_modifiers +endfunction + +function! s:on_lsp_buffer_enabled() abort + augroup lsp#internal#semantic + if !exists('#BufUnload#') + execute 'au BufUnload call setbufvar(' . bufnr() . ', ''lsp_semantic_previous_result_id'', '''')' + endif + augroup END +endfunction + +function! s:supports_full_semantic_request(server) abort + if !lsp#capabilities#has_semantic_tokens(a:server) + return v:false + endif + + let l:capabilities = lsp#get_server_capabilities(a:server)['semanticTokensProvider'] + if !has_key(l:capabilities, 'full') + return v:false + endif + + if type(l:capabilities['full']) ==# v:t_dict + return v:true + endif + + return l:capabilities['full'] +endfunction + +function! s:supports_delta_semantic_request(server) abort + if !lsp#capabilities#has_semantic_tokens(a:server) + return v:false + endif + + let l:capabilities = lsp#get_server_capabilities(a:server)['semanticTokensProvider'] + if !has_key(l:capabilities, 'full') + return v:false + endif + + if type(l:capabilities['full']) !=# v:t_dict + return v:false + endif + + if !has_key(l:capabilities['full'], 'delta') + return v:false + endif + + return l:capabilities['full']['delta'] +endfunction + +function! s:get_server() abort + let l:capability = 's:supports_delta_semantic_request(v:val)' + let l:servers = filter(lsp#get_allowed_servers(), l:capability) + if empty(l:servers) + let l:capability = 's:supports_full_semantic_request(v:val)' + let l:servers = filter(lsp#get_allowed_servers(), l:capability) + endif + if empty(l:servers) + return '' + endif + return l:servers[0] +endfunction + +function! s:semantic_request() abort + let l:server = s:get_server() + if l:server ==# '' + return lsp#callbag#empty() + endif + + if (s:supports_delta_semantic_request(l:server) + \ && getbufvar(bufnr(), 'lsp_semantic_previous_result_id') !=# '') + return s:delta_semantic_request(l:server) + else + return s:full_semantic_request(l:server) + endif +endfunction + +function! s:full_semantic_request(server) abort + return lsp#request(a:server, { + \ 'method': 'textDocument/semanticTokens/full', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier() + \ }}) +endfunction + +function! s:delta_semantic_request(server) abort + return lsp#request(a:server, { + \ 'method': 'textDocument/semanticTokens/full/delta', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'previousResultId': getbufvar(bufname(), 'lsp_semantic_previous_result_id', 0) + \ }}) +endfunction + +" Highlight helper functions {{{1 +function! s:handle_semantic_request(data) abort + if lsp#client#is_error(a:data['response']) + call lsp#log('Skipping semantic highlight: response is invalid') + return + endif + + let l:server = a:data['server_name'] + let l:uri = a:data['request']['params']['textDocument']['uri'] + let l:path = lsp#utils#uri_to_path(l:uri) + let l:bufnr = bufnr(l:path) + + " Skip if the buffer doesn't exist. This might happen when a buffer is + " opened and quickly deleted. + if !bufloaded(l:bufnr) | return | endif + + if has_key(a:data['response']['result'], 'data') + call s:handle_semantic_tokens_response(l:server, l:bufnr, a:data['response']['result']) + elseif has_key(a:data['response']['result'], 'edits') + call s:handle_semantic_tokens_delta_response(l:server, l:bufnr, a:data['response']['result']) + else + " Don't update previous result ID if we could not update local copy + call lsp#log('SemanticHighlight: unsupported semanticTokens method') + return + endif + + if has_key(a:data['response']['result'], 'resultId') + call setbufvar(l:bufnr, 'lsp_semantic_previous_result_id', a:data['response']['result']['resultId']) + endif +endfunction + +function! s:handle_semantic_tokens_response(server, buf, result) abort + let l:highlights = {} + let l:legend = lsp#internal#semantic#get_legend(a:server) + for l:token in s:decode_tokens(a:result['data']) + let [l:key, l:value] = s:add_highlight(a:server, l:legend, a:buf, l:token) + let l:highlights[l:key] = get(l:highlights, l:key, []) + l:value + endfor + call s:apply_highlights(a:server, a:buf, l:highlights) + + call setbufvar(a:buf, 'lsp_semantic_local_data', a:result['data']) +endfunction + +function! s:startpos_compare(edit1, edit2) abort + return a:edit1[0] == a:edit2[0] ? 0 : a:edit1[0] > a:edit2[0] ? -1 : 1 +endfunction + +function! s:handle_semantic_tokens_delta_response(server, buf, result) abort + " The locations given in the edit are all referenced to the state before + " any are applied and sorting is not required from the server, + " therefore the edits must be sorted before applying. + let l:edits = a:result['edits'] + call sort(l:edits, function('s:startpos_compare')) + + let l:localdata = getbufvar(a:buf, 'lsp_semantic_local_data') + for l:edit in l:edits + let l:insertdata = get(l:edit, 'data', []) + let l:localdata = l:localdata[:l:edit['start'] - 1] + \ + l:insertdata + \ + l:localdata[l:edit['start'] + l:edit['deleteCount']:] + endfor + call setbufvar(a:buf, 'lsp_semantic_local_data', l:localdata) + + let l:highlights = {} + let l:legend = lsp#internal#semantic#get_legend(a:server) + for l:token in s:decode_tokens(l:localdata) + let [l:key, l:value] = s:add_highlight(a:server, l:legend, a:buf, l:token) + let l:highlights[l:key] = get(l:highlights, l:key, []) + l:value + endfor + call s:apply_highlights(a:server, a:buf, l:highlights) +endfunction + +function! s:decode_tokens(data) abort + let l:tokens = [] + + let l:i = 0 + let l:line = 0 + let l:char = 0 + while l:i < len(a:data) + let l:line = l:line + a:data[l:i] + if a:data[l:i] > 0 + let l:char = 0 + endif + let l:char = l:char + a:data[l:i + 1] + + call add(l:tokens, { + \ 'pos': {'line': l:line, 'character': l:char}, + \ 'length': a:data[l:i + 2], + \ 'token_idx': a:data[l:i + 3], + \ 'token_modifiers': a:data[l:i + 4] + \ }) + + let l:i = l:i + 5 + endwhile + + return l:tokens +endfunction + +function! s:clear_highlights(server, buf) abort + if s:use_vim_textprops + let l:BeginsWith = {str, prefix -> str[0:len(prefix) - 1] ==# prefix} + let l:IsSemanticTextprop = {_, textprop -> l:BeginsWith(textprop, s:textprop_type_prefix)} + let l:textprop_types = prop_type_list() + call filter(l:textprop_types, l:IsSemanticTextprop) + for l:textprop_type in l:textprop_types + silent! call prop_remove({'type': l:textprop_type, 'bufnr': a:buf, 'all': v:true}) + endfor + elseif s:use_nvim_highlight + call nvim_buf_clear_namespace(a:buf, s:namespace_id, 0, line('$')) + endif +endfunction + +function! s:add_highlight(server, legend, buf, token) abort + let l:startpos = lsp#utils#position#lsp_to_vim(a:buf, a:token['pos']) + let l:endpos = a:token['pos'] + let l:endpos['character'] = l:endpos['character'] + a:token['length'] + let l:endpos = lsp#utils#position#lsp_to_vim(a:buf, l:endpos) + + if s:use_vim_textprops + let l:textprop_name = s:get_textprop_type(a:server, a:legend, a:token['token_idx'], a:token['token_modifiers']) + return [l:textprop_name, [[l:startpos[0], l:startpos[1], l:endpos[0], l:endpos[1]]]] + elseif s:use_nvim_highlight + let l:char = a:token['pos']['character'] + let l:hl_name = s:get_hl_group(a:server, a:legend, a:token['token_idx'], a:token['token_modifiers']) + return [l:hl_name, [[l:startpos[0] - 1, l:startpos[1] - 1, l:endpos[1] - 1]]] + endif +endfunction + +function! s:apply_highlights(server, buf, highlights) abort + call s:clear_highlights(a:server, a:buf) + + if s:use_vim_textprops + for [l:type, l:prop_list] in items(a:highlights) + call prop_add_list({'type': l:type, 'bufnr': a:buf}, l:prop_list) + endfor + elseif s:use_nvim_highlight + call lsp#log(a:highlights) + for [l:hl_name, l:instances] in items(a:highlights) + for l:instance in l:instances + let [l:line, l:startcol, l:endcol] = l:instance + try + call nvim_buf_add_highlight(a:buf, s:namespace_id, l:hl_name, l:line, l:startcol, l:endcol) + catch + call lsp#log('SemanticHighlight: error while adding ' . l:hl_name . ' highlight on line ' . l:line) + endtry + endfor + endfor + end +endfunction + +let s:hl_group_prefix = 'LspSemantic' + +let s:default_highlight_groups = { + \ s:hl_group_prefix . 'Type': 'Type', + \ s:hl_group_prefix . 'Class': 'Type', + \ s:hl_group_prefix . 'Enum': 'Type', + \ s:hl_group_prefix . 'Interface': 'TypeDef', + \ s:hl_group_prefix . 'Struct': 'Type', + \ s:hl_group_prefix . 'TypeParameter': 'Type', + \ s:hl_group_prefix . 'Parameter': 'Identifier', + \ s:hl_group_prefix . 'Variable': 'Identifier', + \ s:hl_group_prefix . 'Property': 'Identifier', + \ s:hl_group_prefix . 'EnumMember': 'Constant', + \ s:hl_group_prefix . 'Event': 'Identifier', + \ s:hl_group_prefix . 'Function': 'Function', + \ s:hl_group_prefix . 'Method': 'Function', + \ s:hl_group_prefix . 'Macro': 'Macro', + \ s:hl_group_prefix . 'Keyword': 'Keyword', + \ s:hl_group_prefix . 'Modifier': 'Type', + \ s:hl_group_prefix . 'Comment': 'Comment', + \ s:hl_group_prefix . 'String': 'String', + \ s:hl_group_prefix . 'Number': 'Number', + \ s:hl_group_prefix . 'Regexp': 'String', + \ s:hl_group_prefix . 'Operator': 'Operator', + \ s:hl_group_prefix . 'Decorator': 'Macro' +\ } + +function! s:get_hl_group(server, legend, token_idx, token_modifiers) abort + " get highlight group name + let l:Capitalise = {str -> toupper(str[0]) . str[1:]} + let l:token_name = l:Capitalise(a:legend['tokenTypes'][a:token_idx]) + let l:token_modifiers = [] + for l:modifier_idx in range(len(a:legend['tokenModifiers'])) + " float2nr(pow(2, a)) is 1 << a + if and(a:token_modifiers, float2nr(pow(2, l:modifier_idx))) + let l:modifier_name = a:legend['tokenModifiers'][l:modifier_idx] + call add(l:token_modifiers, l:Capitalise(l:modifier_name)) + endif + endfor + call sort(l:token_modifiers) + let l:hl_group = s:hl_group_prefix + \ . reduce(l:token_modifiers, {acc, val -> acc . val}, '') + \ . l:token_name + + " create the highlight group if it does not already exist + if !hlexists(l:hl_group) + if has_key(s:default_highlight_groups, l:hl_group) + exec 'highlight link' l:hl_group s:default_highlight_groups[l:hl_group] + else + if a:token_modifiers != 0 + let l:base_hl_group = s:get_hl_group(a:server, a:legend, a:token_idx, 0) + exec 'highlight link' l:hl_group l:base_hl_group + else + exec 'highlight link' l:hl_group 'Normal' + endif + endif + endif + + return l:hl_group +endfunction + +let s:textprop_type_prefix = 'vim-lsp-semantic-' + +function! s:get_textprop_type(server, legend, token_idx, token_modifiers) abort + " get textprop type name + let l:textprop_type = s:textprop_type_prefix . a:server . '-' . a:token_idx . '-' . a:token_modifiers + + " create the textprop type if it does not already exist + if prop_type_get(l:textprop_type) ==# {} + let l:hl_group = s:get_hl_group(a:server, a:legend, a:token_idx, a:token_modifiers) + silent! call prop_type_add(l:textprop_type, { + \ 'highlight': l:hl_group, + \ 'combine': v:true, + \ 'priority': lsp#internal#textprop#priority('semantic')}) + endif + + return l:textprop_type +endfunction + +" vim: fdm=marker diff --git a/autoload/lsp/internal/show_message.vim b/autoload/lsp/internal/show_message.vim new file mode 100644 index 00000000..a56bcb9f --- /dev/null +++ b/autoload/lsp/internal/show_message.vim @@ -0,0 +1,74 @@ +let s:ErrorType = 1 +let s:WarningType = 2 +let s:InfoType = 3 +let s:LogType = 4 + +function! lsp#internal#show_message#_enable() abort + if g:lsp_show_message_log_level ==# 'none' | return | endif + let s:Dispose = lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x-> + \ g:lsp_show_message_log_level !=# 'none' && + \ has_key(x, 'response') && has_key(x['response'], 'method') + \ && x['response']['method'] ==# 'window/showMessage' + \ }), + \ lsp#callbag#tap({x->s:handle_show_message(x['server'], x['response']['params'])}), + \ lsp#callbag#subscribe({ 'error': function('s:on_error') }), + \ ) +endfunction + +function! lsp#internal#show_message#_disable() abort + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! s:on_error(e) abort + call lsp#log('lsp#internal#show_message error', a:e) + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! s:handle_show_message(server, params) abort + let l:level = s:name_to_level(g:lsp_show_message_log_level) + let l:type = a:params['type'] + if l:level < l:type + return + endif + + let l:message = a:params['message'] + try + if l:type == s:ErrorType + echohl ErrorMsg + elseif l:type == s:WarningType + echohl WarningMsg + endif + echom printf('%s: %s: %s', a:server, s:type_to_name(l:type), l:message) + finally + echohl None + endtry +endfunction + +function! s:name_to_level(name) abort + if a:name ==# 'none' + return 0 + elseif a:name ==# 'error' + return s:ErrorType + elseif a:name ==# 'warn' || a:name ==# 'warning' + return s:WarningType + elseif a:name ==# 'info' + return s:InfoType + elseif a:name ==# 'log' + return s:LogType + else + return 0 + endif +endfunction + +function! s:type_to_name(type) abort + return get(['unknown', 'error', 'warning', 'info', 'log'], a:type, 'unknown') +endfunction + diff --git a/autoload/lsp/internal/show_message_request.vim b/autoload/lsp/internal/show_message_request.vim new file mode 100644 index 00000000..2ed6b672 --- /dev/null +++ b/autoload/lsp/internal/show_message_request.vim @@ -0,0 +1,58 @@ +function! lsp#internal#show_message_request#_enable() abort + if !g:lsp_show_message_request_enabled | return | endif + let s:Dispose = lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x-> + \ g:lsp_show_message_request_enabled && + \ has_key(x, 'request') && !has_key(x, 'response') && + \ has_key(x['request'], 'method') && x['request']['method'] ==# 'window/showMessageRequest' + \ }), + \ lsp#callbag#map({x->s:show_message_request(x['server'], x['request'])}), + \ lsp#callbag#map({x->s:send_message_response(x['server'], x['request'], x['action'])}), + \ lsp#callbag#flatten(), + \ lsp#callbag#materialize(), + \ lsp#callbag#subscribe({ 'error': function('s:on_error') }), + \ ) +endfunction + +function! lsp#internal#show_message_request#_disable() abort + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! s:on_error(e) abort + call lsp#log('lsp#internal#show_message_request error', a:e) + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! s:show_message_request(server_name, request) abort + let l:params = a:request['params'] + + let l:selected_action = v:null + + if has_key(l:params, 'actions') && !empty(l:params['actions']) + let l:options = map(copy(l:params['actions']), {i, action -> + \ printf('%d - [%s] %s', i + 1, a:server_name, action['title']) + \ }) + let l:index = inputlist([l:params['message']] + l:options) + if l:index > 0 && l:index <= len(l:index) + let l:selected_action = l:params['actions'][l:index - 1] + endif + else + echom l:params['message'] + endif + + return { 'server': a:server_name, 'request': a:request, 'action': l:selected_action } +endfunction + +function! s:send_message_response(server_name, request, action) abort + return lsp#request(a:server_name, { + \ 'id': a:request['id'], + \ 'result': a:action + \}) +endfunction diff --git a/autoload/lsp/internal/textprop.vim b/autoload/lsp/internal/textprop.vim new file mode 100644 index 00000000..dbfbeedf --- /dev/null +++ b/autoload/lsp/internal/textprop.vim @@ -0,0 +1,13 @@ +" TODO: currently, quickpick is generated via vim-quickpick, 'quickpick' is +" not used. +let s:priorities = { +\ 'quickpick': 1, +\ 'folding': 2, +\ 'semantic': 3, +\ 'diagnostics_highlight': 4, +\ 'document_highlight': 5, +\} + +function! lsp#internal#textprop#priority(name) abort + return get(s:priorities, a:name, 0) +endfunction diff --git a/autoload/lsp/internal/type_hierarchy.vim b/autoload/lsp/internal/type_hierarchy.vim new file mode 100644 index 00000000..d8f9da14 --- /dev/null +++ b/autoload/lsp/internal/type_hierarchy.vim @@ -0,0 +1,91 @@ +function! lsp#internal#type_hierarchy#show() abort + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_type_hierarchy_provider(v:val)') + let l:command_id = lsp#_new_command() + + if len(l:servers) == 0 + return lsp#utils#error('Retrieving type hierarchy not supported for ' . &filetype) + endif + + let l:ctx = { 'counter': len(l:servers), 'list':[], 'last_command_id': l:command_id } + " direction 0 children, 1 parent, 2 both + for l:server in l:servers + call lsp#send_request(l:server, { + \ 'method': 'textDocument/typeHierarchy', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': lsp#get_position(), + \ 'direction': 2, + \ 'resolve': 1, + \ }, + \ 'on_notification': function('s:handle_type_hierarchy', [l:ctx, l:server, 'type hierarchy']), + \ }) + endfor + + echo 'Retrieving type hierarchy ...' +endfunction + +function! s:handle_type_hierarchy(ctx, server, type, data) abort "ctx = {counter, list, last_command_id} + if a:ctx['last_command_id'] != lsp#_last_command() + return + endif + + if lsp#client#is_error(a:data['response']) + call lsp#utils#error('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + return + endif + + if empty(a:data['response']['result']) + echo 'No type hierarchy found' + return + endif + + " Create new buffer in a split + let l:position = 'topleft' + let l:orientation = 'new' + exec l:position . ' ' . 10 . l:orientation + + let l:provider = { + \ 'root': a:data['response']['result'], + \ 'root_state': 'expanded', + \ 'bufnr': bufnr('%'), + \ 'getChildren': function('s:get_children_for_tree_hierarchy'), + \ 'getParent': function('s:get_parent_for_tree_hierarchy'), + \ 'getTreeItem': function('s:get_treeitem_for_tree_hierarchy'), + \ } + + call lsp#utils#tree#new(l:provider) + + echo 'Retrieved type hierarchy' +endfunction + +function! s:hierarchyitem_to_treeitem(hierarchyitem) abort + return { + \ 'id': a:hierarchyitem, + \ 'label': a:hierarchyitem['name'], + \ 'command': function('s:hierarchy_treeitem_command', [a:hierarchyitem]), + \ 'collapsibleState': has_key(a:hierarchyitem, 'parents') && !empty(a:hierarchyitem['parents']) ? 'expanded' : 'none', + \ } +endfunction + +function! s:hierarchy_treeitem_command(hierarchyitem) abort + bwipeout + call lsp#utils#tagstack#_update() + call lsp#utils#location#_open_lsp_location(a:hierarchyitem) +endfunction + +function! s:get_children_for_tree_hierarchy(Callback, ...) dict abort + if a:0 == 0 + call a:Callback('success', [l:self['root']]) + return + else + call a:Callback('success', a:1['parents']) + endif +endfunction + +function! s:get_parent_for_tree_hierarchy(...) dict abort + " TODO +endfunction + +function! s:get_treeitem_for_tree_hierarchy(Callback, object) dict abort + call a:Callback('success', s:hierarchyitem_to_treeitem(a:object)) +endfunction diff --git a/autoload/lsp/internal/ui/popupmenu.vim b/autoload/lsp/internal/ui/popupmenu.vim new file mode 100644 index 00000000..41964bf9 --- /dev/null +++ b/autoload/lsp/internal/ui/popupmenu.vim @@ -0,0 +1,39 @@ +let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown') +let s:Window = vital#lsp#import('VS.Vim.Window') + +function! lsp#internal#ui#popupmenu#open(opt) abort + let l:Callback = remove(a:opt, 'callback') + let l:items = remove(a:opt, 'items') + + let l:items_with_shortcuts= map(l:items, { + \ idx, item -> ((idx < 9) ? '['.(idx+1).'] ' : '').item + \ }) + + function! Filter(id, key) abort closure + if a:key >= 1 && a:key <= len(l:items) + call popup_close(a:id, a:key) + elseif a:key ==# "\" + call win_execute(a:id, 'normal! j') + elseif a:key ==# "\" + call win_execute(a:id, 'normal! k') + else + return popup_filter_menu(a:id, a:key) + endif + + return v:true + endfunction + + let l:popup_opt = extend({ + \ 'callback': funcref('s:callback', [l:Callback]), + \ 'filter': funcref('Filter'), + \ }, a:opt) + + let l:winid = popup_menu(l:items_with_shortcuts, l:popup_opt) + call s:Window.do(l:winid, { -> s:Markdown.apply() }) + execute('doautocmd User lsp_float_opened') +endfunction + +function! s:callback(callback, id, selected) abort + call a:callback(a:id, a:selected) + execute('doautocmd User lsp_float_closed') +endfunction diff --git a/autoload/lsp/internal/ui/quickpick.vim b/autoload/lsp/internal/ui/quickpick.vim new file mode 100644 index 00000000..3fe37558 --- /dev/null +++ b/autoload/lsp/internal/ui/quickpick.vim @@ -0,0 +1,461 @@ +" https://github.com/prabirshrestha/quickpick.vim#968f00787c1a118228aee869351e754bec555298 +" :QuickpickEmbed path=autoload/lsp/internal/ui/quickpick.vim namespace=lsp#internal#ui#quickpick prefix=lsp-quickpick + +let s:has_timer = exists('*timer_start') && exists('*timer_stop') +let s:has_matchfuzzy = exists('*matchfuzzy') +let s:has_matchfuzzypos = exists('*matchfuzzypos') +let s:has_proptype = exists('*prop_type_add') && exists('*prop_type_delete') + +" +" is_floating +" +if has('nvim') + function! s:is_floating(winid) abort + if !s:win_exists(a:winid) + return 0 + endif + let l:config = nvim_win_get_config(a:winid) + return empty(l:config) || !empty(get(l:config, 'relative', '')) + endfunction +else + function! s:is_floating(winid) abort + return s:win_exists(a:winid) && win_id2win(a:winid) == 0 + endfunction +endif + +function! s:win_exists(winid) abort + return winheight(a:winid) != -1 +endfunction + +function! lsp#internal#ui#quickpick#open(opt) abort + call lsp#internal#ui#quickpick#close() " hide existing picker if exists + + " when key is empty, item is a string else it is a dict + " fitems is filtered items and is the item that is filtered + let s:state = extend({ + \ 'items': [], + \ 'highlights': [], + \ 'fitems': [], + \ 'key': '', + \ 'busy': 0, + \ 'busyframes': ['-', '\', '|', '/'], + \ 'filetype': 'lsp-quickpick', + \ 'promptfiletype': 'lsp-quickpick-filter', + \ 'input': '', + \ 'maxheight': 10, + \ 'debounce': 250, + \ 'filter': 1, + \ }, a:opt) + + let s:inputecharpre = 0 + let s:state['busyframe'] = 0 + + let s:state['bufnr'] = bufnr('%') + let s:state['winid'] = win_getid() + let s:state['wininfo'] = getwininfo() + + " create result buffer + exe printf('keepalt botright 3new %s', s:state['filetype']) + let s:state['resultsbufnr'] = bufnr('%') + let s:state['resultswinid'] = win_getid() + if s:has_proptype + call prop_type_add('highlight', { 'highlight': 'Directory', 'bufnr': s:state['resultsbufnr'] }) + endif + + " create prompt buffer + exe printf('keepalt botright 1new %s', s:state['promptfiletype']) + let s:state['promptbufnr'] = bufnr('%') + let s:state['promptwinid'] = win_getid() + + call win_gotoid(s:state['resultswinid']) + call s:set_buffer_options() + setlocal cursorline + call s:update_items() + exec printf('setlocal filetype=' . s:state['filetype']) + call s:notify('open', { 'bufnr': s:state['bufnr'], 'winid': s:state['winid'] , 'resultsbufnr': s:state['resultsbufnr'], 'resultswinid': s:state['resultswinid'] }) + + call win_gotoid(s:state['promptwinid']) + call s:set_buffer_options() + call setline(1, s:state['input']) + + " map keys + inoremap (lsp-quickpick-accept) :call on_accept() + nnoremap (lsp-quickpick-accept) :call on_accept() + + inoremap (lsp-quickpick-close) :call lsp#internal#ui#quickpick#close() + nnoremap (lsp-quickpick-close) :call lsp#internal#ui#quickpick#close() + + inoremap (lsp-quickpick-cancel) :call on_cancel() + nnoremap (lsp-quickpick-cancel) :call on_cancel() + + inoremap (lsp-quickpick-move-next) :call on_move_next() + nnoremap (lsp-quickpick-move-next) :call on_move_next() + + inoremap (lsp-quickpick-move-previous) :call on_move_previous() + nnoremap (lsp-quickpick-move-previous) :call on_move_previous() + + exec printf('setlocal filetype=' . s:state['promptfiletype']) + + if !hasmapto('(lsp-quickpick-accept)') + imap (lsp-quickpick-accept) + nmap (lsp-quickpick-accept) + endif + + if !hasmapto('(lsp-quickpick-cancel)') + imap (lsp-quickpick-cancel) + map (lsp-quickpick-cancel) + imap (lsp-quickpick-cancel) + map (lsp-quickpick-cancel) + endif + + if !hasmapto('(lsp-quickpick-move-next)') + imap (lsp-quickpick-move-next) + nmap (lsp-quickpick-move-next) + imap (lsp-quickpick-move-next) + nmap (lsp-quickpick-move-next) + endif + + if !hasmapto('(lsp-quickpick-move-previous)') + imap (lsp-quickpick-move-previous) + nmap (lsp-quickpick-move-previous) + imap (lsp-quickpick-move-previous) + nmap (lsp-quickpick-move-previous) + endif + + call cursor(line('$'), 0) + call feedkeys('i', 'n') + + augroup lsp#internal#ui#quickpick + autocmd! + autocmd InsertCharPre call s:on_insertcharpre() + autocmd TextChangedI call s:on_inputchanged() + autocmd InsertEnter call s:on_insertenter() + autocmd InsertLeave call s:on_insertleave() + + if exists('##TextChangedP') + autocmd TextChangedP call s:on_inputchanged() + endif + augroup END + + call s:notify_items() + call s:notify_selection() + call lsp#internal#ui#quickpick#busy(s:state['busy']) +endfunction + +function! s:set_buffer_options() abort + " set buffer options + abc + setlocal bufhidden=unload " unload buf when no longer displayed + setlocal buftype=nofile " buffer is not related to any file + setlocal noswapfile " don't create swap file + setlocal nowrap " don't soft-wrap + setlocal nonumber " don't show line numbers + setlocal nolist " don't use list mode (visible tabs etc) + setlocal foldcolumn=0 " don't show a fold column at side + setlocal foldlevel=99 " don't fold anything + setlocal nospell " spell checking off + setlocal nobuflisted " don't show up in the buffer list + setlocal textwidth=0 " don't hardwarp (break long lines) + setlocal nocursorline " highlight the line cursor is off + setlocal nocursorcolumn " disable cursor column + setlocal noundofile " don't enable undo + setlocal winfixheight + if exists('+colorcolumn') | setlocal colorcolumn=0 | endif + if exists('+relativenumber') | setlocal norelativenumber | endif + setlocal signcolumn=yes " for prompt +endfunction + +function! lsp#internal#ui#quickpick#close() abort + if !exists('s:state') + return + endif + + call lsp#internal#ui#quickpick#busy(0) + + call win_gotoid(s:state['winid']) + call s:notify('close', { 'bufnr': s:state['bufnr'], 'winid': s:state['winid'], 'resultsbufnr': s:state['resultsbufnr'], 'resultswinid': s:state['winid'] }) + + augroup lsp#internal#ui#quickpick + autocmd! + augroup END + + exe 'silent! bunload! ' . s:state['promptbufnr'] + exe 'silent! bunload! ' . s:state['resultsbufnr'] + call s:restore_windows() + + let s:inputecharpre = 0 + + unlet s:state +endfunction + +function! s:restore_windows() abort + let [l:tabnr, l:_] = win_id2tabwin(s:state['winid']) + if l:tabnr == 0 + return + endif + + let l:Resizable = {_, info -> + \ info.tabnr == l:tabnr && + \ s:win_exists(info.winid) && + \ !s:is_floating(info.winid) + \ } + let l:wins_to_resize = sort(filter(s:state['wininfo'], l:Resizable), {l, r -> l.winnr - r.winnr}) + let l:open_winids_to_resize = map(filter(getwininfo(), l:Resizable), {_, info -> info.winid}) + + let l:resize_cmd = '' + for l:info in l:wins_to_resize + if index(l:open_winids_to_resize, l:info.winid) == -1 + return + endif + + let l:resize_cmd .= printf('%dresize %d | vert %dresize %d |', l:info.winnr, l:info.height, l:info.winnr, l:info.width) + endfor + + " winrestcmd repeats :resize commands twice after patch-8.2.2631. + " To simulate this behavior, execute the :resize commands twice. + " see https://github.com/vim/vim/issues/7988 + exe 'silent! ' . l:resize_cmd . l:resize_cmd +endfunction + +function! lsp#internal#ui#quickpick#items(items) abort + let s:state['items'] = a:items + call s:update_items() + call s:notify_items() + call s:notify_selection() +endfunction + +function! lsp#internal#ui#quickpick#busy(busy) abort + let s:state['busy'] = a:busy + if a:busy + if !has_key(s:state, 'busytimer') + let s:state['busyframe'] = 0 + let s:state['busytimer'] = timer_start(60, function('s:busy_tick'), { 'repeat': -1 }) + endif + else + if has_key(s:state, 'busytimer') + call timer_stop(s:state['busytimer']) + call remove(s:state, 'busytimer') + redraw + echohl None + echo '' + endif + endif +endfunction + +function! lsp#internal#ui#quickpick#results_winid() abort + if exists('s:state') + return s:state['resultswinid'] + else + return 0 + endif +endfunction + +function! s:busy_tick(...) abort + let s:state['busyframe'] = s:state['busyframe'] + 1 + if s:state['busyframe'] >= len(s:state['busyframes']) + let s:state['busyframe'] = 0 + endif + redraw + echohl Question | echon s:state['busyframes'][s:state['busyframe']] + echohl None +endfunction + +function! s:update_items() abort + call s:win_execute(s:state['resultswinid'], 'silent! %delete _') + + let s:state['highlights'] = [] + + if s:state['filter'] " if filter is enabled + if empty(s:trim(s:state['input'])) + let s:state['fitems'] = s:state['items'] + else + if empty(s:state['key']) " item is string + if s:has_matchfuzzypos + let l:matchfuzzyresult = matchfuzzypos(s:state['items'], s:state['input']) + let l:fitems = l:matchfuzzyresult[0] + let l:highlights = l:matchfuzzyresult[1] + let s:state['fitems'] = l:fitems + let s:state['highlights'] = l:highlights + elseif s:has_matchfuzzy + let s:state['fitems'] = matchfuzzy(s:state['items'], s:state['input']) + else + let s:state['fitems'] = filter(copy(s:state['items']), 'stridx(toupper(v:val), toupper(s:state["input"])) >= 0') + endif + else " item is dict + if s:has_matchfuzzypos + " vim requires matchfuzzypos to have highlights. + " matchfuzzy only patch doesn't support dict search + let l:matchfuzzyresult = matchfuzzypos(s:state['items'], s:state['input'], { 'key': s:state['key'] }) + let l:fitems = l:matchfuzzyresult[0] + let l:highlights = l:matchfuzzyresult[1] + let s:state['fitems'] = l:fitems + let s:state['highlights'] = l:highlights + else + let s:state['fitems'] = filter(copy(s:state['items']), 'stridx(toupper(v:val[s:state["key"]]), toupper(s:state["input"])) >= 0') + endif + endif + endif + else " if filter is disabled + let s:state['fitems'] = s:state['items'] + endif + + + if empty(s:state['key']) " item is string + let l:lines = s:state['fitems'] + else " item is dict + let l:lines = map(copy(s:state['fitems']), 'v:val[s:state["key"]]') + endif + + call setbufline(s:state['resultsbufnr'], 1, l:lines) + + if s:has_proptype && !empty(s:state['highlights']) + let l:i = 0 + for l:line in s:state['highlights'] + for l:pos in l:line + let l:cs = split(getbufline(s:state['resultsbufnr'], l:i + 1)[0], '\zs') + let l:mpos = strlen(join(l:cs[: l:pos - 1], '')) + let l:len = strlen(l:cs[l:pos]) + call prop_add(l:i + 1, l:mpos + 1, { 'length': l:len, 'type': 'highlight', 'bufnr': s:state['resultsbufnr'] }) + endfor + let l:i += 1 + endfor + endif + + call s:win_execute(s:state['resultswinid'], printf('resize %d', min([len(s:state['fitems']), s:state['maxheight']]))) + call s:win_execute(s:state['promptwinid'], 'resize 1') +endfunction + +function! s:on_accept() abort + if win_gotoid(s:state['resultswinid']) + let l:index = line('.') - 1 " line is 1 index, list is 0 index + let l:fitems = s:state['fitems'] + if l:index < 0 || len(l:fitems) <= l:index + let l:items = [] + else + let l:items = [l:fitems[l:index]] + endif + call win_gotoid(s:state['winid']) + call s:notify('accept', { 'items': l:items }) + end +endfunction + +function! s:on_cancel() abort + call win_gotoid(s:state['winid']) + call s:notify('cancel', {}) + call lsp#internal#ui#quickpick#close() +endfunction + +function! s:on_move_next() abort + let l:col = col('.') + call s:win_execute(s:state['resultswinid'], 'normal! j') + call s:notify_selection() +endfunction + +function! s:on_move_previous() abort + let l:col = col('.') + call s:win_execute(s:state['resultswinid'], 'normal! k') + call s:notify_selection() +endfunction + +function! s:notify_items() abort + " items could be huge, so don't send the items as part of data + call s:notify('items', { 'bufnr': s:state['bufnr'], 'winid': s:state['winid'], 'resultsbufnr': s:state['resultsbufnr'], 'resultswinid': s:state['resultswinid'] }) +endfunction + +function! s:notify_selection() abort + let l:original_winid = win_getid() + call win_gotoid(s:state['resultswinid']) + let l:index = line('.') - 1 " line is 1 based, list is 0 based + if l:index < 0 || ((l:index + 1) > len(s:state['fitems'])) + let l:items = [] + else + let l:items = [s:state['fitems'][l:index]] + endif + let l:data = { + \ 'bufnr': s:state['bufnr'], + \ 'winid': s:state['winid'], + \ 'resultsbufnr': s:state['resultsbufnr'], + \ 'resultswinid': s:state['resultswinid'], + \ 'items': l:items, + \ } + noautocmd call win_gotoid(s:state['winid']) + call s:notify('selection', l:data) + noautocmd call win_gotoid(l:original_winid) +endfunction + +function! s:on_inputchanged() abort + if s:inputecharpre + if s:has_timer && s:state['debounce'] > 0 + call s:debounce_onchange() + else + call s:notify_onchange() + endif + endif +endfunction + +function! s:on_insertcharpre() abort + let s:inputecharpre = 1 +endfunction + +function! s:on_insertenter() abort + let s:inputecharpre = 0 +endfunction + +function! s:on_insertleave() abort + if s:has_timer && has_key(s:state, 'debounce_onchange_timer') + call timer_stop(s:state['debounce_onchange_timer']) + call remove(s:state, 'debounce_onchange_timer') + endif +endfunction + +function! s:debounce_onchange() abort + if has_key(s:state, 'debounce_onchange_timer') + call timer_stop(s:state['debounce_onchange_timer']) + call remove(s:state, 'debounce_onchange_timer') + endif + let s:state['debounce_onchange_timer'] = timer_start(s:state['debounce'], function('s:notify_onchange')) +endfunction + +function! s:notify_onchange(...) abort + let s:state['input'] = getbufline(s:state['promptbufnr'], 1)[0] + call s:notify('change', { 'input': s:state['input'] }) + if s:state['filter'] + call s:update_items() + call s:notify_selection() + endif +endfunction + +function! s:notify(name, data) abort + if has_key(s:state, 'on_event') | call s:state['on_event'](a:data, a:name) | endif + if has_key(s:state, 'on_' . a:name) | call s:state['on_' . a:name](a:data, a:name) | endif +endfunction + +if exists('*win_execute') + function! s:win_execute(win_id, cmd) abort + call win_execute(a:win_id, a:cmd) + endfunction +else + function! s:win_execute(winid, cmd) abort + let l:original_winid = win_getid() + if l:original_winid == a:winid + exec a:cmd + else + if win_gotoid(a:winid) + exec a:cmd + call win_gotoid(l:original_winid) + end + endif + endfunction +endif + +if exists('*trim') + function! s:trim(str) abort + return trim(a:str) + endfunction +else + function! s:trim(str) abort + return substitute(a:str, '^\s*\|\s*$', '', 'g') + endfunction +endif + +" vim: set sw=2 ts=2 sts=2 et tw=78 foldmarker={{{,}}} foldmethod=marker spell: diff --git a/autoload/lsp/internal/work_done_progress.vim b/autoload/lsp/internal/work_done_progress.vim new file mode 100644 index 00000000..98fcc9f0 --- /dev/null +++ b/autoload/lsp/internal/work_done_progress.vim @@ -0,0 +1,70 @@ +" https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress + +let s:progress_ui = [] +let s:enabled = 0 + +function! lsp#internal#work_done_progress#_enable() abort + if !g:lsp_work_done_progress_enabled | return | endif + + if s:enabled | return | endif + let s:enabled = 1 + let s:progress_ui = [] + + let s:Dispose = lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'response') && has_key(x['response'], 'method') + \ && x['response']['method'] ==# '$/progress' && has_key(x['response'], 'params') + \ && has_key(x['response']['params'], 'value') && type(x['response']['params']['value']) == type({})}), + \ lsp#callbag#subscribe({'next': {x->s:handle_work_done_progress(x['server'], x['response'])}}) + \ ) +endfunction + +function! s:handle_work_done_progress(server, response) abort + let l:value = a:response['params']['value'] + let l:token = a:response['params']['token'] + let l:new = { + \ 'server': a:server, + \ 'token': l:token, + \ 'title': '', + \ 'message': '', + \ } + + if l:value['kind'] ==# 'end' + let l:new['message'] = '' + let l:new['percentage'] = 100 + call filter(s:progress_ui, {_, x->x['token'] !=# l:token || x['server'] !=# a:server}) + elseif l:value['kind'] ==# 'begin' + let l:new['title'] = l:value['title'] + call filter(s:progress_ui, {_, x->x['token'] !=# l:token || x['server'] !=# a:server}) + call insert(s:progress_ui, l:new) + elseif l:value['kind'] ==# 'report' + let l:new['message'] = get(l:value, 'message', '') + if has_key(l:value, 'percentage') + " l:value['percentage'] is uinteger in specification. + " But some implementation return float. (e.g. clangd11) + " So we round it. + let l:new['percentage'] = float2nr(l:value['percentage'] + 0.5) + endif + let l:idx = match(s:progress_ui, l:token) + let l:new['title'] = s:progress_ui[l:idx]['title'] + call filter(s:progress_ui, {_, x->x['token'] !=# l:token || x['server'] !=# a:server}) + call insert(s:progress_ui, l:new) + endif + doautocmd User lsp_progress_updated +endfunction + +function! lsp#internal#work_done_progress#_disable() abort + if !s:enabled | return | endif + + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif + + let s:enabled = 0 + let s:progress_ui = [] +endfunction + +function! lsp#internal#work_done_progress#get_progress() abort + return s:progress_ui +endfunction diff --git a/autoload/lsp/internal/workspace_symbol/search.vim b/autoload/lsp/internal/workspace_symbol/search.vim new file mode 100644 index 00000000..745ec6b0 --- /dev/null +++ b/autoload/lsp/internal/workspace_symbol/search.vim @@ -0,0 +1,94 @@ +" https://microsoft.github.io/language-server-protocol/specification#workspace_symbol +" options - { +" bufnr: bufnr('%') " optional +" server - 'server_name' " optional +" query: '' " optional +" } +function! lsp#internal#workspace_symbol#search#do(options) abort + if has_key(a:options, 'server') + let l:servers = [a:options['server']] + else + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_symbol_provider(v:val)') + endif + + if len(l:servers) == 0 + echom 'textDocument/workspaceSymbol not supported' + call lsp#utils#error('textDocument/workspaceSymbol not supported') + return + endif + + redraw | echo 'Retrieving workspace symbols ...' + + let l:TextChangeSubject = lsp#callbag#makeSubject() + + " use callbag debounce instead of quickpick debounce + call lsp#internal#ui#quickpick#open({ + \ 'items': [], + \ 'input': get(a:options, 'query', ''), + \ 'key': 'text', + \ 'debounce': 0, + \ 'on_change': function('s:on_change', [l:TextChangeSubject]), + \ 'on_accept': function('s:on_accept'), + \ 'on_close': function('s:on_close'), + \ }) + + let s:Dispose = lsp#callbag#pipe( + \ l:TextChangeSubject, + \ lsp#callbag#debounceTime(250), + \ lsp#callbag#distinctUntilChanged(), + \ lsp#callbag#switchMap({query-> + \ lsp#callbag#pipe( + \ lsp#callbag#fromList(l:servers), + \ lsp#callbag#tap({_->lsp#internal#ui#quickpick#busy(1)}), + \ lsp#callbag#flatMap({server-> + \ lsp#callbag#pipe( + \ lsp#request(server, { + \ 'method': 'workspace/symbol', + \ 'params': { + \ 'query': query + \ } + \ }), + \ lsp#callbag#map({x->{'server': server, 'request': x['request'], 'response': x['response']}}), + \ ) + \ }), + \ lsp#callbag#scan({acc, curr->add(acc, curr)}, []), + \ lsp#callbag#tap({x->s:update_ui_items(x)}), + \ lsp#callbag#tap({'complete': {->lsp#internal#ui#quickpick#busy(0)}}), + \ ) + \ }), + \ lsp#callbag#subscribe({ + \ 'error': {e->s:on_error(e)}, + \ }), + \ ) + " Notify empty query. Some servers may not return results when query is empty + call l:TextChangeSubject(1, '') +endfunction + +function! s:on_change(TextChangeSubject, data, ...) abort + call a:TextChangeSubject(1, a:data['input']) +endfunction + +function! s:update_ui_items(x) abort + let l:items = [] + for l:i in a:x + let l:items += lsp#ui#vim#utils#symbols_to_loc_list(l:i['server'], l:i) + endfor + call lsp#internal#ui#quickpick#items(l:items) +endfunction + +function! s:on_accept(data, name) abort + call lsp#internal#ui#quickpick#close() + call lsp#utils#location#_open_vim_list_item(a:data['items'][0], '') +endfunction + +function! s:on_close(...) abort + if exists('s:Dispose') + call s:Dispose() + unlet s:Dispose + endif +endfunction + +function! s:on_error(e) abort + call lsp#internal#ui#quickpick#close() + call lsp#log('LspWorkspaceSymbolSearch error', a:e) +endfunction diff --git a/autoload/lsp/omni.vim b/autoload/lsp/omni.vim new file mode 100644 index 00000000..160ca10e --- /dev/null +++ b/autoload/lsp/omni.vim @@ -0,0 +1,452 @@ +" vint: -ProhibitUnusedVariable + +" constants {{{ +let s:t_dict = type({}) + +let s:default_completion_item_kinds = { + \ '1': 'text', + \ '2': 'method', + \ '3': 'function', + \ '4': 'constructor', + \ '5': 'field', + \ '6': 'variable', + \ '7': 'class', + \ '8': 'interface', + \ '9': 'module', + \ '10': 'property', + \ '11': 'unit', + \ '12': 'value', + \ '13': 'enum', + \ '14': 'keyword', + \ '15': 'snippet', + \ '16': 'color', + \ '17': 'file', + \ '18': 'reference', + \ '19': 'folder', + \ '20': 'enum member', + \ '21': 'constant', + \ '22': 'struct', + \ '23': 'event', + \ '24': 'operator', + \ '25': 'type parameter', + \ } + +let s:completion_item_kinds = {} + +let s:completion_status_success = 'success' +let s:completion_status_failed = 'failed' +let s:completion_status_pending = 'pending' + +let s:is_user_data_support = has('patch-8.0.1493') +let s:managed_user_data_key_base = 0 +let s:managed_user_data_map = {} + +" }}} + +" completion state +let s:completion = {'counter': 0, 'status': '', 'matches': []} + +function! lsp#omni#complete(findstart, base) abort + let l:info = s:find_complete_servers() + if empty(l:info['server_names']) + return a:findstart ? -1 : [] + endif + + if a:findstart + return col('.') + else + if !g:lsp_async_completion + let s:completion['status'] = s:completion_status_pending + endif + + let l:left = strpart(getline('.'), 0, col('.')-1) + + " Initialize the default startcol. It will be updated if the completion items has textEdit. + let s:completion['startcol'] = s:get_startcol(l:left, l:info['server_names']) + + " The `l:info` variable will be filled with completion results after request was finished. + call s:send_completion_request(l:info) + + if g:lsp_async_completion + " If g:lsp_async_completion == v:true, the `s:display_completions` " will be called by `s:send_completion_request`. + redraw + return exists('v:none') ? v:none : [] + else + " Wait for finished the textDocument/completion request and then call `s:display_completions` explicitly. + call lsp#utils#_wait(-1, {-> s:completion['status'] isnot# s:completion_status_pending || complete_check()}, 10) + call timer_start(0, { timer -> s:display_completions(timer, l:info) }) + + return exists('v:none') ? v:none : [] + endif + endif +endfunction + +function! s:get_filter_label(item) abort + return lsp#utils#_trim(a:item['word']) +endfunction + +function! s:prefix_filter(item, last_typed_word) abort + let l:label = s:get_filter_label(a:item) + + if g:lsp_ignorecase + return stridx(tolower(l:label), tolower(a:last_typed_word)) == 0 + else + return stridx(l:label, a:last_typed_word) == 0 + endif +endfunction + +function! s:contains_filter(item, last_typed_word) abort + let l:label = s:get_filter_label(a:item) + + if g:lsp_ignorecase + return stridx(tolower(l:label), tolower(a:last_typed_word)) >= 0 + else + return stridx(l:label, a:last_typed_word) >= 0 + endif +endfunction + +let s:pair = { +\ '"': '"', +\ '''': '''', +\ '{': '}', +\ '(': ')', +\ '[': ']', +\} + +function! s:display_completions(timer, info) abort + " TODO: Allow multiple servers + let l:server_name = a:info['server_names'][0] + let l:server_info = lsp#get_server_info(l:server_name) + + let l:current_line = strpart(getline('.'), 0, col('.') - 1) + let l:last_typed_word = strpart(l:current_line, s:completion['startcol'] - 1) + + let l:filter = has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'filter') ? l:server_info['config']['filter'] : { 'name': 'prefix' } + if l:filter['name'] ==? 'prefix' + let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:prefix_filter(item, l:last_typed_word)}) + if has_key(s:pair, l:last_typed_word[0]) + let [l:lhs, l:rhs] = [l:last_typed_word[0], s:pair[l:last_typed_word[0]]] + for l:item in s:completion['matches'] + let l:str = l:item['word'] + if len(l:str) > 1 && l:str[0] ==# l:lhs && l:str[-1:] ==# l:rhs + let l:item['word'] = l:str[:-2] + endif + endfor + endif + elseif l:filter['name'] ==? 'contains' + let s:completion['matches'] = filter(s:completion['matches'], {_, item -> s:contains_filter(item, l:last_typed_word)}) + endif + + let s:completion['status'] = '' + + if mode() is# 'i' + call complete(s:completion['startcol'], s:completion['matches']) + endif +endfunction + +function! s:handle_omnicompletion(server_name, complete_counter, info, data) abort + if s:completion['counter'] != a:complete_counter + " ignore old completion results + return + endif + + if lsp#client#is_error(a:data) || !has_key(a:data, 'response') || !has_key(a:data['response'], 'result') + let s:completion['status'] = s:completion_status_failed + return + endif + + let l:result = s:get_completion_result(a:server_name, a:data) + let s:completion['matches'] = l:result['items'] + let s:completion['startcol'] = min([l:result['startcol'], s:completion['startcol']]) + let s:completion['status'] = s:completion_status_success + + if g:lsp_async_completion + call s:display_completions(0, a:info) + endif +endfunction + +function! lsp#omni#get_kind_text(completion_item, ...) abort + let l:server = get(a:, 1, '') + if empty(l:server) " server name + let l:completion_item_kinds = s:default_completion_item_kinds + else + if !has_key(s:completion_item_kinds, l:server) + let l:server_info = lsp#get_server_info(l:server) + if has_key (l:server_info, 'config') && has_key(l:server_info['config'], 'completion_item_kinds') + let s:completion_item_kinds[l:server] = extend(copy(s:default_completion_item_kinds), l:server_info['config']['completion_item_kinds']) + else + let s:completion_item_kinds[l:server] = s:default_completion_item_kinds + endif + endif + let l:completion_item_kinds = s:completion_item_kinds[l:server] + endif + + return has_key(a:completion_item, 'kind') && has_key(l:completion_item_kinds, a:completion_item['kind']) + \ ? l:completion_item_kinds[a:completion_item['kind']] : '' +endfunction + +function! s:get_kind_text_mappings(server) abort + let l:server_name = a:server['name'] + if has_key(s:completion_item_kinds, l:server_name) + return s:completion_item_kinds[l:server_name] + else + if has_key(a:server, 'config') && has_key(a:server['config'], 'completion_item_kinds') + let s:completion_item_kinds[l:server_name] = extend(copy(s:default_completion_item_kinds), a:server['config']['completion_item_kinds']) + else + let s:completion_item_kinds[l:server_name] = s:default_completion_item_kinds + endif + return s:completion_item_kinds[l:server_name] + endif +endfunction + +" auxiliary functions {{{ + +function! s:find_complete_servers() abort + let l:server_names = [] + for l:server_name in lsp#get_allowed_servers() + if lsp#capabilities#has_completion_provider(l:server_name) + " TODO: support triggerCharacters + call add(l:server_names, l:server_name) + endif + endfor + + return { 'server_names': l:server_names } +endfunction + +function! s:send_completion_request(info) abort + let s:completion['counter'] = s:completion['counter'] + 1 + let l:server_name = a:info['server_names'][0] + " TODO: support multiple servers + call lsp#send_request(l:server_name, { + \ 'method': 'textDocument/completion', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': lsp#get_position(), + \ 'context': { 'triggerKind': 1 }, + \ }, + \ 'on_notification': function('s:handle_omnicompletion', [l:server_name, s:completion['counter'], a:info]), + \ }) +endfunction + +function! s:get_completion_result(server_name, data) abort + let l:result = a:data['response']['result'] + + let l:options = { + \ 'server': lsp#get_server_info(a:server_name), + \ 'position': lsp#get_position(), + \ 'response': a:data['response'], + \ } + + return lsp#omni#get_vim_completion_items(l:options) +endfunction + +function! s:sort_by_sorttext(i1, i2) abort + let l:text1 = get(a:i1, 'sortText') + let l:text2 = get(a:i2, 'sortText') + + " sortText is possibly empty string + let l:text1 = !empty(l:text1) ? l:text1 : a:i1['label'] + let l:text2 = !empty(l:text2) ? l:text2 : a:i2['label'] + + if g:lsp_ignorecase + return l:text1 ==? l:text2 ? 0 : l:text1 >? l:text2 ? 1 : -1 + else + return l:text1 ==# l:text2 ? 0 : l:text1 ># l:text2 ? 1 : -1 + endif +endfunction + +" Create vim's completed items from LSP response. +" +" options = { +" server: {}, " needs to be server_info and not server_name +" position: lsp#get_position(), +" response: {}, " needs to be the entire lsp response. errors need to be +" handled before calling the fuction +" } +" +" * The returned` startcol` may be the same as the cursor position, in which case you need to decide which one to use. +" +" @return { 'items': v:completed_item[], 'incomplete': v:t_bool, 'startcol': number } +" +function! lsp#omni#get_vim_completion_items(options) abort + let l:server = a:options['server'] + let l:server_name = l:server['name'] + let l:kind_text_mappings = s:get_kind_text_mappings(l:server) + let l:complete_position = a:options['position'] + let l:current_line = getline('.') + let l:default_startcol = s:get_startcol(strcharpart(l:current_line, 0, l:complete_position['character']), [l:server_name]) + let l:default_start_character = strchars(strpart(l:current_line, 0, l:default_startcol - 1)) + let l:refresh_pattern = s:get_refresh_pattern([l:server_name]) + + let l:result = a:options['response']['result'] + if type(l:result) == type([]) + let l:items = l:result + let l:incomplete = 0 + elseif type(l:result) == type({}) + let l:items = l:result['items'] + let l:incomplete = l:result['isIncomplete'] + else + let l:items = [] + let l:incomplete = 0 + endif + + let l:sort = has_key(l:server, 'config') && has_key(l:server['config'], 'sort') ? l:server['config']['sort'] : v:null + + if len(l:items) > 0 && type(l:sort) == s:t_dict && len(l:items) <= l:sort['max'] + " If first item contains sortText, maybe we can use sortText + call sort(l:items, function('s:sort_by_sorttext')) + endif + + let l:start_character = l:complete_position['character'] + + let l:start_characters = [] " The mapping of item specific start_character. + let l:vim_complete_items = [] + for l:completion_item in l:items + let l:expandable = get(l:completion_item, 'insertTextFormat', 1) == 2 + let l:vim_complete_item = { + \ 'kind': get(l:kind_text_mappings, get(l:completion_item, 'kind', '') , ''), + \ 'dup': 1, + \ 'empty': 1, + \ 'icase': 1, + \ } + let l:range = lsp#utils#text_edit#get_range(get(l:completion_item, 'textEdit', {})) + let l:complete_word = '' + if has_key(l:completion_item, 'textEdit') && type(l:completion_item['textEdit']) == s:t_dict && !empty(l:range) && has_key(l:completion_item['textEdit'], 'newText') + let l:text_edit_new_text = l:completion_item['textEdit']['newText'] + if has_key(l:completion_item, 'filterText') && !empty(l:completion_item['filterText']) && matchstr(l:text_edit_new_text, '^' . l:refresh_pattern) ==# '' + " Use filterText as word. + let l:complete_word = l:completion_item['filterText'] + else + " Use textEdit.newText as word. + let l:complete_word = l:text_edit_new_text + endif + + let l:item_start_character = l:range['start']['character'] + let l:start_character = min([l:item_start_character, l:start_character]) + let l:start_characters += [l:item_start_character] + elseif has_key(l:completion_item, 'insertText') && !empty(l:completion_item['insertText']) + let l:complete_word = l:completion_item['insertText'] + let l:start_characters += [l:default_start_character] + else + let l:complete_word = l:completion_item['label'] + let l:start_characters += [l:default_start_character] + endif + + if l:expandable + let l:vim_complete_item['word'] = lsp#utils#make_valid_word(substitute(l:complete_word, '\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g')) + let l:vim_complete_item['abbr'] = l:completion_item['label'] . '~' + else + let l:vim_complete_item['word'] = l:complete_word + let l:vim_complete_item['abbr'] = l:completion_item['label'] + endif + + if s:is_user_data_support + let l:vim_complete_item['user_data'] = s:create_user_data( + \ l:completion_item, + \ l:server_name, + \ l:complete_position, + \ l:start_characters[-1], + \ l:complete_word) + endif + + let l:vim_complete_items += [l:vim_complete_item] + endfor + + " Add the additional text for startcol correction. + if l:start_character != l:default_start_character + for l:i in range(len(l:start_characters)) + let l:item_start_character = l:start_characters[l:i] + if l:start_character < l:item_start_character + let l:item = l:vim_complete_items[l:i] + let l:item['word'] = strcharpart(l:current_line, l:start_character, l:item_start_character - l:start_character) . l:item['word'] + endif + endfor + endif + let l:startcol = lsp#utils#position#lsp_character_to_vim('%', { 'line': l:complete_position['line'], 'character': l:start_character }) + + return { 'items': l:vim_complete_items, 'incomplete': l:incomplete, 'startcol': l:startcol } +endfunction + +" +" Clear internal user_data map. +" +" This function should call at `CompleteDone` only if not empty `v:completed_item`. +" +function! lsp#omni#_clear_managed_user_data_map() abort + let s:managed_user_data_key_base = 0 + let s:managed_user_data_map = {} +endfunction + +" +" create item's user_data. +" +function! s:create_user_data(completion_item, server_name, complete_position, start_character, complete_word) abort + let l:user_data_key = s:create_user_data_key(s:managed_user_data_key_base) + let s:managed_user_data_map[l:user_data_key] = { + \ 'complete_position': a:complete_position, + \ 'server_name': a:server_name, + \ 'completion_item': a:completion_item, + \ 'start_character': a:start_character, + \ 'complete_word': a:complete_word, + \ } + let s:managed_user_data_key_base += 1 + return l:user_data_key +endfunction + +function! lsp#omni#get_managed_user_data_from_completed_item(completed_item) abort + " the item has no user_data. + if !has_key(a:completed_item, 'user_data') + return {} + endif + + let l:user_data_string = get(a:completed_item, 'user_data', '') + if type(l:user_data_string) != type('') + return {} + endif + + " Check managed user_data. + if has_key(s:managed_user_data_map, l:user_data_string) + return s:managed_user_data_map[l:user_data_string] + endif + + " Check json. + if stridx(l:user_data_string, '"vim-lsp/key"') != -1 + try + let l:user_data = json_decode(l:user_data_string) + if has_key(l:user_data, 'vim-lsp/key') + let l:user_data_key = s:create_user_data_key(l:user_data['vim-lsp/key']) + if has_key(s:managed_user_data_map, l:user_data_key) + return s:managed_user_data_map[l:user_data_key] + endif + endif + catch /.*/ + endtry + endif + return {} +endfunction + +function! lsp#omni#get_completion_item_kinds() abort + return map(keys(s:default_completion_item_kinds), {idx, key -> str2nr(key)}) +endfunction + +function! s:create_user_data_key(base) abort + return '{"vim-lsp/key":"' . a:base . '"}' +endfunction + +function! s:get_startcol(left, server_names) abort + " Initialize the default startcol. It will be updated if the completion items has textEdit. + let l:startcol = 1 + matchstrpos(a:left, s:get_refresh_pattern(a:server_names))[1] + return l:startcol == 0 ? strlen(a:left) + 1 : l:startcol +endfunction + +function! s:get_refresh_pattern(server_names) abort + for l:server_name in a:server_names + let l:server_info = lsp#get_server_info(l:server_name) + if has_key(l:server_info, 'config') && has_key(l:server_info['config'], 'refresh_pattern') + return l:server_info['config']['refresh_pattern'] + endif + endfor + return '\(\k\+$\)' +endfunction + +" }}} diff --git a/autoload/lsp/tag.vim b/autoload/lsp/tag.vim new file mode 100644 index 00000000..d00843bd --- /dev/null +++ b/autoload/lsp/tag.vim @@ -0,0 +1,157 @@ +let s:tag_kind_priority = ['definition', 'declaration', 'implementation', 'type definition'] + +function! s:not_supported(what) abort + call lsp#log(a:what . ' not supported for ' . &filetype) +endfunction + +function! s:location_to_tag(loc) abort + if has_key(a:loc, 'targetUri') + let l:uri = a:loc['targetUri'] + let l:range = a:loc['targetRange'] + else + let l:uri = a:loc['uri'] + let l:range = a:loc['range'] + endif + + if !lsp#utils#is_file_uri(l:uri) + return v:null + endif + + let l:path = lsp#utils#uri_to_path(l:uri) + let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:range['start']) + return { + \ 'filename': l:path, + \ 'cmd': printf('/\%%%dl\%%%dc/', l:line, l:col) + \ } +endfunction + +function! s:handle_locations(ctx, server, type, data) abort + try + if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result') + call lsp#utils#error('Failed to retrieve ' . a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + return + endif + + let l:list = a:ctx['list'] + let l:result = a:data['response']['result'] + if type(l:result) != type([]) + let l:result = [l:result] + endif + for l:loc in l:result + let l:tag = s:location_to_tag(l:loc) + if !empty(l:tag) + call add(l:list, extend(l:tag, { 'name': a:ctx['pattern'], 'kind': a:type })) + endif + endfor + finally + let a:ctx['counter'] -= 1 + endtry +endfunction + +function! s:handle_symbols(ctx, server, data) abort + try + if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result') + call lsp#utils#error('Failed to retrieve workspace symbols for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + return + endif + + let l:list = a:ctx['list'] + for l:symbol in a:data['response']['result'] + let l:tag = s:location_to_tag(l:symbol['location']) + if empty(l:tag) + continue + endif + + let l:tag['name'] = l:symbol['name'] + if has_key(l:symbol, 'kind') + let l:tag['kind'] = lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) + endif + call add(l:list, l:tag) + endfor + finally + let a:ctx['counter'] -= 1 + endtry +endfunction + +function! s:tag_view_sub(ctx, method, params) abort + let l:operation = substitute(a:method, '\u', ' \l\0', 'g') + + let l:capabilities_func = printf('lsp#capabilities#has_%s_provider(v:val)', substitute(l:operation, ' ', '_', 'g')) + let l:servers = filter(lsp#get_allowed_servers(), l:capabilities_func) + if empty(l:servers) + call s:not_supported('retrieving ' . l:operation) + return v:false + endif + + let a:ctx['counter'] += len(l:servers) + for l:server in l:servers + call lsp#send_request(l:server, { + \ 'method': 'textDocument/'.a:method, + \ 'params': a:params, + \ 'on_notification': function('s:handle_locations', [a:ctx, l:server, l:operation]) + \}) + endfor + return v:true +endfunction + +function! s:tag_view(ctx) abort + let l:params = { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': lsp#get_position(), + \ } + return !empty(filter(copy(g:lsp_tagfunc_source_methods), + \ {_, m -> s:tag_view_sub(a:ctx, m, l:params)})) +endfunction + +function! s:tag_search(ctx) abort + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_workspace_symbol_provider(v:val)') + if empty(l:servers) + call s:not_supported('retrieving workspace symbols') + return v:false + endif + + let a:ctx['counter'] = len(l:servers) + for l:server in l:servers + call lsp#send_request(l:server, { + \ 'method': 'workspace/symbol', + \ 'params': { 'query': a:ctx['pattern'] }, + \ 'on_notification': function('s:handle_symbols', [a:ctx, l:server]) + \ }) + endfor + return v:true +endfunction + +function! s:compare_tags(path, a, b) abort + " TODO: custom tag sorting, maybe? + if a:a['filename'] !=# a:b['filename'] + if a:a['filename'] ==# a:path + return -1 + elseif a:b['filename'] ==# a:path + return 1 + endif + endif + let l:rank_a = index(s:tag_kind_priority, get(a:a, 'kind', '')) + let l:rank_b = index(s:tag_kind_priority, get(a:b, 'kind', '')) + if l:rank_a != l:rank_b + return l:rank_a < l:rank_b ? -1 : 1 + endif + if a:a['filename'] !=# a:b['filename'] + return a:a['filename'] <# a:b['filename'] ? -1 : 1 + endif + return str2nr(a:a['cmd']) - str2nr(a:b['cmd']) +endfunction + +function! lsp#tag#tagfunc(pattern, flags, info) abort + if stridx(a:flags, 'i') >= 0 + return v:null + endif + + let l:ctx = { 'pattern': a:pattern, 'counter': 0, 'list': [] } + if !(stridx(a:flags, 'c') >= 0 ? s:tag_view(l:ctx) : s:tag_search(l:ctx)) + " No supported methods so use builtin tag source + return v:null + endif + call lsp#utils#_wait(-1, {-> l:ctx['counter'] == 0}, 50) + call sort(l:ctx['list'], function('s:compare_tags', [a:info['buf_ffname']])) + return l:ctx['list'] +endfunction diff --git a/autoload/lsp/ui/vim.vim b/autoload/lsp/ui/vim.vim new file mode 100644 index 00000000..53309e51 --- /dev/null +++ b/autoload/lsp/ui/vim.vim @@ -0,0 +1,566 @@ +function! s:not_supported(what) abort + return lsp#utils#error(printf("%s not supported for filetype '%s'", a:what, &filetype)) +endfunction + +function! lsp#ui#vim#implementation(in_preview, ...) abort + let l:ctx = { 'in_preview': a:in_preview } + if a:0 + let l:ctx['mods'] = a:1 + endif + call s:list_location('implementation', l:ctx) +endfunction + +function! lsp#ui#vim#type_definition(in_preview, ...) abort + let l:ctx = { 'in_preview': a:in_preview } + if a:0 + let l:ctx['mods'] = a:1 + endif + call s:list_location('typeDefinition', l:ctx) +endfunction + + +function! lsp#ui#vim#declaration(in_preview, ...) abort + let l:ctx = { 'in_preview': a:in_preview } + if a:0 + let l:ctx['mods'] = a:1 + endif + call s:list_location('declaration', l:ctx) +endfunction + +function! lsp#ui#vim#definition(in_preview, ...) abort + let l:ctx = { 'in_preview': a:in_preview } + if a:0 + let l:ctx['mods'] = a:1 + endif + call s:list_location('definition', l:ctx) +endfunction + +function! lsp#ui#vim#references(ctx) abort + let l:ctx = extend({ 'jump_if_one': 0 }, a:ctx) + let l:request_params = { 'context': { 'includeDeclaration': v:true } } + call s:list_location('references', l:ctx, l:request_params) +endfunction + +function! lsp#ui#vim#add_tree_references() abort + let l:ctx = { 'add_tree': v:true } + call lsp#ui#vim#references(l:ctx) +endfunction + +function! s:list_location(method, ctx, ...) abort + " typeDefinition => type definition + let l:operation = substitute(a:method, '\u', ' \l\0', 'g') + + let l:capabilities_func = printf('lsp#capabilities#has_%s_provider(v:val)', substitute(l:operation, ' ', '_', 'g')) + let l:servers = filter(lsp#get_allowed_servers(), l:capabilities_func) + let l:command_id = lsp#_new_command() + + + let l:ctx = extend({ 'counter': len(l:servers), 'list':[], 'last_command_id': l:command_id, 'jump_if_one': 1, 'mods': '', 'in_preview': 0 }, a:ctx) + if len(l:servers) == 0 + call s:not_supported('Retrieving ' . l:operation) + return + endif + + let l:params = { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': lsp#get_position(), + \ } + if a:0 + call extend(l:params, a:1) + endif + for l:server in l:servers + call lsp#send_request(l:server, { + \ 'method': 'textDocument/' . a:method, + \ 'params': l:params, + \ 'on_notification': function('s:handle_location', [l:ctx, l:server, l:operation]), + \ }) + endfor + + echo printf('Retrieving %s ...', l:operation) +endfunction + +function! s:rename(server, new_name, pos) abort + if empty(a:new_name) + echo '... Renaming aborted ...' + return + endif + + " needs to flush existing open buffers + call lsp#send_request(a:server, { + \ 'method': 'textDocument/rename', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': a:pos, + \ 'newName': a:new_name, + \ }, + \ 'on_notification': function('s:handle_workspace_edit', [a:server, lsp#_last_command(), 'rename']), + \ }) + + echo ' ... Renaming ...' +endfunction + +function! lsp#ui#vim#rename() abort + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_rename_prepare_provider(v:val)') + let l:prepare_support = 1 + if len(l:servers) == 0 + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_rename_provider(v:val)') + let l:prepare_support = 0 + endif + + let l:command_id = lsp#_new_command() + + if len(l:servers) == 0 + call s:not_supported('Renaming') + return + endif + + " TODO: ask the user which server it should use to rename if there are multiple + let l:server = l:servers[0] + + if l:prepare_support + call lsp#send_request(l:server, { + \ 'method': 'textDocument/prepareRename', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': lsp#get_position(), + \ }, + \ 'on_notification': function('s:handle_rename_prepare', [l:server, l:command_id, 'rename_prepare', expand(''), lsp#get_position()]), + \ }) + return + endif + + call s:rename(l:server, input('new name: ', expand('')), lsp#get_position()) +endfunction + +function! s:stop_all_servers() abort + for l:server in lsp#get_server_names() + if !lsp#is_server_running(l:server) + continue + endif + + echo 'Stopping' l:server 'server ...' + call lsp#stop_server(l:server) + endfor +endfunction + +function! s:stop_named_server(name) abort + if !lsp#is_valid_server_name(a:name) + call lsp#utils#warning('No LSP servers named "' . a:name . '"') + return + endif + + if lsp#is_server_running(a:name) + echo 'Stopping "' . a:name . '" server...' + call lsp#stop_server(a:name) + else + call lsp#utils#warning( + \ 'Server "' . a:name . '" is not running: ' + \ . lsp#get_server_status(a:name) + \ ) + endif +endfunction + +function! s:stop_buffer_servers() abort + let l:servers = lsp#get_allowed_servers() + let l:servers = + \ filter(l:servers, {idx, name -> lsp#is_server_running(name)}) + + if empty(l:servers) + call lsp#utils#warning('No active LSP servers for the current buffer') + return + endif + + for l:server in l:servers + echo 'Stopping "' . l:server . '" server ...' + call lsp#stop_server(l:server) + endfor +endfunction + +function! lsp#ui#vim#stop_server(stop_all, ...) abort + if a:0 != 0 && a:0 != 1 + call lsp#utils#error( + \ 'lsp#ui#vim#stop_server(): expected 1 optional "name" argument.' + \ . ' Got: "' . join(a:000, '", "') . '".') + return + endif + let l:stop_all = a:stop_all ==# '!' + let l:name = get(a:000, 0, '') + + if l:stop_all + if !empty(l:name) + call lsp#utils#error( + \ '"!" stops all servers: name is ignored: "' . l:name . '"') + endif + + call s:stop_all_servers() + return + endif + + if !empty(l:name) + call s:stop_named_server(l:name) + return + endif + + call s:stop_buffer_servers() +endfunction + +function! lsp#ui#vim#workspace_symbol(query) abort + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_workspace_symbol_provider(v:val)') + let l:command_id = lsp#_new_command() + + if len(l:servers) == 0 + call s:not_supported('Retrieving workspace symbols') + return + endif + + if !empty(a:query) + let l:query = a:query + else + let l:query = inputdialog('query>', '', "\") + if l:query ==# "\" + return + endif + endif + + for l:server in l:servers + call lsp#send_request(l:server, { + \ 'method': 'workspace/symbol', + \ 'params': { + \ 'query': l:query, + \ }, + \ 'on_notification': function('s:handle_symbol', [l:server, l:command_id, 'workspaceSymbol']), + \ }) + endfor + + redraw + echo 'Retrieving workspace symbols ...' +endfunction + +function! lsp#ui#vim#document_symbol() abort + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_document_symbol_provider(v:val)') + let l:command_id = lsp#_new_command() + + if len(l:servers) == 0 + call s:not_supported('Retrieving symbols') + return + endif + + for l:server in l:servers + call lsp#send_request(l:server, { + \ 'method': 'textDocument/documentSymbol', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ }, + \ 'on_notification': function('s:handle_symbol', [l:server, l:command_id, 'documentSymbol']), + \ }) + endfor + + echo 'Retrieving document symbols ...' +endfunction + +function! s:handle_symbol(server, last_command_id, type, data) abort + if a:last_command_id != lsp#_last_command() + return + endif + + if lsp#client#is_error(a:data['response']) + call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + return + endif + + let l:list = lsp#ui#vim#utils#symbols_to_loc_list(a:server, a:data) + + call lsp#ui#vim#utils#setqflist(l:list, a:type) + + if empty(l:list) + call lsp#utils#error('No ' . a:type .' found') + else + echo 'Retrieved ' . a:type + botright copen + endif +endfunction + +function! s:handle_location(ctx, server, type, data) abort "ctx = {counter, list, last_command_id, jump_if_one, mods, in_preview} + if a:ctx['last_command_id'] != lsp#_last_command() + return + endif + + let a:ctx['counter'] = a:ctx['counter'] - 1 + + if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result') + call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + else + let a:ctx['list'] = a:ctx['list'] + lsp#utils#location#_lsp_to_vim_list(a:data['response']['result']) + endif + + if a:ctx['counter'] == 0 + if empty(a:ctx['list']) + call lsp#utils#error('No ' . a:type .' found') + else + call lsp#utils#tagstack#_update() + + let l:loc = a:ctx['list'][0] + + if len(a:ctx['list']) == 1 && a:ctx['jump_if_one'] && !a:ctx['in_preview'] + call lsp#utils#location#_open_vim_list_item(l:loc, a:ctx['mods']) + echo 'Retrieved ' . a:type + redraw + elseif !a:ctx['in_preview'] + if get(a:ctx, 'add_tree', v:false) + let l:qf = getqflist({'idx' : 0, 'items': []}) + let l:pos = l:qf.idx + let l:parent = l:qf.items + let l:level = count(l:parent[l:pos-1].text, g:lsp_tree_incoming_prefix) + let a:ctx['list'] = extend(l:parent, map(a:ctx['list'], 'extend(v:val, {"text": repeat("' . g:lsp_tree_incoming_prefix . '", l:level+1) . v:val.text})'), l:pos) + endif + call lsp#ui#vim#utils#setqflist(a:ctx['list'], a:type) + echo 'Retrieved ' . a:type + botright copen + if get(a:ctx, 'add_tree', v:false) + " move the cursor to the newly added item + execute l:pos + 1 + endif + else + let l:lines = readfile(l:loc['filename']) + if has_key(l:loc,'viewstart') " showing a locationLink + let l:view = l:lines[l:loc['viewstart'] : l:loc['viewend']] + call lsp#ui#vim#output#preview(a:server, l:view, { + \ 'statusline': ' LSP Peek ' . a:type, + \ 'filetype': &filetype + \ }) + else " showing a location + call lsp#ui#vim#output#preview(a:server, l:lines, { + \ 'statusline': ' LSP Peek ' . a:type, + \ 'cursor': { 'line': l:loc['lnum'], 'col': l:loc['col'], 'align': g:lsp_peek_alignment }, + \ 'filetype': &filetype + \ }) + endif + endif + endif + endif +endfunction + +function! s:handle_rename_prepare(server, last_command_id, type, cword, position, data) abort + if a:last_command_id != lsp#_last_command() + return + endif + + if lsp#client#is_error(a:data['response']) + call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + return + endif + let l:result = a:data['response']['result'] + + " Check response: null. + if empty(l:result) + echo 'The ' . a:server . ' returns for ' . a:type . ' (The rename request may be invalid at the given position).' + return + endif + + " Check response: { defaultBehavior: boolean }. + if has_key(l:result, 'defaultBehavior') + call timer_start(1, {x->s:rename(a:server, input('new name: ', a:cword), a:position)}) + return + endif + + " Check response: { placeholder: string } + if has_key(l:result, 'placeholder') && !empty(l:result['placeholder']) + call timer_start(1, {x->s:rename(a:server, input('new name: ', a:cword), a:position)}) + return + endif + + " Check response: { range: Range } | Range + let l:range = get(l:result, 'range', l:result) + let l:lines = getline(1, '$') + let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim('%', l:range['start']) + let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim('%', l:range['end']) + if l:start_line ==# l:end_line + let l:name = l:lines[l:start_line - 1][l:start_col - 1 : l:end_col - 2] + else + let l:name = l:lines[l:start_line - 1][l:start_col - 1 :] + for l:i in range(l:start_line, l:end_line - 2) + let l:name .= "\n" . l:lines[l:i] + endfor + if l:end_col - 2 < 0 + let l:name .= "\n" + else + let l:name .= l:lines[l:end_line - 1][: l:end_col - 2] + endif + endif + + call timer_start(1, {x->s:rename(a:server, input('new name: ', l:name), l:range['start'])}) +endfunction + +function! s:handle_workspace_edit(server, last_command_id, type, data) abort + if a:last_command_id != lsp#_last_command() + return + endif + + if lsp#client#is_error(a:data['response']) + call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + return + endif + + call lsp#utils#workspace_edit#apply_workspace_edit(a:data['response']['result']) + + echo 'Renamed' +endfunction + +function! s:handle_text_edit(server, last_command_id, type, data) abort + if a:last_command_id != lsp#_last_command() + return + endif + + if lsp#client#is_error(a:data['response']) + call lsp#utils#error('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + return + endif + + call lsp#utils#text_edit#apply_text_edits(a:data['request']['params']['textDocument']['uri'], a:data['response']['result']) + + redraw | echo 'Document formatted' +endfunction + +function! lsp#ui#vim#code_action(opts) abort + call lsp#ui#vim#code_action#do(extend({ + \ 'sync': v:false, + \ 'selection': v:false, + \ 'query': '', + \ }, a:opts)) +endfunction + +function! lsp#ui#vim#code_lens() abort + call lsp#ui#vim#code_lens#do({ + \ 'sync': v:false, + \ }) +endfunction + +function! lsp#ui#vim#add_tree_call_hierarchy_incoming() abort + let l:ctx = { 'add_tree': v:true } + call lsp#ui#vim#call_hierarchy_incoming(l:ctx) +endfunction + +function! lsp#ui#vim#call_hierarchy_incoming(ctx) abort + let l:ctx = extend({ 'method': 'incomingCalls', 'key': 'from' }, a:ctx) + call s:prepare_call_hierarchy(l:ctx) +endfunction + +function! lsp#ui#vim#call_hierarchy_outgoing() abort + let l:ctx = { 'method': 'outgoingCalls', 'key': 'to' } + call s:prepare_call_hierarchy(l:ctx) +endfunction + +function! s:prepare_call_hierarchy(ctx) abort + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_call_hierarchy_provider(v:val)') + let l:command_id = lsp#_new_command() + + let l:ctx = extend({ 'counter': len(l:servers), 'list':[], 'last_command_id': l:command_id }, a:ctx) + if len(l:servers) == 0 + call s:not_supported('Retrieving call hierarchy') + return + endif + + for l:server in l:servers + call lsp#send_request(l:server, { + \ 'method': 'textDocument/prepareCallHierarchy', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': lsp#get_position(), + \ }, + \ 'on_notification': function('s:handle_prepare_call_hierarchy', [l:ctx, l:server, 'prepare_call_hierarchy']), + \ }) + endfor + + echo 'Preparing call hierarchy ...' +endfunction + +function! s:handle_prepare_call_hierarchy(ctx, server, type, data) abort + if a:ctx['last_command_id'] != lsp#_last_command() + return + endif + + if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result') + call lsp#utils#error('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + return + endif + if empty(a:data['response']['result']) + call lsp#utils#warning('Failed to '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + return + endif + + for l:item in a:data['response']['result'] + call s:call_hierarchy(a:ctx, a:server, l:item) + endfor +endfunction + +function! s:call_hierarchy(ctx, server, item) abort + call lsp#send_request(a:server, { + \ 'method': 'callHierarchy/' . a:ctx['method'], + \ 'params': { + \ 'item': a:item, + \ }, + \ 'on_notification': function('s:handle_call_hierarchy', [a:ctx, a:server, 'call_hierarchy']), + \ }) +endfunction + +function! s:handle_call_hierarchy(ctx, server, type, data) abort + if a:ctx['last_command_id'] != lsp#_last_command() + return + endif + + let a:ctx['counter'] = a:ctx['counter'] - 1 + + if lsp#client#is_error(a:data['response']) || !has_key(a:data['response'], 'result') + call lsp#utils#error('Failed to retrieve '. a:type . ' for ' . a:server . ': ' . lsp#client#error_message(a:data['response'])) + elseif a:data['response']['result'] isnot v:null + for l:item in a:data['response']['result'] + let l:loc = s:hierarchy_item_to_vim(l:item[a:ctx['key']], a:server) + if l:loc isnot v:null + let a:ctx['list'] += [l:loc] + endif + endfor + endif + + if a:ctx['counter'] == 0 + if empty(a:ctx['list']) + call lsp#utils#error('No ' . a:type .' found') + else + call lsp#utils#tagstack#_update() + if get(a:ctx, 'add_tree', v:false) + let l:qf = getqflist({'idx' : 0, 'items': []}) + let l:pos = l:qf.idx + let l:parent = l:qf.items + let l:level = count(l:parent[l:pos-1].text, g:lsp_tree_incoming_prefix) + let a:ctx['list'] = extend(l:parent, map(a:ctx['list'], 'extend(v:val, {"text": repeat("' . g:lsp_tree_incoming_prefix . '", l:level+1) . v:val.text})'), l:pos) + endif + call lsp#ui#vim#utils#setqflist(a:ctx['list'], a:type) + echo 'Retrieved ' . a:type + botright copen + if get(a:ctx, 'add_tree', v:false) + " move the cursor to the newly added item + execute l:pos + 1 + endif + endif + endif +endfunction + +function! s:hierarchy_item_to_vim(item, server) abort + let l:uri = a:item['uri'] + if !lsp#utils#is_file_uri(l:uri) + return v:null + endif + + let l:path = lsp#utils#uri_to_path(l:uri) + let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, a:item['range']['start']) + let l:text = '[' . lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, a:item['kind']) . '] ' . a:item['name'] + if has_key(a:item, 'detail') + let l:text .= ": " . a:item['detail'] + endif + + return { + \ 'filename': l:path, + \ 'lnum': l:line, + \ 'col': l:col, + \ 'text': l:text, + \ } +endfunction diff --git a/autoload/lsp/ui/vim/code_action.vim b/autoload/lsp/ui/vim/code_action.vim new file mode 100644 index 00000000..16073dac --- /dev/null +++ b/autoload/lsp/ui/vim/code_action.vim @@ -0,0 +1,207 @@ +" vint: -ProhibitUnusedVariable + +function! lsp#ui#vim#code_action#complete(input, command, len) abort + let l:server_names = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)') + let l:kinds = [] + for l:server_name in l:server_names + let l:kinds += lsp#capabilities#get_code_action_kinds(l:server_name) + endfor + return filter(copy(l:kinds), { _, kind -> kind =~ '^' . a:input }) +endfunction + +" +" @param option = { +" selection: v:true | v:false = Provide by CommandLine like `:'<,'>LspCodeAction` +" sync: v:true | v:false = Specify enable synchronous request. Example use case is `BufWritePre` +" query: string = Specify code action kind query. If query provided and then filtered code action is only one, invoke code action immediately. +" ui: 'float' | 'preview' +" } +" +function! lsp#ui#vim#code_action#do(option) abort + let l:selection = get(a:option, 'selection', v:false) + let l:sync = get(a:option, 'sync', v:false) + let l:query = get(a:option, 'query', '') + let l:ui = get(a:option, 'ui', g:lsp_code_action_ui) + if empty(l:ui) + let l:ui = lsp#utils#_has_popup_menu() ? 'float' : 'preview' + endif + + let l:server_names = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_action_provider(v:val)') + if len(l:server_names) == 0 + return lsp#utils#error('Code action not supported for ' . &filetype) + endif + + if l:selection + let l:range = lsp#utils#range#_get_recent_visual_range() + else + let l:range = lsp#utils#range#_get_current_line_range() + endif + + let l:ctx = { + \ 'count': len(l:server_names), + \ 'results': [], + \} + let l:bufnr = bufnr('%') + let l:command_id = lsp#_new_command() + for l:server_name in l:server_names + let l:diagnostic = lsp#internal#diagnostics#under_cursor#get_diagnostic({'server': l:server_name}) + call lsp#send_request(l:server_name, { + \ 'method': 'textDocument/codeAction', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'range': empty(l:diagnostic) || l:selection ? l:range : l:diagnostic['range'], + \ 'context': { + \ 'diagnostics' : empty(l:diagnostic) ? [] : [l:diagnostic], + \ 'only': ['', 'quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source', 'source.organizeImports'], + \ }, + \ }, + \ 'sync': l:sync, + \ 'on_notification': function('s:handle_code_action', [l:ui, l:ctx, l:server_name, l:command_id, l:sync, l:query, l:bufnr]), + \ }) + endfor + echo 'Retrieving code actions ...' +endfunction + +function! s:handle_code_action(ui, ctx, server_name, command_id, sync, query, bufnr, data) abort + " Ignore old request. + if a:command_id != lsp#_last_command() + return + endif + + call add(a:ctx['results'], { + \ 'server_name': a:server_name, + \ 'data': a:data, + \}) + let a:ctx['count'] -= 1 + if a:ctx['count'] ># 0 + return + endif + + let l:total_code_actions = [] + for l:result in a:ctx['results'] + let l:server_name = l:result['server_name'] + let l:data = l:result['data'] + " Check response error. + if lsp#client#is_error(l:data['response']) + call lsp#utils#error('Failed to CodeAction for ' . l:server_name . ': ' . lsp#client#error_message(l:data['response'])) + continue + endif + + " Check code actions. + let l:code_actions = l:data['response']['result'] + + " Filter code actions. + if !empty(a:query) + let l:code_actions = filter(l:code_actions, { _, action -> get(action, 'kind', '') =~# '^' . a:query }) + endif + if empty(l:code_actions) + continue + endif + + for l:code_action in l:code_actions + let l:item = { + \ 'server_name': l:server_name, + \ 'code_action': l:code_action, + \ } + if get(l:code_action, 'isPreferred', v:false) + let l:total_code_actions = [l:item] + l:total_code_actions + else + call add(l:total_code_actions, l:item) + endif + endfor + endfor + + if len(l:total_code_actions) == 0 + echo 'No code actions found' + return + endif + call lsp#log('s:handle_code_action', l:total_code_actions) + + if len(l:total_code_actions) == 1 && !empty(a:query) + let l:action = l:total_code_actions[0] + if s:handle_disabled_action(l:action) | return | endif + " Clear 'Retrieving code actions ...' message + echo '' + call s:handle_one_code_action(l:action['server_name'], a:sync, a:bufnr, l:action['code_action']) + return + endif + + " Prompt to choose code actions when empty query provided. + let l:items = [] + for l:action in l:total_code_actions + let l:title = printf('[%s] %s', l:action['server_name'], l:action['code_action']['title']) + if has_key(l:action['code_action'], 'kind') && l:action['code_action']['kind'] !=# '' + let l:title .= ' (' . l:action['code_action']['kind'] . ')' + endif + call add(l:items, { 'title': l:title, 'item': l:action }) + endfor + + if lsp#utils#_has_popup_menu() && a:ui ==? 'float' + call lsp#internal#ui#popupmenu#open({ + \ 'title': 'Code actions', + \ 'items': mapnew(l:items, { idx, item -> item.title}), + \ 'pos': 'topleft', + \ 'line': 'cursor+1', + \ 'col': 'cursor', + \ 'callback': funcref('s:popup_accept_code_action', [a:sync, a:bufnr, l:items]), + \ }) + else + call lsp#internal#ui#quickpick#open({ + \ 'items': l:items, + \ 'key': 'title', + \ 'on_accept': funcref('s:quickpick_accept_code_action', [a:sync, a:bufnr]), + \ }) + endif +endfunction + +function! s:popup_accept_code_action(sync, bufnr, items, id, selected, ...) abort + if a:selected <= 0 | return | endif + let l:item = a:items[a:selected - 1]['item'] + if s:handle_disabled_action(l:item) | return | endif + call s:handle_one_code_action(l:item['server_name'], a:sync, a:bufnr, l:item['code_action']) + execute('doautocmd User lsp_float_closed') +endfunction + +function! s:quickpick_accept_code_action(sync, bufnr, data, ...) abort + call lsp#internal#ui#quickpick#close() + if empty(a:data['items']) | return | endif + let l:selected = a:data['items'][0]['item'] + if s:handle_disabled_action(l:selected) | return | endif + call s:handle_one_code_action(l:selected['server_name'], a:sync, a:bufnr, l:selected['code_action']) +endfunction + +function! s:handle_disabled_action(code_action) abort + if has_key(a:code_action, 'disabled') + echo 'This action is disabled: ' . a:code_action['disabled']['reason'] + return v:true + endif + return v:false +endfunction + +function! s:handle_one_code_action(server_name, sync, bufnr, command_or_code_action) abort + " has WorkspaceEdit. + if has_key(a:command_or_code_action, 'edit') + call lsp#utils#workspace_edit#apply_workspace_edit(a:command_or_code_action['edit']) + endif + + " Command. + if has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type('') + call lsp#ui#vim#execute_command#_execute({ + \ 'server_name': a:server_name, + \ 'command_name': get(a:command_or_code_action, 'command', ''), + \ 'command_args': get(a:command_or_code_action, 'arguments', v:null), + \ 'sync': a:sync, + \ 'bufnr': a:bufnr, + \ }) + + " has Command. + elseif has_key(a:command_or_code_action, 'command') && type(a:command_or_code_action['command']) == type({}) + call lsp#ui#vim#execute_command#_execute({ + \ 'server_name': a:server_name, + \ 'command_name': get(a:command_or_code_action['command'], 'command', ''), + \ 'command_args': get(a:command_or_code_action['command'], 'arguments', v:null), + \ 'sync': a:sync, + \ 'bufnr': a:bufnr, + \ }) + endif +endfunction diff --git a/autoload/lsp/ui/vim/code_lens.vim b/autoload/lsp/ui/vim/code_lens.vim new file mode 100644 index 00000000..b080b8d6 --- /dev/null +++ b/autoload/lsp/ui/vim/code_lens.vim @@ -0,0 +1,137 @@ +" https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens + +" @param option = { +" } +" +function! lsp#ui#vim#code_lens#do(option) abort + let l:sync = get(a:option, 'sync', v:false) + + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_code_lens_provider(v:val)') + if len(l:servers) == 0 + return lsp#utils#error('Code lens not supported for ' . &filetype) + endif + + redraw | echo 'Retrieving codelens ...' + + let l:bufnr = bufnr('%') + + call lsp#callbag#pipe( + \ lsp#callbag#fromList(l:servers), + \ lsp#callbag#flatMap({server-> + \ lsp#callbag#pipe( + \ lsp#request(server, { + \ 'method': 'textDocument/codeLens', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(l:bufnr), + \ }, + \ }), + \ lsp#callbag#map({x->x['response']['result']}), + \ lsp#callbag#filter({codelenses->!empty(codelenses)}), + \ lsp#callbag#flatMap({codelenses-> + \ lsp#callbag#pipe( + \ lsp#callbag#fromList(codelenses), + \ lsp#callbag#flatMap({codelens-> + \ has_key(codelens, 'command') ? lsp#callbag#of(codelens) : s:resolve_codelens(server, codelens)}), + \ ) + \ }), + \ lsp#callbag#map({codelens->{ 'server': server, 'codelens': codelens }}), + \ ) + \ }), + \ lsp#callbag#reduce({acc,curr->add(acc, curr)}, []), + \ lsp#callbag#flatMap({x->s:chooseCodeLens(x, l:bufnr)}), + \ lsp#callbag#tap({x-> lsp#ui#vim#execute_command#_execute({ + \ 'server_name': x['server'], + \ 'command_name': get(x['codelens']['command'], 'command', ''), + \ 'command_args': get(x['codelens']['command'], 'arguments', v:null), + \ 'bufnr': l:bufnr, + \ })}), + \ lsp#callbag#takeUntil(lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x->has_key(x, 'command')}), + \ )), + \ lsp#callbag#subscribe({ + \ 'error': {e->lsp#utils#error('Error running codelens ' . json_encode(e))}, + \ }), + \ ) +endfunction + +function! s:resolve_codelens(server, codelens) abort + " TODO: return callbag#lsp#empty() if codelens resolve not supported by server + return lsp#callbag#pipe( + \ lsp#request(a:server, { + \ 'method': 'codeLens/resolve', + \ 'params': a:codelens + \ }), + \ lsp#callbag#map({x->x['response']['result']}), + \ ) +endfunction + +function! s:chooseCodeLens(items, bufnr) abort + redraw | echo 'Select codelens:' + if empty(a:items) + return lsp#callbag#throwError('No codelens found') + endif + return lsp#callbag#create(function('s:quickpick_open', [a:items, a:bufnr])) +endfunction + +function! lsp#ui#vim#code_lens#_get_subtitle(item) abort + " Since element of arguments property of Command interface is defined as any in LSP spec, it is + " up to the language server implementation. + " Currently this only impacts rust-analyzer. See #1118 for more details. + + if !has_key(a:item['codelens']['command'], 'arguments') + return '' + endif + + let l:arguments = a:item['codelens']['command']['arguments'] + for l:argument in l:arguments + if type(l:argument) != type({}) || !has_key(l:argument, 'label') + return '' + endif + endfor + + return ': ' . join(map(copy(l:arguments), 'v:val["label"]'), ' > ') +endfunction + +function! s:quickpick_open(items, bufnr, next, error, complete) abort + if empty(a:items) + return lsp#callbag#empty() + endif + + let l:items = [] + for l:item in a:items + let l:title = printf("[%s] %s%s\t| L%s:%s", + \ l:item['server'], + \ l:item['codelens']['command']['title'], + \ lsp#ui#vim#code_lens#_get_subtitle(l:item), + \ lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['codelens']['range']['start']), + \ getbufline(a:bufnr, lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['codelens']['range']['start']))[0]) + call add(l:items, { 'title': l:title, 'item': l:item }) + endfor + + call lsp#internal#ui#quickpick#open({ + \ 'items': l:items, + \ 'key': 'title', + \ 'on_accept': function('s:quickpick_accept', [a:next, a:error, a:complete]), + \ 'on_cancel': function('s:quickpick_cancel', [a:next, a:error, a:complete]), + \ }) + + return function('s:quickpick_dispose') +endfunction + +function! s:quickpick_dispose() abort + call lsp#internal#ui#quickpick#close() +endfunction + +function! s:quickpick_accept(next, error, complete, data, ...) abort + call lsp#internal#ui#quickpick#close() + let l:items = a:data['items'] + if len(l:items) > 0 + call a:next(l:items[0]['item']) + endif + call a:complete() +endfunction + +function! s:quickpick_cancel(next, error, complete, ...) abort + call a:complete() +endfunction diff --git a/autoload/lsp/ui/vim/completion.vim b/autoload/lsp/ui/vim/completion.vim new file mode 100644 index 00000000..5989ba0e --- /dev/null +++ b/autoload/lsp/ui/vim/completion.vim @@ -0,0 +1,299 @@ +" vint: -ProhibitUnusedVariable +" +let s:context = {} + +function! lsp#ui#vim#completion#_setup() abort + augroup lsp_ui_vim_completion + autocmd! + autocmd CompleteDone * call s:on_complete_done() + augroup END +endfunction + +function! lsp#ui#vim#completion#_disable() abort + augroup lsp_ui_vim_completion + autocmd! + augroup END +endfunction + +" +" After CompleteDone, v:complete_item's word has been inserted into the line. +" Yet not inserted commit characters. +" +" below example uses | as cursor position. +" +" 1. `call getbuf|` +" 2. select `getbufline` item. +" 3. Insert commit characters. e.g. `(` +" 4. fire CompleteDone, then the line is `call getbufline|` +" 5. call feedkeys to call `s:on_complete_done_after` +" 6. then the line is `call getbufline(|` in `s:on_complete_done_after` +" +function! s:on_complete_done() abort + " Sometimes, vim occurs `CompleteDone` unexpectedly. + " We try to detect it by checking empty completed_item. + if empty(v:completed_item) || get(v:completed_item, 'word', '') ==# '' && get(v:completed_item, 'abbr', '') ==# '' + doautocmd User lsp_complete_done + return + endif + + " Try to get managed user_data. + let l:managed_user_data = lsp#omni#get_managed_user_data_from_completed_item(v:completed_item) + + " Clear managed user_data. + call lsp#omni#_clear_managed_user_data_map() + + " If managed user_data does not exists, skip it. + if empty(l:managed_user_data) + doautocmd User lsp_complete_done + return + endif + + let s:context['done_line'] = getline('.') + let s:context['done_line_nr'] = line('.') + let s:context['done_position'] = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2]) + let s:context['complete_position'] = l:managed_user_data['complete_position'] + let s:context['server_name'] = l:managed_user_data['server_name'] + let s:context['completion_item'] = l:managed_user_data['completion_item'] + let s:context['start_character'] = l:managed_user_data['start_character'] + let s:context['complete_word'] = l:managed_user_data['complete_word'] + call feedkeys(printf("\=%d_on_complete_done_after()\", s:SID()), 'n') +endfunction + +" +" Apply textEdit or insertText(snippet) and additionalTextEdits. +" +function! s:on_complete_done_after() abort + " Clear message line. feedkeys above leave garbage on message line. + echo '' + + " Ignore process if the mode() is not insert-mode after feedkeys. + if mode(1)[0] !=# 'i' + return '' + endif + + let l:done_line = s:context['done_line'] + let l:done_line_nr = s:context['done_line_nr'] + let l:done_position = s:context['done_position'] + let l:complete_position = s:context['complete_position'] + let l:server_name = s:context['server_name'] + let l:completion_item = s:context['completion_item'] + let l:start_character = s:context['start_character'] + let l:complete_word = s:context['complete_word'] + + " check the commit characters are or . + if line('.') ==# l:done_line_nr && strlen(getline('.')) < strlen(l:done_line) + doautocmd User lsp_complete_done + return '' + endif + + " Do nothing if text_edit is disabled. + if !g:lsp_text_edit_enabled + doautocmd User lsp_complete_done + return '' + endif + + let l:completion_item = s:resolve_completion_item(l:completion_item, l:server_name) + + " clear completed string if need. + let l:is_expandable = s:is_expandable(l:done_line, l:done_position, l:complete_position, l:completion_item, l:complete_word) + if l:is_expandable + call s:clear_auto_inserted_text(l:done_line, l:done_position, l:complete_position) + endif + + " apply additionalTextEdits. + if has_key(l:completion_item, 'additionalTextEdits') && !empty(l:completion_item['additionalTextEdits']) + call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), l:completion_item['additionalTextEdits']) + endif + + " snippet or textEdit. + if l:is_expandable + " At this timing, the cursor may have been moved by additionalTextEdit, so we use overflow information instead of textEdit itself. + if type(get(l:completion_item, 'textEdit', v:null)) == type({}) + let l:range = lsp#utils#text_edit#get_range(l:completion_item['textEdit']) + let l:overflow_before = max([0, l:start_character - l:range['start']['character']]) + let l:overflow_after = max([0, l:range['end']['character'] - l:complete_position['character']]) + let l:text = l:completion_item['textEdit']['newText'] + else + let l:overflow_before = 0 + let l:overflow_after = 0 + let l:text = s:get_completion_text(l:completion_item) + endif + + " apply snipept or text_edit + let l:position = lsp#utils#position#vim_to_lsp('%', getpos('.')[1 : 2]) + let l:range = { + \ 'start': { + \ 'line': l:position['line'], + \ 'character': l:position['character'] - (l:complete_position['character'] - l:start_character) - l:overflow_before, + \ }, + \ 'end': { + \ 'line': l:position['line'], + \ 'character': l:position['character'] + l:overflow_after, + \ } + \ } + + if get(l:completion_item, 'insertTextFormat', 1) == 2 + " insert Snippet. + call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': '' }]) + call cursor(lsp#utils#position#lsp_to_vim('%', l:range['start'])) + if exists('g:lsp_snippet_expand') && len(g:lsp_snippet_expand) > 0 + call g:lsp_snippet_expand[0]({ 'snippet': l:text }) + else + call s:simple_expand_text(l:text) + endif + else + " apply TextEdit. + call lsp#utils#text_edit#apply_text_edits('%', [{ 'range': l:range, 'newText': l:text }]) + + " The VSCode always apply completion word as snippet. + " It means we should place cursor to end of new inserted text as snippet does. + let l:lines = lsp#utils#_split_by_eol(l:text) + let l:start = l:range.start + let l:start.line += len(l:lines) - 1 + let l:start.character += strchars(l:lines[-1]) + call cursor(lsp#utils#position#lsp_to_vim('%', l:start)) + endif + endif + + doautocmd User lsp_complete_done + return '' +endfunction + +" +" is_expandable +" +function! s:is_expandable(done_line, done_position, complete_position, completion_item, complete_word) abort + if get(a:completion_item, 'textEdit', v:null) isnot# v:null + let l:range = lsp#utils#text_edit#get_range(a:completion_item['textEdit']) + if l:range['start']['line'] != l:range['end']['line'] + return v:true + endif + + " compute if textEdit will change text. + let l:completed_before = strcharpart(a:done_line, 0, a:complete_position['character']) + let l:completed_after = strcharpart(a:done_line, a:done_position['character'], strchars(a:done_line) - a:done_position['character']) + let l:completed_line = l:completed_before . l:completed_after + let l:text_edit_before = strcharpart(l:completed_line, 0, l:range['start']['character']) + let l:text_edit_after = strcharpart(l:completed_line, l:range['end']['character'], strchars(l:completed_line) - l:range['end']['character']) + return a:done_line !=# l:text_edit_before . s:trim_unmeaning_tabstop(a:completion_item['textEdit']['newText']) . l:text_edit_after + endif + return s:get_completion_text(a:completion_item) !=# s:trim_unmeaning_tabstop(a:complete_word) +endfunction + +" +" trim_unmeaning_tabstop +" +function! s:trim_unmeaning_tabstop(text) abort + return substitute(a:text, '\%(\$0\|\${0}\)$', '', 'g') +endfunction + +" +" Try `completionItem/resolve` if it possible. +" +function! s:resolve_completion_item(completion_item, server_name) abort + " server_name is not provided. + if empty(a:server_name) + return a:completion_item + endif + + " check server capabilities. + if !lsp#capabilities#has_completion_resolve_provider(a:server_name) + return a:completion_item + endif + + let l:ctx = {} + let l:ctx['response'] = {} + function! l:ctx['callback'](data) abort + let l:self['response'] = a:data['response'] + endfunction + + try + call lsp#send_request(a:server_name, { + \ 'method': 'completionItem/resolve', + \ 'params': a:completion_item, + \ 'sync': 1, + \ 'sync_timeout': g:lsp_completion_resolve_timeout, + \ 'on_notification': function(l:ctx['callback'], [], l:ctx) + \ }) + catch /.*/ + call lsp#log('s:resolve_completion_item', 'request timeout.') + endtry + + if empty(l:ctx['response']) + return a:completion_item + endif + + if lsp#client#is_error(l:ctx['response']) + return a:completion_item + endif + + if empty(l:ctx['response']['result']) + return a:completion_item + endif + + return l:ctx['response']['result'] +endfunction + +" +" Remove additional inserted text +" +" LSP server knows only `complete_position` so we should remove inserted text until complete_position. +" +function! s:clear_auto_inserted_text(done_line, done_position, complete_position) abort + let l:before = strcharpart(a:done_line, 0, a:complete_position['character']) + let l:after = strcharpart(a:done_line, a:done_position['character'], (strchars(a:done_line) - a:done_position['character'])) + call setline(a:done_position['line'] + 1, l:before . l:after) + call cursor([a:done_position['line'] + 1, strlen(l:before) + 1]) +endfunction + +" +" Expand text +" +function! s:simple_expand_text(text) abort + let l:pos = { + \ 'line': line('.') - 1, + \ 'character': lsp#utils#to_char('%', line('.'), col('.')) + \ } + + " Remove placeholders and get first placeholder position that use to cursor position. + " e.g. `|getbufline(${1:expr}, ${2:lnum})${0}` to getbufline(|,) + let l:text = substitute(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)', '\=substitute(submatch(1), "\\", "", "g")', 'g') + let l:offset = match(a:text, '\$\%({[0-9]\+\%(:\(\\.\|[^}]\+\)*\)}\|[0-9]\+\)') + if l:offset == -1 + let l:offset = strchars(l:text) + endif + + call lsp#utils#text_edit#apply_text_edits(lsp#utils#get_buffer_uri(bufnr('%')), [{ + \ 'range': { + \ 'start': l:pos, + \ 'end': l:pos + \ }, + \ 'newText': l:text + \ }]) + + let l:pos = lsp#utils#position#lsp_to_vim('%', { + \ 'line': l:pos['line'], + \ 'character': l:pos['character'] + l:offset + \ }) + call cursor(l:pos) +endfunction + +" +" Get completion text from CompletionItem. Fallback to label when insertText +" is falsy +" +function! s:get_completion_text(completion_item) abort + let l:text = get(a:completion_item, 'insertText', '') + if empty(l:text) + let l:text = a:completion_item['label'] + endif + return l:text +endfunction + +" +" Get script id that uses to call `s:` function in feedkeys. +" +function! s:SID() abort + return matchstr(expand(''), '\zs\d\+\ze_SID$') +endfunction + diff --git a/autoload/lsp/ui/vim/execute_command.vim b/autoload/lsp/ui/vim/execute_command.vim new file mode 100644 index 00000000..107b94c6 --- /dev/null +++ b/autoload/lsp/ui/vim/execute_command.vim @@ -0,0 +1,64 @@ +let s:commands = {} + +" +" @param {name} = string +" @param {callback} = funcref +" +function! lsp#ui#vim#execute_command#_register(command_name, callback) abort + if has_key(s:commands, a:command_name) + throw printf('lsp#ui#vim#execute_command#_register_command: %s already registered.', a:command_name) + endif + + let s:commands[a:command_name] = a:callback +endfunction + +" +" TODO: This method does not handle any return value. +" +function! lsp#ui#vim#execute_command#_execute(params) abort + let l:command_name = a:params['command_name'] + let l:command_args = get(a:params, 'command_args', v:null) + let l:server_name = get(a:params, 'server_name', '') + let l:bufnr = get(a:params, 'bufnr', -1) + let l:sync = get(a:params, 'sync', v:false) + + " create command. + let l:command = { 'command': l:command_name } + if l:command_args isnot v:null + let l:command['arguments'] = l:command_args + endif + + " execute command on local. + if has_key(s:commands, l:command_name) + try + call s:commands[l:command_name]({ + \ 'bufnr': l:bufnr, + \ 'server_name': l:server_name, + \ 'command': l:command, + \ }) + catch /.*/ + call lsp#utils#error(printf('Execute command failed: %s', string(a:params))) + endtry + return + endif + + " execute command on server. + if !empty(l:server_name) + call lsp#send_request(l:server_name, { + \ 'method': 'workspace/executeCommand', + \ 'params': l:command, + \ 'sync': l:sync, + \ 'on_notification': function('s:handle_execute_command', [l:server_name, l:command]), + \ }) + endif +endfunction + +" +" handle workspace/executeCommand response +" +function! s:handle_execute_command(server_name, command, data) abort + if lsp#client#is_error(a:data['response']) + call lsp#utils#error('Execute command failed on ' . a:server_name . ': ' . string(a:command) . ' -> ' . string(a:data)) + endif +endfunction + diff --git a/autoload/lsp/ui/vim/folding.vim b/autoload/lsp/ui/vim/folding.vim new file mode 100644 index 00000000..b8204f5d --- /dev/null +++ b/autoload/lsp/ui/vim/folding.vim @@ -0,0 +1,196 @@ +let s:folding_ranges = {} +let s:textprop_name = 'vim-lsp-folding-linenr' + +" imports +let s:Buffer = vital#lsp#import('VS.Vim.Buffer') + +function! s:find_servers() abort + return filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_folding_range_provider(v:val)') +endfunction + +function! lsp#ui#vim#folding#fold(sync) abort + let l:servers = s:find_servers() + + if len(l:servers) == 0 + call lsp#utils#error('Folding not supported for ' . &filetype) + return + endif + + let l:server = l:servers[0] + call lsp#ui#vim#folding#send_request(l:server, bufnr('%'), a:sync) +endfunction + +function! s:set_textprops(buf) abort + " Use zero-width text properties to act as a sort of "mark" in the buffer. + " This is used to remember the line numbers at the time the request was + " sent. We will let Vim handle updating the line numbers when the user + " inserts or deletes text. + + " Skip if the buffer doesn't exist. This might happen when a buffer is + " opened and quickly deleted. + if !bufloaded(a:buf) | return | endif + + " Create text property, if not already defined + silent! call prop_type_add(s:textprop_name, {'bufnr': a:buf, 'priority': lsp#internal#textprop#priority('folding')}) + + let l:line_count = s:Buffer.get_line_count(a:buf) + + " First, clear all markers from the previous run + call prop_remove({'type': s:textprop_name, 'bufnr': a:buf}, 1, l:line_count) + + " Add markers to each line + let l:i = 1 + while l:i <= l:line_count + call prop_add(l:i, 1, {'bufnr': a:buf, 'type': s:textprop_name, 'id': l:i}) + let l:i += 1 + endwhile +endfunction + +function! lsp#ui#vim#folding#send_request(server_name, buf, sync) abort + if !lsp#capabilities#has_folding_range_provider(a:server_name) + return + endif + + if !g:lsp_fold_enabled + call lsp#log('Skip sending fold request: folding was disabled explicitly') + return + endif + + if has('textprop') + call s:set_textprops(a:buf) + endif + + call lsp#send_request(a:server_name, { + \ 'method': 'textDocument/foldingRange', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(a:buf) + \ }, + \ 'on_notification': function('s:handle_fold_request', [a:server_name]), + \ 'sync': a:sync, + \ 'bufnr': a:buf + \ }) +endfunction + +function! s:foldexpr(server, buf, linenr) abort + let l:foldlevel = 0 + let l:prefix = '' + + for l:folding_range in s:folding_ranges[a:server][a:buf] + if type(l:folding_range) == type({}) && + \ has_key(l:folding_range, 'startLine') && + \ has_key(l:folding_range, 'endLine') + let l:start = l:folding_range['startLine'] + 1 + let l:end = l:folding_range['endLine'] + 1 + + if (l:start <= a:linenr) && (a:linenr <= l:end) + let l:foldlevel += 1 + endif + + if l:start == a:linenr + let l:prefix = '>' + elseif l:end == a:linenr + let l:prefix = '<' + endif + endif + endfor + + " Only return marker if a fold starts/ends at this line. + " Otherwise, return '='. + return (l:prefix ==# '') ? '=' : (l:prefix . l:foldlevel) +endfunction + +" Searches for text property of the correct type on the given line. +" Returns the original linenr on success, or -1 if no textprop of the correct +" type is associated with this line. +function! s:get_textprop_line(linenr) abort + let l:props = filter(prop_list(a:linenr), {idx, prop -> prop['type'] ==# s:textprop_name}) + + if empty(l:props) + return -1 + else + return l:props[0]['id'] + endif +endfunction + +function! lsp#ui#vim#folding#foldexpr() abort + let l:servers = s:find_servers() + + if len(l:servers) == 0 + return + endif + + let l:server = l:servers[0] + + if has('textprop') + " Does the current line have a textprop with original line info? + let l:textprop_line = s:get_textprop_line(v:lnum) + + if l:textprop_line == -1 + " No information for current line available, so use indent for + " previous line. + return '=' + else + " Info available, use foldexpr as it would be with original line + " number + return s:foldexpr(l:server, bufnr('%'), l:textprop_line) + endif + else + return s:foldexpr(l:server, bufnr('%'), v:lnum) + endif +endfunction + +function! lsp#ui#vim#folding#foldtext() abort + let l:num_lines = v:foldend - v:foldstart + 1 + let l:summary = getline(v:foldstart) . '...' + + " Join all lines in the fold + let l:combined_lines = '' + let l:i = v:foldstart + while l:i <= v:foldend + let l:combined_lines .= getline(l:i) . ' ' + let l:i += 1 + endwhile + + " Check if we're in a comment + let l:comment_regex = '\V' . substitute(&l:commentstring, '%s', '\\.\\*', '') + if l:combined_lines =~? l:comment_regex + let l:summary = l:combined_lines + endif + + return l:summary . ' (' . l:num_lines . ' ' . (l:num_lines == 1 ? 'line' : 'lines') . ') ' +endfunction + +function! s:handle_fold_request(server, data) abort + if lsp#client#is_error(a:data) || !has_key(a:data, 'response') || !has_key(a:data['response'], 'result') + return + endif + + let l:result = a:data['response']['result'] + + if type(l:result) != type([]) + return + endif + + let l:uri = a:data['request']['params']['textDocument']['uri'] + let l:path = lsp#utils#uri_to_path(l:uri) + let l:bufnr = bufnr(l:path) + + if l:bufnr < 0 + return + endif + + if !has_key(s:folding_ranges, a:server) + let s:folding_ranges[a:server] = {} + endif + let s:folding_ranges[a:server][l:bufnr] = l:result + + " Set 'foldmethod' back to 'expr', which forces a re-evaluation of + " 'foldexpr'. Only do this if the user hasn't changed 'foldmethod', + " and this is the correct buffer. + for l:winid in win_findbuf(l:bufnr) + if getwinvar(l:winid, '&foldmethod') ==# 'expr' + call setwinvar(l:winid, '&foldmethod', 'expr') + endif + endfor +endfunction + diff --git a/autoload/lsp/ui/vim/output.vim b/autoload/lsp/ui/vim/output.vim new file mode 100644 index 00000000..9955f3be --- /dev/null +++ b/autoload/lsp/ui/vim/output.vim @@ -0,0 +1,452 @@ +let s:use_vim_popup = has('patch-8.1.1517') && g:lsp_preview_float && !has('nvim') +let s:use_nvim_float = exists('*nvim_open_win') && g:lsp_preview_float && has('nvim') +let s:use_preview = !s:use_vim_popup && !s:use_nvim_float + +function! s:import_modules() abort + if exists('s:Markdown') | return | endif + let s:Markdown = vital#lsp#import('VS.Vim.Syntax.Markdown') + let s:MarkupContent = vital#lsp#import('VS.LSP.MarkupContent') + let s:Window = vital#lsp#import('VS.Vim.Window') + let s:Text = vital#lsp#import('VS.LSP.Text') +endfunction + +let s:winid = v:false +let s:prevwin = v:false +let s:preview_data = v:false + +function! s:vim_popup_closed(...) abort + let s:preview_data = v:false +endfunction + +function! lsp#ui#vim#output#closepreview() abort + if win_getid() ==# s:winid + " Don't close if window got focus + return + endif + + if s:winid == v:false + return + endif + + "closing floats in vim8.1 must use popup_close() + "nvim must use nvim_win_close. pclose is not reliable and does not always work + if s:use_vim_popup && s:winid + call popup_close(s:winid) + elseif s:use_nvim_float && s:winid + silent! call nvim_win_close(s:winid, 0) + else + pclose + endif + let s:winid = v:false + let s:preview_data = v:false + augroup lsp_float_preview_close + augroup end + autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized * + doautocmd User lsp_float_closed +endfunction + +function! lsp#ui#vim#output#focuspreview() abort + if s:is_cmdwin() + return + endif + + " This does not work for vim8.1 popup but will work for nvim and old preview + if s:winid + if win_getid() !=# s:winid + let s:prevwin = win_getid() + call win_gotoid(s:winid) + elseif s:prevwin + " Temporarily disable hooks + " TODO: remove this when closing logic is able to distinguish different move directions + autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized * + call win_gotoid(s:prevwin) + call s:add_float_closing_hooks() + let s:prevwin = v:false + endif + endif +endfunction + +function! s:bufwidth() abort + let l:width = winwidth(0) + let l:numberwidth = max([&numberwidth, strlen(line('$'))+1]) + let l:numwidth = (&number || &relativenumber)? l:numberwidth : 0 + let l:foldwidth = &foldcolumn + + if &signcolumn ==? 'yes' + let l:signwidth = 2 + elseif &signcolumn ==? 'auto' + let l:signs = execute(printf('sign place buffer=%d', bufnr(''))) + let l:signs = split(l:signs, "\n") + let l:signwidth = len(l:signs)>2? 2: 0 + else + let l:signwidth = 0 + endif + return l:width - l:numwidth - l:foldwidth - l:signwidth +endfunction + + +function! s:get_float_positioning(height, width) abort + let l:height = a:height + let l:width = a:width + " TODO: add option to configure it 'docked' at the bottom/top/right + + " NOTE: screencol() and screenrow() start from (1,1) + " but the popup window co-ordinates start from (0,0) + " Very convenient! + " For a simple single-line 'tooltip', the following + " two lines are enough to determine the position + + let l:col = screencol() + let l:row = screenrow() + + let l:height = min([l:height, max([&lines - &cmdheight - l:row, &previewheight])]) + + let l:style = 'minimal' + let l:border = 'double' + " Positioning is not window but screen relative + let l:opts = { + \ 'relative': 'editor', + \ 'row': l:row, + \ 'col': l:col, + \ 'width': l:width, + \ 'height': l:height, + \ 'style': l:style, + \ 'border': l:border, + \ } + return l:opts +endfunction + +function! lsp#ui#vim#output#floatingpreview(data) abort + if s:use_nvim_float + let l:buf = nvim_create_buf(v:false, v:true) + + " Try to get as much space around the cursor, but at least 10x10 + let l:width = max([s:bufwidth(), 10]) + let l:height = max([&lines - winline() + 1, winline() - 1, 10]) + + if g:lsp_preview_max_height > 0 + let l:height = min([g:lsp_preview_max_height, l:height]) + endif + + let l:opts = s:get_float_positioning(l:height, l:width) + + let s:winid = nvim_open_win(l:buf, v:false, l:opts) + call nvim_win_set_option(s:winid, 'winhl', 'Normal:Pmenu,NormalNC:Pmenu') + call nvim_win_set_option(s:winid, 'foldenable', v:false) + call nvim_win_set_option(s:winid, 'wrap', v:true) + call nvim_win_set_option(s:winid, 'statusline', '') + call nvim_win_set_option(s:winid, 'number', v:false) + call nvim_win_set_option(s:winid, 'relativenumber', v:false) + call nvim_win_set_option(s:winid, 'cursorline', v:false) + call nvim_win_set_option(s:winid, 'cursorcolumn', v:false) + call nvim_win_set_option(s:winid, 'colorcolumn', '') + call nvim_win_set_option(s:winid, 'signcolumn', 'no') + " Enable closing the preview with esc, but map only in the scratch buffer + call nvim_buf_set_keymap(l:buf, 'n', '', ':pclose', {'silent': v:true}) + elseif s:use_vim_popup + let l:options = { + \ 'moved': 'any', + \ 'border': [1, 1, 1, 1], + \ 'callback': function('s:vim_popup_closed') + \ } + + if g:lsp_preview_max_width > 0 + let l:options['maxwidth'] = g:lsp_preview_max_width + endif + + if g:lsp_preview_max_height > 0 + let l:options['maxheight'] = g:lsp_preview_max_height + endif + + let s:winid = popup_atcursor('...', l:options) + endif + return s:winid +endfunction + +function! lsp#ui#vim#output#setcontent(winid, lines, ft) abort + if s:use_vim_popup + " vim popup + call setbufline(winbufnr(a:winid), 1, a:lines) + call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover') + elseif s:use_nvim_float + " nvim floating + call nvim_buf_set_lines(winbufnr(a:winid), 0, -1, v:false, a:lines) + call nvim_buf_set_option(winbufnr(a:winid), 'readonly', v:true) + call nvim_buf_set_option(winbufnr(a:winid), 'modifiable', v:false) + call nvim_buf_set_option(winbufnr(a:winid), 'filetype', a:ft.'.lsp-hover') + call nvim_win_set_cursor(a:winid, [1, 0]) + elseif s:use_preview + " preview window + call setbufline(winbufnr(a:winid), 1, a:lines) + call setbufvar(winbufnr(a:winid), '&filetype', a:ft . '.lsp-hover') + endif +endfunction + +function! lsp#ui#vim#output#adjust_float_placement(bufferlines, maxwidth) abort + if s:use_nvim_float + let l:win_config = {} + let l:height = min([winheight(s:winid), a:bufferlines]) + let l:width = min([winwidth(s:winid), a:maxwidth]) + let l:win_config = s:get_float_positioning(l:height, l:width) + call nvim_win_set_config(s:winid, l:win_config ) + endif +endfunction + +function! s:add_float_closing_hooks() abort + if g:lsp_preview_autoclose + augroup lsp_float_preview_close + autocmd! lsp_float_preview_close CursorMoved,CursorMovedI,VimResized * + autocmd CursorMoved,CursorMovedI,VimResized * call lsp#ui#vim#output#closepreview() + augroup END + endif +endfunction + +function! lsp#ui#vim#output#getpreviewwinid() abort + return s:winid +endfunction + +function! s:open_preview(data) abort + if s:use_vim_popup || s:use_nvim_float + let l:winid = lsp#ui#vim#output#floatingpreview(a:data) + else + execute &previewheight.'new' + let l:winid = win_getid() + endif + return l:winid +endfunction + +function! s:set_cursor(current_window_id, options) abort + if !has_key(a:options, 'cursor') + return + endif + + if s:use_nvim_float + " Neovim floats + " Go back to the preview window to set the cursor + call win_gotoid(s:winid) + let l:old_scrolloff = &scrolloff + let &scrolloff = 0 + + call nvim_win_set_cursor(s:winid, [a:options['cursor']['line'], a:options['cursor']['col']]) + call s:align_preview(a:options) + + " Finally, go back to the original window + call win_gotoid(a:current_window_id) + + let &scrolloff = l:old_scrolloff + elseif s:use_vim_popup + " Vim popups + function! AlignVimPopup(timer) closure abort + call s:align_preview(a:options) + endfunction + call timer_start(0, function('AlignVimPopup')) + else + " Preview + " Don't use 'scrolloff', it might mess up the cursor's position + let &l:scrolloff = 0 + call cursor(a:options['cursor']['line'], a:options['cursor']['col']) + call s:align_preview(a:options) + endif +endfunction + +function! s:align_preview(options) abort + if !has_key(a:options, 'cursor') || + \ !has_key(a:options['cursor'], 'align') + return + endif + + let l:align = a:options['cursor']['align'] + + if s:use_vim_popup + " Vim popups + let l:pos = popup_getpos(s:winid) + let l:below = winline() < winheight(0) / 2 + if l:below + let l:height = min([l:pos['core_height'], winheight(0) - winline() - 2]) + else + let l:height = min([l:pos['core_height'], winline() - 3]) + endif + let l:width = l:pos['core_width'] + + let l:options = { + \ 'minwidth': l:width, + \ 'maxwidth': l:width, + \ 'minheight': l:height, + \ 'maxheight': l:height, + \ 'pos': l:below ? 'topleft' : 'botleft', + \ 'line': l:below ? 'cursor+1' : 'cursor-1' + \ } + + if l:align ==? 'top' + let l:options['firstline'] = a:options['cursor']['line'] + elseif l:align ==? 'center' + let l:options['firstline'] = a:options['cursor']['line'] - (l:height - 1) / 2 + elseif l:align ==? 'bottom' + let l:options['firstline'] = a:options['cursor']['line'] - l:height + 1 + endif + + call popup_setoptions(s:winid, l:options) + redraw! + else + " Preview and Neovim floats + if l:align ==? 'top' + normal! zt + elseif l:align ==? 'center' + normal! zz + elseif l:align ==? 'bottom' + normal! zb + endif + endif +endfunction + +function! lsp#ui#vim#output#get_size_info(winid) abort + " Get size information while still having the buffer active + let l:buffer = winbufnr(a:winid) + let l:maxwidth = max(map(getbufline(l:buffer, 1, '$'), 'strdisplaywidth(v:val)')) + let l:bufferlines = 0 + if g:lsp_preview_max_width > 0 + let l:maxwidth = min([g:lsp_preview_max_width, l:maxwidth]) + + " Determine, for each line, how many "virtual" lines it spans, and add + " these together for all lines in the buffer + for l:line in getbufline(l:buffer, 1, '$') + let l:num_lines = str2nr(string(ceil(strdisplaywidth(l:line) * 1.0 / g:lsp_preview_max_width))) + let l:bufferlines += max([l:num_lines, 1]) + endfor + else + if s:use_vim_popup + let l:bufferlines = line('$', a:winid) + elseif s:use_nvim_float + let l:bufferlines = nvim_buf_line_count(winbufnr(a:winid)) + endif + endif + + return [l:bufferlines, l:maxwidth] +endfunction + +function! lsp#ui#vim#output#float_supported() abort + return s:use_vim_popup || s:use_nvim_float +endfunction + +function! lsp#ui#vim#output#preview(server, data, options) abort + if s:is_cmdwin() + return + endif + + if s:winid && type(s:preview_data) ==# type(a:data) + \ && s:preview_data ==# a:data + \ && type(g:lsp_preview_doubletap) ==# 3 + \ && len(g:lsp_preview_doubletap) >= 1 + \ && type(g:lsp_preview_doubletap[0]) ==# 2 + \ && index(['i', 's'], mode()[0]) ==# -1 + echo '' + return call(g:lsp_preview_doubletap[0], []) + endif + " Close any previously opened preview window + call lsp#ui#vim#output#closepreview() + + let l:current_window_id = win_getid() + + let s:winid = s:open_preview(a:data) + + let s:preview_data = a:data + let l:lines = [] + let l:syntax_lines = [] + let l:ft = lsp#ui#vim#output#append(a:data, l:lines, l:syntax_lines) + + if has_key(a:options, 'filetype') + let l:ft = a:options['filetype'] + endif + + let l:do_conceal = g:lsp_hover_conceal + let l:server_info = a:server !=# '' ? lsp#get_server_info(a:server) : {} + let l:config = get(l:server_info, 'config', {}) + let l:do_conceal = get(l:config, 'hover_conceal', l:do_conceal) + + call setbufvar(winbufnr(s:winid), 'lsp_syntax_highlights', l:syntax_lines) + call setbufvar(winbufnr(s:winid), 'lsp_do_conceal', l:do_conceal) + call lsp#ui#vim#output#setcontent(s:winid, l:lines, l:ft) + + let [l:bufferlines, l:maxwidth] = lsp#ui#vim#output#get_size_info(s:winid) + + if s:use_preview + " Set statusline + if has_key(a:options, 'statusline') + let &l:statusline = a:options['statusline'] + endif + + call s:set_cursor(l:current_window_id, a:options) + endif + + " Go to the previous window to adjust positioning + call win_gotoid(l:current_window_id) + + echo '' + + if s:winid && (s:use_vim_popup || s:use_nvim_float) + if s:use_nvim_float + " Neovim floats + call lsp#ui#vim#output#adjust_float_placement(l:bufferlines, l:maxwidth) + call s:set_cursor(l:current_window_id, a:options) + call s:add_float_closing_hooks() + elseif s:use_vim_popup + " Vim popups + call s:set_cursor(l:current_window_id, a:options) + endif + doautocmd User lsp_float_opened + endif + + if l:ft ==? 'markdown' + call s:import_modules() + call s:Window.do(s:winid, {->s:Markdown.apply()}) + endif + + if !g:lsp_preview_keep_focus + " set the focus to the preview window + call win_gotoid(s:winid) + endif + return '' +endfunction + +function! s:escape_string_for_display(str) abort + return substitute(substitute(a:str, '\r\n', '\n', 'g'), '\r', '\n', 'g') +endfunction + +function! lsp#ui#vim#output#append(data, lines, syntax_lines) abort + if type(a:data) == type([]) + for l:entry in a:data + call lsp#ui#vim#output#append(l:entry, a:lines, a:syntax_lines) + endfor + + return 'markdown' + elseif type(a:data) ==# type('') + call extend(a:lines, split(s:escape_string_for_display(a:data), "\n", v:true)) + return 'markdown' + elseif type(a:data) ==# type({}) && has_key(a:data, 'language') + let l:new_lines = split(s:escape_string_for_display(a:data.value), '\n') + + let l:i = 1 + while l:i <= len(l:new_lines) + call add(a:syntax_lines, { 'line': len(a:lines) + l:i, 'language': a:data.language }) + let l:i += 1 + endwhile + + call extend(a:lines, l:new_lines) + return 'markdown' + elseif type(a:data) ==# type({}) && has_key(a:data, 'kind') + if a:data.kind ==? 'markdown' + call s:import_modules() + let l:detail = s:MarkupContent.normalize(a:data.value, { + \ 'compact': !g:lsp_preview_fixup_conceal + \ }) + call extend(a:lines, s:Text.split_by_eol(l:detail)) + else + call extend(a:lines, split(s:escape_string_for_display(a:data.value), '\n', v:true)) + endif + return a:data.kind ==? 'plaintext' ? 'text' : a:data.kind + endif +endfunction + +function! s:is_cmdwin() abort + return getcmdwintype() !=# '' +endfunction diff --git a/autoload/lsp/ui/vim/signature_help.vim b/autoload/lsp/ui/vim/signature_help.vim new file mode 100644 index 00000000..26f6a6ae --- /dev/null +++ b/autoload/lsp/ui/vim/signature_help.vim @@ -0,0 +1,160 @@ +" vint: -ProhibitUnusedVariable +let s:debounce_timer_id = 0 + +function! s:not_supported(what) abort + return lsp#utils#error(a:what.' not supported for '.&filetype) +endfunction + +function! lsp#ui#vim#signature_help#get_signature_help_under_cursor() abort + let l:servers = filter(lsp#get_allowed_servers(), 'lsp#capabilities#has_signature_help_provider(v:val)') + + if len(l:servers) == 0 + call s:not_supported('Retrieving signature help') + return + endif + + let l:position = lsp#get_position() + for l:server in l:servers + call lsp#send_request(l:server, { + \ 'method': 'textDocument/signatureHelp', + \ 'params': { + \ 'textDocument': lsp#get_text_document_identifier(), + \ 'position': l:position, + \ }, + \ 'on_notification': function('s:handle_signature_help', [l:server]), + \ }) + endfor + + call lsp#log('Retrieving signature help') + return +endfunction + +function! s:handle_signature_help(server, data) abort + if lsp#client#is_error(a:data['response']) + call lsp#utils#error('Failed to retrieve signature help information for ' . a:server) + return + endif + + if !has_key(a:data['response'], 'result') + return + endif + + if !empty(a:data['response']['result']) && !empty(a:data['response']['result']['signatures']) + " Get current signature. + let l:signatures = get(a:data['response']['result'], 'signatures', []) + let l:signature_index = get(a:data['response']['result'], 'activeSignature', 0) + let l:signature = get(l:signatures, l:signature_index, {}) + if empty(l:signature) + return + endif + + " Signature label. + let l:label = l:signature['label'] + + " Mark current parameter. + if has_key(a:data['response']['result'], 'activeParameter') + let l:parameters = get(l:signature, 'parameters', []) + let l:parameter_index = a:data['response']['result']['activeParameter'] + let l:parameter = get(l:parameters, l:parameter_index, {}) + let l:parameter_label = s:get_parameter_label(l:signature, l:parameter) + if !empty(l:parameter_label) + let l:label = substitute(l:label, '\V\(' . escape(l:parameter_label, '\/?') . '\)', '`\1`', 'g') + endif + endif + + let l:contents = [l:label] + + if exists('l:parameter') + let l:parameter_doc = s:get_parameter_doc(l:parameter) + if !empty(l:parameter_doc) + call add(l:contents, '') + call add(l:contents, l:parameter_doc) + call add(l:contents, '') + endif + endif + + if has_key(l:signature, 'documentation') + call add(l:contents, l:signature['documentation']) + endif + + call lsp#ui#vim#output#preview(a:server, l:contents, {'statusline': ' LSP SignatureHelp'}) + return + else + " signature help is used while inserting. So this must be graceful. + "call lsp#utils#error('No signature help information found') + endif +endfunction + +function! s:get_parameter_label(signature, parameter) abort + if has_key(a:parameter, 'label') + if type(a:parameter['label']) == type([]) + let l:string_range = a:parameter['label'] + return strcharpart( + \ a:signature['label'], + \ l:string_range[0], + \ l:string_range[1] - l:string_range[0]) + endif + return a:parameter['label'] + endif + return '' +endfunction + +function! s:get_parameter_doc(parameter) abort + if !has_key(a:parameter, 'documentation') + return '' + endif + + let l:doc = copy(a:parameter['documentation']) + if type(l:doc) == type({}) + let l:doc['value'] = printf('***%s*** - %s', a:parameter['label'], l:doc['value']) + return l:doc + endif + return printf('***%s*** - %s', a:parameter['label'], l:doc) +endfunction + +function! s:on_cursor_moved() abort + let l:bufnr = bufnr('%') + call timer_stop(s:debounce_timer_id) + if g:lsp_signature_help_enabled + let s:debounce_timer_id = timer_start(g:lsp_signature_help_delay, function('s:on_text_changed_after', [l:bufnr]), { 'repeat': 1 }) + endif +endfunction + +function! s:on_text_changed_after(bufnr, timer) abort + if bufnr('%') != a:bufnr + return + endif + if index(['i', 's'], mode()[0]) == -1 + return + endif + if win_id2win(lsp#ui#vim#output#getpreviewwinid()) >= 1 + return + endif + + " Cache trigger chars since this loop is heavy + let l:chars = get(b:, 'lsp_signature_help_trigger_character', []) + if empty(l:chars) + for l:server_name in lsp#get_allowed_servers(a:bufnr) + let l:chars += lsp#capabilities#get_signature_help_trigger_characters(l:server_name) + endfor + let b:lsp_signature_help_trigger_character = l:chars + endif + + if index(l:chars, lsp#utils#_get_before_char_skip_white()) >= 0 + call lsp#ui#vim#signature_help#get_signature_help_under_cursor() + endif +endfunction + +function! lsp#ui#vim#signature_help#setup() abort + augroup _lsp_signature_help_ + autocmd! + autocmd CursorMoved,CursorMovedI * call s:on_cursor_moved() + augroup END +endfunction + +function! lsp#ui#vim#signature_help#_disable() abort + augroup _lsp_signature_help_ + autocmd! + augroup END +endfunction + diff --git a/autoload/lsp/ui/vim/utils.vim b/autoload/lsp/ui/vim/utils.vim new file mode 100644 index 00000000..126afd4d --- /dev/null +++ b/autoload/lsp/ui/vim/utils.vim @@ -0,0 +1,174 @@ +let s:default_symbol_kinds = { + \ '1': 'file', + \ '2': 'module', + \ '3': 'namespace', + \ '4': 'package', + \ '5': 'class', + \ '6': 'method', + \ '7': 'property', + \ '8': 'field', + \ '9': 'constructor', + \ '10': 'enum', + \ '11': 'interface', + \ '12': 'function', + \ '13': 'variable', + \ '14': 'constant', + \ '15': 'string', + \ '16': 'number', + \ '17': 'boolean', + \ '18': 'array', + \ '19': 'object', + \ '20': 'key', + \ '21': 'null', + \ '22': 'enum member', + \ '23': 'struct', + \ '24': 'event', + \ '25': 'operator', + \ '26': 'type parameter', + \ } + +let s:symbol_kinds = {} + +let s:diagnostic_severity = { + \ 1: 'Error', + \ 2: 'Warning', + \ 3: 'Information', + \ 4: 'Hint', + \ } + +function! s:symbols_to_loc_list_children(server, path, list, symbols, depth) abort + for l:symbol in a:symbols + let [l:line, l:col] = lsp#utils#position#lsp_to_vim(a:path, l:symbol['range']['start']) + + call add(a:list, { + \ 'filename': a:path, + \ 'lnum': l:line, + \ 'col': l:col, + \ 'text': lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) . ' : ' . printf('%' . a:depth. 's', ' ') . l:symbol['name'], + \ }) + if has_key(l:symbol, 'children') && !empty(l:symbol['children']) + call s:symbols_to_loc_list_children(a:server, a:path, a:list, l:symbol['children'], a:depth + 1) + endif + endfor +endfunction + +function! lsp#ui#vim#utils#symbols_to_loc_list(server, result) abort + if !has_key(a:result['response'], 'result') + return [] + endif + + let l:list = [] + + let l:locations = type(a:result['response']['result']) == type({}) ? [a:result['response']['result']] : a:result['response']['result'] + + if !empty(l:locations) " some servers also return null so check to make sure it isn't empty + for l:symbol in a:result['response']['result'] + if has_key(l:symbol, 'location') + let l:location = l:symbol['location'] + if lsp#utils#is_file_uri(l:location['uri']) + let l:path = lsp#utils#uri_to_path(l:location['uri']) + let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:location['range']['start']) + call add(l:list, { + \ 'filename': l:path, + \ 'lnum': l:line, + \ 'col': l:col, + \ 'text': lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) . ' : ' . (g:lsp_document_symbol_detail ? l:symbol['detail'] : l:symbol['name']), + \ }) + endif + else + let l:location = a:result['request']['params']['textDocument']['uri'] + if lsp#utils#is_file_uri(l:location) + let l:path = lsp#utils#uri_to_path(l:location) + let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:symbol['range']['start']) + call add(l:list, { + \ 'filename': l:path, + \ 'lnum': l:line, + \ 'col': l:col, + \ 'text': lsp#ui#vim#utils#_get_symbol_text_from_kind(a:server, l:symbol['kind']) . ' : ' . (g:lsp_document_symbol_detail ? l:symbol['detail'] : l:symbol['name']), + \ }) + if has_key(l:symbol, 'children') && !empty(l:symbol['children']) + call s:symbols_to_loc_list_children(a:server, l:path, l:list, l:symbol['children'], 1) + endif + endif + endif + endfor + endif + + return l:list +endfunction + +function! lsp#ui#vim#utils#diagnostics_to_loc_list(result) abort + if !has_key(a:result['response'], 'params') + return + endif + + let l:uri = a:result['response']['params']['uri'] + let l:diagnostics = lsp#utils#iteratable(a:result['response']['params']['diagnostics']) + + let l:list = [] + + if !empty(l:diagnostics) && lsp#utils#is_file_uri(l:uri) + let l:path = lsp#utils#uri_to_path(l:uri) + for l:item in l:diagnostics + let l:severity_text = '' + if has_key(l:item, 'severity') && !empty(l:item['severity']) + let l:severity_text = s:get_diagnostic_severity_text(l:item['severity']) + endif + let l:text = '' + if has_key(l:item, 'source') && !empty(l:item['source']) + let l:text .= l:item['source'] . ':' + endif + if l:severity_text !=# '' + let l:text .= l:severity_text . ':' + endif + if has_key(l:item, 'code') && !empty(l:item['code']) + let l:text .= l:item['code'] . ':' + endif + let l:text .= l:item['message'] + let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:item['range']['start']) + let l:location_item = { + \ 'filename': l:path, + \ 'lnum': l:line, + \ 'col': l:col, + \ 'text': l:text, + \ } + if l:severity_text !=# '' + " 'E' for error, 'W' for warning, 'I' for information, 'H' for hint + let l:location_item['type'] = l:severity_text[0] + endif + call add(l:list, l:location_item) + endfor + endif + + return l:list +endfunction + +function! lsp#ui#vim#utils#_get_symbol_text_from_kind(server, kind) abort + if !has_key(s:symbol_kinds, a:server) + let l:server_info = lsp#get_server_info(a:server) + if has_key (l:server_info, 'config') && has_key(l:server_info['config'], 'symbol_kinds') + let s:symbol_kinds[a:server] = extend(copy(s:default_symbol_kinds), l:server_info['config']['symbol_kinds']) + else + let s:symbol_kinds[a:server] = s:default_symbol_kinds + endif + endif + return get(s:symbol_kinds[a:server], a:kind, 'unknown symbol ' . a:kind) +endfunction + +function! lsp#ui#vim#utils#get_symbol_kinds() abort + return map(keys(s:default_symbol_kinds), {idx, key -> str2nr(key)}) +endfunction + +function! s:get_diagnostic_severity_text(severity) abort + return s:diagnostic_severity[a:severity] +endfunction + +function! lsp#ui#vim#utils#setqflist(list, type) abort + if has('patch-8.2.2147') + call setqflist(a:list) + call setqflist([], 'a', {'title': a:type}) + else + call setqflist([]) + call setqflist(a:list) + endif +endfunction diff --git a/autoload/lsp/utils.vim b/autoload/lsp/utils.vim new file mode 100644 index 00000000..c4609965 --- /dev/null +++ b/autoload/lsp/utils.vim @@ -0,0 +1,523 @@ +let s:has_lua = has('nvim-0.4.0') || (has('lua') && has('patch-8.2.0775')) +function! lsp#utils#has_lua() abort + return s:has_lua +endfunction + +let s:has_native_lsp_client = !has('nvim') && has('patch-8.2.4780') +function! lsp#utils#has_native_lsp_client() abort + return s:has_native_lsp_client +endfunction + +let s:has_virtual_text = exists('*nvim_buf_set_virtual_text') && exists('*nvim_create_namespace') +function! lsp#utils#_has_nvim_virtual_text() abort + return s:has_virtual_text +endfunction + +let s:has_signs = exists('*sign_define') && (has('nvim') || has('patch-8.1.0772')) +function! lsp#utils#_has_signs() abort + return s:has_signs +endfunction + +let s:has_nvim_buf_highlight = exists('*nvim_buf_add_highlight') && has('nvim') +function! lsp#utils#_has_nvim_buf_highlight() abort + return s:has_nvim_buf_highlight +endfunction + +" https://github.com/prabirshrestha/vim-lsp/issues/399#issuecomment-500585549 +let s:has_textprops = exists('*prop_add') && has('patch-8.1.1035') +function! lsp#utils#_has_textprops() abort + return s:has_textprops +endfunction + +let s:has_vim9textprops = exists('*prop_add') && has('patch-9.0.0178') +function! lsp#utils#_has_vim_virtual_text() abort + return s:has_vim9textprops +endfunction + +let s:has_prop_remove_types = exists('*prop_remove') && has('patch-9.0.0233') +function! lsp#utils#_has_prop_remove_types() abort + return s:has_prop_remove_types +endfunction + +let s:has_higlights = has('nvim') ? lsp#utils#_has_nvim_buf_highlight() : lsp#utils#_has_textprops() +function! lsp#utils#_has_highlights() abort + return s:has_higlights +endfunction + +let s:has_popup_menu = exists('*popup_menu') +function! lsp#utils#_has_popup_menu() abort + return s:has_popup_menu +endfunction + +function! lsp#utils#is_file_uri(uri) abort + return stridx(a:uri, 'file:///') == 0 +endfunction + +function! lsp#utils#is_remote_uri(uri) abort + return a:uri =~# '^\w\+::' || a:uri =~# '^[a-z][a-z0-9+.-]*://' +endfunction + +function! s:decode_uri(uri) abort + let l:ret = substitute(a:uri, '[?#].*', '', '') + return substitute(l:ret, '%\(\x\x\)', '\=printf("%c", str2nr(submatch(1), 16))', 'g') +endfunction + +function! s:urlencode_char(c) abort + return printf('%%%02X', char2nr(a:c)) +endfunction + +function! s:get_prefix(path) abort + return matchstr(a:path, '\(^\w\+::\|^\w\+://\)') +endfunction + +function! s:encode_uri(path, start_pos_encode, default_prefix) abort + let l:prefix = s:get_prefix(a:path) + let l:path = a:path[len(l:prefix):] + if len(l:prefix) == 0 + let l:prefix = a:default_prefix + endif + + let l:result = strpart(a:path, 0, a:start_pos_encode) + + for l:i in range(a:start_pos_encode, len(l:path) - 1) + " Don't encode '/' here, `path` is expected to be a valid path. + if l:path[l:i] =~# '^[a-zA-Z0-9_.~/@-]$' + let l:result .= l:path[l:i] + else + let l:result .= s:urlencode_char(l:path[l:i]) + endif + endfor + + return l:prefix . l:result +endfunction + +let s:path_to_uri_cache = {} +if has('win32') || has('win64') || has('win32unix') + function! lsp#utils#path_to_uri(path) abort + if has_key(s:path_to_uri_cache, a:path) + return s:path_to_uri_cache[a:path] + endif + + if empty(a:path) || lsp#utils#is_remote_uri(a:path) + let s:path_to_uri_cache[a:path] = a:path + return s:path_to_uri_cache[a:path] + else + " Transform cygwin paths to windows paths + let l:path = a:path + if has('win32unix') + let l:path = substitute(a:path, '\c^/\([a-z]\)/', '\U\1:/', '') + endif + + " You must not encode the volume information on the path if + " present + let l:end_pos_volume = matchstrpos(l:path, '\c[A-Z]:')[2] + + if l:end_pos_volume == -1 + let l:end_pos_volume = 0 + endif + + let s:path_to_uri_cache[l:path] = s:encode_uri(substitute(l:path, '\', '/', 'g'), l:end_pos_volume, 'file:///') + return s:path_to_uri_cache[l:path] + endif + endfunction +else + function! lsp#utils#path_to_uri(path) abort + if has_key(s:path_to_uri_cache, a:path) + return s:path_to_uri_cache[a:path] + endif + + if empty(a:path) || lsp#utils#is_remote_uri(a:path) + let s:path_to_uri_cache[a:path] = a:path + return s:path_to_uri_cache[a:path] + else + let s:path_to_uri_cache[a:path] = s:encode_uri(a:path, 0, 'file://') + return s:path_to_uri_cache[a:path] + endif + endfunction +endif + +let s:uri_to_path_cache = {} +if has('win32') || has('win64') || has('win32unix') + function! lsp#utils#uri_to_path(uri) abort + if has_key(s:uri_to_path_cache, a:uri) + return s:uri_to_path_cache[a:uri] + endif + + let l:path = substitute(s:decode_uri(a:uri[len('file:///'):]), '/', '\\', 'g') + + " Transform windows paths to cygwin paths + if has('win32unix') + let l:path = substitute(l:path, '\c^\([A-Z]\):\\', '/\l\1/', '') + let l:path = substitute(l:path, '\\', '/', 'g') + endif + + let s:uri_to_path_cache[a:uri] = l:path + return s:uri_to_path_cache[a:uri] + endfunction +else + function! lsp#utils#uri_to_path(uri) abort + if has_key(s:uri_to_path_cache, a:uri) + return s:uri_to_path_cache[a:uri] + endif + + let s:uri_to_path_cache[a:uri] = s:decode_uri(a:uri[len('file://'):]) + return s:uri_to_path_cache[a:uri] + endfunction +endif + +if has('win32') || has('win64') + function! lsp#utils#normalize_uri(uri) abort + " Refer to https://github.com/microsoft/language-server-protocol/pull/1019 on normalization of urls. + " TODO: after the discussion is settled, modify this function. + let l:ret = substitute(a:uri, '^file:///[a-zA-Z]\zs%3[aA]', ':', '') + return substitute(l:ret, '^file:///\zs\([A-Z]\)', "\\=tolower(submatch(1))", '') + endfunction +else + function! lsp#utils#normalize_uri(uri) abort + return a:uri + endfunction +endif + +function! lsp#utils#get_default_root_uri() abort + return lsp#utils#path_to_uri(getcwd()) +endfunction + +function! lsp#utils#get_buffer_path(...) abort + return expand((a:0 > 0 ? '#' . a:1 : '%') . ':p') +endfunction + +function! lsp#utils#get_buffer_uri(...) abort + let l:name = a:0 > 0 ? bufname(a:1) : expand('%') + if empty(l:name) + let l:nr = a:0 > 0 ? a:1 : bufnr('%') + let l:name = printf('%s/__NO_NAME_%d__', getcwd(), l:nr) + endif + return lsp#utils#path_to_uri(fnamemodify(l:name, ':p')) +endfunction + +" Find a nearest to a `path` parent directory `directoryname` by traversing the filesystem upwards +function! lsp#utils#find_nearest_parent_directory(path, directoryname) abort + let l:relative_path = finddir(a:directoryname, fnameescape(a:path) . ';') + + if !empty(l:relative_path) + return fnamemodify(l:relative_path, ':p') + else + return '' + endif +endfunction + +" Find a nearest to a `path` parent filename `filename` by traversing the filesystem upwards +function! lsp#utils#find_nearest_parent_file(path, filename) abort + let l:relative_path = findfile(a:filename, fnameescape(a:path) . ';') + + if !empty(l:relative_path) + return fnamemodify(l:relative_path, ':p') + else + return '' + endif +endfunction + +function! lsp#utils#_compare_nearest_path(matches, lhs, rhs) abort + let l:llhs = len(a:lhs) + let l:lrhs = len(a:rhs) + if l:llhs ># l:lrhs + return -1 + elseif l:llhs <# l:lrhs + return 1 + endif + if a:matches[a:lhs] ># a:matches[a:rhs] + return -1 + elseif a:matches[a:lhs] <# a:matches[a:rhs] + return 1 + endif + return 0 +endfunction + +function! lsp#utils#_nearest_path(matches) abort + return empty(a:matches) ? + \ '' : + \ sort(keys(a:matches), function('lsp#utils#_compare_nearest_path', [a:matches]))[0] +endfunction + +" Find a nearest to a `path` parent filename `filename` by traversing the filesystem upwards +" The filename ending with '/' or '\' will be regarded as directory name, +" otherwith as file name +function! lsp#utils#find_nearest_parent_file_directory(path, filename) abort + if type(a:filename) == 3 + let l:matched_paths = {} + for l:current_name in a:filename + let l:path = lsp#utils#find_nearest_parent_file_directory(a:path, l:current_name) + + if !empty(l:path) + if has_key(l:matched_paths, l:path) + let l:matched_paths[l:path] += 1 + else + let l:matched_paths[l:path] = 1 + endif + endif + endfor + + return lsp#utils#_nearest_path(l:matched_paths) + elseif type(a:filename) == 1 + if a:filename[-1:] ==# '/' || a:filename[-1:] ==# '\' + let l:modify_str = ':p:h:h' + let l:path = lsp#utils#find_nearest_parent_directory(a:path, a:filename[:-2]) + else + let l:modify_str = ':p:h' + let l:path = lsp#utils#find_nearest_parent_file(a:path, a:filename) + endif + + return empty(l:path) ? '' : fnamemodify(l:path, l:modify_str) + else + echoerr "The type of argument \"filename\" must be String or List" + endif +endfunction + +if exists('*matchstrpos') + function! lsp#utils#matchstrpos(expr, pattern) abort + return matchstrpos(a:expr, a:pattern) + endfunction +else + function! lsp#utils#matchstrpos(expr, pattern) abort + return [matchstr(a:expr, a:pattern), match(a:expr, a:pattern), matchend(a:expr, a:pattern)] + endfunction +endif + +function! lsp#utils#empty_complete(...) abort + return [] +endfunction + +function! lsp#utils#error(msg) abort + echohl ErrorMsg + echom a:msg + echohl NONE +endfunction + +function! lsp#utils#warning(msg) abort + echohl WarningMsg + echom a:msg + echohl NONE +endfunction + + +function! lsp#utils#echo_with_truncation(msg) abort + let l:msg = a:msg + + if &laststatus == 0 || (&laststatus == 1 && tabpagewinnr(tabpagenr(), '$') == 1) + let l:winwidth = winwidth(0) + + if &ruler + let l:winwidth -= 18 + endif + else + let l:winwidth = &columns + endif + + if &showcmd + let l:winwidth -= 12 + endif + + if l:winwidth > 5 && l:winwidth < strdisplaywidth(l:msg) + let l:msg = l:msg[:l:winwidth - 5] . '...' + endif + + exec 'echo l:msg' +endfunction + +" Convert a byte-index (1-based) to a character-index (0-based) +" This function requires a buffer specifier (expr, see :help bufname()), +" a line number (lnum, 1-based), and a byte-index (char, 1-based). +function! lsp#utils#to_char(expr, lnum, col) abort + let l:lines = getbufline(a:expr, a:lnum) + if l:lines == [] + if type(a:expr) != v:t_string || !filereadable(a:expr) + " invalid a:expr + return a:col - 1 + endif + " a:expr is a file that is not yet loaded as a buffer + let l:lines = readfile(a:expr, '', a:lnum) + endif + let l:linestr = l:lines[-1] + return strchars(strpart(l:linestr, 0, a:col - 1)) +endfunction + +function! s:get_base64_alphabet() abort + let l:alphabet = [] + + " Uppercase letters + for l:c in range(char2nr('A'), char2nr('Z')) + call add(l:alphabet, nr2char(l:c)) + endfor + + " Lowercase letters + for l:c in range(char2nr('a'), char2nr('z')) + call add(l:alphabet, nr2char(l:c)) + endfor + + " Numbers + for l:c in range(char2nr('0'), char2nr('9')) + call add(l:alphabet, nr2char(l:c)) + endfor + + " Symbols + call add(l:alphabet, '+') + call add(l:alphabet, '/') + + return l:alphabet +endfunction + +if exists('*trim') + function! lsp#utils#_trim(string) abort + return trim(a:string) + endfunction +else + function! lsp#utils#_trim(string) abort + return substitute(a:string, '^\s*\|\s*$', '', 'g') + endfunction +endif + +function! lsp#utils#_get_before_line() abort + let l:text = getline('.') + let l:idx = min([strlen(l:text), col('.') - 2]) + let l:idx = max([l:idx, -1]) + if l:idx == -1 + return '' + endif + return l:text[0 : l:idx] +endfunction + +function! lsp#utils#_get_before_char_skip_white() abort + let l:current_lnum = line('.') + + let l:lnum = l:current_lnum + while l:lnum > 0 + if l:lnum == l:current_lnum + let l:text = lsp#utils#_get_before_line() + else + let l:text = getline(l:lnum) + endif + let l:match = matchlist(l:text, '\([^[:blank:]]\)\s*$') + if get(l:match, 1, v:null) isnot v:null + return l:match[1] + endif + let l:lnum -= 1 + endwhile + + return '' +endfunction + +let s:alphabet = s:get_base64_alphabet() + +function! lsp#utils#base64_decode(data) abort + let l:ret = [] + + " Process base64 string in chunks of 4 chars + for l:group in split(a:data, '.\{4}\zs') + let l:group_dec = 0 + + " Convert 4 chars to 3 octets + for l:char in split(l:group, '\zs') + let l:group_dec = l:group_dec * 64 + let l:group_dec += max([index(s:alphabet, l:char), 0]) + endfor + + " Split the number representing the 3 octets into the individual + " octets + let l:octets = [] + let l:i = 0 + while l:i < 3 + call add(l:octets, l:group_dec % 256) + let l:group_dec = l:group_dec / 256 + let l:i += 1 + endwhile + + call extend(l:ret, reverse(l:octets)) + endfor + + " Handle padding + if len(a:data) >= 2 + if strpart(a:data, len(a:data) - 2) ==# '==' + call remove(l:ret, -2, -1) + elseif strpart(a:data, len(a:data) - 1) ==# '=' + call remove(l:ret, -1, -1) + endif + endif + + return l:ret +endfunction + +function! lsp#utils#make_valid_word(str) abort + let l:str = substitute(a:str, '\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g') + let l:str = substitute(l:str, '\\\(.\)', '\1', 'g') + let l:valid = matchstr(l:str, '^[^"'' (<{\[\t\r\n]\+') + if empty(l:valid) + return l:str + endif + if l:valid =~# ':$' + return l:valid[:-2] + endif + return l:valid +endfunction + +function! lsp#utils#_split_by_eol(text) abort + return split(a:text, '\r\n\|\r\|\n', v:true) +endfunction + +" parse command options like "-key" or "-key=value" +function! lsp#utils#parse_command_options(params) abort + let l:result = {} + for l:param in a:params + let l:match = matchlist(l:param, '-\{1,2}\zs\([^=]*\)\(=\(.*\)\)\?\m') + let l:result[l:match[1]] = l:match[3] + endfor + return l:result +endfunction + +function! lsp#utils#is_large_window(winid) abort + let l:buffer_size = line2byte(line('$', a:winid)) + return g:lsp_max_buffer_size >= 0 && l:buffer_size >= g:lsp_max_buffer_size +endfunction + +" polyfill for the neovim wait function +if exists('*wait') + function! lsp#utils#_wait(timeout, condition, ...) abort + if type(a:timeout) != type(0) + return -3 + endif + if type(get(a:000, 0, 0)) != type(0) + return -3 + endif + while 1 + let l:result=call('wait', extend([a:timeout, a:condition], a:000)) + if l:result != -3 " ignore spurious errors + return l:result + endif + endwhile + endfunction +else + function! lsp#utils#_wait(timeout, condition, ...) abort + try + let l:timeout = a:timeout / 1000.0 + let l:interval = get(a:000, 0, 200) + let l:Condition = a:condition + if type(l:Condition) != type(function('eval')) + let l:Condition = function('eval', l:Condition) + endif + let l:start = reltime() + while l:timeout < 0 || reltimefloat(reltime(l:start)) < l:timeout + if l:Condition() + return 0 + endif + + execute 'sleep ' . l:interval . 'm' + endwhile + return -1 + catch /^Vim:Interrupt$/ + return -2 + endtry + endfunction +endif + +function! lsp#utils#iteratable(list) abort + return type(a:list) !=# v:t_list ? [] : a:list +endfunction diff --git a/autoload/lsp/utils/args.vim b/autoload/lsp/utils/args.vim new file mode 100644 index 00000000..7b266be8 --- /dev/null +++ b/autoload/lsp/utils/args.vim @@ -0,0 +1,42 @@ +function! lsp#utils#args#_parse(args, opt, remainder_key) abort + let l:result = {} + let l:is_opts = v:true + let l:remainder = [] + for l:item in split(a:args, ' ') + if l:item[:1] !=# '--' + let l:is_opts = v:false + endif + + if l:is_opts == v:false + call add(l:remainder, l:item) + continue + endif + + let l:parts = split(l:item, '=') + let l:key = l:parts[0] + let l:value = get(l:parts, 1, '') + let l:key = l:key[2:] + + if has_key(a:opt, l:key) + if has_key(a:opt[l:key], 'type') + let l:type = a:opt[l:key]['type'] + if l:type == type(v:true) + if l:value ==# 'false' || l:value ==# '0' || l:value ==# '' + let l:value = 0 + else + let l:value = 1 + endif + elseif l:type ==# type(0) + let l:value = str2nr(l:value) + endif + endif + endif + let l:result[l:key] = l:value + endfor + + if a:remainder_key != v:null + let l:result[a:remainder_key] = join(l:remainder) + endif + + return l:result +endfunction diff --git a/autoload/lsp/utils/buffer.vim b/autoload/lsp/utils/buffer.vim new file mode 100644 index 00000000..fcf7c53a --- /dev/null +++ b/autoload/lsp/utils/buffer.vim @@ -0,0 +1,74 @@ +let s:fixendofline_exists = exists('+fixendofline') + +function! s:get_fixendofline(buf) abort + let l:eol = getbufvar(a:buf, '&endofline') + let l:binary = getbufvar(a:buf, '&binary') + + if s:fixendofline_exists + let l:fixeol = getbufvar(a:buf, '&fixendofline') + + if !l:binary + " When 'binary' is off and 'fixeol' is on, 'endofline' is not used + " + " When 'binary' is off and 'fixeol' is off, 'endofline' is used to + " remember the presence of a + return l:fixeol || l:eol + else + " When 'binary' is on, the value of 'fixeol' doesn't matter + return l:eol + endif + else + " When 'binary' is off the value of 'endofline' is not used + " + " When 'binary' is on 'endofline' is used to remember the presence of + " a + return !l:binary || l:eol + endif +endfunction + +function! lsp#utils#buffer#_get_fixendofline(bufnr) abort + return s:get_fixendofline(a:bufnr) +endfunction + +function! lsp#utils#buffer#_get_lines(buf) abort + let l:lines = getbufline(a:buf, 1, '$') + if s:get_fixendofline(a:buf) + let l:lines += [''] + endif + return l:lines +endfunction + +" @params {location} = { +" 'uri': 'file://....', +" 'range': { +" 'start': { 'line': 1, 'character': 1 }, +" 'end': { 'line': 1, 'character': 1 }, +" } +" } +function! lsp#utils#buffer#_open_lsp_location(location) abort + let l:path = lsp#utils#uri_to_path(a:location['uri']) + let l:bufnr = bufnr(l:path) + + let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim(l:bufnr, a:location['range']['start']) + let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim(l:bufnr, a:location['range']['end']) + + normal! m' + if &modified && !&hidden + let l:cmd = l:bufnr !=# -1 ? 'sb ' . l:bufnr : 'split ' . fnameescape(l:path) + else + let l:cmd = l:bufnr !=# -1 ? 'b ' . l:bufnr : 'edit ' . fnameescape(l:path) + endif + execute l:cmd . ' | call cursor('.l:start_line.','.l:start_col.')' + + normal! V + call setpos("'<", [l:bufnr, l:start_line, l:start_col]) + call setpos("'>", [l:bufnr, l:end_line, l:end_col]) +endfunction + +function! lsp#utils#buffer#get_indent_size(bufnr) abort + let l:shiftwidth = getbufvar(a:bufnr, '&shiftwidth') + if getbufvar(a:bufnr, '&shiftwidth') + return l:shiftwidth + endif + return getbufvar(a:bufnr, '&tabstop') +endfunction diff --git a/autoload/lsp/utils/diff.vim b/autoload/lsp/utils/diff.vim new file mode 100644 index 00000000..4969c9f7 --- /dev/null +++ b/autoload/lsp/utils/diff.vim @@ -0,0 +1,165 @@ +" This is copied from https://github.com/natebosch/vim-lsc/blob/master/autoload/lsc/diff.vim +" +" Computes a simplistic diff between [old] and [new]. +" +" Returns a dict with keys `range`, `rangeLength`, and `text` matching the LSP +" definition of `TextDocumentContentChangeEvent`. +" +" Finds a single change between the common prefix, and common postfix. +let s:has_lua = has('nvim-0.4.0') || (has('lua') && has('patch-8.2.0775')) +" lua array and neovim vim list index starts with 1 while vim lists starts with 0. +" starting patch-8.2.1066 vim lists array index was changed to start with 1. +let s:lua_array_start_index = has('nvim-0.4.0') || has('patch-8.2.1066') + +function! s:init_lua() abort + lua <= l:line_count + return [l:line_count - 1, strchars(a:old[l:line_count - 1])] + endif + let l:old_line = a:old[l:i] + let l:new_line = a:new[l:i] + let l:length = min([strchars(l:old_line), strchars(l:new_line)]) + let l:j = 0 + while l:j < l:length + if strgetchar(l:old_line, l:j) != strgetchar(l:new_line, l:j) | break | endif + let l:j += 1 + endwhile + return [l:i, l:j] +endfunction + +function! s:LastDifference(old, new, start_char) abort + let l:line_count = min([len(a:old), len(a:new)]) + if l:line_count == 0 | return [0, 0] | endif + if g:lsp_use_lua && s:has_lua + let l:eval = has('nvim') ? 'vim.api.nvim_eval' : 'vim.eval' + let l:i = luaeval('vimlsp_last_difference(' + \.l:eval.'("a:old"),'.l:eval.'("a:new"),'.s:lua_array_start_index.','.l:line_count.')') + else + for l:i in range(-1, -1 * l:line_count, -1) + if a:old[l:i] !=# a:new[l:i] | break | endif + endfor + endif + if l:i <= -1 * l:line_count + let l:i = -1 * l:line_count + let l:old_line = strcharpart(a:old[l:i], a:start_char) + let l:new_line = strcharpart(a:new[l:i], a:start_char) + else + let l:old_line = a:old[l:i] + let l:new_line = a:new[l:i] + endif + let l:old_line_length = strchars(l:old_line) + let l:new_line_length = strchars(l:new_line) + let l:length = min([l:old_line_length, l:new_line_length]) + let l:j = -1 + while l:j >= -1 * l:length + if strgetchar(l:old_line, l:old_line_length + l:j) != + \ strgetchar(l:new_line, l:new_line_length + l:j) + break + endif + let l:j -= 1 + endwhile + return [l:i, l:j] +endfunction + +function! s:ExtractText(lines, start_line, start_char, end_line, end_char) abort + if a:start_line == len(a:lines) + a:end_line + if a:end_line == 0 | return '' | endif + let l:line = a:lines[a:start_line] + let l:length = strchars(l:line) + a:end_char - a:start_char + 1 + return strcharpart(l:line, a:start_char, l:length) + endif + let l:result = strcharpart(a:lines[a:start_line], a:start_char) . "\n" + for l:line in a:lines[a:start_line + 1:a:end_line - 1] + let l:result .= l:line . "\n" + endfor + if a:end_line != 0 + let l:line = a:lines[a:end_line] + let l:length = strchars(l:line) + a:end_char + 1 + let l:result .= strcharpart(l:line, 0, l:length) + endif + return l:result +endfunction + +function! s:Length(lines, start_line, start_char, end_line, end_char) abort + let l:adj_end_line = len(a:lines) + a:end_line + if l:adj_end_line >= len(a:lines) + let l:adj_end_char = a:end_char - 1 + else + let l:adj_end_char = strchars(a:lines[l:adj_end_line]) + a:end_char + endif + if a:start_line == l:adj_end_line + return l:adj_end_char - a:start_char + 1 + endif + let l:result = strchars(a:lines[a:start_line]) - a:start_char + 1 + let l:line = a:start_line + 1 + while l:line < l:adj_end_line + let l:result += strchars(a:lines[l:line]) + 1 + let l:line += 1 + endwhile + let l:result += l:adj_end_char + 1 + return l:result +endfunction diff --git a/autoload/lsp/utils/job.vim b/autoload/lsp/utils/job.vim new file mode 100644 index 00000000..b3c1efc0 --- /dev/null +++ b/autoload/lsp/utils/job.vim @@ -0,0 +1,409 @@ +" https://github.com/prabirshrestha/async.vim#2082d13bb195f3203d41a308b89417426a7deca1 (dirty) +" :AsyncEmbed path=./autoload/lsp/utils/job.vim namespace=lsp#utils#job + +" Author: Prabir Shrestha +" Website: https://github.com/prabirshrestha/async.vim +" License: The MIT License {{{ +" The MIT License (MIT) +" +" Copyright (c) 2016 Prabir Shrestha +" +" Permission is hereby granted, free of charge, to any person obtaining a copy +" of this software and associated documentation files (the "Software"), to deal +" in the Software without restriction, including without limitation the rights +" to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +" copies of the Software, and to permit persons to whom the Software is +" furnished to do so, subject to the following conditions: +" +" The above copyright notice and this permission notice shall be included in all +" copies or substantial portions of the Software. +" +" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +" IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +" FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +" AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +" LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +" OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +" SOFTWARE. +" }}} + +let s:save_cpo = &cpo +set cpo&vim + +let s:jobidseq = 0 +let s:jobs = {} " { job, opts, type: 'vimjob|nvimjob'} +let s:job_type_nvimjob = 'nvimjob' +let s:job_type_vimjob = 'vimjob' +let s:job_error_unsupported_job_type = -2 " unsupported job type + +function! s:noop(...) abort +endfunction + +function! s:job_supported_types() abort + let l:supported_types = [] + if has('nvim') + let l:supported_types += [s:job_type_nvimjob] + endif + if !has('nvim') && has('job') && has('channel') && has('lambda') + let l:supported_types += [s:job_type_vimjob] + endif + return l:supported_types +endfunction + +function! s:job_supports_type(type) abort + return index(s:job_supported_types(), a:type) >= 0 +endfunction + +function! s:out_cb(jobid, opts, job, data) abort + call a:opts.on_stdout(a:jobid, a:data, 'stdout') +endfunction + +function! s:out_cb_array(jobid, opts, job, data) abort + call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout') +endfunction + +function! s:err_cb(jobid, opts, job, data) abort + call a:opts.on_stderr(a:jobid, a:data, 'stderr') +endfunction + +function! s:err_cb_array(jobid, opts, job, data) abort + call a:opts.on_stderr(a:jobid, split(a:data, "\n", 1), 'stderr') +endfunction + +function! s:exit_cb(jobid, opts, job, status) abort + if has_key(a:opts, 'on_exit') + call a:opts.on_exit(a:jobid, a:status, 'exit') + endif + if has_key(s:jobs, a:jobid) + call remove(s:jobs, a:jobid) + endif +endfunction + +function! s:on_stdout(jobid, data, event) abort + let l:jobinfo = s:jobs[a:jobid] + call l:jobinfo.opts.on_stdout(a:jobid, a:data, a:event) +endfunction + +function! s:on_stdout_string(jobid, data, event) abort + let l:jobinfo = s:jobs[a:jobid] + call l:jobinfo.opts.on_stdout(a:jobid, join(a:data, "\n"), a:event) +endfunction + +function! s:on_stderr(jobid, data, event) abort + let l:jobinfo = s:jobs[a:jobid] + call l:jobinfo.opts.on_stderr(a:jobid, a:data, a:event) +endfunction + +function! s:on_stderr_string(jobid, data, event) abort + let l:jobinfo = s:jobs[a:jobid] + call l:jobinfo.opts.on_stderr(a:jobid, join(a:data, "\n"), a:event) +endfunction + +function! s:on_exit(jobid, status, event) abort + if has_key(s:jobs, a:jobid) + let l:jobinfo = s:jobs[a:jobid] + if has_key(l:jobinfo.opts, 'on_exit') + call l:jobinfo.opts.on_exit(a:jobid, a:status, a:event) + endif + if has_key(s:jobs, a:jobid) + call remove(s:jobs, a:jobid) + endif + endif +endfunction + +function! s:job_start(cmd, opts) abort + let l:jobtypes = s:job_supported_types() + let l:jobtype = '' + + if has_key(a:opts, 'type') + if type(a:opts.type) == type('') + if !s:job_supports_type(a:opts.type) + return s:job_error_unsupported_job_type + endif + let l:jobtype = a:opts.type + else + let l:jobtypes = a:opts.type + endif + endif + + if empty(l:jobtype) + " find the best jobtype + for l:jobtype2 in l:jobtypes + if s:job_supports_type(l:jobtype2) + let l:jobtype = l:jobtype2 + endif + endfor + endif + + if l:jobtype ==? '' + return s:job_error_unsupported_job_type + endif + + " options shared by both vim and neovim + let l:jobopt = {} + if has_key(a:opts, 'cwd') + let l:jobopt.cwd = a:opts.cwd + endif + if has_key(a:opts, 'env') + let l:jobopt.env = a:opts.env + endif + + let l:normalize = get(a:opts, 'normalize', 'array') " array/string/raw + + if l:jobtype == s:job_type_nvimjob + if l:normalize ==# 'string' + let l:jobopt['on_stdout'] = has_key(a:opts, 'on_stdout') ? function('s:on_stdout_string') : function('s:noop') + let l:jobopt['on_stderr'] = has_key(a:opts, 'on_stderr') ? function('s:on_stderr_string') : function('s:noop') + else " array or raw + let l:jobopt['on_stdout'] = has_key(a:opts, 'on_stdout') ? function('s:on_stdout') : function('s:noop') + let l:jobopt['on_stderr'] = has_key(a:opts, 'on_stderr') ? function('s:on_stderr') : function('s:noop') + endif + call extend(l:jobopt, { 'on_exit': function('s:on_exit') }) + let l:job = jobstart(a:cmd, l:jobopt) + if l:job <= 0 + return l:job + endif + let l:jobid = l:job " nvimjobid and internal jobid is same + let s:jobs[l:jobid] = { + \ 'type': s:job_type_nvimjob, + \ 'opts': a:opts, + \ } + let s:jobs[l:jobid].job = l:job + elseif l:jobtype == s:job_type_vimjob + let s:jobidseq = s:jobidseq + 1 + let l:jobid = s:jobidseq + if l:normalize ==# 'array' + let l:jobopt['out_cb'] = has_key(a:opts, 'on_stdout') ? function('s:out_cb_array', [l:jobid, a:opts]) : function('s:noop') + let l:jobopt['err_cb'] = has_key(a:opts, 'on_stderr') ? function('s:err_cb_array', [l:jobid, a:opts]) : function('s:noop') + else " raw or string + let l:jobopt['out_cb'] = has_key(a:opts, 'on_stdout') ? function('s:out_cb', [l:jobid, a:opts]) : function('s:noop') + let l:jobopt['err_cb'] = has_key(a:opts, 'on_stderr') ? function('s:err_cb', [l:jobid, a:opts]) : function('s:noop') + endif + call extend(l:jobopt, { + \ 'exit_cb': function('s:exit_cb', [l:jobid, a:opts]), + \ 'mode': 'raw', + \ }) + if has('patch-8.1.889') + let l:jobopt['noblock'] = 1 + endif + let l:job = job_start(a:cmd, l:jobopt) + if job_status(l:job) !=? 'run' + return -1 + endif + let s:jobs[l:jobid] = { + \ 'type': s:job_type_vimjob, + \ 'opts': a:opts, + \ 'job': l:job, + \ 'channel': job_getchannel(l:job), + \ 'buffer': '' + \ } + else + return s:job_error_unsupported_job_type + endif + + return l:jobid +endfunction + +function! s:job_stop(jobid) abort + if has_key(s:jobs, a:jobid) + let l:jobinfo = s:jobs[a:jobid] + if l:jobinfo.type == s:job_type_nvimjob + " See: vital-Whisky/System.Job + try + call jobstop(a:jobid) + catch /^Vim\%((\a\+)\)\=:E900/ + " NOTE: + " Vim does not raise exception even the job has already closed so fail + " silently for 'E900: Invalid job id' exception + endtry + elseif l:jobinfo.type == s:job_type_vimjob + if type(s:jobs[a:jobid].job) == v:t_job + call job_stop(s:jobs[a:jobid].job) + elseif type(s:jobs[a:jobid].job) == v:t_channel + call ch_close(s:jobs[a:jobid].job) + endif + endif + endif +endfunction + +function! s:job_send(jobid, data, opts) abort + let l:jobinfo = s:jobs[a:jobid] + let l:close_stdin = get(a:opts, 'close_stdin', 0) + if l:jobinfo.type == s:job_type_nvimjob + call jobsend(a:jobid, a:data) + if l:close_stdin + call chanclose(a:jobid, 'stdin') + endif + elseif l:jobinfo.type == s:job_type_vimjob + " There is no easy way to know when ch_sendraw() finishes writing data + " on a non-blocking channels -- has('patch-8.1.889') -- and because of + " this, we cannot safely call ch_close_in(). So when we find ourselves + " in this situation (i.e. noblock=1 and close stdin after send) we fall + " back to using s:flush_vim_sendraw() and wait for transmit buffer to be + " empty + " + " Ref: https://groups.google.com/d/topic/vim_dev/UNNulkqb60k/discussion + if has('patch-8.1.818') && (!has('patch-8.1.889') || !l:close_stdin) + call ch_sendraw(l:jobinfo.channel, a:data) + else + let l:jobinfo.buffer .= a:data + call s:flush_vim_sendraw(a:jobid, v:null) + endif + if l:close_stdin + while len(l:jobinfo.buffer) != 0 + sleep 1m + endwhile + call ch_close_in(l:jobinfo.channel) + endif + endif +endfunction + +function! s:flush_vim_sendraw(jobid, timer) abort + " https://github.com/vim/vim/issues/2548 + " https://github.com/natebosch/vim-lsc/issues/67#issuecomment-357469091 + let l:jobinfo = s:jobs[a:jobid] + sleep 1m + if len(l:jobinfo.buffer) <= 4096 + call ch_sendraw(l:jobinfo.channel, l:jobinfo.buffer) + let l:jobinfo.buffer = '' + else + let l:to_send = l:jobinfo.buffer[:4095] + let l:jobinfo.buffer = l:jobinfo.buffer[4096:] + call ch_sendraw(l:jobinfo.channel, l:to_send) + call timer_start(1, function('s:flush_vim_sendraw', [a:jobid])) + endif +endfunction + +function! s:job_wait_single(jobid, timeout, start) abort + if !has_key(s:jobs, a:jobid) + return -3 + endif + + let l:jobinfo = s:jobs[a:jobid] + if l:jobinfo.type == s:job_type_nvimjob + let l:timeout = a:timeout - reltimefloat(reltime(a:start)) * 1000 + return jobwait([a:jobid], float2nr(l:timeout))[0] + elseif l:jobinfo.type == s:job_type_vimjob + let l:timeout = a:timeout / 1000.0 + try + while l:timeout < 0 || reltimefloat(reltime(a:start)) < l:timeout + let l:info = job_info(l:jobinfo.job) + if l:info.status ==# 'dead' + return l:info.exitval + elseif l:info.status ==# 'fail' + return -3 + endif + sleep 1m + endwhile + catch /^Vim:Interrupt$/ + return -2 + endtry + endif + return -1 +endfunction + +function! s:job_wait(jobids, timeout) abort + let l:start = reltime() + let l:exitcode = 0 + let l:ret = [] + for l:jobid in a:jobids + if l:exitcode != -2 " Not interrupted. + let l:exitcode = s:job_wait_single(l:jobid, a:timeout, l:start) + endif + let l:ret += [l:exitcode] + endfor + return l:ret +endfunction + +function! s:job_pid(jobid) abort + if !has_key(s:jobs, a:jobid) + return 0 + endif + + let l:jobinfo = s:jobs[a:jobid] + if l:jobinfo.type == s:job_type_nvimjob + return jobpid(a:jobid) + elseif l:jobinfo.type == s:job_type_vimjob + let l:vimjobinfo = job_info(a:jobid) + if type(l:vimjobinfo) == type({}) && has_key(l:vimjobinfo, 'process') + return l:vimjobinfo['process'] + endif + endif + return 0 +endfunction + +function! s:callback_cb(jobid, opts, ch, data) abort + if has_key(a:opts, 'on_stdout') + call a:opts.on_stdout(a:jobid, a:data, 'stdout') + endif +endfunction + +function! s:callback_cb_array(jobid, opts, ch, data) abort + if has_key(a:opts, 'on_stdout') + call a:opts.on_stdout(a:jobid, split(a:data, "\n", 1), 'stdout') + endif +endfunction + +function! s:close_cb(jobid, opts, ch) abort + if has_key(a:opts, 'on_exit') + call a:opts.on_exit(a:jobid, 'closed', 'exit') + endif + if has_key(s:jobs, a:jobid) + call remove(s:jobs, a:jobid) + endif +endfunction + +" public apis {{{ +function! lsp#utils#job#start(cmd, opts) abort + return s:job_start(a:cmd, a:opts) +endfunction + +function! lsp#utils#job#stop(jobid) abort + call s:job_stop(a:jobid) +endfunction + +function! lsp#utils#job#send(jobid, data, ...) abort + let l:opts = get(a:000, 0, {}) + call s:job_send(a:jobid, a:data, l:opts) +endfunction + +function! lsp#utils#job#wait(jobids, ...) abort + let l:timeout = get(a:000, 0, -1) + return s:job_wait(a:jobids, l:timeout) +endfunction + +function! lsp#utils#job#pid(jobid) abort + return s:job_pid(a:jobid) +endfunction + +function! lsp#utils#job#connect(addr, opts) abort + let s:jobidseq = s:jobidseq + 1 + let l:jobid = s:jobidseq + let l:retry = 0 + let l:normalize = get(a:opts, 'normalize', 'array') " array/string/raw + while l:retry < 5 + let l:ch = ch_open(a:addr, {'waittime': 1000}) + call ch_setoptions(l:ch, { + \ 'callback': function(l:normalize ==# 'array' ? 's:callback_cb_array' : 's:callback_cb', [l:jobid, a:opts]), + \ 'close_cb': function('s:close_cb', [l:jobid, a:opts]), + \ 'mode': 'raw', + \}) + if ch_status(l:ch) ==# 'open' + break + endif + sleep 100m + let l:retry += 1 + endwhile + let s:jobs[l:jobid] = { + \ 'type': s:job_type_vimjob, + \ 'opts': a:opts, + \ 'job': l:ch, + \ 'channel': l:ch, + \ 'buffer': '' + \} + return l:jobid +endfunction +" }}} + +let &cpo = s:save_cpo +unlet s:save_cpo diff --git a/autoload/lsp/utils/location.vim b/autoload/lsp/utils/location.vim new file mode 100644 index 00000000..7ad6c9fc --- /dev/null +++ b/autoload/lsp/utils/location.vim @@ -0,0 +1,135 @@ +function! s:open_location(path, line, col, ...) abort + normal! m' + let l:mods = a:0 ? a:1 : '' + let l:buffer = bufnr(a:path) + if l:mods ==# '' && &modified && !&hidden && l:buffer != bufnr('%') + let l:mods = &splitbelow ? 'rightbelow' : 'leftabove' + endif + if l:mods ==# '' + if l:buffer == bufnr('%') + let l:cmd = '' + else + let l:cmd = (l:buffer !=# -1 ? 'b ' . l:buffer : 'edit ' . fnameescape(a:path)) . ' | ' + endif + else + let l:cmd = l:mods . ' ' . (l:buffer !=# -1 ? 'sb ' . l:buffer : 'split ' . fnameescape(a:path)) . ' | ' + endif + execute l:cmd . 'call cursor('.a:line.','.a:col.')' +endfunction + +" @param location = { +" 'filename', +" 'lnum', +" 'col', +" } +function! lsp#utils#location#_open_vim_list_item(location, mods) abort + call s:open_location(a:location['filename'], a:location['lnum'], a:location['col'], a:mods) +endfunction + +" @params {location} = { +" 'uri': 'file://....', +" 'range': { +" 'start': { 'line': 1, 'character': 1 }, +" 'end': { 'line': 1, 'character': 1 }, +" } +" } +function! lsp#utils#location#_open_lsp_location(location) abort + let l:path = lsp#utils#uri_to_path(a:location['uri']) + let l:bufnr = bufnr(l:path) + + let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim(l:bufnr, a:location['range']['start']) + let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim(l:bufnr, a:location['range']['end']) + + call s:open_location(l:path, l:start_line, l:start_col) + + normal! V + call setpos("'<", [l:bufnr, l:start_line, l:start_col]) + call setpos("'>", [l:bufnr, l:end_line, l:end_col]) +endfunction + +" @param loc = Location | LocationLink +" @param cache = {} empty dict +" @returns { +" 'filename', +" 'lnum', +" 'col', +" 'text', +" 'viewstart?', +" 'viewend?', +" } +function! s:lsp_location_item_to_vim(loc, cache) abort + if has_key(a:loc, 'targetUri') " LocationLink + let l:uri = a:loc['targetUri'] + let l:range = a:loc['targetSelectionRange'] + let l:use_link = 1 + else " Location + let l:uri = a:loc['uri'] + let l:range = a:loc['range'] + let l:use_link = 0 + endif + + if !lsp#utils#is_file_uri(l:uri) + return v:null + endif + + let l:path = lsp#utils#uri_to_path(l:uri) + let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:range['start']) + + let l:index = l:line - 1 + if has_key(a:cache, l:path) + let l:text = a:cache[l:path][l:index] + else + let l:contents = getbufline(l:path, 1, '$') + if !empty(l:contents) + let l:text = get(l:contents, l:index, '') + else + let l:contents = readfile(l:path) + let a:cache[l:path] = l:contents + let l:text = get(l:contents, l:index, '') + endif + endif + + if l:use_link + " viewstart/end decremented to account for incrementing in _lsp_to_vim + return { + \ 'filename': l:path, + \ 'lnum': l:line, + \ 'col': l:col, + \ 'text': l:text, + \ 'viewstart': lsp#utils#position#lsp_to_vim(l:path, a:loc['targetRange']['start'])[0] - 1, + \ 'viewend': lsp#utils#position#lsp_to_vim(l:path, a:loc['targetRange']['end'])[0] - 1, + \ } + else + return { + \ 'filename': l:path, + \ 'lnum': l:line, + \ 'col': l:col, + \ 'text': l:text, + \ } + endif +endfunction + +" @summary Use this to convert loc to vim list that is compatible with +" quickfix and locllist items +" @param loc = v:null | Location | Location[] | LocationLink +" @returns [] +function! lsp#utils#location#_lsp_to_vim_list(loc) abort + let l:result = [] + let l:cache = {} + if empty(a:loc) " v:null + return l:result + elseif type(a:loc) == type([]) " Location[] + for l:location in a:loc + let l:vim_loc = s:lsp_location_item_to_vim(l:location, l:cache) + if !empty(l:vim_loc) " https:// uri will return empty + call add(l:result, l:vim_loc) + endif + endfor + else " Location or LocationLink + let l:vim_loc = s:lsp_location_item_to_vim(a:loc, l:cache) + if !empty(l:vim_loc) " https:// uri will return empty + call add(l:result, l:vim_loc) + endif + endif + return l:result +endfunction diff --git a/autoload/lsp/utils/position.vim b/autoload/lsp/utils/position.vim new file mode 100644 index 00000000..6868f252 --- /dev/null +++ b/autoload/lsp/utils/position.vim @@ -0,0 +1,91 @@ +" This function can be error prone if the caller forgets to use +1 to vim line +" so use lsp#utils#position#lsp_to_vim instead +" Convert a character-index (0-based) to byte-index (1-based) +" This function requires a buffer specifier (expr, see :help bufname()), +" a line number (lnum, 1-based), and a character-index (char, 0-based). +function! s:to_col(expr, lnum, char) abort + let l:lines = getbufline(a:expr, a:lnum) + if l:lines == [] + if type(a:expr) != v:t_string || !filereadable(a:expr) + " invalid a:expr + return a:char + 1 + endif + " a:expr is a file that is not yet loaded as a buffer + let l:lines = readfile(a:expr, '', a:lnum) + if l:lines == [] + " when the file is empty. a:char should be 0 in the case + return a:char + 1 + endif + endif + let l:linestr = l:lines[-1] + return strlen(strcharpart(l:linestr, 0, a:char)) + 1 +endfunction + +" The inverse version of `s:to_col`. +" Convert [lnum, col] to LSP's `Position`. +function! s:to_char(expr, lnum, col) abort + let l:lines = getbufline(a:expr, a:lnum) + if l:lines == [] + if type(a:expr) != v:t_string || !filereadable(a:expr) + " invalid a:expr + return a:col - 1 + endif + " a:expr is a file that is not yet loaded as a buffer + let l:lines = readfile(a:expr, '', a:lnum) + endif + let l:linestr = l:lines[-1] + return strchars(strpart(l:linestr, 0, a:col - 1)) +endfunction + +" @param expr = see :help bufname() +" @param position = { +" 'line': 1, +" 'character': 1 +" } +" @returns [ +" line, +" col +" ] +function! lsp#utils#position#lsp_to_vim(expr, position) abort + let l:line = lsp#utils#position#lsp_line_to_vim(a:expr, a:position) + let l:col = lsp#utils#position#lsp_character_to_vim(a:expr, a:position) + return [l:line, l:col] +endfunction + +" @param expr = see :help bufname() +" @param position = { +" 'line': 1, +" 'character': 1 +" } +" @returns +" line +function! lsp#utils#position#lsp_line_to_vim(expr, position) abort + return a:position['line'] + 1 +endfunction + +" @param expr = see :help bufname() +" @param position = { +" 'line': 1, +" 'character': 1 +" } +" @returns +" line +function! lsp#utils#position#lsp_character_to_vim(expr, position) abort + let l:line = a:position['line'] + 1 " optimize function overhead by not calling lsp_line_to_vim + let l:char = a:position['character'] + return s:to_col(a:expr, l:line, l:char) +endfunction + +" @param expr = :help bufname() +" @param pos = [lnum, col] +" @returns { +" 'line': line, +" 'character': character +" } +function! lsp#utils#position#vim_to_lsp(expr, pos) abort + return { + \ 'line': a:pos[0] - 1, + \ 'character': s:to_char(a:expr, a:pos[0], a:pos[1]) + \ } +endfunction + diff --git a/autoload/lsp/utils/range.vim b/autoload/lsp/utils/range.vim new file mode 100644 index 00000000..3e176328 --- /dev/null +++ b/autoload/lsp/utils/range.vim @@ -0,0 +1,82 @@ +" +" Returns recent visual-mode range. +" +function! lsp#utils#range#_get_recent_visual_range() abort + let l:start_pos = getpos("'<")[1 : 2] + let l:end_pos = getpos("'>")[1 : 2] + let l:end_pos[1] += 1 " To exclusive + + " Fix line selection. + let l:end_line = getline(l:end_pos[0]) + if l:end_pos[1] > strlen(l:end_line) + let l:end_pos[1] = strlen(l:end_line) + 1 + endif + + let l:range = {} + let l:range['start'] = lsp#utils#position#vim_to_lsp('%', l:start_pos) + let l:range['end'] = lsp#utils#position#vim_to_lsp('%', l:end_pos) + return l:range +endfunction + +" +" Returns current line range. +" +function! lsp#utils#range#_get_current_line_range() abort + let l:pos = getpos('.')[1 : 2] + let l:range = {} + let l:range['start'] = lsp#utils#position#vim_to_lsp('%', l:pos) + let l:range['end'] = lsp#utils#position#vim_to_lsp('%', [l:pos[0], l:pos[1] + strlen(getline(l:pos[0])) + 1]) + return l:range +endfunction + +" Convert a LSP range to one or more vim match positions. +" If the range spans over multiple lines, break it down to multiple +" positions, one for each line. +" Return a list of positions. +function! lsp#utils#range#lsp_to_vim(bufnr, range) abort + let l:position = [] + + let [l:start_line, l:start_col] = lsp#utils#position#lsp_to_vim(a:bufnr, a:range['start']) + let [l:end_line, l:end_col] = lsp#utils#position#lsp_to_vim(a:bufnr, a:range['end']) + if l:end_line == l:start_line + let l:position = [[ + \ l:start_line, + \ l:start_col, + \ l:end_col - l:start_col + \ ]] + else + " First line + let l:position = [[ + \ l:start_line, + \ l:start_col, + \ 999 + \ ]] + + " Last line + call add(l:position, [ + \ l:end_line, + \ 1, + \ l:end_col + \ ]) + + " Lines in the middle + let l:middle_lines = map( + \ range(l:start_line + 1, l:end_line - 1), + \ {_, l -> [l, 0, 999]} + \ ) + + call extend(l:position, l:middle_lines) + endif + + return l:position +endfunction + +function! lsp#utils#range#get_range() abort + let l:char = lsp#utils#to_char('%', line('$'), col('$')) + return {'start': {'line': 0, 'character': 0}, 'end': {'line': line('$')-1, 'character': l:char}} +endfunction + +function! lsp#utils#range#get_range_curline() abort + let l:char = lsp#utils#to_char('%', line('.'), col('$')) + return {'start': {'line': line('.')-1, 'character': 0}, 'end': {'line': line('.')-1, 'character': l:char}} +endfunction diff --git a/autoload/lsp/utils/step.vim b/autoload/lsp/utils/step.vim new file mode 100644 index 00000000..eed5ebd1 --- /dev/null +++ b/autoload/lsp/utils/step.vim @@ -0,0 +1,19 @@ +function! s:next(steps, current_index, result) abort + if len(a:steps) == a:current_index + return + endif + let l:Step = a:steps[a:current_index] + let l:ctx = { + \ 'callback': function('s:callback', [a:steps, a:current_index]), + \ 'result': a:result + \ } + call call(l:Step, [l:ctx]) +endfunction + +function! s:callback(steps, current_index, ...) abort + call s:next(a:steps, a:current_index + 1, a:000) +endfunction + +function! lsp#utils#step#start(steps) abort + call s:next(a:steps, 0, []) +endfunction diff --git a/autoload/lsp/utils/tagstack.vim b/autoload/lsp/utils/tagstack.vim new file mode 100644 index 00000000..839711d2 --- /dev/null +++ b/autoload/lsp/utils/tagstack.vim @@ -0,0 +1,33 @@ +if exists('*gettagstack') && exists('*settagstack') + function! lsp#utils#tagstack#_update() abort + let l:bufnr = bufnr('%') + let l:item = {'bufnr': l:bufnr, 'from': [l:bufnr, line('.'), col('.'), 0], 'tagname': expand('')} + let l:winid = win_getid() + + let l:stack = gettagstack(l:winid) + if l:stack['length'] == l:stack['curidx'] + " Replace the last items with item. + let l:action = 'r' + let l:stack['items'][l:stack['curidx']-1] = l:item + elseif l:stack['length'] > l:stack['curidx'] + " Replace items after used items with item. + let l:action = 'r' + if l:stack['curidx'] > 1 + let l:stack['items'] = add(l:stack['items'][:l:stack['curidx']-2], l:item) + else + let l:stack['items'] = [l:item] + endif + else + " Append item. + let l:action = 'a' + let l:stack['items'] = [l:item] + endif + let l:stack['curidx'] += 1 + + call settagstack(l:winid, l:stack, l:action) + endfunction +else + function! lsp#utils#tagstack#_update() abort + " do nothing + endfunction +endif diff --git a/autoload/lsp/utils/text_edit.vim b/autoload/lsp/utils/text_edit.vim new file mode 100644 index 00000000..e6e0c835 --- /dev/null +++ b/autoload/lsp/utils/text_edit.vim @@ -0,0 +1,228 @@ +function! lsp#utils#text_edit#get_range(text_edit) abort + if type(a:text_edit) != v:t_dict + return v:null + endif + let l:insert = get(a:text_edit, 'insert', v:null) + if type(l:insert) == v:t_dict + return l:insert + endif + return get(a:text_edit, 'range', v:null) +endfunction + +function! lsp#utils#text_edit#apply_text_edits(uri, text_edits) abort + let l:current_bufname = bufname('%') + let l:target_bufname = lsp#utils#uri_to_path(a:uri) + let l:cursor_position = lsp#get_position() + + call s:_switch(l:target_bufname) + for l:text_edit in s:_normalize(a:text_edits) + call s:_apply(bufnr(l:target_bufname), l:text_edit, l:cursor_position) + endfor + call s:_switch(l:current_bufname) + + if bufnr(l:current_bufname) == bufnr(l:target_bufname) + call cursor(lsp#utils#position#lsp_to_vim('%', l:cursor_position)) + endif +endfunction + +" @summary Use this to convert textedit to vim list that is compatible with +" quickfix and locllist items +" @param uri = DocumentUri +" @param text_edit = TextEdit | TextEdit[] +" @returns [] +function! lsp#utils#text_edit#_lsp_to_vim_list(uri, text_edit) abort + let l:result = [] + let l:cache = {} + if type(a:text_edit) == type([]) " TextEdit[] + for l:text_edit in a:text_edit + let l:vim_loc = s:lsp_text_edit_item_to_vim(a:uri, l:text_edit, l:cache) + if !empty(l:vim_loc) + call add(l:result, l:vim_loc) + endif + endfor + else " TextEdit + let l:vim_loc = s:lsp_text_edit_item_to_vim(a:uri, a:text_edit, l:cache) + if !empty(l:vim_loc) + call add(l:result, l:vim_loc) + endif + endif + return l:result +endfunction + +" @param uri = DocumentUri +" @param text_edit = TextEdit +" @param cache = {} empty dict +" @returns { +" 'filename', +" 'lnum', +" 'col', +" 'text', +" } +function! s:lsp_text_edit_item_to_vim(uri, text_edit, cache) abort + if !lsp#utils#is_file_uri(a:uri) + return v:null + endif + + let l:path = lsp#utils#uri_to_path(a:uri) + let l:range = a:text_edit['range'] + let [l:line, l:col] = lsp#utils#position#lsp_to_vim(l:path, l:range['start']) + + let l:index = l:line - 1 + if has_key(a:cache, l:path) + let l:text = a:cache[l:path][l:index] + else + let l:contents = getbufline(l:path, 1, '$') + if !empty(l:contents) + let l:text = get(l:contents, l:index, '') + else + let l:contents = readfile(l:path) + let a:cache[l:path] = l:contents + let l:text = get(l:contents, l:index, '') + endif + endif + + return { + \ 'filename': l:path, + \ 'lnum': l:line, + \ 'col': l:col, + \ 'text': l:text + \ } +endfunction + +" +" _apply +" +function! s:_apply(bufnr, text_edit, cursor_position) abort + " create before/after line. + let l:start_line = getline(a:text_edit['range']['start']['line'] + 1) + let l:end_line = getline(a:text_edit['range']['end']['line'] + 1) + let l:before_line = strcharpart(l:start_line, 0, a:text_edit['range']['start']['character']) + let l:after_line = strcharpart(l:end_line, a:text_edit['range']['end']['character'], strchars(l:end_line) - a:text_edit['range']['end']['character']) + + " create new lines. + let l:new_lines = lsp#utils#_split_by_eol(a:text_edit['newText']) + let l:new_lines[0] = l:before_line . l:new_lines[0] + let l:new_lines[-1] = l:new_lines[-1] . l:after_line + + " save length. + let l:new_lines_len = len(l:new_lines) + let l:range_len = (a:text_edit['range']['end']['line'] - a:text_edit['range']['start']['line']) + 1 + + " fixendofline + let l:buffer_length = len(getbufline(a:bufnr, '^', '$')) + let l:should_fixendofline = lsp#utils#buffer#_get_fixendofline(a:bufnr) + let l:should_fixendofline = l:should_fixendofline && l:new_lines[-1] ==# '' + let l:should_fixendofline = l:should_fixendofline && l:buffer_length <= a:text_edit['range']['end']['line'] + let l:should_fixendofline = l:should_fixendofline && a:text_edit['range']['end']['character'] == 0 + if l:should_fixendofline + call remove(l:new_lines, -1) + endif + + " fix cursor pos + if a:text_edit['range']['end']['line'] < a:cursor_position['line'] + " fix cursor line + let a:cursor_position['line'] += l:new_lines_len - l:range_len + elseif a:text_edit['range']['end']['line'] == a:cursor_position['line'] && a:text_edit['range']['end']['character'] <= a:cursor_position['character'] + " fix cursor line and col + let a:cursor_position['line'] += l:new_lines_len - l:range_len + let l:end_character = strchars(l:new_lines[-1]) - strchars(l:after_line) + let l:end_offset = a:cursor_position['character'] - a:text_edit['range']['end']['character'] + let a:cursor_position['character'] = l:end_character + l:end_offset + endif + + " append or delete lines. + if l:new_lines_len > l:range_len + call append(a:text_edit['range']['start']['line'], repeat([''], l:new_lines_len - l:range_len)) + elseif l:new_lines_len < l:range_len + let l:offset = l:range_len - l:new_lines_len + call s:delete(a:bufnr, a:text_edit['range']['start']['line'] + 1, a:text_edit['range']['start']['line'] + l:offset) + endif + + " set lines. + call setline(a:text_edit['range']['start']['line'] + 1, l:new_lines) +endfunction + +" +" _normalize +" +function! s:_normalize(text_edits) abort + let l:text_edits = type(a:text_edits) == type([]) ? a:text_edits : [a:text_edits] + let l:text_edits = filter(copy(l:text_edits), { _, text_edit -> type(text_edit) == type({}) }) + let l:text_edits = s:_range(l:text_edits) + let l:text_edits = sort(copy(l:text_edits), function('s:_compare', [], {})) + let l:text_edits = s:_check(l:text_edits) + return reverse(l:text_edits) +endfunction + +" +" _range +" +function! s:_range(text_edits) abort + for l:text_edit in a:text_edits + if l:text_edit.range.start.line > l:text_edit.range.end.line || ( + \ l:text_edit.range.start.line == l:text_edit.range.end.line && + \ l:text_edit.range.start.character > l:text_edit.range.end.character + \ ) + let l:text_edit.range = { 'start': l:text_edit.range.end, 'end': l:text_edit.range.start } + endif + endfor + return a:text_edits +endfunction + +" +" _check +" +" LSP Spec says `multiple text edits can not overlap those ranges`. +" This function check it. But does not throw error. +" +function! s:_check(text_edits) abort + if len(a:text_edits) > 1 + let l:range = a:text_edits[0].range + for l:text_edit in a:text_edits[1 : -1] + if l:range.end.line > l:text_edit.range.start.line || ( + \ l:range.end.line == l:text_edit.range.start.line && + \ l:range.end.character > l:text_edit.range.start.character + \ ) + call lsp#log('text_edit: range overlapped.') + endif + let l:range = l:text_edit.range + endfor + endif + return a:text_edits +endfunction + +" +" _compare +" +function! s:_compare(text_edit1, text_edit2) abort + let l:diff = a:text_edit1.range.start.line - a:text_edit2.range.start.line + if l:diff == 0 + return a:text_edit1.range.start.character - a:text_edit2.range.start.character + endif + return l:diff +endfunction + +" +" _switch +" +function! s:_switch(path) abort + if bufnr(a:path) == -1 + execute printf('badd %s', fnameescape(a:path)) + endif + execute printf('keepalt keepjumps %sbuffer!', bufnr(a:path)) +endfunction + +" +" delete +" +function! s:delete(bufnr, start, end) abort + if exists('*deletebufline') + call deletebufline(a:bufnr, a:start, a:end) + else + let l:foldenable = &foldenable + setlocal nofoldenable + execute printf('%s,%sdelete _', a:start, a:end) + let &foldenable = l:foldenable + endif +endfunction + diff --git a/autoload/lsp/utils/tree.vim b/autoload/lsp/utils/tree.vim new file mode 100644 index 00000000..b7da56da --- /dev/null +++ b/autoload/lsp/utils/tree.vim @@ -0,0 +1,295 @@ +" This file is part of an installation of vim-yggdrasil, a vim/neovim tree viewer library. +" The source code of vim-yggdrasil is available at https://github.com/m-pilia/vim-yggdrasil +" +" vim-yggdrasil is free software, distributed under the MIT license. +" The full license is available at https://github.com/m-pilia/vim-yggdrasil/blob/master/LICENSE +" +" Yggdrasil version (git SHA-1): 043d0ab53dcdd0d91b7c7cd205791d64d4ed9624 +" +" This installation was generated on 2020-03-15T14:47:27-0700 with the following vim command: +" :YggdrasilPlant -plugin_dir=./ -namespace=lsp/utils + +scriptencoding utf-8 + +" Callback to retrieve the tree item representation of an object. +function! s:node_get_tree_item_cb(node, object, status, tree_item) abort + if a:status ==? 'success' + let l:new_node = s:node_new(a:node.tree, a:object, a:tree_item, a:node) + call add(a:node.children, l:new_node) + call s:tree_render(l:new_node.tree) + endif +endfunction + +" Callback to retrieve the children objects of a node. +function! s:node_get_children_cb(node, status, childObjectList) abort + for l:childObject in a:childObjectList + let l:Callback = function('s:node_get_tree_item_cb', [a:node, l:childObject]) + call a:node.tree.provider.getTreeItem(l:Callback, l:childObject) + endfor +endfunction + +" Set the node to be collapsed or expanded. +" +" When {collapsed} evaluates to 0 the node is expanded, when it is 1 the node is +" collapsed, when it is equal to -1 the node is toggled (it is expanded if it +" was collapsed, and vice versa). +function! s:node_set_collapsed(collapsed) dict abort + let l:self.collapsed = a:collapsed < 0 ? !l:self.collapsed : !!a:collapsed +endfunction + +" Given a funcref {Condition}, return a list of all nodes in the subtree of +" {node} for which {Condition} evaluates to v:true. +function! s:search_subtree(node, Condition) abort + if a:Condition(a:node) + return [a:node] + endif + if len(a:node.children) < 1 + return [] + endif + let l:result = [] + for l:child in a:node.children + let l:result = l:result + s:search_subtree(l:child, a:Condition) + endfor + return l:result +endfunction + +" Execute the action associated to a node +function! s:node_exec() dict abort + if has_key(l:self.tree_item, 'command') + call l:self.tree_item.command() + endif +endfunction + +" Return the depth level of the node in the tree. The level is defined +" recursively: the root has depth 0, and each node has depth equal to the depth +" of its parent increased by 1. +function! s:node_level() dict abort + if l:self.parent == {} + return 0 + endif + return 1 + l:self.parent.level() +endf + +" Return the string representation of the node. The {level} argument represents +" the depth level of the node in the tree and it is passed for convenience, to +" simplify the implementation and to avoid re-computing the depth. +function! s:node_render(level) dict abort + let l:indent = repeat(' ', 2 * a:level) + let l:mark = '• ' + + if len(l:self.children) > 0 || l:self.lazy_open != v:false + let l:mark = l:self.collapsed ? '▸ ' : '▾ ' + endif + + let l:label = split(l:self.tree_item.label, "\n") + call extend(l:self.tree.index, map(range(len(l:label)), 'l:self')) + + let l:repr = l:indent . l:mark . l:label[0] + \ . join(map(l:label[1:], {_, l -> "\n" . l:indent . ' ' . l})) + + let l:lines = [l:repr] + if !l:self.collapsed + if l:self.lazy_open + let l:self.lazy_open = v:false + let l:Callback = function('s:node_get_children_cb', [l:self]) + call l:self.tree.provider.getChildren(l:Callback, l:self.object) + endif + for l:child in l:self.children + call add(l:lines, l:child.render(a:level + 1)) + endfor + endif + + return join(l:lines, "\n") +endfunction + +" Insert a new node in the tree, internally represented by a unique progressive +" integer identifier {id}. The node represents a certain {object} (children of +" {parent}) belonging to a given {tree}, having an associated action to be +" triggered on execution defined by the function object {exec}. If {collapsed} +" is true the node will be rendered as collapsed in the view. If {lazy_open} is +" true, the children of the node will be fetched when the node is expanded by +" the user. +function! s:node_new(tree, object, tree_item, parent) abort + let a:tree.maxid += 1 + return { + \ 'id': a:tree.maxid, + \ 'tree': a:tree, + \ 'object': a:object, + \ 'tree_item': a:tree_item, + \ 'parent': a:parent, + \ 'collapsed': a:tree_item.collapsibleState ==? 'collapsed', + \ 'lazy_open': a:tree_item.collapsibleState !=? 'none', + \ 'children': [], + \ 'level': function('s:node_level'), + \ 'exec': function('s:node_exec'), + \ 'set_collapsed': function('s:node_set_collapsed'), + \ 'render': function('s:node_render'), + \ } +endfunction + +" Callback that sets the root node of a given {tree}, creating a new node +" with a {tree_item} representation for the given {object}. If {status} is +" equal to 'success', the root node is set and the tree view is updated +" accordingly, otherwise nothing happens. +function! s:tree_set_root_cb(tree, object, status, tree_item) abort + if a:status ==? 'success' + let a:tree.maxid = -1 + let a:tree.root = s:node_new(a:tree, a:object, a:tree_item, {}) + call s:tree_render(a:tree) + endif +endfunction + +" Return the node currently under the cursor from the given {tree}. +function! s:get_node_under_cursor(tree) abort + let l:index = min([line('.'), len(a:tree.index) - 1]) + return a:tree.index[l:index] +endfunction + +" Expand or collapse the node under cursor, and render the tree. +" Please refer to *s:node_set_collapsed()* for details about the +" arguments and behaviour. +function! s:tree_set_collapsed_under_cursor(collapsed) dict abort + let l:node = s:get_node_under_cursor(l:self) + call l:node.set_collapsed(a:collapsed) + call s:tree_render(l:self) +endfunction + +" Run the action associated to the node currently under the cursor. +function! s:tree_exec_node_under_cursor() dict abort + call s:get_node_under_cursor(l:self).exec() +endfunction + +" Render the {tree}. This will replace the content of the buffer with the +" tree view. Clear the index, setting it to a list containing a guard +" value for index 0 (line numbers are one-based). +function! s:tree_render(tree) abort + if &filetype !=# 'lsp-tree' + return + endif + + let l:cursor = getpos('.') + let a:tree.index = [-1] + let l:text = a:tree.root.render(0) + + setlocal modifiable + silent 1,$delete _ + silent 0put=l:text + $d + setlocal nomodifiable + + call setpos('.', l:cursor) +endfunction + +" If {status} equals 'success', update all nodes of {tree} representing +" an {obect} with given {tree_item} representation. +function! s:node_update(tree, object, status, tree_item) abort + if a:status !=? 'success' + return + endif + for l:node in s:search_subtree(a:tree.root, {n -> n.object == a:object}) + let l:node.tree_item = a:tree_item + let l:node.children = [] + let l:node.lazy_open = a:tree_item.collapsibleState !=? 'none' + endfor + call s:tree_render(a:tree) +endfunction + +" Update the view if nodes have changed. If called with no arguments, +" update the whole tree. If called with an {object} as argument, update +" all the subtrees of nodes corresponding to {object}. +function! s:tree_update(...) dict abort + if a:0 < 1 + call l:self.provider.getChildren({status, obj -> + \ l:self.provider.getTreeItem(function('s:tree_set_root_cb', [l:self, obj[0]]), obj[0])}) + else + call l:self.provider.getTreeItem(function('s:node_update', [l:self, a:1]), a:1) + endif +endfunction + +" Destroy the tree view. Wipe out the buffer containing it. +function! s:tree_wipe() dict abort + execute 'bwipeout' . l:self.bufnr +endfunction + +" Apply syntax to an lsp-tree buffer +function! s:filetype_syntax() abort + syntax clear + syntax match LspTreeMarkLeaf "•" contained + syntax match LspTreeMarkCollapsed "▸" contained + syntax match LspTreeMarkExpanded "▾" contained + syntax match LspTreeNode "\v^(\s|[▸▾•])*.*" + \ contains=LspTreeMarkLeaf,LspTreeMarkCollapsed,LspTreeMarkExpanded + + highlight def link LspTreeMarkLeaf Type + highlight def link LspTreeMarkExpanded Type + highlight def link LspTreeMarkCollapsed Macro +endfunction + +" Apply local settings to an lsp-tree buffer +function! s:filetype_settings() abort + setlocal bufhidden=wipe + setlocal buftype=nofile + setlocal foldcolumn=0 + setlocal foldmethod=manual + setlocal nobuflisted + setlocal nofoldenable + setlocal nohlsearch + setlocal nolist + setlocal nomodifiable + setlocal nonumber + setlocal nospell + setlocal noswapfile + setlocal nowrap + + nnoremap (lsp-tree-toggle-node) + \ :call b:lsp_tree.set_collapsed_under_cursor(-1) + + nnoremap (lsp-tree-open-node) + \ :call b:lsp_tree.set_collapsed_under_cursor(v:false) + + nnoremap (lsp-tree-close-node) + \ :call b:lsp_tree.set_collapsed_under_cursor(v:true) + + nnoremap (lsp-tree-execute-node) + \ :call b:lsp_tree.exec_node_under_cursor() + + nnoremap (lsp-tree-wipe-tree) + \ :call b:lsp_tree.wipe() + + if !exists('g:lsp_tree_no_default_maps') + nmap o (lsp-tree-toggle-node) + nmap (lsp-tree-execute-node) + nmap q (lsp-tree-wipe-tree) + endif +endfunction + +" Turns the current buffer into an lsp-tree tree view. Tree data is retrieved +" from the given {provider}, and the state of the tree is stored in a +" buffer-local variable called b:lsp_tree. +" +" The {bufnr} stores the buffer number of the view, {maxid} is the highest +" known internal identifier of the nodes. The {index} is a list that +" maps line numbers to nodes. +function! lsp#utils#tree#new(provider) abort + let b:lsp_tree = { + \ 'bufnr': bufnr('%'), + \ 'maxid': -1, + \ 'root': {}, + \ 'index': [], + \ 'provider': a:provider, + \ 'set_collapsed_under_cursor': function('s:tree_set_collapsed_under_cursor'), + \ 'exec_node_under_cursor': function('s:tree_exec_node_under_cursor'), + \ 'update': function('s:tree_update'), + \ 'wipe': function('s:tree_wipe'), + \ } + + augroup vim_lsp_tree + autocmd! + autocmd FileType lsp-tree call s:filetype_syntax() | call s:filetype_settings() + autocmd BufEnter call s:tree_render(b:lsp_tree) + augroup END + + setlocal filetype=lsp-tree + + call b:lsp_tree.update() +endfunction diff --git a/autoload/lsp/utils/workspace_config.vim b/autoload/lsp/utils/workspace_config.vim new file mode 100644 index 00000000..0304f29f --- /dev/null +++ b/autoload/lsp/utils/workspace_config.vim @@ -0,0 +1,35 @@ +function! lsp#utils#workspace_config#get(server_name) abort + try + let l:server_info = lsp#get_server_info(a:server_name) + let l:config_type = type(l:server_info['workspace_config']) + + if l:config_type == v:t_func + let l:config = l:server_info['workspace_config'](l:server_info) + else + let l:config = l:server_info['workspace_config'] + endif + + return l:config + catch + return v:null + endtry +endfunction + +function! lsp#utils#workspace_config#projection(config, item) abort + try + let l:config = a:config + + for l:section in split(a:item['section'], '\.') + let l:config = l:config[l:section] + endfor + + return l:config + catch + return v:null + endtry +endfunction + +function! lsp#utils#workspace_config#get_value(server_name, item) abort + let l:config = lsp#utils#workspace_config#get(a:server_name) + return lsp#utils#workspace_config#projection(l:config, a:item) +endfunction diff --git a/autoload/lsp/utils/workspace_edit.vim b/autoload/lsp/utils/workspace_edit.vim new file mode 100644 index 00000000..5dafd26c --- /dev/null +++ b/autoload/lsp/utils/workspace_edit.vim @@ -0,0 +1,27 @@ +" Applies WorkspaceEdit changes. +function! lsp#utils#workspace_edit#apply_workspace_edit(workspace_edit) abort + let l:loclist_items = [] + + if has_key(a:workspace_edit, 'documentChanges') + for l:text_document_edit in a:workspace_edit['documentChanges'] + let l:loclist_items += s:_apply(l:text_document_edit['textDocument']['uri'], l:text_document_edit['edits']) + endfor + elseif has_key(a:workspace_edit, 'changes') + for [l:uri, l:text_edits] in items(a:workspace_edit['changes']) + let l:loclist_items += s:_apply(l:uri, l:text_edits) + endfor + endif + + if g:lsp_show_workspace_edits + call setloclist(0, l:loclist_items, 'r') + execute 'lopen' + endif +endfunction + +" +" _apply +" +function! s:_apply(uri, text_edits) abort + call lsp#utils#text_edit#apply_text_edits(a:uri, a:text_edits) + return lsp#utils#text_edit#_lsp_to_vim_list(a:uri, a:text_edits) +endfunction diff --git a/autoload/vital/_lsp.vim b/autoload/vital/_lsp.vim new file mode 100644 index 00000000..55104952 --- /dev/null +++ b/autoload/vital/_lsp.vim @@ -0,0 +1,9 @@ +let s:_plugin_name = expand(':t:r') + +function! vital#{s:_plugin_name}#new() abort + return vital#{s:_plugin_name[1:]}#new() +endfunction + +function! vital#{s:_plugin_name}#function(funcname) abort + silent! return function(a:funcname) +endfunction diff --git a/autoload/vital/_lsp/VS/LSP/MarkupContent.vim b/autoload/vital/_lsp/VS/LSP/MarkupContent.vim new file mode 100644 index 00000000..a54bd7ca --- /dev/null +++ b/autoload/vital/_lsp/VS/LSP/MarkupContent.vim @@ -0,0 +1,66 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_lsp#VS#LSP#MarkupContent#import() abort', printf("return map({'_vital_depends': '', 'normalize': '', '_vital_loaded': ''}, \"vital#_lsp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +" +" _vital_loaded +" +function! s:_vital_loaded(V) abort + let s:Text = a:V.import('VS.LSP.Text') +endfunction + +" +" _vital_depends +" +function! s:_vital_depends() abort + return ['VS.LSP.Text'] +endfunction + +" +" normalize +" +function! s:normalize(markup_content, ...) abort + let l:option = get(a:000, 0, {}) + let l:option.compact = get(l:option, 'compact', v:true) + + let l:normalized = '' + if type(a:markup_content) == type('') + let l:normalized = a:markup_content + elseif type(a:markup_content) == type([]) + let l:normalized = join(a:markup_content, "\n") + elseif type(a:markup_content) == type({}) + let l:normalized = a:markup_content.value + if has_key(a:markup_content, 'language') + let l:normalized = join([ + \ '```' . a:markup_content.language, + \ l:normalized, + \ '```' + \ ], "\n") + endif + endif + let l:normalized = s:Text.normalize_eol(l:normalized) + let l:normalized = s:_format(l:normalized, l:option.compact) + return l:normalized +endfunction + +" +" _format +" +function! s:_format(string, compact) abort + let l:string = a:string + if a:compact + let l:string = substitute(l:string, "\\%(\\s\\|\n\\)*```\\s*\\(\\w\\+\\)\\%(\\s\\|\n\\)\\+", "\n\n```\\1 ", 'g') + let l:string = substitute(l:string, "\\%(\\s\\|\n\\)\\+```\\%(\\s*\\%(\\%$\\|\n\\)\\)\\+", " ```\n\n", 'g') + else + let l:string = substitute(l:string, "```\n\\zs\\%(\\s\\|\n\\)\\+", "", 'g') + endif + let l:string = substitute(l:string, "\\%^\\%(\\s\\|\n\\)*", '', 'g') + let l:string = substitute(l:string, "\\%(\\s\\|\n\\)*\\%$", '', 'g') + return l:string +endfunction + diff --git a/autoload/vital/_lsp/VS/LSP/Text.vim b/autoload/vital/_lsp/VS/LSP/Text.vim new file mode 100644 index 00000000..f25040b9 --- /dev/null +++ b/autoload/vital/_lsp/VS/LSP/Text.vim @@ -0,0 +1,23 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_lsp#VS#LSP#Text#import() abort', printf("return map({'normalize_eol': '', 'split_by_eol': ''}, \"vital#_lsp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +" +" normalize_eol +" +function! s:normalize_eol(text) abort + return substitute(a:text, "\r\n\\|\r", "\n", 'g') +endfunction + +" +" split_by_eol +" +function! s:split_by_eol(text) abort + return split(a:text, "\r\n\\|\r\\|\n", v:true) +endfunction + diff --git a/autoload/vital/_lsp/VS/Vim/Buffer.vim b/autoload/vital/_lsp/VS/Vim/Buffer.vim new file mode 100644 index 00000000..08d18bb0 --- /dev/null +++ b/autoload/vital/_lsp/VS/Vim/Buffer.vim @@ -0,0 +1,140 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_lsp#VS#Vim#Buffer#import() abort', printf("return map({'add': '', 'do': '', 'create': '', 'get_line_count': '', 'pseudo': '', 'ensure': '', 'load': ''}, \"vital#_lsp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +let s:Do = { -> {} } + +let g:___VS_Vim_Buffer_id = get(g:, '___VS_Vim_Buffer_id', 0) + +" +" get_line_count +" +if exists('*nvim_buf_line_count') + function! s:get_line_count(bufnr) abort + return nvim_buf_line_count(a:bufnr) + endfunction +elseif has('patch-8.2.0019') + function! s:get_line_count(bufnr) abort + return getbufinfo(a:bufnr)[0].linecount + endfunction +else + function! s:get_line_count(bufnr) abort + if bufnr('%') == bufnr(a:bufnr) + return line('$') + endif + return len(getbufline(a:bufnr, '^', '$')) + endfunction +endif + +" +" create +" +function! s:create(...) abort + let g:___VS_Vim_Buffer_id += 1 + let l:bufname = printf('VS.Vim.Buffer: %s: %s', + \ g:___VS_Vim_Buffer_id, + \ get(a:000, 0, 'VS.Vim.Buffer.Default') + \ ) + return s:load(l:bufname) +endfunction + +" +" ensure +" +function! s:ensure(expr) abort + if !bufexists(a:expr) + if type(a:expr) == type(0) + throw printf('VS.Vim.Buffer: `%s` is not valid expr.', a:expr) + endif + call s:add(a:expr) + endif + return bufnr(a:expr) +endfunction + +" +" add +" +if exists('*bufadd') + function! s:add(name) abort + let l:bufnr = bufadd(a:name) + call setbufvar(l:bufnr, '&buflisted', 1) + endfunction +else + function! s:add(name) abort + badd `=a:name` + endfunction +endif + +" +" load +" +if exists('*bufload') + function! s:load(expr) abort + let l:bufnr = s:ensure(a:expr) + if !bufloaded(l:bufnr) + call bufload(l:bufnr) + endif + return l:bufnr + endfunction +else + function! s:load(expr) abort + let l:curr_bufnr = bufnr('%') + try + let l:bufnr = s:ensure(a:expr) + execute printf('keepalt keepjumps silent %sbuffer', l:bufnr) + catch /.*/ + echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) + finally + execute printf('noautocmd keepalt keepjumps silent %sbuffer', l:curr_bufnr) + endtry + return l:bufnr + endfunction +endif + +" +" do +" +function! s:do(bufnr, func) abort + let l:curr_bufnr = bufnr('%') + if l:curr_bufnr == a:bufnr + call a:func() + return + endif + + try + execute printf('noautocmd keepalt keepjumps silent %sbuffer', a:bufnr) + call a:func() + catch /.*/ + echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) + finally + execute printf('noautocmd keepalt keepjumps silent %sbuffer', l:curr_bufnr) + endtry +endfunction + +" +" pseudo +" +function! s:pseudo(filepath) abort + if !filereadable(a:filepath) + throw printf('VS.Vim.Buffer: `%s` is not valid filepath.', a:filepath) + endif + + " create pseudo buffer + let l:bufname = printf('VSVimBufferPseudo://%s', a:filepath) + if bufexists(l:bufname) + return s:ensure(l:bufname) + endif + + let l:bufnr = s:ensure(l:bufname) + let l:group = printf('VS_Vim_Buffer_pseudo:%s', l:bufnr) + execute printf('augroup %s', l:group) + execute printf('autocmd BufReadCmd call setline(1, readfile(bufname("%")[20 : -1])) | try | filetype detect | catch /.*/ | endtry | augroup %s | autocmd! | augroup END', l:bufnr, l:group) + augroup END + return l:bufnr +endfunction + diff --git a/autoload/vital/_lsp/VS/Vim/Syntax/Markdown.vim b/autoload/vital/_lsp/VS/Vim/Syntax/Markdown.vim new file mode 100644 index 00000000..8dddd3a2 --- /dev/null +++ b/autoload/vital/_lsp/VS/Vim/Syntax/Markdown.vim @@ -0,0 +1,155 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_lsp#VS#Vim#Syntax#Markdown#import() abort', printf("return map({'apply': ''}, \"vital#_lsp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +" +" apply +" +" TODO: Refactor +" +function! s:apply(...) abort + let l:args = get(a:000, 0, {}) + let l:text = has_key(l:args, 'text') ? l:args.text : getbufline('%', 1, '$') + let l:text = type(l:text) == v:t_list ? join(l:text, "\n") : l:text + + call s:_execute('syntax sync clear') + if !exists('b:___VS_Vim_Syntax_Markdown') + " Avoid automatic highlighting by built-in runtime syntax. + if !has_key(g:, 'markdown_fenced_languages') + call s:_execute('runtime! syntax/markdown.vim') + else + let l:markdown_fenced_languages = g:markdown_fenced_languages + unlet g:markdown_fenced_languages + call s:_execute('runtime! syntax/markdown.vim') + let g:markdown_fenced_languages = l:markdown_fenced_languages + endif + + " Remove markdownCodeBlock because we support it manually. + call s:_clear('markdownCodeBlock') " runtime + call s:_clear('mkdCode') " plasticboy/vim-markdown + + " Modify markdownCode (`codes...`) + call s:_clear('markdownCode') + syntax region markdownCode matchgroup=Conceal start=/\%(``\)\@!`/ matchgroup=Conceal end=/\%(``\)\@!`/ containedin=TOP keepend concealends + + " Modify markdownEscape (_bold\_text_) @see nvim's syntax/lsp_markdown.vim + call s:_clear('markdownEscape') + syntax region markdownEscape matchgroup=markdownEscape start=/\\\ze[\\\x60*{}\[\]()#+\-,.!_>~|"$%&'\/:;<=?@^ ]/ end=/./ containedin=ALL keepend oneline concealends + + " Add syntax for basic html entities. + syntax match vital_vs_vim_syntax_markdown_entities_lt /</ containedin=ALL conceal cchar=< + syntax match vital_vs_vim_syntax_markdown_entities_gt />/ containedin=ALL conceal cchar=> + syntax match vital_vs_vim_syntax_markdown_entities_amp /&/ containedin=ALL conceal cchar=& + syntax match vital_vs_vim_syntax_markdown_entities_quot /"/ containedin=ALL conceal cchar=" + syntax match vital_vs_vim_syntax_markdown_entities_nbsp / / containedin=ALL conceal cchar= + + let b:___VS_Vim_Syntax_Markdown = {} + let b:___VS_Vim_Syntax_Markdown.marks = {} + let b:___VS_Vim_Syntax_Markdown.filetypes = {} + endif + + for [l:mark, l:filetype] in items(s:_get_filetype_map(l:text)) + try + let l:mark_group = substitute(toupper(l:mark), '\.', '_', 'g') + if has_key(b:___VS_Vim_Syntax_Markdown.marks, l:mark_group) + continue + endif + let b:___VS_Vim_Syntax_Markdown.marks[l:mark_group] = v:true + + let l:filetype_group = substitute(toupper(l:filetype), '\.', '_', 'g') + if !has_key(b:___VS_Vim_Syntax_Markdown.filetypes, l:filetype_group) + call s:_execute('syntax include @%s syntax/%s.vim', l:filetype_group, l:filetype) + let b:___VS_Vim_Syntax_Markdown.filetypes[l:filetype_group] = v:true + endif + + call s:_execute('syntax region %s matchgroup=Conceal start=/%s/ matchgroup=Conceal end=/%s/ contains=@%s containedin=TOP keepend concealends', + \ l:mark_group, + \ printf('```\s*%s\s*', l:mark), + \ '```\s*\%(' . "\n" . '\|$\)', + \ l:filetype_group + \ ) + catch /.*/ + unsilent echomsg printf('Fail to apply "syntax/%s.vim". Add "let g:markdown_fenced_languages = ["%s=$FILETYPE"]" to enable syntax', l:filetype, l:filetype) + endtry + endfor +endfunction + +" +" _clear +" +function! s:_clear(group) abort + try + execute printf('silent! syntax clear %s', a:group) + catch /.*/ + endtry +endfunction + +" +" _execute +" +function! s:_execute(command, ...) abort + let b:current_syntax = '' + unlet b:current_syntax + + let g:main_syntax = '' + unlet g:main_syntax + + execute call('printf', [a:command] + a:000) +endfunction + +" +" _get_filetype_map +" +function! s:_get_filetype_map(text) abort + let l:filetype_map = {} + for l:mark in s:_find_marks(a:text) + let l:filetype_map[l:mark] = s:_get_filetype_from_mark(l:mark) + endfor + return l:filetype_map +endfunction + +" +" _find_marks +" +function! s:_find_marks(text) abort + let l:marks = {} + + " find from buffer contents. + let l:text = a:text + let l:pos = 0 + while 1 + let l:match = matchstrpos(l:text, '```\s*\zs\w\+', l:pos, 1) + if empty(l:match[0]) + break + endif + let l:marks[l:match[0]] = v:true + let l:pos = l:match[2] + endwhile + + return keys(l:marks) +endfunction + +" +" _get_filetype_from_mark +" +function! s:_get_filetype_from_mark(mark) abort + for l:config in get(g:, 'markdown_fenced_languages', []) + if l:config !~# '=' + if l:config ==# a:mark + return a:mark + endif + else + let l:config = split(l:config, '=') + if l:config[0] ==# a:mark + return l:config[1] + endif + endif + endfor + return a:mark +endfunction + diff --git a/autoload/vital/_lsp/VS/Vim/Window.vim b/autoload/vital/_lsp/VS/Vim/Window.vim new file mode 100644 index 00000000..db54988f --- /dev/null +++ b/autoload/vital/_lsp/VS/Vim/Window.vim @@ -0,0 +1,207 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_lsp#VS#Vim#Window#import() abort', printf("return map({'info': '', 'do': '', 'is_floating': '', 'find': '', 'scroll': '', 'screenpos': ''}, \"vital#_lsp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +let s:Do = { -> {} } + +" +" do +" +function! s:do(winid, func) abort + let l:curr_winid = win_getid() + if l:curr_winid == a:winid + call a:func() + return + endif + + if !has('nvim') && exists('*win_execute') + let s:Do = a:func + try + noautocmd keepalt keepjumps call win_execute(a:winid, 'call s:Do()') + catch /.*/ + echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) + endtry + unlet s:Do + return + endif + + noautocmd keepalt keepjumps call win_gotoid(a:winid) + try + call a:func() + catch /.*/ + echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint }) + endtry + noautocmd keepalt keepjumps call win_gotoid(l:curr_winid) +endfunction + +" +" info +" +if has('nvim') + function! s:info(winid) abort + let l:info = getwininfo(a:winid)[0] + + if s:is_floating(a:winid) + let l:config = nvim_win_get_config(a:winid) + let l:config.border = get(l:config, 'border', 'none') + if type(l:config.border) !=# type([]) + if index(['rounded', 'single', 'double', 'solid'], l:config.border) >= 0 + let l:width_off = 2 + let l:height_off = 2 + elseif l:config.border ==# 'shadow' + let l:width_off = 1 + let l:height_off = 1 + else + let l:width_off = 0 + let l:height_off = 0 + endif + else + let l:has_top = v:false + let l:has_top = l:has_top || get(l:config.border, 0, '') !=# '' + let l:has_top = l:has_top || get(l:config.border, 1, '') !=# '' + let l:has_top = l:has_top || get(l:config.border, 2, '') !=# '' + let l:has_right = v:false + let l:has_right = l:has_right || get(l:config.border, 2, '') !=# '' + let l:has_right = l:has_right || get(l:config.border, 3, '') !=# '' + let l:has_right = l:has_right || get(l:config.border, 4, '') !=# '' + let l:has_bottom = v:false + let l:has_bottom = l:has_bottom || get(l:config.border, 4, '') !=# '' + let l:has_bottom = l:has_bottom || get(l:config.border, 5, '') !=# '' + let l:has_bottom = l:has_bottom || get(l:config.border, 6, '') !=# '' + let l:has_left = v:false + let l:has_left = l:has_left || get(l:config.border, 6, '') !=# '' + let l:has_left = l:has_left || get(l:config.border, 7, '') !=# '' + let l:has_left = l:has_left || get(l:config.border, 0, '') !=# '' + + let l:width_off = (l:has_left ? 1 : 0) + (l:has_right ? 1 : 0) + let l:height_off = (l:has_top ? 1 : 0) + (l:has_bottom ? 1 : 0) + endif + let l:left = get(l:config, '') + let l:info.core_width = l:config.width - l:width_off + let l:info.core_height = l:config.height - l:height_off + else + let l:info.core_width = l:info.width + let l:info.core_height = l:info.height + endif + + return { + \ 'width': l:info.width, + \ 'height': l:info.height, + \ 'core_width': l:info.core_width, + \ 'core_height': l:info.core_height, + \ 'topline': l:info.topline, + \ } + endfunction +else + function! s:info(winid) abort + if s:is_floating(a:winid) + let l:info = popup_getpos(a:winid) + return { + \ 'width': l:info.width, + \ 'height': l:info.height, + \ 'core_width': l:info.core_width, + \ 'core_height': l:info.core_height, + \ 'topline': l:info.firstline + \ } + endif + + let l:ctx = {} + let l:ctx.info = {} + function! l:ctx.callback() abort + let self.info.width = winwidth(0) + let self.info.height = winheight(0) + let self.info.core_width = self.info.width + let self.info.core_height = self.info.height + let self.info.topline = line('w0') + endfunction + call s:do(a:winid, { -> l:ctx.callback() }) + return l:ctx.info + endfunction +endif + +" +" find +" +function! s:find(callback) abort + let l:winids = [] + let l:winids += map(range(1, tabpagewinnr(tabpagenr(), '$')), 'win_getid(v:val)') + let l:winids += s:_get_visible_popup_winids() + return filter(l:winids, 'a:callback(v:val)') +endfunction + +" +" is_floating +" +if has('nvim') + function! s:is_floating(winid) abort + let l:config = nvim_win_get_config(a:winid) + return empty(l:config) || !empty(get(l:config, 'relative', '')) + endfunction +else + function! s:is_floating(winid) abort + return winheight(a:winid) != -1 && win_id2win(a:winid) == 0 + endfunction +endif + +" +" scroll +" +function! s:scroll(winid, topline) abort + let l:ctx = {} + function! l:ctx.callback(winid, topline) abort + let l:wininfo = s:info(a:winid) + let l:topline = a:topline + let l:topline = min([l:topline, line('$') - l:wininfo.core_height + 1]) + let l:topline = max([l:topline, 1]) + + if l:topline == l:wininfo.topline + return + endif + + if !has('nvim') && s:is_floating(a:winid) + call popup_setoptions(a:winid, { + \ 'firstline': l:topline, + \ }) + else + let l:delta = l:topline - l:wininfo.topline + let l:key = l:delta > 0 ? "\" : "\" + execute printf('noautocmd silent normal! %s', repeat(l:key, abs(l:delta))) + endif + endfunction + call s:do(a:winid, { -> l:ctx.callback(a:winid, a:topline) }) +endfunction + +" +" screenpos +" +" @param {[number, number]} pos - position on the current buffer. +" +function! s:screenpos(pos) abort + let l:y = a:pos[0] + let l:x = a:pos[1] + get(a:pos, 2, 0) + + let l:view = winsaveview() + let l:scroll_x = l:view.leftcol + let l:scroll_y = l:view.topline + + let l:winpos = win_screenpos(win_getid()) + let l:y = l:winpos[0] + l:y - l:scroll_y + let l:x = l:winpos[1] + l:x - l:scroll_x + return [l:y, l:x + (wincol() - virtcol('.')) - 1] +endfunction + +" +" _get_visible_popup_winids +" +function! s:_get_visible_popup_winids() abort + if !exists('*popup_list') + return [] + endif + return filter(popup_list(), 'popup_getpos(v:val).visible') +endfunction + diff --git a/autoload/vital/_lsp/VS/Vim/Window/FloatingWindow.vim b/autoload/vital/_lsp/VS/Vim/Window/FloatingWindow.vim new file mode 100644 index 00000000..fa1f888f --- /dev/null +++ b/autoload/vital/_lsp/VS/Vim/Window/FloatingWindow.vim @@ -0,0 +1,515 @@ +" ___vital___ +" NOTE: lines between '" ___vital___' is generated by :Vitalize. +" Do not modify the code nor insert new lines before '" ___vital___' +function! s:_SID() abort + return matchstr(expand(''), '\zs\d\+\ze__SID$') +endfunction +execute join(['function! vital#_lsp#VS#Vim#Window#FloatingWindow#import() abort', printf("return map({'_vital_depends': '', 'is_available': '', 'new': '', '_vital_loaded': ''}, \"vital#_lsp#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +delfunction s:_SID +" ___vital___ +" +" _vital_loaded +" +function! s:_vital_loaded(V) abort + let s:Window = a:V.import('VS.Vim.Window') +endfunction + +" +" _vital_depends +" +function! s:_vital_depends() abort + return ['VS.Vim.Window'] +endfunction + +" +" managed floating windows. +" +let s:floating_windows = {} + +" +" is_available +" +function! s:is_available() abort + if has('nvim') + return v:true + endif + return exists('*popup_create') && exists('*popup_close') && exists('*popup_move') && exists('*popup_getpos') +endfunction + +" +" new +" +function! s:new(...) abort + call s:_init() + + return s:FloatingWindow.new(get(a:000, 0, {})) +endfunction + +" +" _notify_opened +" +" @param {number} winid +" @param {VS.Vim.Window.FloatingWindow} floating_window +" +function! s:_notify_opened(winid, floating_window) abort + let s:floating_windows[a:winid] = a:floating_window + call a:floating_window._on_opened() +endfunction + +" +" _notify_closed +" +function! s:_notify_closed() abort + for [l:winid, l:floating_window] in items(s:floating_windows) + if winheight(l:winid) == -1 + call l:floating_window._on_closed() + unlet s:floating_windows[l:winid] + endif + endfor +endfunction + +let s:FloatingWindow = {} + +" +" new +" +" @param {function?} args.on_opened +" @param {function?} args.on_closed +" +function! s:FloatingWindow.new(args) abort + return extend(deepcopy(s:FloatingWindow), { + \ '_winid': v:null, + \ '_bufnr': v:null, + \ '_vars': {}, + \ '_on_opened': get(a:args, 'on_opened', { -> {} }), + \ '_on_closed': get(a:args, 'on_closed', { -> {} }), + \ }) +endfunction + +" +" get_size +" +" @param {number?} args.minwidth +" @param {number?} args.maxwidth +" @param {number?} args.minheight +" @param {number?} args.maxheight +" @param {boolean?} args.wrap +" +function! s:FloatingWindow.get_size(args) abort + if self._bufnr is# v:null + throw 'VS.Vim.Window.FloatingWindow: Failed to detect bufnr.' + endif + + let l:maxwidth = get(a:args, 'maxwidth', -1) + let l:minwidth = get(a:args, 'minwidth', -1) + let l:maxheight = get(a:args, 'maxheight', -1) + let l:minheight = get(a:args, 'minheight', -1) + let l:lines = getbufline(self._bufnr, '^', '$') + + " width + let l:width = 0 + for l:line in l:lines + let l:width = max([l:width, strdisplaywidth(l:line)]) + endfor + + let l:width = l:minwidth == -1 ? l:width : max([l:minwidth, l:width]) + let l:width = l:maxwidth == -1 ? l:width : min([l:maxwidth, l:width]) + + " height + if get(a:args, 'wrap', get(self._vars, '&wrap', 0)) + let l:height = 0 + for l:line in l:lines + let l:height += max([1, float2nr(ceil(strdisplaywidth(l:line) / str2float('' . l:width)))]) + endfor + else + let l:height = len(l:lines) + endif + let l:height = l:minheight == -1 ? l:height : max([l:minheight, l:height]) + let l:height = l:maxheight == -1 ? l:height : min([l:maxheight, l:height]) + + return { + \ 'width': max([1, l:width]), + \ 'height': max([1, l:height]), + \ } +endfunction + +" +" set_bufnr +" +" @param {number} bufnr +" +function! s:FloatingWindow.set_bufnr(bufnr) abort + let self._bufnr = a:bufnr +endfunction + +" +" get_bufnr +" +function! s:FloatingWindow.get_bufnr() abort + return self._bufnr +endfunction + +" +" get_winid +" +function! s:FloatingWindow.get_winid() abort + if self.is_visible() + return self._winid + endif + return v:null +endfunction + +" +" info +" +function! s:FloatingWindow.info() abort + if self.is_visible() + return s:_info(self._winid) + endif + return v:null +endfunction + +" +" set_var +" +" @param {string} key +" @param {unknown} value +" +function! s:FloatingWindow.set_var(key, value) abort + let self._vars[a:key] = a:value + if self.is_visible() + call setwinvar(self._winid, a:key, a:value) + endif +endfunction + +" +" get_var +" +" @param {string} key +" +function! s:FloatingWindow.get_var(key) abort + return self._vars[a:key] +endfunction + +" +" open +" +" @param {number} args.row 0-based indexing +" @param {number} args.col 0-based indexing +" @param {number} args.width +" @param {number} args.height +" @param {boolean|[string]?} args.border - boolean, or list of characters +" clockwise from top-left (same as nvim_open_win() in neovim) +" @param {number?} args.topline +" @param {string?} args.origin - topleft/topright/botleft/botright +" +function! s:FloatingWindow.open(args) abort + let l:style = { + \ 'row': a:args.row, + \ 'col': a:args.col, + \ 'width': a:args.width, + \ 'height': a:args.height, + \ 'border': get(a:args, 'border', v:false), + \ 'topline': get(a:args, 'topline', 1), + \ 'origin': get(a:args, 'origin', 'topleft'), + \ } + + let l:will_move = self.is_visible() + if l:will_move + let self._winid = s:_move(self, self._winid, self._bufnr, l:style) + else + let self._winid = s:_open(self._bufnr, l:style, { -> self._on_closed() }) + endif + for [l:key, l:value] in items(self._vars) + call setwinvar(self._winid, l:key, l:value) + endfor + if !l:will_move + call s:_notify_opened(self._winid, self) + endif +endfunction + +" +" close +" +function! s:FloatingWindow.close() abort + if self.is_visible() + call s:_close(self._winid) + endif + let self._winid = v:null +endfunction + +" +" enter +" +function! s:FloatingWindow.enter() abort + call s:_enter(self._winid) +endfunction + +" +" is_visible +" +function! s:FloatingWindow.is_visible() abort + return s:_exists(self._winid) ? v:true : v:false +endfunction + +" +" open +" +if has('nvim') + function! s:_open(bufnr, style, callback) abort + let l:winid = nvim_open_win(a:bufnr, v:false, s:_style(a:style)) + call s:Window.scroll(l:winid, a:style.topline) + return l:winid + endfunction +else + function! s:_open(bufnr, style, callback) abort + return popup_create(a:bufnr, extend(s:_style(a:style), { + \ 'callback': a:callback, + \ }, 'force')) + endfunction +endif + +" +" close +" +if has('nvim') + function! s:_close(winid) abort + call nvim_win_close(a:winid, v:true) + call s:_notify_closed() + endfunction +else + function! s:_close(winid) abort + call popup_close(a:winid) + endfunction +endif + +" +" move +" +if has('nvim') + function! s:_move(self, winid, bufnr, style) abort + call nvim_win_set_config(a:winid, s:_style(a:style)) + if a:bufnr != nvim_win_get_buf(a:winid) + call nvim_win_set_buf(a:winid, a:bufnr) + endif + call s:Window.scroll(a:winid, a:style.topline) + return a:winid + endfunction +else + function! s:_move(self, winid, bufnr, style) abort + " vim's popup window can't change bufnr so open new popup in here. + if a:bufnr != winbufnr(a:winid) + let l:On_closed = a:self._on_closed + let a:self._on_closed = { -> {} } + call s:_close(a:winid) + let a:self._on_closed = l:On_closed + return s:_open(a:bufnr, a:style, { -> a:self._on_closed() }) + endif + let l:style = s:_style(a:style) + call popup_move(a:winid, l:style) + call popup_setoptions(a:winid, l:style) + return a:winid + endfunction +endif + +" +" enter +" +if has('nvim') + function! s:_enter(winid) abort + call win_gotoid(a:winid) + endfunction +else + function! s:_enter(winid) abort + " not supported. + endfunction +endif + +" +" exists +" +if has('nvim') + function! s:_exists(winid) abort + try + return type(a:winid) == type(0) && nvim_win_is_valid(a:winid) && nvim_win_get_number(a:winid) != -1 + catch /.*/ + return v:false + endtry + endfunction +else + function! s:_exists(winid) abort + return type(a:winid) == type(0) && winheight(a:winid) != -1 + endfunction +endif + +" +" info +" +if has('nvim') + function! s:_info(winid) abort + let l:info = getwininfo(a:winid)[0] + return { + \ 'row': l:info.winrow, + \ 'col': l:info.wincol, + \ 'width': l:info.width, + \ 'height': l:info.height, + \ 'topline': l:info.topline, + \ } + endfunction +else + function! s:_info(winid) abort + let l:pos = popup_getpos(a:winid) + return { + \ 'row': l:pos.core_line, + \ 'col': l:pos.core_col, + \ 'width': l:pos.core_width, + \ 'height': l:pos.core_height, + \ 'topline': l:pos.firstline, + \ } + endfunction +endif + +" +" style +" +if has('nvim') + function! s:_style(style) abort + let l:style = s:_resolve_origin(a:style) + let l:style = s:_resolve_border(l:style) + let l:style = { + \ 'relative': 'editor', + \ 'row': l:style.row - 1, + \ 'col': l:style.col - 1, + \ 'width': l:style.width, + \ 'height': l:style.height, + \ 'focusable': v:true, + \ 'style': 'minimal', + \ 'border': has_key(l:style, 'border') ? l:style.border : 'none', + \ } + if !exists('*win_execute') " We can't detect neovim features via patch version so we try it by function existence. + unlet l:style.border + endif + return l:style + endfunction +else + function! s:_style(style) abort + let l:style = s:_resolve_origin(a:style) + let l:style = s:_resolve_border(l:style) + return { + \ 'line': l:style.row, + \ 'col': l:style.col, + \ 'pos': 'topleft', + \ 'wrap': v:false, + \ 'moved': [0, 0, 0], + \ 'scrollbar': 1, + \ 'maxwidth': l:style.width, + \ 'maxheight': l:style.height, + \ 'minwidth': l:style.width, + \ 'minheight': l:style.height, + \ 'tabpage': 0, + \ 'firstline': l:style.topline, + \ 'padding': [0, 0, 0, 0], + \ 'border': has_key(l:style, 'border') ? [1, 1, 1, 1] : [0, 0, 0, 0], + \ 'borderchars': get(l:style, 'border', []), + \ 'fixed': v:true, + \ } + endfunction +endif + +" +" _resolve_origin +" +function! s:_resolve_origin(style) abort + if index(['topleft', 'topright', 'bottomleft', 'bottomright', 'topcenter', 'bottomcenter'], a:style.origin) == -1 + let a:style.origin = 'topleft' + endif + + if a:style.origin ==# 'topleft' + let a:style.col = a:style.col + let a:style.row = a:style.row + elseif a:style.origin ==# 'topright' + let a:style.col = a:style.col - (a:style.width - 1) + let a:style.row = a:style.row + elseif a:style.origin ==# 'bottomleft' + let a:style.col = a:style.col + let a:style.row = a:style.row - (a:style.height - 1) + elseif a:style.origin ==# 'bottomright' + let a:style.col = a:style.col - (a:style.width - 1) + let a:style.row = a:style.row - (a:style.height - 1) + elseif a:style.origin ==# 'topcenter' + let a:style.col = a:style.col - float2nr(a:style.width / 2) + let a:style.row = a:style.row + elseif a:style.origin ==# 'bottomcenter' + let a:style.col = a:style.col - float2nr(a:style.width / 2) + let a:style.row = a:style.row - (a:style.height - 1) + elseif a:style.origin ==# 'centercenter' + let a:style.col = a:style.col - float2nr(a:style.width / 2) + let a:style.row = a:style.row - float2nr(a:style.height / 2) + endif + return a:style +endfunction + +if has('nvim') + function! s:_resolve_border(style) abort + let l:border = get(a:style, 'border', v:null) + if !empty(l:border) + if type(l:border) != type([]) + if &ambiwidth ==# 'single' + let a:style.border = ['┌', '─', '┐', '│', '┘', '─', '└', '│'] + else + let a:style.border = ['+', '-', '+', '|', '+', '-', '+', '|'] + endif + endif + elseif has_key(a:style, 'border') + unlet a:style.border + endif + return a:style + endfunction +else + function! s:_resolve_border(style) abort + let l:border = get(a:style, 'border', v:null) + if !empty(get(a:style, 'border', v:null)) + if type(l:border) != type([]) + if &ambiwidth ==# 'single' + let a:style.border = ['─', '│', '─', '│', '┌', '┐', '┘', '└'] + else + let a:style.border = ['-', '|', '-', '|', '+', '+', '+', '+'] + endif + else + " Emulate nvim behavior for lists of 1/2/4 elements + let l:topleft = l:border[0] + let l:top = get(l:border, 1, l:topleft) + let l:topright = get(l:border, 2, l:topleft) + let l:right = get(l:border, 3, l:top) + let l:bottomright = get(l:border, 4, l:topleft) + let l:bottom = get(l:border, 5, l:top) + let l:bottomleft = get(l:border, 6, l:topright) + let l:left = get(l:border, 7, l:right) + let a:style.border = [ + \ l:top, l:right, l:bottom, l:left, + \ l:topleft, l:topright, l:bottomright, l:bottomleft, + \ ] + endif + elseif has_key(a:style, 'border') + unlet a:style.border + endif + return a:style + endfunction +endif + +" +" init +" +let s:has_init = v:false +let s:filepath = expand(':p') +function! s:_init() abort + if s:has_init || !has('nvim') + return + endif + let s:has_init = v:true + execute printf('augroup VS_Vim_Window_FloatingWindow:%s', s:filepath) + autocmd! + autocmd WinEnter * call _notify_closed() + augroup END +endfunction + diff --git a/autoload/vital/lsp.vim b/autoload/vital/lsp.vim new file mode 100644 index 00000000..5b7b1027 --- /dev/null +++ b/autoload/vital/lsp.vim @@ -0,0 +1,334 @@ +let s:plugin_name = expand(':t:r') +let s:vital_base_dir = expand(':h') +let s:project_root = expand(':h:h:h') +let s:is_vital_vim = s:plugin_name is# 'vital' + +let s:loaded = {} +let s:cache_sid = {} + +function! vital#{s:plugin_name}#new() abort + return s:new(s:plugin_name) +endfunction + +function! vital#{s:plugin_name}#import(...) abort + if !exists('s:V') + let s:V = s:new(s:plugin_name) + endif + return call(s:V.import, a:000, s:V) +endfunction + +let s:Vital = {} + +function! s:new(plugin_name) abort + let base = deepcopy(s:Vital) + let base._plugin_name = a:plugin_name + return base +endfunction + +function! s:vital_files() abort + if !exists('s:vital_files') + let s:vital_files = map( + \ s:is_vital_vim ? s:_global_vital_files() : s:_self_vital_files(), + \ 'fnamemodify(v:val, ":p:gs?[\\\\/]?/?")') + endif + return copy(s:vital_files) +endfunction +let s:Vital.vital_files = function('s:vital_files') + +function! s:import(name, ...) abort dict + let target = {} + let functions = [] + for a in a:000 + if type(a) == type({}) + let target = a + elseif type(a) == type([]) + let functions = a + endif + unlet a + endfor + let module = self._import(a:name) + if empty(functions) + call extend(target, module, 'keep') + else + for f in functions + if has_key(module, f) && !has_key(target, f) + let target[f] = module[f] + endif + endfor + endif + return target +endfunction +let s:Vital.import = function('s:import') + +function! s:load(...) abort dict + for arg in a:000 + let [name; as] = type(arg) == type([]) ? arg[: 1] : [arg, arg] + let target = split(join(as, ''), '\W\+') + let dict = self + let dict_type = type({}) + while !empty(target) + let ns = remove(target, 0) + if !has_key(dict, ns) + let dict[ns] = {} + endif + if type(dict[ns]) == dict_type + let dict = dict[ns] + else + unlet dict + break + endif + endwhile + if exists('dict') + call extend(dict, self._import(name)) + endif + unlet arg + endfor + return self +endfunction +let s:Vital.load = function('s:load') + +function! s:unload() abort dict + let s:loaded = {} + let s:cache_sid = {} + unlet! s:vital_files +endfunction +let s:Vital.unload = function('s:unload') + +function! s:exists(name) abort dict + if a:name !~# '\v^\u\w*%(\.\u\w*)*$' + throw 'vital: Invalid module name: ' . a:name + endif + return s:_module_path(a:name) isnot# '' +endfunction +let s:Vital.exists = function('s:exists') + +function! s:search(pattern) abort dict + let paths = s:_extract_files(a:pattern, self.vital_files()) + let modules = sort(map(paths, 's:_file2module(v:val)')) + return uniq(modules) +endfunction +let s:Vital.search = function('s:search') + +function! s:plugin_name() abort dict + return self._plugin_name +endfunction +let s:Vital.plugin_name = function('s:plugin_name') + +function! s:_self_vital_files() abort + let builtin = printf('%s/__%s__/', s:vital_base_dir, s:plugin_name) + let installed = printf('%s/_%s/', s:vital_base_dir, s:plugin_name) + let base = builtin . ',' . installed + return split(globpath(base, '**/*.vim', 1), "\n") +endfunction + +function! s:_global_vital_files() abort + let pattern = 'autoload/vital/__*__/**/*.vim' + return split(globpath(&runtimepath, pattern, 1), "\n") +endfunction + +function! s:_extract_files(pattern, files) abort + let tr = {'.': '/', '*': '[^/]*', '**': '.*'} + let target = substitute(a:pattern, '\.\|\*\*\?', '\=tr[submatch(0)]', 'g') + let regexp = printf('autoload/vital/[^/]\+/%s.vim$', target) + return filter(a:files, 'v:val =~# regexp') +endfunction + +function! s:_file2module(file) abort + let filename = fnamemodify(a:file, ':p:gs?[\\/]?/?') + let tail = matchstr(filename, 'autoload/vital/_\w\+/\zs.*\ze\.vim$') + return join(split(tail, '[\\/]\+'), '.') +endfunction + +" @param {string} name e.g. Data.List +function! s:_import(name) abort dict + if has_key(s:loaded, a:name) + return copy(s:loaded[a:name]) + endif + let module = self._get_module(a:name) + if has_key(module, '_vital_created') + call module._vital_created(module) + endif + let export_module = filter(copy(module), 'v:key =~# "^\\a"') + " Cache module before calling module._vital_loaded() to avoid cyclic + " dependences but remove the cache if module._vital_loaded() fails. + " let s:loaded[a:name] = export_module + let s:loaded[a:name] = export_module + if has_key(module, '_vital_loaded') + try + call module._vital_loaded(vital#{s:plugin_name}#new()) + catch + unlet s:loaded[a:name] + throw 'vital: fail to call ._vital_loaded(): ' . v:exception . " from:\n" . s:_format_throwpoint(v:throwpoint) + endtry + endif + return copy(s:loaded[a:name]) +endfunction +let s:Vital._import = function('s:_import') + +function! s:_format_throwpoint(throwpoint) abort + let funcs = [] + let stack = matchstr(a:throwpoint, '^function \zs.*, .\{-} \d\+$') + for line in split(stack, '\.\.') + let m = matchlist(line, '^\(.\+\)\%(\[\(\d\+\)\]\|, .\{-} \(\d\+\)\)$') + if !empty(m) + let [name, lnum, lnum2] = m[1:3] + if empty(lnum) + let lnum = lnum2 + endif + let info = s:_get_func_info(name) + if !empty(info) + let attrs = empty(info.attrs) ? '' : join([''] + info.attrs) + let flnum = info.lnum == 0 ? '' : printf(' Line:%d', info.lnum + lnum) + call add(funcs, printf('function %s(...)%s Line:%d (%s%s)', + \ info.funcname, attrs, lnum, info.filename, flnum)) + continue + endif + endif + " fallback when function information cannot be detected + call add(funcs, line) + endfor + return join(funcs, "\n") +endfunction + +" @vimlint(EVL102, 1, l:_) +" @vimlint(EVL102, 1, l:__) +function! s:_get_func_info(name) abort + let name = a:name + if a:name =~# '^\d\+$' " is anonymous-function + let name = printf('{%s}', a:name) + elseif a:name =~# '^\d\+$' " is lambda-function + let name = printf("{'%s'}", a:name) + endif + if !exists('*' . name) + return {} + endif + let body = execute(printf('verbose function %s', name)) + let lines = split(body, "\n") + let signature = matchstr(lines[0], '^\s*\zs.*') + let [_, file, lnum; __] = matchlist(lines[1], + \ '^\t\%(Last set from\|.\{-}:\)\s*\zs\(.\{-}\)\%( \S\+ \(\d\+\)\)\?$') + return { + \ 'filename': substitute(file, '[/\\]\+', '/', 'g'), + \ 'lnum': 0 + lnum, + \ 'funcname': a:name, + \ 'arguments': split(matchstr(signature, '(\zs.*\ze)'), '\s*,\s*'), + \ 'attrs': filter(['dict', 'abort', 'range', 'closure'], 'signature =~# (").*" . v:val)'), + \ } +endfunction +" @vimlint(EVL102, 0, l:__) +" @vimlint(EVL102, 0, l:_) + +" s:_get_module() returns module object which has all script local functions. +function! s:_get_module(name) abort dict + let funcname = s:_import_func_name(self.plugin_name(), a:name) + try + return call(funcname, []) + catch /^Vim\%((\a\+)\)\?:E117:/ + return s:_get_builtin_module(a:name) + endtry +endfunction + +function! s:_get_builtin_module(name) abort + return s:sid2sfuncs(s:_module_sid(a:name)) +endfunction + +if s:is_vital_vim + " For vital.vim, we can use s:_get_builtin_module directly + let s:Vital._get_module = function('s:_get_builtin_module') +else + let s:Vital._get_module = function('s:_get_module') +endif + +function! s:_import_func_name(plugin_name, module_name) abort + return printf('vital#_%s#%s#import', a:plugin_name, s:_dot_to_sharp(a:module_name)) +endfunction + +function! s:_module_sid(name) abort + let path = s:_module_path(a:name) + if !filereadable(path) + throw 'vital: module not found: ' . a:name + endif + let vital_dir = s:is_vital_vim ? '__\w\+__' : printf('_\{1,2}%s\%%(__\)\?', s:plugin_name) + let base = join([vital_dir, ''], '[/\\]\+') + let p = base . substitute('' . a:name, '\.', '[/\\\\]\\+', 'g') + let sid = s:_sid(path, p) + if !sid + call s:_source(path) + let sid = s:_sid(path, p) + if !sid + throw printf('vital: cannot get from path: %s', path) + endif + endif + return sid +endfunction + +function! s:_module_path(name) abort + return get(s:_extract_files(a:name, s:vital_files()), 0, '') +endfunction + +function! s:_module_sid_base_dir() abort + return s:is_vital_vim ? &rtp : s:project_root +endfunction + +function! s:_dot_to_sharp(name) abort + return substitute(a:name, '\.', '#', 'g') +endfunction + +function! s:_source(path) abort + execute 'source' fnameescape(a:path) +endfunction + +" @vimlint(EVL102, 1, l:_) +" @vimlint(EVL102, 1, l:__) +function! s:_sid(path, filter_pattern) abort + let unified_path = s:_unify_path(a:path) + if has_key(s:cache_sid, unified_path) + return s:cache_sid[unified_path] + endif + for line in filter(split(execute(':scriptnames'), "\n"), 'v:val =~# a:filter_pattern') + let [_, sid, path; __] = matchlist(line, '^\s*\(\d\+\):\s\+\(.\+\)\s*$') + if s:_unify_path(path) is# unified_path + let s:cache_sid[unified_path] = sid + return s:cache_sid[unified_path] + endif + endfor + return 0 +endfunction + +if filereadable(expand(':r') . '.VIM') " is case-insensitive or not + let s:_unify_path_cache = {} + " resolve() is slow, so we cache results. + " Note: On windows, vim can't expand path names from 8.3 formats. + " So if getting full path via and $HOME was set as 8.3 format, + " vital load duplicated scripts. Below's :~ avoid this issue. + function! s:_unify_path(path) abort + if has_key(s:_unify_path_cache, a:path) + return s:_unify_path_cache[a:path] + endif + let value = tolower(fnamemodify(resolve(fnamemodify( + \ a:path, ':p')), ':~:gs?[\\/]?/?')) + let s:_unify_path_cache[a:path] = value + return value + endfunction +else + function! s:_unify_path(path) abort + return resolve(fnamemodify(a:path, ':p:gs?[\\/]?/?')) + endfunction +endif + +" copied and modified from Vim.ScriptLocal +let s:SNR = join(map(range(len("\")), '"[\\x" . printf("%0x", char2nr("\"[v:val])) . "]"'), '') +function! s:sid2sfuncs(sid) abort + let fs = split(execute(printf(':function /^%s%s_', s:SNR, a:sid)), "\n") + let r = {} + let pattern = printf('\m^function\s%d_\zs\w\{-}\ze(', a:sid) + for fname in map(fs, 'matchstr(v:val, pattern)') + let r[fname] = function(s:_sfuncname(a:sid, fname)) + endfor + return r +endfunction + +"" Return funcname of script local functions with SID +function! s:_sfuncname(sid, funcname) abort + return printf('%s_%s', a:sid, a:funcname) +endfunction diff --git a/autoload/vital/lsp.vital b/autoload/vital/lsp.vital new file mode 100644 index 00000000..1737ce2f --- /dev/null +++ b/autoload/vital/lsp.vital @@ -0,0 +1,9 @@ +lsp +969a97cb6b3e634490ba168db0f2606c410cf9a7 + +VS.LSP.MarkupContent +VS.LSP.Text +VS.Vim.Buffer +VS.Vim.Syntax.Markdown +VS.Vim.Window +VS.Vim.Window.FloatingWindow diff --git a/doc/vim-lsp.txt b/doc/vim-lsp.txt new file mode 100644 index 00000000..bf5676ea --- /dev/null +++ b/doc/vim-lsp.txt @@ -0,0 +1,2286 @@ +*vim-lsp.txt* Async Language Server Protocol (LSP) for Vim 8 and Neovim. +*vim-lsp* + +============================================================================== +CONTENTS *vim-lsp-contents* + + Introduction |vim-lsp-introduction| + Install |vim-lsp-install| + Performance |vim-lsp-performance| + Language Servers |vim-lsp-language-servers| + Configure |vim-lsp-configure| + vim-lsp-settings |vim-lsp-settings_plugin| + Wiki |vim-lsp-configure-wiki| + Health Check |vim-lsp-healthcheck| + Options |vim-lsp-options| + g:lsp_auto_enable |g:lsp_auto_enable| + g:lsp_use_native_client |g:lsp_use_native_client| + g:lsp_preview_keep_focus |g:lsp_preview_keep_focus| + g:lsp_preview_float |g:lsp_preview_float| + g:lsp_preview_autoclose |g:lsp_preview_autoclose| + g:lsp_preview_doubletap |g:lsp_preview_doubletap| + g:lsp_insert_text_enabled |g:lsp_insert_text_enabled| + g:lsp_text_edit_enabled |g:lsp_text_edit_enabled| + g:lsp_completion_documentation_enabled + |g:lsp_completion_documentation_enabled| + g:lsp_completion_documentation_delay + |g:lsp_completion_documentation_delay| + g:lsp_diagnostics_enabled |g:lsp_diagnostics_enabled| + g:lsp_diagnostics_echo_cursor |g:lsp_diagnostics_echo_cursor| + g:lsp_diagnostics_echo_delay |g:lsp_diagnostics_echo_delay| + g:lsp_diagnostics_float_cursor |g:lsp_diagnostics_float_cursor| + g:lsp_diagnostics_float_delay |g:lsp_diagnostics_float_delay| + g:lsp_diagnostics_float_insert_mode_enabled + |g:lsp_diagnostics_float_insert_mode_enabled| + g:lsp_diagnostics_highlights_enabled + |g:lsp_diagnostics_highlights_enabled| + g:lsp_diagnostics_highlights_insert_mode_enabled + |g:lsp_diagnostics_highlights_insert_mode_enabled| + g:lsp_diagnostics_highlights_delay + |g:lsp_diagnostics_highlights_delay| + g:lsp_diagnostics_signs_enabled |g:lsp_diagnostics_signs_enabled| + g:lsp_diagnostics_signs_insert_mode_enabled + |g:lsp_diagnostics_signs_insert_mode_enabled| + g:lsp_diagnostics_signs_delay |g:lsp_diagnostics_signs_delay| + g:lsp_diagnostics_signs_priority |g:lsp_diagnostics_signs_priority| + g:lsp_diagnostics_signs_priority_map + |g:lsp_diagnostics_signs_priority_map| + g:lsp_diagnostics_virtual_text_enabled + |g:lsp_diagnostics_virtual_text_enabled| + g:lsp_diagnostics_virtual_text_insert_mode_enabled + |g:lsp_diagnostics_virtual_text_insert_mode_enabled| + g:lsp_diagnostics_virtual_text_delay + |g:lsp_diagnostics_virtual_text_delay| + g:lsp_diagnostics_virtual_text_align + |g:lsp_diagnostics_virtual_text_align| + g:lsp_diagnostics_virtual_text_padding_left + |g:lsp_diagnostics_virtual_text_padding_left| + g:lsp_diagnostics_virtual_text_wrap + |g:lsp_diagnostics_virtual_text_wrap| + g:lsp_document_code_action_signs_enabled + |g:lsp_document_code_action_signs_enabled| + g:lsp_document_code_action_signs_delay + |g:lsp_document_code_action_signs_delay| + g:lsp_inlay_hints_enabled + |g:lsp_inlay_hints_enabled| + g:lsp_inlay_hints_delay + |g:lsp_inlay_hints_delay| + g:lsp_inlay_hints_mode + |g:lsp_inlay_hints_mode| + g:lsp_tree_incoming_prefix |g:lsp_tree_incoming_prefix| + g:lsp_format_sync_timeout |g:lsp_format_sync_timeout| + g:lsp_use_event_queue |g:lsp_use_event_queue| + g:lsp_max_buffer_size |g:lsp_max_buffer_size| + g:lsp_document_highlight_enabled |g:lsp_document_highlight_enabled| + g:lsp_document_highlight_delay |g:lsp_document_highlight_delay| + g:lsp_get_supported_capabilities |g:lsp_get_supported_capabilities| + g:lsp_document_symbol_detail |g:lsp_document_symbol_detail| + g:lsp_peek_alignment |g:lsp_peek_alignment| + g:lsp_preview_max_width |g:lsp_preview_max_width| + g:lsp_preview_max_height |g:lsp_preview_max_height| + g:lsp_preview_fixup_conceal |g:lsp_preview_fixup_conceal| + g:lsp_float_max_width |g:lsp_float_max_width| + g:lsp_signature_help_enabled |g:lsp_signature_help_enabled| + g:lsp_fold_enabled |g:lsp_fold_enabled| + g:lsp_hover_conceal |g:lsp_hover_conceal| + g:lsp_hover_ui |g:lsp_hover_ui| + g:lsp_ignorecase |g:lsp_ignorecase| + g:lsp_log_file |g:lsp_log_file| + g:lsp_log_verbose |g:lsp_log_verbose| + g:lsp_semantic_enabled |g:lsp_semantic_enabled| + g:lsp_semantic_delay |g:lsp_semantic_delay| + g:lsp_text_document_did_save_delay |g:lsp_text_document_did_save_delay| + g:lsp_snippet_expand |g:lsp_snippet_expand| + g:lsp_completion_resolve_timeout |g:lsp_completion_resolve_timeout| + g:lsp_tagfunc_source_methods |g:lsp_tagfunc_source_methods| + g:lsp_show_message_request_enabled |g:lsp_show_message_request_enabled| + g:lsp_work_done_progress_enabled |g:lsp_work_done_progress_enabled| + g:lsp_show_message_log_level |g:lsp_show_message_log_level| + g:lsp_untitled_buffer_enabled |g:lsp_untitled_buffer_enabled| + Functions |vim-lsp-functions| + lsp#enable |lsp#enable()| + lsp#disable |lsp#disable()| + lsp#register_server |lsp#register_server()| + lsp#register_command |lsp#register_command()| + lsp#stream |lsp#stream()| + lsp#stop_server |lsp#stop_server()| + lsp#utils#find_nearest_parent_file_directory() + |lsp#utils#find_nearest_parent_file_directory()| + lsp#enable_diagnostics_for_buffer() |lsp#enable_diagnostics_for_buffer()| + lsp#disable_diagnostics_for_buffer()|lsp#disable_diagnostics_for_buffer()| + lsp#get_buffer_diagnostics_counts() |lsp#get_buffer_diagnostics_counts()| + lsp#get_buffer_first_error_line() |lsp#get_buffer_first_error_line()| + lsp#get_progress() |lsp#get_progress()| + lsp#document_hover_preview_winid() |lsp#document_hover_preview_winid()| + Commands |vim-lsp-commands| + LspAddTreeCallHierarchyIncoming |:LspAddTreeCallHierarchyIncoming| + LspAddTreeReferences |:LspAddTreeReferences| + LspCallHierarchyIncoming |:LspCallHierarchyIncoming| + LspCallHierarchyOutgoing |:LspCallHierarchyOutgoing| + LspCodeAction |:LspCodeAction| + LspCodeActionSync |:LspCodeActionSync| + LspCodeLens |:LspCodeLens| + LspDocumentDiagnostics |:LspDocumentDiagnostics| + LspDeclaration |:LspDeclaration| + LspDefinition |:LspDefinition| + LspDocumentFold |:LspDocumentFold| + LspDocumentFoldSync |:LspDocumentFoldSync| + LspDocumentFormat |:LspDocumentFormat| + LspDocumentFormatSync |:LspDocumentFormatSync| + LspDocumentRangeFormat |:LspDocumentRangeFormat| + LspDocumentRangeFormatSync |:LspDocumentRangeFormatSync| + LspDocumentSymbol |:LspDocumentSymbol| + LspDocumentSymbolSearch |:LspDocumentSymbolSearch| + LspHover |:LspHover| + LspNextDiagnostic |:LspNextDiagnostic| + LspNextError |:LspNextError| + LspNextReference |:LspNextReference| + LspNextWarning |:LspNextWarning| + LspPeekDeclaration |:LspPeekDeclaration| + LspPeekDefinition |:LspPeekDefinition| + LspPeekImplementation |:LspPeekImplementation| + LspPeekTypeDefinition |:LspPeekTypeDefinition| + LspPreviousDiagnostic |:LspPreviousDiagnostic| + LspPreviousError |:LspPreviousError| + LspPreviousReference |:LspPreviousReference| + LspPreviousWarning |:LspPreviousWarning| + LspImplementation |:LspImplementation| + LspReferences |:LspReferences| + LspRename |:LspRename| + LspSemanticHighlightGroups |:LspSemanticHighlightGroups| + LspTypeDefinition |:LspTypeDefinition| + LspTypeHierarchy |:LspTypeHierarchy| + LspWorkspaceSymbol |:LspWorkspaceSymbol| + LspWorkspaceSymbolSearch |:LspWorkspaceSymbolSearch| + LspStatus |:LspStatus| + LspStopServer |:LspStopServer| + Autocommands |vim-lsp-autocommands| + lsp_setup |lsp_setup| + lsp_complete_done |lsp_complete_done| + lsp_float_opened |lsp_float_opened| + lsp_float_closed |lsp_float_closed| + lsp_float_focused |lsp_float_focused| + lsp_register_server |lsp_register_server| + lsp_unregister_server |lsp_unregister_server| + lsp_server_init |lsp_server_init| + lsp_server_exit |lsp_server_exit| + lsp_buffer_enabled |lsp_buffer_enabled| + lsp_diagnostics_updated |lsp_diagnostics_updated| + lsp_progress_updated |lsp_progress_updated| + Mappings |vim-lsp-mappings| + (lsp-preview-close) |(lsp-preview-close)| + (lsp-preview-focus) |(lsp-preview-focus)| + Autocomplete |vim-lsp-autocomplete| + omnifunc |vim-lsp-omnifunc| + asyncomplete.vim |vim-lsp-asyncomplete| + Tagfunc |vim-lsp-tagfunc| + Snippets |vim-lsp-snippets| + Folding |vim-lsp-folding| + Semantic highlighting |vim-lsp-semantic| + Popup Formatting |vim-lsp-popup-format| + Workspace Folders |vim-lsp-workspace-folders| + License |vim-lsp-license| + Maintainers |vim-lsp-maintainers| + + +============================================================================== +INTRODUCTION *vim-lsp-introduction* + +Async Language Server Protocol (LSP) for Vim 8 and Neovim. + +For more information on LSP refer to the official website at +https://microsoft.github.io/language-server-protocol/ + +============================================================================== +INSTALL *vim-lsp-install* + +Install vim-lsp plugin. Below is a sample using plug.vim +> + Plug 'prabirshrestha/vim-lsp' + +============================================================================== +PERFORMANCE *vim-lsp-performance* + +While Vim script is very portable it has performance implications. If you would +like to improve performance make sure you have vim/neovim with lua support. +Currently only diff algorithm uses lua internally if available. +Following is the default value used to detect lua. +> + let g:lsp_use_lua = has('nvim-0.4.0') || (has('lua') && has('patch-8.2.0775')) + +Windows users can download the binaries from the following url and place +lua53.dll in the `PATH` or besides `vim.exe` or `gvim.exe` executables. + +32 bit: +http://downloads.sourceforge.net/luabinaries/lua-5.3.2_Win32_dllw4_lib.zip + +64bit: +http://downloads.sourceforge.net/luabinaries/lua-5.3.2_Win64_dllw4_lib.zip + +If you are using vim set `let g:lsp_use_native_client = 1` and make sure you +are running vim 8.2.4780+. + +Set |g:lsp_semantic_enabled| to 0. + +Set |g:lsp_format_sync_timeout| to a reasonable value such as `1000`. + +============================================================================== +LANGUAGE SERVERS *vim-lsp-language-servers* + +CONFIGURE *vim-lsp-configure* +vim-lsp doesn't ship with any language servers. The user is responsible for +configuring the language servers correctly. + +Here is an example of configuring the python language server protocol based +on pylsp (https://github.com/python-lsp/python-lsp-server) + +1. Make sure the language server is available locally in the machine. + For python, pip package manager can be used to install the language server. +> + pip install python-lsp-server + +2. Register the language server in your .vimrc +> + if (executable('pylsp')) + au User lsp_setup call lsp#register_server({ + \ 'name': 'pylsp', + \ 'cmd': {server_info->['pylsp']}, + \ 'allowlist': ['python'] + \ }) + endif +< + For more details refer to |lsp#register_server()|. + +3. Configure your settings for the buffer + Use |lsp_buffer_enabled| autocommand to configure the buffer. +> + function! s:on_lsp_buffer_enabled() abort + setlocal omnifunc=lsp#complete + setlocal signcolumn=yes + nmap gd (lsp-definition) + nmap (lsp-rename) + endfunction + + augroup lsp_install + au! + autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled() + augroup END +< +TCP SERVERS *vim-lsp-tcp* +You can use tcp to connect to LSP servers that don't support stdio. Set host +and port to tcp. The Godot game engine uses 6008 as its LSP port and godot +ftplugins define gdscript or gdscript3 filetype: > + + au User lsp_setup + \ call lsp#register_server({ + \ 'name': 'godot', + \ 'tcp': "localhost:6008", + \ 'allowlist': ['gdscript3', 'gdscript'] + \ }) +> +VIM-LSP-SETTINGS *vim-lsp-settings_plugin* +Refer to [vim-lsp-settings](https://github.com/mattn/vim-lsp-settings) on how +to automatically register various language servers. +> + Plug 'prabirshrestha/vim-lsp' + Plug 'mattn/vim-lsp-settings' + +HEALTH CHECK *vim-lsp-healthcheck* +vim-lsp supports the |:CheckHealth| command which can be useful when debugging +lsp configuration issues. + +This command is implemented in vim with the +[vim-healthcheck](https://github.com/rhysd/vim-healthcheck) plugin. + +WIKI *vim-lsp-configure-wiki* +For documentation on how to configure other language servers refer +to https://github.com/prabirshrestha/vim-lsp/wiki/Servers + +============================================================================== +Options *vim-lsp-options* + +g:lsp_auto_enable *g:lsp_auto_enable* + Type: |Number| + Default: `1` + + Auto enable vim-lsp plugin during startup. Set to `0` to disable auto + enabling vim-lsp during startup. + + Example: > + let g:lsp_auto_enable = 1 + let g:lsp_auto_enable = 0 + +g:lsp_use_native_client *g:lsp_use_native_client* + Type: |Number| + Default: `0` + + Enable native lsp client support for vim 8.2.4780+. No impact for neovim. + TCP language servers are not supported and should be set to 0 if one is + used. + + Example: > + let g:lsp_use_native_client = 1 + let g:lsp_use_native_client = 0 + +g:lsp_preview_keep_focus *g:lsp_preview_keep_focus* + Type: |Number| + Default: `1` + + Indicates whether to keep the focus on current window or move the focus + to the |preview-window| when a |preview-window| is opened by vim-lsp. + Certain commands such as |:LspHover| opens the result in a + |preview-window|. + + Example: > + " Keep the focus in current window + let g:lsp_preview_keep_focus = 1 + + " Do not keep the focus in current window. + " Move the focus to |preview-window|. + let g:lsp_preview_keep_focus = 0 +< + * |preview-window| can be closed using the default vim mapping - ``. + * |preview-window| can be also automatically closed after completion with + the following auto command: > + autocmd! CompleteDone * if pumvisible() == 0 | pclose | endif +< * |preview-window| can be suppressed with: > + set completeopt-=preview +< +g:lsp_preview_float *g:lsp_preview_float* + Type: |Number| + Default: `1` + + If set and nvim_win_open() or popup_create is available, hover information + are shown in a floating window as |preview-window| at the cursor position. + The |preview-window| is closed automatically on cursor moves, unless it is + focused. While focused it may be closed with . + + This feature requires neovim 0.4.0 (current master) or + Vim8.1 with has('patch-8.1.1517'). + + Example: > + " Opens preview windows as floating + let g:lsp_preview_float = 1 + + " Opens preview windows as normal windows + let g:lsp_preview_float = 0 +< + After opening an autocmd User event lsp_float_opened is issued, as well as + and lsp_float_closed upon closing. This can be used to alter the preview + window (using |lsp#document_hover_preview_winid()| to get the window id), + setup custom bindings while a preview is open, or change the highlighting + of the window. + + Example of custom keybindings: > + " Close preview window with + autocmd User lsp_float_opened nmap + \ (lsp-preview-close) + autocmd User lsp_float_closed nunmap +< + + Example of customising the highlighting: > + highlight PopupWindow ctermbg=lightblue guibg=lightblue + + augroup lsp_float_colours + autocmd! + if !has('nvim') + autocmd User lsp_float_opened + \ call setwinvar(lsp#document_hover_preview_winid(), + \ '&wincolor', 'PopupWindow') + else + autocmd User lsp_float_opened + \ call nvim_win_set_option( + \ lsp#document_hover_preview_winid(), + \ 'winhighlight', 'Normal:PopupWindow') + endif + augroup end +< + +g:lsp_preview_autoclose *g:lsp_preview_autoclose* + Type: |Number| + Default: `1` + + Indicates if an opened floating preview shall be automatically closed upon + movement of the cursor. If set to 1, the window will close automatically + if the cursor is moved and the preview is not focused. If set to 0, it + will remain open until explicitly closed (e.g. with + |(lsp-preview-close)|, or when focused). + + Example: > + " Preview closes on cursor move + let g:lsp_preview_autoclose = 1 + + " Preview remains open and waits for an explicit call + let g:lsp_preview_autoclose = 0 + +g:lsp_preview_doubletap *g:lsp_preview_doubletap* + Type: |List| + Default: `[function('lsp#ui#vim#output#focuspreview')]` + + When preview is called twice with the same data while the preview is still + open, the function in `lsp_preview_doubletap` is called instead. To + disable this and just "refresh" the preview, set to ´0´. + + Example: > + " Focus preview on repeated preview (does not work for vim8.1 popups) + let g:lsp_preview_doubletap = [function('lsp#ui#vim#output#focuspreview')] + + " Closes the preview window on the second call to preview + let g:lsp_preview_doubletap = [function('lsp#ui#vim#output#closepreview')] + + " Disables double tap feature; refreshes the preview on consecutive taps + let g:lsp_preview_doubletap = 0 + +g:lsp_insert_text_enabled *g:lsp_insert_text_enabled* + Type: |Number| + Default: `1` + + Enable support for completion insertText property. Set to `0` to disable + using insertText. + + Example: > + let g:lsp_insert_text_enabled = 1 + let g:lsp_insert_text_enabled = 0 + +g:lsp_text_edit_enabled *g:lsp_text_edit_enabled* + Type: |Number| + Default: `1` + + Enable support for completion textEdit property. Set to `0` to disable + using textEdit. + + Example: > + let g:lsp_text_edit_enabled = 1 + let g:lsp_text_edit_enabled = 0 + +g:lsp_completion_documentation_enabled *g:lsp_completion_documentation_enabled* + Type: |Number| + Default: `1` + + Enables floating window documentation for complete items. + + Example: > + let g:lsp_completion_documentation_enabled = 1 + let g:lsp_completion_documentation_enabled = 0 + +g:lsp_completion_documentation_delay *g:lsp_completion_documentation_delay* + Type: |Number| + Default: `80` + + Time in milliseconds to delay the completion documentation popup. Might + help with performance. Set this to `0` to disable debouncing. + + Example: > + let g:lsp_completion_documentation_delay = 120 + let g:lsp_completion_documentation_delay = 0 + +g:lsp_diagnostics_enabled *g:lsp_diagnostics_enabled* + Type: |Number| + Default: `1` + + Enable support for document diagnostics like warnings and error messages. + enabling vim-lsp during startup. + Refer to |g:lsp_diagnostics_signs_enabled| to enable signs column. + Refer to |g:lsp_diagnostics_virtual_text_enabled| to enable virtual text. + + Example: > + let g:lsp_diagnostics_enabled = 1 + let g:lsp_diagnostics_enabled = 0 +< +g:lsp_diagnostics_echo_cursor *g:lsp_diagnostics_echo_cursor* + Type: |Number| + Default: `0` + + Enables echo of diagnostic error for the current line to status. Requires + |g:lsp_diagnostics_enabled| set to 1. + + Example: > + let g:lsp_diagnostics_echo_cursor = 1 + let g:lsp_diagnostics_echo_cursor = 0 + +g:lsp_diagnostics_echo_delay *g:lsp_diagnostics_echo_delay* + Type: |Number| + Default: `500` + + Delay milliseconds to echo diagnostic error for the current line to status. + Requires |g:lsp_diagnostics_enabled| and |g:lsp_diagnostics_echo_cursor| set + to 1. + + Example: > + let g:lsp_diagnostics_echo_delay = 200 + let g:lsp_diagnostics_echo_delay = 1000 + +g:lsp_diagnostics_float_cursor *g:lsp_diagnostics_float_cursor* + Type: |Number| + Default: `0` + + Enables a floating window of diagnostic error for the current line to + status. Requires nvim_win_open() or popup_create is available, and + |g:lsp_diagnostics_enabled| set to 1. + + Example: > + let g:lsp_diagnostics_float_cursor = 1 + let g:lsp_diagnostics_float_cursor = 0 + +g:lsp_diagnostics_float_delay *g:lsp_diagnostics_float_delay* + Type: |Number| + Default: `500` + + Delay milliseconds to show diagnostic error for the current line to status + in a float window. Requires Enables float of diagnostic error for the + current line to status. Requires |g:lsp_diagnostics_enabled| and + |g:lsp_diagnostics_float_cursor| set to 1. + + Example: > + let g:lsp_diagnostics_float_delay = 200 + let g:lsp_diagnostics_float_delay = 1000 + +g:lsp_diagnostics_float_insert_mode_enabled + *g:lsp_diagnostics_float_insert_mode_enabled* + Type: |Boolean| + Default: `1` + + Indicates whether to enable float of diagnostic error for the current line + to status when in |insertmode|. Requires |g:lsp_diagnostics_enabled| and + |g:lsp_diagnostics_float_cursor| set to 1. + + Example: > + let g:lsp_diagnostics_float_insert_mode_enabled = 0 + +g:lsp_format_sync_timeout *g:lsp_format_sync_timeout* + Type: |Number| + Default: `-1` + + Timeout milliseconds to abort `:LspDocumentFormatSync` or + `:LspDocumentRangeFormatSync`. Set to `-1` to disable timeout. Using + `BufWritePre` to execute sync commands may cause vim to hang when using + some language servers as starting the language server may be slow. Set the + timeout value to cancel sync format. + + Example: > + let g:lsp_format_sync_timeout = -1 + let g:lsp_format_sync_timeout = 1000 + +g:lsp_diagnostics_highlights_enabled *g:lsp_diagnostics_highlights_enabled* + Type: |Number| + Default: `1` for neovim 0.3+ and vim with patch-8.1.1035 + + Enables highlighting of diagnostics. Requires NeoVim with version 0.3 or + Vim 8.1.1035 or newer. + + Example: > + let g:lsp_diagnostics_highlights_enabled = 1 + let g:lsp_diagnostics_highlights_enabled = 0 +< + To change the style of the highlighting, you can set or link + `LspErrorHighlight`, `LspWarningHighlight`, `LspInformationHighlight` and + `LspHintHighlight` highlight groups. + + Example: > + highlight link LspErrorHighlight Error + +g:lsp_diagnostics_highlights_insert_mode_enabled + *g:lsp_diagnostics_highlights_insert_mode_enabled* + Type: |Number| + Default: `1` + + Indicates whether to enable diagnostics highlighting when in |insertmode|. + Requires |g:lsp_diagnostics_highlights_enabled|. + + Example: > + let g:lsp_diagnostics_highlights_insert_mode_enabled = 1 + let g:lsp_diagnostics_highlights_insert_mode_enabled = 0 + +g:lsp_diagnostics_highlights_delay *g:lsp_diagnostics_highlights_delay* + Type: |Number| + Default: `500` + + Delay milliseconds to update diagnostics highlights. Requires + |g:lsp_diagnostics_highlights_enabled|. + + Example: > + let g:lsp_diagnostics_highlights_delay = 200 + let g:lsp_diagnostics_highlights_delay = 1000 + +g:lsp_diagnostics_signs_enabled + *g:lsp_diagnostics_signs_enabled* + Type: |Number| + Default: `1` for vim/neovim with patch 8.1.0772 + + Enables signs for diagnostics. Requires NeoVim with |sign_define| or Vim + with |sign_define| and patch 8.1.0772 or newer and + |g:lsp_diagnostics_enabled| set to `1`. + + Example: > + let g:lsp_diagnostics_signs_enabled = 1 + let g:lsp_diagnostics_signs_enabled = 0 +< + Four groups of signs are defined and used: + `LspError`, `LspWarning`, `LspInformation`, `LspHint`. + + It is possible to set custom text or icon that will be used for each sign + (note that icons are only available in GUI). + + `LspError` defaults to `E>`. + `LspHint` defaults to `H>`. + `LspInformation` defaults to `I>`. + `LspWarning` defaults to `W>`. + + To do this, set some of the following globals: + `g:lsp_diagnostics_signs_error`, `g:lsp_diagnostics_signs_warning`, + `g:lsp_diagnostics_signs_information`, `g:lsp_diagnostics_signs_hint`. + + They should be set to a dict, that contains either text that will be used + as sign in terminal, or icon that will be used for GUI, or both. + + Example: > + let g:lsp_diagnostics_signs_error = {'text': '✗'} + let g:lsp_diagnostics_signs_warning = {'text': '‼', 'icon': '/path/to/some/icon'} " icons require GUI + let g:lsp_diagnostics_signs_hint = {'icon': '/path/to/some/other/icon'} " icons require GUI + +g:lsp_diagnostics_signs_insert_mode_enabled + *g:lsp_diagnostics_signs_insert_mode_enabled* + Type: |Number| + Default: `1` + + Indicates whether to enable diagnostics signs column when in |insertmode|. + Requires |g:lsp_diagnostics_signs_enabled|. + + Example: > + let g:lsp_diagnostics_signs_insert_mode_enabled = 1 + let g:lsp_diagnostics_signs_insert_mode_enabled = 0 + +g:lsp_diagnostics_signs_delay *g:lsp_diagnostics_signs_delay* + Type: |Number| + Default: `500` + + Delay milliseconds to update diagnostics signs column. Requires + |g:lsp_diagnostics_signs_enabled|. + + Example: > + let g:lsp_diagnostics_signs_delay = 200 + let g:lsp_diagnostics_signs_delay = 1000 + +g:lsp_diagnostics_signs_priority *g:lsp_diagnostics_signs_priority* + Type: |Number| + Default: `10` + + Configures the |sign-priority| for placed signs. Signs placed by other + plugins have a priority of 10 by default. Requires + |g:lsp_diagnostics_signs_enabled| set to 1. + + Example: > + let g:lsp_diagnostics_signs_priority = 11 + let g:lsp_diagnostics_signs_priority = 9 + +g:lsp_diagnostics_signs_priority_map *g:lsp_diagnostics_signs_priority_map* + Type: |Dict| + Default: `{}` + + Overrides |g:lsp_diagnostics_signs_priority| per severity level or per server + name and severity level. Requires |g:lsp_diagnostics_signs_enabled| set to 1. + + Example: > + let g:lsp_diagnostics_signs_priority_map = { + \'LspError': 11, + \'LspWarning': 7, + \'clangd_LspWarning': 11, + \'clangd_LspInformation': 11 + \} + +g:lsp_diagnostics_virtual_text_enabled + *g:lsp_diagnostics_virtual_text_enabled* + Type: |Number| + Default: `1` for neovim 0.3+ + + Enables virtual text to be shown next to diagnostic errors. Requires + NeoVim with version 0.3 or newer or Vim with |virtual-text| and + patch 9.0.0178, and |g:lsp_diagnostics_enabled| set to `1`. + Virtual text uses the same highlight groups used for signs (eg LspErrorText), + but can be uniquely defined if you want to have different highlight groups + for signs and virtual text. To set unique virtual text highlighting, you + can set or link `LspErrorVirtualText`, `LspWarningVirtualText`, + `LspInformationVirtualText` and `LspHintVirtualText` highlight groups. + + Example: > + let g:lsp_diagnostics_virtual_text_enabled = 1 + let g:lsp_diagnostics_virtual_text_enabled = 0 + +g:lsp_diagnostics_virtual_text_insert_mode_enabled + *g:lsp_diagnostics_virtual_text_insert_mode_enabled* + Type: |Number| + Default: `0` + + Indicates whether to enable diagnostics virtual text when in |insertmode|. + Requires |g:lsp_diagnostics_virtual_text_enabled|. + + Example: > + let g:lsp_diagnostics_virtual_text_insert_mode_enabled = 1 + let g:lsp_diagnostics_virtual_text_insert_mode_enabled = 0 + +g:lsp_diagnostics_virtual_text_delay *g:lsp_diagnostics_virtual_text_delay* + Type: |Number| + Default: `500` + + Delay milliseconds to update diagnostics virtual text. Requires + |g:lsp_diagnostics_virtual_text_enabled|. + + Example: > + let g:lsp_diagnostics_virtual_text_delay = 200 + let g:lsp_diagnostics_virtual_text_delay = 1000 + +g:lsp_diagnostics_virtual_text_prefix *g:lsp_diagnostics_virtual_text_prefix* + Type: |String| + Default: `""` + + Adds the prefix to the diagnostics to be shown as virtual text. Requires + |g:lsp_diagnostics_virtual_text_enabled|. + + Example: > + let g:lsp_diagnostics_virtual_text_prefix = "> " + let g:lsp_diagnostics_virtual_text_prefix = " ‣ " + +g:lsp_diagnostics_virtual_text_align *g:lsp_diagnostics_virtual_text_align* + Type: |String| + Default: `"below"` + + Determines the align of the diagnostics virtual text. Requires + |g:lsp_diagnostics_virtual_text_enabled|. + + Possible values are: + + after after the end of the line + right right aligned in the window (unless the text wraps to the next + screen line) + below in the next screen line + above just above the line + + Only one "right" property can fit in each line, if there are two or more + these will go in a separate line (still right aligned). + + This value is passed as the "text_align" property in a |prop_add()| call. + + Example: > + let g:lsp_diagnostics_virtual_text_align = "right" + +g:lsp_diagnostics_virtual_text_padding_left + *g:lsp_diagnostics_virtual_text_padding_left* + Type: |Number| + Default: `1` + + Determines the left padding of the diagnostics virtual text. Requires + |g:lsp_diagnostics_virtual_text_enabled|. + + Example: > + let g:lsp_diagnostics_virtual_text_padding_left = 2 + +g:lsp_diagnostics_virtual_text_wrap *g:lsp_diagnostics_virtual_text_wrap* + Type: |String| + Default: `"wrap"` + + Determines whether or not to wrap the diagnostics virtual text. Possible + values are one of `'wrap'`, `'truncate'`. Requires + |g:lsp_diagnostics_virtual_text_enabled|. + + Example: > + let g:lsp_diagnostics_virtual_text_wrap = "truncate" + +g:lsp_document_code_action_signs_enabled + *g:lsp_document_code_action_signs_enabled* + Type: |Number| + Default: `1` + + Enables signs for code actions. Requires NeoVim with |sign_define| or Vim + with |sign_define| and patch 8.1.0772 or newer. + + Example: > + let g:lsp_document_code_action_signs_enabled = 1 + let g:lsp_document_code_action_signs_enabled = 0 +< + `LspCodeActionText` sign is defined and used. + + It is possible to set custom text or icon that will be used for sign + (note that icons are only available in GUI). + + `LspCodeActionText` defaults to `A>`. + + To do this, set the following globals: + `g:lsp_document_code_action_signs_hint`. + They should be set to a dict, that contains either text that will be used + as sign in terminal, or icon that will be used for GUI, or both. + + Example: > + let g:lsp_document_code_action_signs_hint = {'text': 'A>'} + let g:lsp_document_code_action_signs_hint = {'text': '‼', 'icon': '/path/to/some/icon'} " icons require GUI + let g:lsp_document_code_action_signs_hint = {'icon': '/path/to/some/other/icon'} " icons require GUI + +g:lsp_document_code_action_signs_delay + *g:lsp_document_code_action_signs_delay* + Type: |Number| + Default: `500` + + Delay milliseconds to update code action signs. Requires + |g:lsp_document_code_action_signs_enabled|. + + Example: > + let g:lsp_document_code_action_signs_delay = 200 + let g:lsp_document_code_action_signs_delay = 1000 +> +g:lsp_inlay_hints_enabled + *g:lsp_inlay_hints_enabled* + Type: |Number| + Default: `0` + + Enables inlay-hints. Requires Vim9 with |virtual-text|. + patch 9.0.0167 or newer. + + Example: > + let g:lsp_inlay_hints_enabled = 1 + let g:lsp_inlay_hints_enabled = 0 +< + To change the style of the inlay-hints, you can set or link the + `lspInlayHintsType` and `lspInlayHintsParameter` highlight group. + + Example: > + highlight lspInlayHintsType ctermfg=red guifg=red + \ ctermbg=green guibg=green + highlight lspInlayHintsParameter ctermfg=red guifg=red + \ ctermbg=green guibg=green + +g:lsp_inlay_hints_delay + *g:lsp_inlay_hints_delay* + Type: |Number| + Default: `350` + + Delay milliseconds to update inlay-hints. Requires + |g:lsp_inlay_hints_enabled|. + + Example: > + let g:lsp_inlay_hints_delay = 200 + let g:lsp_inlay_hints_delay = 1000 +> +g:lsp_inlay_hints_mode + *g:lsp_inlay_hints_mode* + Type: |Dict| + Default: `{}` + + This mode currently only include "curline" and "!curline". + + Example: > + let g:lsp_inlay_hints_mode = { + \ 'normal': ['curline'], + \} +< + "curline" show hint only for current line. "!curline" show hints except + current line. Default show all hints. +> +g:lsp_tree_incoming_prefix *g:lsp_tree_incoming_prefix* + Type: |String| + Default: `"<= "` + + Specifies the prefix of items added by following commands. + * |LspAddTreeCallHierarchyIncoming| + * |LspAddTreeReferences| + + Example: > + let g:lsp_tree_incoming_prefix = "← " + let g:lsp_tree_incoming_prefix = "⬅️ " + +g:lsp_use_event_queue *g:lsp_use_event_queue* + Type: |Number| + Default: `1` for neovim or vim with patch-8.1.0889 + + Enable event queue which improves performance by reducing the + communication between client and server. + + Example: > + let g:lsp_use_event_queue = 1 + let g:lsp_use_event_queue = 0 + +g:lsp_max_buffer_size *g:lsp_max_buffer_size* + Type: |Number| + Default: `5000000` + + To improve performance, if a buffer is larger than + `g:lsp_max_buffer_size` (measured in bytes), the following features + are disabled: + * Semantic highlighting + + This functionality can be disabled by setting `g:lsp_max_buffer_size` + to a negative value. + + Example: > + let g:lsp_max_buffer_size = 10000000 + let g:lsp_max_buffer_size = -1 + +g:lsp_document_highlight_enabled *g:lsp_document_highlight_enabled* + Type: |Number| + Default: `1` for neovim or vim with patch-8.1.1035 + + Enables highlighting of the references to the symbol under the cursor. + Requires NeoVim with version 0.3 or Vim 8.1.1035 or newer. + + Example: > + let g:lsp_document_highlight_enabled = 1 + let g:lsp_document_highlight_enabled = 0 +< + + To change the style of the highlighting, you can set or link the + `lspReference` highlight group. + + Example: > + highlight lspReference ctermfg=red guifg=red ctermbg=green guibg=green + +g:lsp_document_highlight_delay *g:lsp_document_highlight_delay* + Type: |Number| + Default: `350` + + Delay milliseconds to highlight references. Requires + |g:lsp_document_highlight_enabled| set to 1. + + Example: > + let g:lsp_document_highlight_delay = 200 + let g:lsp_document_highlight_delay = 1000 + +g:lsp_get_supported_capabilities *g:lsp_get_supported_capabilities* + Type: |List| + Default: `[function('lsp#default_get_supported_capabilities')]` + + A |List| containing one element of type |Funcref|. This element is a + reference to the function that vim-lsp should use to obtain the supported + LSP capabilities. Changing this variable allows customizing which + capabilities vim-lsp sends to a language server. + + Note: You can obtain the default supported capabilities of vim-lsp by + calling `lsp#default_get_supported_capabilities` from within your + function. + +g:lsp_document_symbol_detail *g:lsp_document_symbol_detail* + Type: |Number| + Default: `0` + + Determines whether document symbol shows details or not. Set to `1` to + show details. + + Note: showing details needs to turn on setting below: > + \ 'capabilities': { + \ 'textDocument': { + \ 'documentSymbol': { + \ 'hierarchicalDocumentSymbolSupport': v:true, + \ }, + \ }, + \ }, + +g:lsp_peek_alignment *g:lsp_peek_alignment* + Type: |String| + Default: `"center"` + + Determines how to align the location of interest for e.g. + |:LspPeekDefinition|. Three values are possible: `"top"`, `"center"` and + `"bottom"`, which place the location of interest at the first, middle and + last lines of the preview/popup/floating window, respectively. + +g:lsp_preview_max_width *g:lsp_preview_max_width* + Type: |Number| + Default: `-1` + + If positive, determines the maximum width of the preview window in + characters. Lines longer than `g:lsp_preview_max_width` will be wrapped to + fit in the preview window. Use a value of `-1` to disable setting a + maximum width. + +g:lsp_preview_max_height *g:lsp_preview_max_height* + Type: |Number| + Default: `-1` + + If positive, determines the maximum height of the preview window in + characters. Use a value of `-1` to disable setting a maximum height. + +g:lsp_preview_fixup_conceal *g:lsp_preview_fixup_conceal* + Type: |Number| + Default: `0` + + If negative, all markdown documents are not converted as compact format. + That's useful in vim. vim's popup doesn't shrink correctly if the + buffer content uses conceals. + +g:lsp_float_max_width *g:lsp_float_max_width* + Type: |Number| + Default: `-1` + + If positive, determines the maximum width of the float windows in + characters. Lines longer than `g:lsp_float_max_width` will be wrapped to fit + in the float window. + If set to 0, float windows can stretch to the width of the screen. + Otherwise, the maximum width of the floating windows is set to 40% of the + screen. + +g:lsp_signature_help_enabled *g:lsp_signature_help_enabled* + Type: |Number| + Default: `1` + + Enable support for signature help. Set to `0` to disable. + + Example: > + let g:lsp_signature_help_enabled = 1 + let g:lsp_signature_help_enabled = 0 + +g:lsp_signature_help_delay *g:lsp_signature_help_delay* + Type: |Number| + Default: `200` + + The waiting time in milliseconds before sending textDocument/signatureHelp + to LSP servers. + + Example: > + let g:lsp_signature_help_delay = 100 + let g:lsp_signature_help_delay = 500 + +g:lsp_show_workspace_edits *g:lsp_show_workspace_edits* + Type: |Boolean| + Default: `0` + + Enable showing changes made in a workspace edit in the |location-list|. + Set to `0` to disable. + + Example: > + let g:lsp_show_workspace_edits = 1 + let g:lsp_show_workspace_edits = 0 + +g:lsp_fold_enabled *g:lsp_fold_enabled* + Type: |Number| + Default: `1` + + Determines whether or not folding is enabled globally. Set to `0` to + disable sending requests. + +g:lsp_hover_conceal *g:lsp_hover_conceal* + Type: |Boolean| + Default: `1` + + If `true` (`1`), 'conceallevel' is set to `2` for hover windows. This + means that, for example, asterisks in markdown hovers are hidden, but the + text is still displayed bold. You may want to disable this if the filetype + of the popup has custom conceals which you don't want to use, or if + you're using Vim in a terminal. + + To override this setting per server, see + |vim-lsp-server_info-hover_conceal|. + +g:lsp_hover_ui *g:lsp_hover_ui* + Type: |String| + Default: `''` + + Controls default UI behavior for |LspHover|. + If empty string, defaults to `float` if popup is supported in vim or + floating window is supported in neovim else uses |preview-window|. + + Example: > + let g:lsp_hover_ui = '' + let g:lsp_hover_ui = 'float' + let g:lsp_hover_ui = 'preview' + +g:lsp_ignorecase *g:lsp_ignorecase* + Type: |Boolean| + Default: the value of 'ignorecase' + + Determines whether or not case should be ignored when filtering or sorting + completion items. + See |vim-lsp-completion-filter| or |vim-lsp-completion-sort|. + By default, the value of 'ignorecase' is used. + +g:lsp_log_file *g:lsp_log_file* + Type: |String| + Default: `''` + + Determines whether or not logging should be written to a file. + To disable log use empty string. + + Example: > + let g:lsp_log_file = '' + let g:lsp_log_file = expand('~/vim-lsp.log') + +g:lsp_log_verbose *g:lsp_log_verbose* + Type: |Number| + Default: `'1'` + + Determines whether or not verbose logging should be enabled. This usually + includes logging the entire request and response from the LSP servers and + clients which can have significant performance impact. Requires + |g:lsp_log_file| to be set else there is no impact on enabling or + disabling this flag. + + Example: > + let g:lsp_log_verbose = 1 + let g:lsp_log_verbose = 0 + +g:lsp_semantic_enabled *g:lsp_semantic_enabled* + Type: |Boolean| + Default: `0` + + Determines whether or not semantic highlighting is enabled globally. Set + to `1` to enable sending requests. + +g:lsp_semantic_delay *g:lsp_semantic_delay* + Type: |Number| + Default: `500` + + Modifications which occur within |g:lsp_semantic_delay| of one another are + lumped into a single `semanticTokens` request. Sets the maximum rate at + which the semantic highlighting can update. + +g:lsp_text_document_did_save_delay *g:lsp_text_document_did_save_delay* + Type: |Number| + Default: `-1` + + The waiting time in milliseconds before sending textDocument/didSave to + LSP servers, -1 by default means no delay. If >= 0, will delay using + |timer_start()| with {time} is the number. + +g:lsp_snippet_expand *g:lsp_snippet_expand* + Type: |List| + + The integration point to other snippet plugin. + vim-lsp may invoke the first item of this value when it needs snippet + expansion. + +g:lsp_completion_resolve_timeout *g:lsp_completion_resolve_timeout* + Type: |Number| + Default: `200` + + The `completionItem/resolve` request's timeout value. + If your vim freeze at `CompleteDone`, you can set this value to 0. + +g:lsp_tagfunc_source_methods *g:lsp_tagfunc_source_methods* + Type: |List| + Default: `['definition', 'declaration', 'implementation', 'typeDefinition']` + + The LSP methods to call to get symbols for tag lookup. See + |vim-lsp-tagfunc|. + +g:lsp_show_message_request_enabled *g:lsp_show_message_request_enabled* + Type: |Number| + Default: `1` + + Determines whether or not `window/showMessageRequest` should show message to + the user or if it should be ignored. Set to `1` to enable. + +g:lsp_work_done_progress_enabled *g:lsp_work_done_progress_enabled* + Type: |Number| + Default: `0` + + Determines whether or not to ask the server to send `$/progress` + notifications. This can be intercepted by listening to |lsp#stream()|. + Set to `1` to enable. + +g:lsp_show_message_log_level *g:lsp_show_message_log_level* + Type: |String| + Default: `'warning'` + + Determines log level of messages sent from LSP servers. Possible values + are one of `'none'`, `'error'`, `'warning'`, `'info'`, `'log'`. Messages + are filtered by the value set to this variable. For example, when + `'warning'` is set, `'error'` level and `'warning'` level messages are + shown but `'info'` level and `'log'` level messages are not shown. Setting + `'none'` disables to show messages completely. + +g:lsp_untitled_buffer_enabled *g:lsp_untitled_buffer_enabled* + Type: |Number| + Default: `1` + + Determines whether or not vim-lsp plugin is enabled for untitled buffer. + Set to `0` to disable for untitled buffer. + +============================================================================== +FUNCTIONS *vim-lsp-functions* + +lsp#enable() *lsp#enable()* + +Enables vim-lsp plugin. + + Example: > + :call lsp#enable() + +lsp#disable() *lsp#disable()* + +Disables vim-lsp plugin. + + Example: > + :call lsp#disable() + +lsp#register_server({server-info}) *lsp#register_server()* + +Used to register the language server with vim-lsp. This method takes +one parameter which is a vim |dict| and is referred to as |vim-lsp-server_info| + + Example: > + if (executable('pylsp')) + au User lsp_setup call lsp#register_server({ + \ 'name': 'name-of-server', + \ 'cmd': {server_info->['server-exectuable']}, + \ 'allowlist': ['filetype to allowlist'], + \ 'blocklist': ['filetype to blocklist'], + \ 'config': {}, + \ 'workspace_config': {'param': {'enabled': v:true}}, + \ 'languageId': {server_info->'python'}, + \ }) + endif +< + Note: + * checking for executable is optional but can be used to avoid + unnecessary server registration. + * au User lsp_setup is optional and used to delay registering the + language server after .vimrc has been loaded. It is recommended + to use it if possible. + +server_info *vim-lsp-server_info* +The vim |dict| containing information about the server. +> + { + 'name': 'name of the server', + 'cmd': {server_info->['server_executable']}, + 'allowlist': ['filetype'], + 'blocklist': ['filetype'], + 'config': {}, + 'workspace_config': {}, + 'languageId': {server_info->'filetype'}, + } +< + * name: + required + Name of the language server. Needs to be unique. + + * cmd: + required + Function or array which represents command line to start the language + server. + + When function, it takes |vim-lsp-server_info| as parameter and returns the + language server executable to run along with the appropriate arguments + when the appropriate filetype is loaded. This function will only be + called when the server has not started. + Return empty array to ignore starting the server. + + When array, the first element is the language server executable and + the rest are the appropriate arguments. It is useful when the command line + can be determined statically and |vim-lsp-server_info| is not necessary. + + Example: > + 'cmd': ['pylsp'] +< + Function can be complex based on custom requirements. + For example: + - Use binary from local node_modules folder instead of a global + node_modules folder. + - Use different executable based on custom config. + - Return empty array to ignore starting server due to missing + config value required by the server (ex: missing package.json) + - Instead of checking for server executable before calling + register_server it can also be checked here. + + Cross-platform compatibility notes: + It is recommended to use &shell with &shellcmdflag when running script + files that can be executed specially on windows where *.bat and *.cmd + files cannot be started without running the shell first. This is common + for executable installed by npm for nodejs. + + Example: > + 'cmd': {server_info-> + \ [&shell, &shellcmdflag, 'typescript-language-server --stdio']} +< + * allowlist: + optional + String array of filetypes to run the language server. + + Example: > + 'allowlist': ['javascript', 'typescript'] +< + '*' is treated as any filetype. + + * blocklist: + optional + String array of filetypes not to run the language server. + + Example: > + 'blocklist': ['javascript', 'typescript'] +< + '*' is treated as any filetype. + + allowlist and blocklist can be used together. The following example + says to run the language server for all filetypes except javascript + and typescript. blocklist always takes higher priority over allowlist. +> + 'allowlist': ['*'] + 'blocklist': ['javascript', 'typescript'] +< + * workspace_config: + optional + vim |dict| or a function returning a vim |dict| + Used to pass workspace configuration to the server after + initialization. Configuration settings are language-server specific. + + Example: > + 'workspace_config': {'pylsp': {'plugins': \ + {'pydocstyle': {'enabled': v:true}}}} +< + * languageId: + optional function returning |string| + By default the languageId is the current filetype. If you're using a sub + filetype like 'ios.swift' your language server may not return anything + because it does not know this language. + In this case you might want to overwrite the languageId with this key. + + Example: > + 'languageId': {server_info->'typescript'} +< + * config: + optional vim |dict| + Used to pass additional custom config. + + For example: > + 'config': { 'prefer_local': 1 } +< + This can then be used by cmd function. +> + function! s:myserver_cmd(server_info) abort + let l:config = get(a:server_info, 'config', {}) + let l:prefer_local = get(l:config, 'prefer_local', 1) + if (l:prefer_local) + return ['./local-executable'] + else + return ['/bin/global-exectuable'] + endif + endfunction + + 'cmd': function('s:myserver_cmd') +< + Using the `config` key, you can also specify a custom 'typed word + pattern', or a custom filter for completion items, see + |vim-lsp-completion-filter|. + + The following per-server configuration options are supported by vim-lsp. + + * hover_conceal *vim-lsp-server_info-hover_conceal* + Type: |Boolean| + Default: |g:lsp_hover_conceal| + + This takes precedence over the value of |g:lsp_hover_conceal|, to + allow overriding this setting per server. + + Example: > + 'config': { 'hover_conceal': 1 } +< + * symbol_kinds + Type: |Dict| + Default: |{}| + + This allows overriding the default text mappings for symbol kinds + (e.g., "module", "method") per server. Useful for abbreviating or + removing the kind text. + + Example: > + 'config': { 'symbol_kinds': {'26': 'type' } } +< + * completion_item_kinds + Type: |Dict| + Default: |{}| + + This allows overriding the default text mappings for completion + item kinds (e.g., "module", "method") per server. Useful for + abbreviating or removing the kind text. + + Example: > + 'config': { 'completion_item_kinds': {'26': 'type' } } +< + * diagnostics + Type: |Boolean| + Default: |v:true| + + This allows disablingdiagnostics per server. Useful when dealing + with multiple servers (One for diagnostic only) + + Example: > + 'config': { 'diagnostics': v:false } +< + * env: + optional vim |dict| + Used to pass environment variables to the cmd. + Example: > + 'env': { 'GOFLAGS': '-tags=wireinject' } +< + +refresh_pattern *vim-lsp-refresh_pattern* + Type: |String| (|pattern|) + Default: `'\k*$'` + +Vim-lsp will automatically detect start column of completion so far when +invoking completion. It does this by checking the textEdit's range of each +completion item. + +You can use a |regexp| to determine what you want to start completion with +matched text so far. The pattern is matched against the current line, from +column 0 up until the cursor's position. Thus, |/$| means "current cursor +position" in this context. + +For example: > + 'config': { 'refresh_pattern': '\k*$' } +< +This uses all characters in `'iskeyword'` in front of the cursor as typed +word. + +This key is also used to align the completion menu: the completion menu is +placed so its left border is at the column that matches the start of the +`refresh_pattern`. + +filter *vim-lsp-completion-filter* + +You can filter the completion items returned from the server by specifying a +completion filter using the `filter` key in the server info's `config` |dict|. +The value of the `filter` key is itself a |dict| containing at least a key +`name`, which specifies which filter to use. + +The case (in)sensitivity of the matching is determined by |g:lsp_ignorecase|. + + Example: > + 'config': { 'filter': { 'name': 'none' } } +< + Available filters are: + - `none` (default) + Do not filter completion items, use all items returned from the + language server. + + - `prefix` + Only allow completion items that are a prefix of the already typed + word. + + - `contains` + Only allow completion items that contain the already typed word. + +Note: After triggering completion with |i_CTRL-X_CTRL-O|, further filtering is +only possible by adding to the already typed prefix (even if you're using the +`contains` filter). If you'd like to retrigger the filtering, you will have to +press CTRL-X CTRL-O again. + +sort *vim-lsp-completion-sort* + +You can sort the completion items returned from the server by using the `sort` +key in the server info's `config` |dict|. +The value of the `sort` key is itself a |dict| containing at least a key +`max`, which specifies max number of completion items count before giving up +sorting for performance reason. + +The case (in)sensitivity of the matching is determined by |g:lsp_ignorecase|. + + Example: > + 'config': { 'sort': { 'max': 100 } } + +lsp#register_command({command-name}, {callback}) *lsp#register_command()* + +Some language server expects handling custom command in the client. +You can use this function to add custom command handler. + +{command-name} is unique id to specify command. +{callback} is funcref that accepts below argument. +> + callback({ + 'command': { + 'command': string, + 'arguments': [...] + } + }) +< + +For example, the rust-analyzer expects the client handles some custom command +as below example. +> + function! s:rust_analyzer_apply_source_change(context) + let l:command = get(a:context, 'command', {}) + + let l:workspace_edit = get(l:command['arguments'][0], 'workspaceEdit', {}) + if !empty(l:workspace_edit) + call lsp#utils#workspace_edit#apply_workspace_edit(l:workspace_edit) + endif + + let l:cursor_position = get(l:command['arguments'][0], 'cursorPosition', {}) + if !empty(l:cursor_position) + call cursor(lsp#utils#position#lsp_to_vim('%', l:cursor_position)) + endif + endfunction + call lsp#register_command('rust-analyzer.applySourceChange', function('s:rust_analyzer_apply_source_change')) +< +lsp#stream() *lsp#stream()* + +Stream api to listen to responses and notifications from language server or +vim-lsp. Always verify the existence of request, response and server before +accessing. Subscribing to stream should never throw an error. +> + function! s:on_textDocumentDiagnostics(x) abort + echom 'Diagnostics for ' . a:x['server'] . ' ' . json_encode(a:x['response']) + endfunction + + au User lsp_setup call lsp#callbag#pipe( + \ lsp#stream(), + \ lsp#callbag#filter({x-> has_key(x, 'response') && !has_key(x['response'], 'error') && get(x['response'], 'method', '') == 'textDocument/publishDiagnostics'}), + \ lsp#callbag#subscribe({ 'next':{x->s:on_textDocumentDiagnostics(x)} }), + \ ) +< +Custom vim-lsp notifications streams: +vimp-lsp events mimic lsp server notifications. +* `server` is always `$vimlsp`. +* `response` `method` is always prefixed with ``$/vimlsp/` + + +|$/vimlsp/lsp_server_exit| + This is similar to |lsp_server_exit| autocommand. + + Example: > + { + "server": "$vimlsp", + "response": { + "method": "$/vimlsp/lsp_server_exit", + "params": { "server": "$vimlsp" } + } + } +< +lsp#stop_server({name-of-server}) *lsp#stop_server()* + +Used to stop the server. + + Example: > + call lsp#stop_server('name-of-server') +< + Note: + * If the server is not running or is not registered it is a noop. + * The server is forcefully stopped without sending shutdown request. + +lsp#get_server_status({name-of-server}) *lsp#get_server_status()* + +Get the status of a server. + + Example: > + call lsp#get_server_status('name-of-server') +< + Returns one of "unknown server", " "exited", "starting", "failed", + "running", "not running". + + +lsp#utils#position#lsp_to_vim({expr}, {position}) *lsp#utils#position#lsp_to_vim()* + +Convert LSP's position to vim's pos ([lnum, col]). + +{expr} is same of bufname argument. +{position} is LSP's position params. + + +lsp#utils#position#vim_to_lsp({expr}, {pos}) *lsp#utils#position#vim_to_lsp()* + +Convert vim's pos to LSP's position ({ 'line': ..., 'character': ... }). + +{expr} is same of bufname argument. +{pos} is vim's position params. + + *lsp#utils#find_nearest_parent_file_directory()* +lsp#utils#find_nearest_parent_file_directory({path}, {filename}) + +Find the nearest parent directory which contains the specific files or +diretories. The method has two parameters. The first is the path where +searching starts. The second is the files or directories names which +you want to find. The return value is the directory path which is found +the most times. +This method is mainly used to generate 'root_uri' when registering server. + + Example: > + if executable('ccls') + au User lsp_setup call lsp#register_server({ + \ 'name': 'ccls', + \ 'cmd': {server_info->['ccls']}, + \ 'root_uri':{server_info->lsp#utils#path_to_uri( + \ lsp#utils#find_nearest_parent_file_directory( + \ lsp#utils#get_buffer_path(), + \ ['.ccls', 'compile_commands.json', '.git/'] + \ ))}, + \ 'initialization_options': {}, + \ 'allowlist': ['c', 'cpp', 'objc', 'objcpp', 'cc'], + \ }) + endif +< + Note: + * The second parameter can be a |String| or a string |List|. + * For the second parameter, the string ends with '/' or '\' will + be regarded as a directory name, otherwise as a file name. + * If there is not directory with the specific files or diretories + found, the method will return an empty string. + +lsp#enable_diagnostics_for_buffer() *lsp#enable_diagnostic_for_buffer()* + +Re-enable diagnostics for the specified buffer. By default diagnostics are +enabled for all buffers. + + Example: > + :call lsp#enable_diagnostics_for_buffer() + :call lsp#enable_diagnostics_for_buffer(bufnr('%')) + +lsp#disable_diagnostics_for_buffer() *lsp#disable_diagnostics_for_buffer()* + +Diable diagnostics for the specified buffer. By default diagnostics are +enabled for all buffers. + + Example: > + :call lsp#enable_diagnostics_for_buffer() + :call lsp#enable_diagnostics_for_buffer(bufnr('%')) + +Diagnostics can be disabled for buffer to temporarily avoid conflicts with +other plugins. + + Example: > + augroup LspEasyMotion + autocmd! + autocmd User EasyMotionPromptBegin call lsp#disable_diagnostics_for_buffer() + autocmd User EasyMotionPromptEnd call lsp#enable_diagnostics_for_buffer() + augroup END + +lsp#get_buffer_diagnostics_counts() *lsp#get_buffer_diagnostics_counts()* + +Get dict with diagnostic counts for current buffer. Useful e.g. for display +in status line. + + Returns dictionary with keys "error", "warning", "information", "hint". + +lsp#get_buffer_first_error_line() *lsp#get_buffer_first_error_line()* + +Get line number of first error in current buffer. + + Returns |Number| or |v:null| if there are no errors. + +lsp#get_progress() *lsp#get_progress()* + + Return UI |List| of |Dict| with window/workDoneProgress + The |List| is most recently update order. + The |Dict| has keys as follows. + * server + Type: |String| + * token + Type: |String| + * title + Type: |String| + * messages + Type: |String| + * percentage + Type: |Number| + 0 - 100 or not exist + +lsp#document_hover_preview_winid() *lsp#document_hover_preview_winid()* + + Returns |windowid| of the current hover preview window or |v:null| if it does not + exist. + +lsp#scroll(count) *lsp#scroll()* + + Scroll current displayed floating/popup window with specified count. + + Example: > + nnoremap lsp#scroll(+4) + nnoremap lsp#scroll(-4) + +============================================================================== +Commands *vim-lsp-commands* + +LspAddTreeCallHierarchyIncoming *:LspAddTreeCallHierarchyIncoming* + +Just like |LspCallHierarchyIncoming| , but instead of making a new list the +result is appended to the current list. + +LspAddTreeReferences *:LspAddTreeReferences* + +Just like |LspReferences| , but instead of making a new list the result is +appended to the current list. + +LspCallHierarchyIncoming *:LspCallHierarchyIncoming* + +Find incoming call hierarchy for the symbol under cursor. + +LspCallHierarchyOutgoing *:LspCallHierarchyOutgoing* + +Find outgoing call hierarchy for the symbol under cursor. + +LspCodeAction [--ui=float|preview] [{CodeActionKind}] *:LspCodeAction* + +Gets a list of possible commands that can be applied to a file so it can be +fixed (quick fix). + +If the optional {CodeActionKind} specified, will invoke code action +immediately when matched code action is one only. + +LspCodeActionSync [--ui=float|preview] [{CodeActionKind}] *:LspCodeActionSync* + +Same as |:LspCodeAction| but synchronous. Useful when running |:autocmd| +commands such as organize imports before save. + + Example: > + autocmd BufWritePre + \ call execute('LspCodeActionSync source.organizeImports') + +LspCodeLens *:LspCodeLens* + +Gets a list of possible commands that can be executed on the current document. + +LspDocumentDiagnostics *:LspDocumentDiagnostics* + +Gets the document diagnostics and opens in |location-list|. By default +diagnostics are filtered for current buffer. + +Arguments: + + --buffers Defaults to empty string, i.e. shows diagnostics for current + buffer. To show diagnostic for all buffers use `--buffers=*`. + + Example: > + :LspDocumentDiagnostics + :LspDocumentDiagnostics --buffers=* + +LspDeclaration *:LspDeclaration* + +Go to declaration. Useful for languages such as C/C++ where there is a clear +distinction between declaration and definition. +This accepts ||. + +Also see |:LspPeekDeclaration|. + +LspDefinition *:LspDefinition* + +Go to definition. +This accepts ||. + +Also see |:LspPeekDefinition|. + +LspDocumentFold *:LspDocumentFold* + +Recalculate folds for the current buffer. + +LspDocumentFoldSync *:LspDocumentFoldSync* + +Same as |:LspDocumentFold|, but synchronous. + +LspDocumentFormat *:LspDocumentFormat* + +Format the entire document. + +LspDocumentFormatSync *:LspDocumentFormatSync* + +Same as |:LspDocumentFormat| but synchronous. Useful when running |:autocmd| +commands such as formatting before save. Set |g:lsp_format_sync_timeout| to +configure timeouts. + + Example: > + autocmd BufWritePre LspDocumentFormatSync + +Note that this may slow down vim. + +LspDocumentRangeFormat *:LspDocumentRangeFormat* + +Format the current document selection. + +LspDocumentRangeFormatSync *:LspDocumentRangeFormatSync* + +Same as |:LspDocumentRangeFormat| but synchronous. Useful when running :autocmd +commands. Set |g:lsp_format_sync_timeout| to configure timeouts. + +Note that this may slow down vim. + +LspDocumentSymbol *:LspDocumentSymbol* + +Gets the symbols for the current document. + +LspDocumentSymbolSearch *:LspDocumentSymbolSearch* + +Search the symbols for the current document and navigate. + +LspHover [--ui=float|preview] *:LspHover* + +Gets the hover information and displays it in the |preview-window|. + + * |preview-window| can be closed using the default vim mapping - ``. + * To control the default focus of |preview-window| for |:LspHover| + configure |g:lsp_preview_keep_focus|. + * If using neovim with nvim_win_open() available, |g:lsp_preview_float| can + be set to enable a floating preview at the cursor which is closed + automatically on cursormove if not focused and can be closed with if + focused. + + Example: > + :LspHover + :LspHover --ui=float + :LspHover --ui=preview + +LspNextDiagnostic [-wrap=0] *:LspNextDiagnostic* + +Jump to Next diagnostics including error, warning, information, hint. +With '-wrap=0', stop wrapping around the end of file. + +LspNextError [-wrap=0] *:LspNextError* + +Jump to Next err diagnostics +With '-wrap=0', stop wrapping around the end of file. + +LspNextReference *:LspNextReference* + +Jump to the next reference of the symbol under cursor. + +LspNextWarning [-wrap=0] *:LspNextWarning* + +Jump to Next warning diagnostics +With '-wrap=0', stop wrapping around the end of file. + +LspPeekDeclaration *:LspPeekDeclaration* + +Like |:LspDeclaration|, but opens the declaration in the |preview-window| +instead of the current window. + +Also see |g:lsp_peek_alignment| and |g:lsp_preview_float|. + +LspPeekDefinition *:LspPeekDefinition* + +Like |:LspDefinition|, but opens the definition in the |preview-window| +instead of the current window. + +Also see |g:lsp_peek_alignment| and |g:lsp_preview_float|. + +LspPeekImplementation *:LspPeekImplementation* + +Like |:LspImplementation|, but opens the implementation in the +|preview-window| instead of the current window. + +Also see |g:lsp_peek_alignment| and |g:lsp_preview_float|. + +LspPeekTypeDefinition *:LspPeekTypeDefinition* + +Like |:LspTypeDefinition|, but opens the type definition in the +|preview-window| instead of the current window. + +Also see |g:lsp_peek_alignment| and |g:lsp_preview_float|. + +LspPreviousDiagnostic [-wrap=0] *:LspPreviousDiagnostic* + +Jump to Previous diagnostics including error, warning, information, hint. +With '-wrap=0', stop wrapping around the top of file. + +LspPreviousError [-wrap=0] *:LspPreviousError* + +Jump to Previous err diagnostics +With '-wrap=0', stop wrapping around the top of file. + +LspPreviousReference *:LspPreviousReference* + +Jump to the previous reference of the symbol under cursor. + +LspPreviousWarning [-wrap=0] *:LspPreviousWarning* + +Jump to Previous warning diagnostics +With '-wrap=0', stop wrapping around the top of file. + +LspImplementation *:LspImplementation* + +Find all implementation of interface. +This accepts ||. + +Also see |:LspPeekImplementation|. + +LspReferences *:LspReferences* + +Find all references. + +LspRename *:LspRename* + +Rename the symbol. + +LspSemanticHighlightGroups *:LspSemanticHighlightGroups* + +List the highlight groups provided by the current semantic tokens server. + +LspTypeDefinition *:LspTypeDefinition* + +Go to the type definition. +This accepts ||. + +LspTypeHierarchy *:LspTypeHierarchy* + +View type hierarchy for the symbol under cursor. + +Also see |:LspPeekTypeDefinition|. + +LspWorkspaceSymbol *:LspWorkspaceSymbol* + +Search and show workspace symbols in quickfix. +Servers may choose to return empty results if the search query is empty. + +LspWorkspaceSymbolSearch *:LspWorkspaceSymbolSearch* + +Search the workspace symbols for all servers and navigate using quickpick. +Servers may choose to return empty results if the search query is empty. + +LspStatus *:LspStatus* + +Prints the status of all registered servers. Use `:verbose LspStatus` to +additionally show each server's workspace_config. +See also |vim-lsp-healthcheck|. + +LspStopServer[!] [name] *:LspStopServer* + +:LspStopServer + +Stops all active servers that handle files matching the current buffer type. +This is often what you want. For example, if you have multiple files of +different types open, `LspStopServer` will only stop the server for the +current buffer. Shows an error if there are no active LSP servers for the +current buffer. + +:LspStopServer! + +Stops all active servers, regardless of the current buffer type. Shows a +message for every stopped server. + +:LspStopServer name + +Stops a server named 'name', comparing the provided ID with the value of the +the 'name' property in the |lsp#register_server()| call. Shows an error if +'name' does not match a defined and currently running server. + +Completion should list only currently running servers for the 'name' argument. + +============================================================================== +Autocommands *vim-lsp-autocommands* + +lsp_setup *lsp_setup* + +This autocommand is run once after vim-lsp is enabled. The server should be +registered when this event is triggered. + +lsp_complete_done *lsp_complete_done* + +This autocommand is run after Insert mode completion is done, similar to +|CompleteDone|. However, the difference is that |lsp_complete_done| is run +only after vim-lsp has finished executing its internal |CompleteDone| +autocommands (e.g. applying text edits). It is thus ideal to use for snippet +expansion, or custom post processing of completed items. Just like +|CompleteDone|, the Vim variable |v:completed_item| contains information about +the completed item. It is guaranteed that vim-lsp does not change the content +of this variable during its |CompleteDone| autocommands. + +lsp_float_opened *lsp_float_opened* + +This autocommand is run after the floating window is shown for preview. +See also |preview-window| + +lsp_float_closed *lsp_float_closed* + +This autocommand is run after the floating window is closed. +See also |preview-window| + +lsp_float_focused *lsp_float_focused* + +This autocommand is run after the floating window is focused. Only supported in +neovim. + +You can map `(lsp-float-close)` to close the floating window. + +lsp_register_server *lsp_register_server* + +This autocommand is run after the server is registered. + +lsp_unregister_server *lsp_unregister_server* + +This autocommand is run after the server is unregistered. + +lsp_server_init *lsp_server_init* + +This autocommand is run after the server is initialized. + +lsp_server_exit *lsp_server_exit* + +This autocommand is run after the server is exited. + +lsp_buffer_enabled *lsp_buffer_enabled* + +This autocommand is run after vim-lsp is enabled for the buffer. This event is +triggered immediately when the buffer is currently active. If the buffer is not +current active, the event will be triggered when the buffer will be active. + +lsp_diagnostics_updated *lsp_diagnostics_updated* + +This autocommand us run after every time after new diagnostics received and +processed by vim-lsp. +> + function! DoSomething + echo lsp#get_buffer_diagnostics_counts() + endfunction + + augroup OnLSP + autocmd! + autocmd User lsp_diagnostics_updated call DoSomething() + augroup END +< +lsp_progress_updated *lsp_progress_updated* + +This autocommand is run after every time after progress updated and +processed by vim-lsp. Used for statusline plugins. + +============================================================================== +Mappings *vim-lsp-mappings* + +To map keys to the feature of vim-lsp, use mappings: +> + autocmd FileType python,go nmap gd (lsp-definition) +< +Available plug mappings are following: + + nnoremap (lsp-call-hierarchy-incoming) + nnoremap (lsp-call-hierarchy-outgoing) + nnoremap (lsp-code-action) + nnoremap (lsp-code-action-float) + nnoremap (lsp-code-action-preview) + nnoremap (lsp-code-lens) + nnoremap (lsp-declaration) + nnoremap (lsp-peek-declaration) + nnoremap (lsp-definition) + nnoremap (lsp-peek-definition) + nnoremap (lsp-document-symbol) + nnoremap (lsp-document-symbol-search) + nnoremap (lsp-document-diagnostics) + nnoremap (lsp-hover) + nnoremap (lsp-hover-float) + nnoremap (lsp-hover-preview) + nnoremap (lsp-next-diagnostic) + nnoremap (lsp-next-diagnostic-nowrap) + nnoremap (lsp-next-error) + nnoremap (lsp-next-error-nowrap) + nnoremap (lsp-next-reference) + nnoremap (lsp-next-warning) + nnoremap (lsp-next-warning-nowrap) + nnoremap (lsp-preview-close) + nnoremap (lsp-preview-focus) + nnoremap (lsp-previous-diagnostic) + nnoremap (lsp-previous-diagnostic-nowrap) + nnoremap (lsp-previous-error) + nnoremap (lsp-previous-error-nowrap) + nnoremap (lsp-previous-reference) + nnoremap (lsp-previous-warning) + nnoremap (lsp-previous-warning-nowrap) + nnoremap (lsp-references) + nnoremap (lsp-rename) + nnoremap (lsp-workspace-symbol) + nnoremap (lsp-workspace-symbol-search) + nnoremap (lsp-document-format) + vnoremap (lsp-document-format) + nnoremap (lsp-document-range-format) + xnoremap (lsp-document-range-format) + nnoremap (lsp-implementation) + nnoremap (lsp-peek-implementation) + nnoremap (lsp-type-definition) + nnoremap (lsp-peek-type-definition) + nnoremap (lsp-type-hierarchy) + nnoremap (lsp-status) + nnoremap (lsp-signature-help) + +See also |vim-lsp-commands| + +(lsp-preview-close) *(lsp-preview-close)* + +Closes an opened preview window + +(lsp-preview-focus) *(lsp-preview-focus)* + +Transfers focus to an opened preview window or back to the previous window if +focus is already on the preview window. + + +============================================================================== +Autocomplete *vim-lsp-autocomplete* + +omnifunc *vim-lsp-omnifunc* + +vim-lsp by default only provides basic omnifunc support for autocomplete. + +Completion can be made asynchronous by setting g:lsp_async_completion. +Note that this may cause unexpected behavior in some plugins such as +MUcomplete. + +If you would like to have more advanced features please use asyncomplete.vim +as described below. + + Example: > + autocmd FileType typescript setlocal omnifunc=lsp#complete + +asyncomplete.vim *vim-lsp-asyncomplete* + +asyncomplete.vim is a async auto complete plugin for vim8 and neovim written +in pure vim script. https://github.com/prabirshrestha/asyncomplete.vim + +Example: > + Plug 'prabirshrestha/vim-lsp' + Plug 'prabirshrestha/asyncomplete.vim' + Plug 'prabirshrestha/asyncomplete-lsp.vim' + +For additional configuration refer to asyncomplete.vim docs. + +============================================================================== +Tagfunc *vim-lsp-tagfunc* + +vim-lsp can integrate with vim's native tag functionality for navigating code +using the |'tagfunc'| option (requires vim/neovim with patch-8.1.1228). + + Example: > + autocmd FileType typescript setlocal tagfunc=lsp#tagfunc + +============================================================================== +Snippets *vim-lsp-snippets* + +To integrate snippets in vim-lsp, you will first have to install a third-party +snippet plugin, and a plugin that integrates it in vim-lsp. At the moment, +you have two options: + +1. vim-vsnip +https://github.com/hrsh7th/vim-vsnip +https://github.com/hrsh7th/vim-vsnip-integ + +2. UltiSnips and vim-lsp-ultisnips +https://github.com/SirVer/ultisnips +https://github.com/thomasfaingnaert/vim-lsp-ultisnips + +3. neosnippet.vim and vim-lsp-neosnippet +https://github.com/Shougo/neosnippet.vim +https://github.com/thomasfaingnaert/vim-lsp-neosnippet + +Refer to the readme and docs of vim-vsnip, vim-lsp-ultisnips and +vim-lsp-neosnippet for more information and configuration options. + +============================================================================== +Folding *vim-lsp-folding* + +You can also let the language server handle folding for you. To enable this +feature, you will have to set 'foldmethod', 'foldexpr' and 'foldtext' (the +latter is optional) correctly: +> + set foldmethod=expr + \ foldexpr=lsp#ui#vim#folding#foldexpr() + \ foldtext=lsp#ui#vim#folding#foldtext() + +Also, make sure you have not disabled folding globally, see +|g:lsp_fold_enabled|. + +You may want to enable this only for certain filetypes, e.g. for Javascript +only: +> + augroup lsp_folding + autocmd! + autocmd FileType javascript setlocal + \ foldmethod=expr + \ foldexpr=lsp#ui#vim#folding#foldexpr() + \ foldtext=lsp#ui#vim#folding#foldtext() + augroup end + +To display open and closed folds at the side of the window, see +'foldcolumn'. +If you want to remove the dashes at the end of the folds, you can change +the fold item of 'fillchars'. + +============================================================================== +Semantic highlighting *vim-lsp-semantic* + +To use semantic highlighting, you need Neovim highlights, or Vim with the +|textprop| feature enabled at compile time. + +To enable semantic highlighting, |g:lsp_semantic_enabled| should be set to `1` +(it is `0` by default). You can check if semantic highlighting is enabled +by running: > + echo lsp#internal#semantic#is_enabled() + +vim-lsp provides |highlight| groups for each of the token types supported by +the current LSP server. This includes highlight groups for each of the +standard set of token types: +* `LspSemanticType` +* `LspSemanticClass` +* `LspSemanticEnum` +* `LspSemanticInterface` +* `LspSemanticStruct` +* `LspSemanticTypeParameter` +* `LspSemanticParameter` +* `LspSemanticVariable` +* `LspSemanticProperty` +* `LspSemanticEnumMember` +* `LspSemanticEvents` +* `LspSemanticFunction` +* `LspSemanticMethod` +* `LspSemanticKeyword` +* `LspSemanticModifier` +* `LspSemanticComment` +* `LspSemanticString` +* `LspSemanticNumber` +* `LspSemanticRegexp` +* `LspSemanticOperator` +as well as additional highlight groups for any further types supported by the +server. For example, clangd provides `LspNamespace`. + +The standard set of token types have sensible defaults provided, however +any other types require manual configuration. The types provided by the +current buffer's semantic tokens server can be found by running +|:LspSemanticTokenTypes|. + +LSP servers may also provide modifiers for each of the tokens. The standard +set is: +* `Declaration` +* `Definition` +* `Readonly` +* `Static` +* `Deprecated` +* `Abstract` +* `Async` +* `Modification` +* `Documentation` +* `DefaultLibrary` +Servers may also provide their own modifiers. The full set of types provided +by the current buffer's semantic tokens server can be found by running +|:LspSemanticTokenModifiers|. + +If modifiers are applied to a token, the name of the |highlight| group will +be prepended with each of the modifier names, for example a static default +library function will use the highlight group +`LspSemanticStaticDefaultLibraryFunction`. By default, any modified highlight +groups are linked to their unmodified equivalent. + +============================================================================== +Popup Formatting *vim-lsp-popup-format* + +Popup windows use the |gq| operator for formatting content to the window. + +For customization, see +|formatprg|. + +============================================================================== +Workspace Folders *vim-lsp-workspace-folders* + +Workspace folders is an experimental feature of vim-lsp. To enable workspace +folders set `let g:lsp_experimental_workspace_folders = 1`. In the future this +flag will be removed and workspace folders will be enabled by default. + +When a new buffer is opened, if the server supports workspace folder, it will +call `root_uri` function to detect the workspace folder. If the folder is not +part of workspace folder, it will automatically notify the server to add the +workspace folder. + +============================================================================= +License *vim-lsp-license* + +The MIT License (MIT) + +Full license text: https://github.com/prabirshrestha/vim-lsp/blob/master/LICENSE + +============================================================================== +Maintainers *vim-lsp-maintainers* + +* Prabir Shrestha (author, maintainer): https://github.com/prabirshrestha +* mattn (maintainer): https://github.com/mattn +* hrsh7th (maintainer): https://github.com/hrsh7th +* Thomas Faingnaert (maintainer): https://github.com/thomasfaingnaert +* rhysd (maintainer): https://github.com/rhysd + +vim:tw=78:ts=8:ft=help:norl:noet:fen:noet: diff --git a/ftplugin/lsp-hover.vim b/ftplugin/lsp-hover.vim new file mode 100644 index 00000000..4f1a7f46 --- /dev/null +++ b/ftplugin/lsp-hover.vim @@ -0,0 +1,24 @@ +" No usual did_ftplugin header here as we NEED to run this always + +if has('patch-8.1.1517') && g:lsp_preview_float && !has('nvim') + " Can not set buftype or popup_close will fail with 'not a popup window' + setlocal bufhidden=wipe noswapfile nobuflisted +else + setlocal previewwindow buftype=nofile bufhidden=wipe noswapfile nobuflisted +endif + +if has('conceal') && b:lsp_do_conceal + setlocal conceallevel=2 +endif + +setlocal nocursorline nofoldenable nonumber norelativenumber + +if has('syntax') + setlocal nospell +endif + +let b:undo_ftplugin = 'setlocal pvw< bt< bh< swf< bl< cul< fen<' . + \ (has('syntax') ? ' spell<' : '') . + \ ' number< relativenumber<' . + \ (has('conceal') && b:lsp_do_conceal ? ' conceallevel<' : '') . + \ ' | unlet! g:markdown_fenced_languages' diff --git a/minimal.vimrc b/minimal.vimrc new file mode 100644 index 00000000..cc9e8aee --- /dev/null +++ b/minimal.vimrc @@ -0,0 +1,35 @@ +" install: curl https://raw.githubusercontent.com/prabirshrestha/vim-lsp/master/minimal.vimrc -o /tmp/minimal.vimrc +" uninstall: rm /tmp/plug.vim && rm -rf /tmp/plugged +" run vim/neovim with minimal.vimrc +" vim -u minimal.vimrc +" :PlugInstall + +set nocompatible hidden laststatus=2 + +if !filereadable(expand('~/plug.vim')) + silent !curl -fLo ~/plug.vim + \ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim +endif + +source ~/plug.vim + +call plug#begin('~/.vim.plugged') +Plug 'prabirshrestha/asyncomplete.vim' +Plug 'prabirshrestha/vim-lsp' +Plug 'prabirshrestha/asyncomplete-lsp.vim' +call plug#end() + +imap (asyncomplete_force_refresh) +inoremap pumvisible() ? "\" : "\" +inoremap pumvisible() ? "\" : "\" +inoremap pumvisible() ? "\\" : "\" +autocmd! CompleteDone * if pumvisible() == 0 | pclose | endif + +if executable('pylsp') + " pip install python-lsp-server + au User lsp_setup call lsp#register_server({ + \ 'name': 'pylsp', + \ 'cmd': {server_info->['pylsp']}, + \ 'allowlist': ['python'], + \ }) +endif diff --git a/plugin/lsp.vim b/plugin/lsp.vim new file mode 100644 index 00000000..11a2b442 --- /dev/null +++ b/plugin/lsp.vim @@ -0,0 +1,240 @@ +if exists('g:lsp_loaded') || !exists('*json_encode') || !has('timers') || !has('lambda') + finish +endif +let g:lsp_loaded = 1 + +let g:lsp_use_lua = get(g:, 'lsp_use_lua', has('nvim-0.4.0') || (has('lua') && has('patch-8.2.0775'))) +let g:lsp_use_native_client = get(g:, 'lsp_use_native_client', 0) +let g:lsp_auto_enable = get(g:, 'lsp_auto_enable', 1) +let g:lsp_async_completion = get(g:, 'lsp_async_completion', 0) +let g:lsp_log_file = get(g:, 'lsp_log_file', '') +let g:lsp_log_verbose = get(g:, 'lsp_log_verbose', 1) +let g:lsp_debug_servers = get(g:, 'lsp_debug_servers', []) +let g:lsp_format_sync_timeout = get(g:, 'lsp_format_sync_timeout', -1) +let g:lsp_max_buffer_size = get(g:, 'lsp_max_buffer_size', 5000000) + +let g:lsp_completion_documentation_enabled = get(g:, 'lsp_completion_documentation_enabled', 1) +let g:lsp_completion_documentation_delay = get(g:, 'lsp_completion_documention_delay', 80) + +let g:lsp_diagnostics_enabled = get(g:, 'lsp_diagnostics_enabled', 1) +let g:lsp_diagnostics_echo_cursor = get(g:, 'lsp_diagnostics_echo_cursor', 0) +let g:lsp_diagnostics_echo_delay = get(g:, 'lsp_diagnostics_echo_delay', 500) +let g:lsp_diagnostics_float_cursor = get(g:, 'lsp_diagnostics_float_cursor', 0) +let g:lsp_diagnostics_float_delay = get(g:, 'lsp_diagnostics_float_delay', 500) +let g:lsp_diagnostics_float_insert_mode_enabled = get(g:, 'lsp_diagnostics_float_insert_mode_enabled', 1) +let g:lsp_diagnostics_highlights_enabled = get(g:, 'lsp_diagnostics_highlights_enabled', lsp#utils#_has_highlights()) +let g:lsp_diagnostics_highlights_insert_mode_enabled = get(g:, 'lsp_diagnostics_highlights_insert_mode_enabled', 1) +let g:lsp_diagnostics_highlights_delay = get(g:, 'lsp_diagnostics_highlights_delay', 500) +let g:lsp_diagnostics_signs_enabled = get(g:, 'lsp_diagnostics_signs_enabled', lsp#utils#_has_signs()) +let g:lsp_diagnostics_signs_insert_mode_enabled = get(g:, 'lsp_diagnostics_signs_insert_mode_enabled', 1) +let g:lsp_diagnostics_signs_delay = get(g:, 'lsp_diagnostics_signs_delay', 500) +let g:lsp_diagnostics_signs_error = get(g:, 'lsp_diagnostics_signs_error', {}) +let g:lsp_diagnostics_signs_warning = get(g:, 'lsp_diagnostics_signs_warning', {}) +let g:lsp_diagnostics_signs_information = get(g:, 'lsp_diagnostics_signs_information', {}) +let g:lsp_diagnostics_signs_hint = get(g:, 'lsp_diagnostics_signs_hint', {}) +let g:lsp_diagnostics_signs_priority = get(g:, 'lsp_diagnostics_signs_priority', 10) +let g:lsp_diagnostics_signs_priority_map = get(g:, 'lsp_diagnostics_signs_priority_map', {}) +let g:lsp_diagnostics_virtual_text_enabled = get(g:, 'lsp_diagnostics_virtual_text_enabled', lsp#utils#_has_nvim_virtual_text() || lsp#utils#_has_vim_virtual_text()) +let g:lsp_diagnostics_virtual_text_insert_mode_enabled = get(g:, 'lsp_diagnostics_virtual_text_insert_mode_enabled', 0) +let g:lsp_diagnostics_virtual_text_delay = get(g:, 'lsp_diagnostics_virtual_text_delay', 500) +let g:lsp_diagnostics_virtual_text_prefix = get(g:, 'lsp_diagnostics_virtual_text_prefix', '') +let g:lsp_diagnostics_virtual_text_align = get(g:, 'lsp_diagnostics_virtual_text_align', 'below') +let g:lsp_diagnostics_virtual_text_wrap = get(g:, 'lsp_diagnostics_virtual_text_wrap', 'wrap') +let g:lsp_diagnostics_virtual_text_padding_left = get(g:, 'lsp_diagnostics_virtual_text_padding_left', 1) + +let g:lsp_document_code_action_signs_enabled = get(g:, 'lsp_document_code_action_signs_enabled', 1) +let g:lsp_document_code_action_signs_delay = get(g:, 'lsp_document_code_action_signs_delay', 500) +let g:lsp_document_code_action_signs_hint = get(g:, 'lsp_document_code_action_signs_hint', {}) +let g:lsp_document_code_action_signs_priority = get(g:, 'lsp_document_code_action_signs_priority', 10) + +let g:lsp_tree_incoming_prefix = get(g:, 'lsp_tree_incoming_prefix', '<= ') + +let g:lsp_preview_keep_focus = get(g:, 'lsp_preview_keep_focus', 1) +let g:lsp_use_event_queue = get(g:, 'lsp_use_event_queue', has('nvim') || has('patch-8.1.0889')) +let g:lsp_insert_text_enabled= get(g:, 'lsp_insert_text_enabled', 1) +let g:lsp_text_edit_enabled = get(g:, 'lsp_text_edit_enabled', has('patch-8.0.1493')) +let g:lsp_document_highlight_enabled = get(g:, 'lsp_document_highlight_enabled', 1) +let g:lsp_document_highlight_delay = get(g:, 'lsp_document_highlight_delay', 350) +let g:lsp_preview_float = get(g:, 'lsp_preview_float', 1) +let g:lsp_preview_autoclose = get(g:, 'lsp_preview_autoclose', 1) +let g:lsp_preview_doubletap = get(g:, 'lsp_preview_doubletap', [function('lsp#ui#vim#output#focuspreview')]) +let g:lsp_preview_fixup_conceal = get(g:, 'lsp_preview_fixup_conceal', 0) +let g:lsp_peek_alignment = get(g:, 'lsp_peek_alignment', 'center') +let g:lsp_preview_max_width = get(g:, 'lsp_preview_max_width', -1) +let g:lsp_preview_max_height = get(g:, 'lsp_preview_max_height', -1) +let g:lsp_float_max_width = get(g:, 'lsp_float_max_width', -1) +let g:lsp_signature_help_enabled = get(g:, 'lsp_signature_help_enabled', 1) +let g:lsp_signature_help_delay = get(g:, 'lsp_signature_help_delay', 200) +let g:lsp_show_workspace_edits = get(g:, 'lsp_show_workspace_edits', 0) +let g:lsp_fold_enabled = get(g:, 'lsp_fold_enabled', 1) +let g:lsp_hover_conceal = get(g:, 'lsp_hover_conceal', 1) +let g:lsp_hover_ui = get(g:, 'lsp_hover_ui', '') +let g:lsp_ignorecase = get(g:, 'lsp_ignorecase', &ignorecase) +let g:lsp_semantic_enabled = get(g:, 'lsp_semantic_enabled', 0) +let g:lsp_semantic_delay = get(g:, 'lsp_semantic_delay', 500) +let g:lsp_text_document_did_save_delay = get(g:, 'lsp_text_document_did_save_delay', -1) +let g:lsp_completion_resolve_timeout = get(g:, 'lsp_completion_resolve_timeout', 200) +let g:lsp_tagfunc_source_methods = get(g:, 'lsp_tagfunc_source_methods', ['definition', 'declaration', 'implementation', 'typeDefinition']) +let g:lsp_show_message_request_enabled = get(g:, 'lsp_show_message_request_enabled', 1) +let g:lsp_show_message_log_level = get(g:, 'lsp_show_message_log_level', 'warning') +let g:lsp_work_done_progress_enabled = get(g:, 'lsp_work_done_progress_enabled', 0) +let g:lsp_untitled_buffer_enabled = get(g:, 'lsp_untitled_buffer_enabled', 1) +let g:lsp_inlay_hints_enabled = get(g:, 'lsp_inlay_hints_enabled', 0) +let g:lsp_inlay_hints_delay = get(g:, 'lsp_inlay_hints_delay', 350) +let g:lsp_code_action_ui = get(g:, 'lsp_code_action_ui', 'preview') + +let g:lsp_get_supported_capabilities = get(g:, 'lsp_get_supported_capabilities', [function('lsp#default_get_supported_capabilities')]) + +let g:lsp_document_symbol_detail = get(g:, 'lsp_document_symbol_detail', 0) + +let g:lsp_experimental_workspace_folders = get(g:, 'lsp_experimental_workspace_folders', 0) + +if g:lsp_auto_enable + augroup lsp_auto_enable + autocmd! + autocmd VimEnter * call lsp#enable() + augroup END +endif + +command! LspAddTreeCallHierarchyIncoming call lsp#ui#vim#add_tree_call_hierarchy_incoming() +command! LspCallHierarchyIncoming call lsp#ui#vim#call_hierarchy_incoming({}) +command! LspCallHierarchyOutgoing call lsp#ui#vim#call_hierarchy_outgoing() +command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeAction call lsp#ui#vim#code_action#do( + \ extend({ 'sync': v:false, 'selection': != 0 }, lsp#utils#args#_parse(, { + \ 'ui': { 'type': type('') }, + \ }, 'query'))) +command! -range -nargs=* -complete=customlist,lsp#ui#vim#code_action#complete LspCodeActionSync call lsp#ui#vim#code_action#do( + \ extend({ 'sync': v:true, 'selection': != 0 }, lsp#utils#args#_parse(, { + \ 'ui': { 'type': type('') }, + \ }, 'query'))) +command! LspCodeLens call lsp#ui#vim#code_lens#do({}) +command! LspDeclaration call lsp#ui#vim#declaration(0, ) +command! LspPeekDeclaration call lsp#ui#vim#declaration(1) +command! LspDefinition call lsp#ui#vim#definition(0, ) +command! LspPeekDefinition call lsp#ui#vim#definition(1) +command! LspDocumentSymbol call lsp#ui#vim#document_symbol() +command! LspDocumentSymbolSearch call lsp#internal#document_symbol#search#do({}) +command! -nargs=? LspDocumentDiagnostics call lsp#internal#diagnostics#document_diagnostics_command#do( + \ extend({}, lsp#utils#args#_parse(, { + \ 'buffers': {'type': type('')}, + \ }, v:null))) +command! -nargs=? -complete=customlist,lsp#utils#empty_complete LspHover call lsp#internal#document_hover#under_cursor#do( + \ extend({}, lsp#utils#args#_parse(, { + \ 'ui': { 'type': type('') }, + \ }, v:null))) +command! -nargs=* LspNextError call lsp#internal#diagnostics#movement#_next_error() +command! -nargs=* LspPreviousError call lsp#internal#diagnostics#movement#_previous_error() +command! -nargs=* LspNextWarning call lsp#internal#diagnostics#movement#_next_warning() +command! -nargs=* LspPreviousWarning call lsp#internal#diagnostics#movement#_previous_warning() +command! -nargs=* LspNextDiagnostic call lsp#internal#diagnostics#movement#_next_diagnostics() +command! -nargs=* LspPreviousDiagnostic call lsp#internal#diagnostics#movement#_previous_diagnostics() +command! LspReferences call lsp#ui#vim#references({}) +command! LspAddTreeReferences call lsp#ui#vim#add_tree_references() +command! LspRename call lsp#ui#vim#rename() +command! LspTypeDefinition call lsp#ui#vim#type_definition(0, ) +command! LspTypeHierarchy call lsp#internal#type_hierarchy#show() +command! LspPeekTypeDefinition call lsp#ui#vim#type_definition(1) +command! -nargs=? LspWorkspaceSymbol call lsp#ui#vim#workspace_symbol() +command! -nargs=? LspWorkspaceSymbolSearch call lsp#internal#workspace_symbol#search#do({'query': }) +command! -range LspDocumentFormat call lsp#internal#document_formatting#format({ 'bufnr': bufnr('%') }) +command! -range -nargs=? LspDocumentFormatSync call lsp#internal#document_formatting#format( + \ extend({'bufnr': bufnr('%'), 'sync': 1 }, lsp#utils#args#_parse(, { + \ 'timeout': {'type': type(0)}, + \ 'sleep': {'type': type(0)}, + \ }, v:null))) +command! -range LspDocumentRangeFormat call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') }) +command! -range -nargs=? LspDocumentRangeFormatSync call lsp#internal#document_range_formatting#format( + \ extend({'bufnr': bufnr('%'), 'sync': 1 }, lsp#utils#args#_parse(, { + \ 'timeout': {'type': type(0)}, + \ 'sleep': {'type': type(0)}, + \ }, v:null))) +command! LspImplementation call lsp#ui#vim#implementation(0, ) +command! LspPeekImplementation call lsp#ui#vim#implementation(1) +command! -nargs=0 LspStatus call lsp#print_server_status() +command! LspNextReference call lsp#internal#document_highlight#jump(+1) +command! LspPreviousReference call lsp#internal#document_highlight#jump(-1) +command! -nargs=? -bang -complete=customlist,lsp#server_complete_running LspStopServer call lsp#ui#vim#stop_server("", ) +command! -nargs=? -complete=customlist,lsp#utils#empty_complete LspSignatureHelp call lsp#ui#vim#signature_help#get_signature_help_under_cursor() +command! LspDocumentFold call lsp#ui#vim#folding#fold(0) +command! LspDocumentFoldSync call lsp#ui#vim#folding#fold(1) +command! -nargs=0 LspSemanticTokenTypes echo lsp#internal#semantic#get_token_types() +command! -nargs=0 LspSemanticTokenModifiers echo lsp#internal#semantic#get_token_modifiers() + +nnoremap (lsp-call-hierarchy-incoming) :call lsp#ui#vim#call_hierarchy_incoming({}) +nnoremap (lsp-call-hierarchy-outgoing) :call lsp#ui#vim#call_hierarchy_outgoing() +nnoremap (lsp-code-action) :call lsp#ui#vim#code_action({}) +nnoremap (lsp-code-action-float) :call lsp#ui#vim#code_action({ 'ui': 'float' }) +nnoremap (lsp-code-action-preview) :call lsp#ui#vim#code_action({ 'ui': 'preview' }) +nnoremap (lsp-code-lens) :call lsp#ui#vim#code_lens() +nnoremap (lsp-declaration) :call lsp#ui#vim#declaration(0) +nnoremap (lsp-peek-declaration) :call lsp#ui#vim#declaration(1) +nnoremap (lsp-definition) :call lsp#ui#vim#definition(0) +nnoremap (lsp-peek-definition) :call lsp#ui#vim#definition(1) +nnoremap (lsp-document-symbol) :call lsp#ui#vim#document_symbol() +nnoremap (lsp-document-symbol-search) :call lsp#internal#document_symbol#search#do({}) +nnoremap (lsp-document-diagnostics) :call lsp#internal#diagnostics#document_diagnostics_command#do({}) +nnoremap (lsp-hover) :call lsp#internal#document_hover#under_cursor#do({}) +nnoremap (lsp-hover-float) :call lsp#internal#document_hover#under_cursor#do({ 'ui': 'float' }) +nnoremap (lsp-hover-preview) :call lsp#internal#document_hover#under_cursor#do({ 'ui': 'preview' }) +nnoremap (lsp-preview-close) :call lsp#ui#vim#output#closepreview() +nnoremap (lsp-preview-focus) :call lsp#ui#vim#output#focuspreview() +nnoremap (lsp-next-error) :call lsp#internal#diagnostics#movement#_next_error() +nnoremap (lsp-next-error-nowrap) :call lsp#internal#diagnostics#movement#_next_error("-wrap=0") +nnoremap (lsp-previous-error) :call lsp#internal#diagnostics#movement#_previous_error() +nnoremap (lsp-previous-error-nowrap) :call lsp#internal#diagnostics#movement#_previous_error("-wrap=0") +nnoremap (lsp-next-warning) :call lsp#internal#diagnostics#movement#_next_warning() +nnoremap (lsp-next-warning-nowrap) :call lsp#internal#diagnostics#movement#_next_warning("-wrap=0") +nnoremap (lsp-previous-warning) :call lsp#internal#diagnostics#movement#_previous_warning() +nnoremap (lsp-previous-warning-nowrap) :call lsp#internal#diagnostics#movement#_previous_warning("-wrap=0") +nnoremap (lsp-next-diagnostic) :call lsp#internal#diagnostics#movement#_next_diagnostics() +nnoremap (lsp-next-diagnostic-nowrap) :call lsp#internal#diagnostics#movement#_next_diagnostics("-wrap=0") +nnoremap (lsp-previous-diagnostic) :call lsp#internal#diagnostics#movement#_previous_diagnostics() +nnoremap (lsp-previous-diagnostic-nowrap) :call lsp#internal#diagnostics#movement#_previous_diagnostics("-wrap=0") +nnoremap (lsp-references) :call lsp#ui#vim#references({}) +nnoremap (lsp-rename) :call lsp#ui#vim#rename() +nnoremap (lsp-type-definition) :call lsp#ui#vim#type_definition(0) +nnoremap (lsp-type-hierarchy) :call lsp#internal#type_hierarchy#show() +nnoremap (lsp-peek-type-definition) :call lsp#ui#vim#type_definition(1) +nnoremap (lsp-workspace-symbol) :call lsp#ui#vim#workspace_symbol('') +nnoremap (lsp-workspace-symbol-search) :call lsp#internal#workspace_symbol#search#do({}) +nnoremap (lsp-document-format) :call lsp#internal#document_formatting#format({ 'bufnr': bufnr('%') }) +vnoremap (lsp-document-format) :silent call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') }) +nnoremap (lsp-document-range-format) :set opfunc=lsp#internal#document_range_formatting#opfuncg@ +xnoremap (lsp-document-range-format) :silent call lsp#internal#document_range_formatting#format({ 'bufnr': bufnr('%') }) +nnoremap (lsp-implementation) :call lsp#ui#vim#implementation(0) +nnoremap (lsp-peek-implementation) :call lsp#ui#vim#implementation(1) +nnoremap (lsp-status) :echo lsp#get_server_status() +nnoremap (lsp-next-reference) :call lsp#internal#document_highlight#jump(+1) +nnoremap (lsp-previous-reference) :call lsp#internal#document_highlight#jump(-1) +nnoremap (lsp-signature-help) :call lsp#ui#vim#signature_help#get_signature_help_under_cursor() + +if has('gui_running') + anoremenu L&sp.Goto.Definition :LspDefinition + anoremenu L&sp.Goto.Declaration :LspDeclaration + anoremenu L&sp.Goto.Implementation :LspImplementation + anoremenu L&sp.Goto.TypeDef :LspTypeDefinition + + anoremenu L&sp.Show\ Signature :LspShowSignature + anoremenu L&sp.Show\ References :LspReferences + anoremenu L&sp.Show\ Detail :LspHover + + anoremenu L&sp.Symbol\ Search :LspDocumentSymbolSearch + anoremenu L&sp.Outgoing\ Calls :LspCallHierarchyOutgoing + anoremenu L&sp.Incoming\ Calls :LspCallHierarchyIncoming + anoremenu L&sp.Rename :LspRename + anoremenu L&sp.Code\ Action :LspCodeAction + + anoremenu L&sp.Diagnostics.Next :LspNextDiagnostic + anoremenu L&sp.Diagnostics.Prev :LspPrevDiagnostic + + if &mousemodel =~? 'popup' + anoremenu PopUp.L&sp.Go\ to\ Definition + \ :LspDefinition + anoremenu PopUp.L&sp.Go\ to\ Declaration + \ :LspDeclaration + anoremenu PopUp.L&sp.Find\ All\ References + \ :LspReferences + anoremenu PopUp.L&sp.Show\ Detail + \ :LspHover + endif +endif diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..39a2b6e9 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ] +} diff --git a/syntax/lsp-hover.vim b/syntax/lsp-hover.vim new file mode 100644 index 00000000..22d3e9f7 --- /dev/null +++ b/syntax/lsp-hover.vim @@ -0,0 +1,50 @@ +" Converts a markdown language (```foo) to the corresponding Vim filetype +function! s:get_vim_filetype(lang_markdown) abort + " If the user hasn't customised markdown fenced languages, just return the + " markdown language + if !exists('g:markdown_fenced_languages') + return a:lang_markdown + endif + + " Otherwise, check if it has an entry for the given language + for l:type in g:markdown_fenced_languages + let l:vim_type = substitute(matchstr(l:type, '[^=]*$'), '\..*', '', '') + let l:markdown_type = matchstr(l:type, '[^=]*') + + if l:markdown_type ==# a:lang_markdown + " Found entry + return l:vim_type + endif + endfor + + " Not found + return a:lang_markdown +endfunction + +function! s:do_highlight() abort + if exists('b:lsp_syntax_highlights') + for l:entry in b:lsp_syntax_highlights + let l:line = l:entry['line'] + let l:lang = l:entry['language'] + let l:ft = s:get_vim_filetype(l:lang) + + execute printf('syntax match markdownHighlight%s contains=@markdownHighlight%s /^\%%%sl.*$/', l:ft, l:ft, l:line) + endfor + endif +endfunction + +function! s:cleanup_markdown() abort + " Don't highlight _ in words + syntax match markdownError "\w\@<=\w\@=" + + " Conceal escaped characters + " Workaround for: https://github.com/palantir/python-language-server/issues/386 + if has('conceal') + for l:escaped_char in ['`', '*', '_', '{', '}', '(', ')', '<', '>', '#', '+', '.', '!', '-'] + execute printf('syntax region vimLspMarkdownEscape matchgroup=Conceal start="\\\ze[%s]" end="[%s]\zs" concealends', l:escaped_char, l:escaped_char) + endfor + end +endfunction + +call s:do_highlight() +call s:cleanup_markdown() diff --git a/test/.themisrc b/test/.themisrc new file mode 100644 index 00000000..00b7c27a --- /dev/null +++ b/test/.themisrc @@ -0,0 +1,10 @@ +set encoding=utf-8 +call themis#option('recursive', 1) +call themis#option('reporter', 'spec') +call themis#helper('command').with(themis#helper('assert')) + +set runtimepath+=./test/utils + +" let g:lsp_log_verbose = 1 +" let g:lsp_log_file = expand("~/.config/nvim/data/lsp.log") +autocmd BufNewFile,BufRead *.rs setfiletype rust diff --git a/test/integration/go/document_formatting.vimspec b/test/integration/go/document_formatting.vimspec new file mode 100644 index 00000000..03ce6a78 --- /dev/null +++ b/test/integration/go/document_formatting.vimspec @@ -0,0 +1,40 @@ +Describe integration#go#document_formatting + Before + %bwipeout! + if lsp#test#hasproject('go') + call lsp#test#openproject('go', {}) + endif + End + + After all + %bwipeout! + call lsp#test#closeproject('go') + End + + It should correctly format document when using LspDocumentFormatSync + if !lsp#test#hasproject('go') + Skip go project not supported + endif + + Assert Equals(lsp#get_server_status('gopls'), 'running') + + execute printf('keepalt keepjumps edit %s', lsp#test#projectdir('go') . '/documentformat.go') + let l:original = getline(1, "$") + + call execute('LspDocumentFormatSync') + + let got = getline(1, "$") + let want = [ + \ 'package main', + \ '', + \ 'func main() {', + \ ' print("hello, world!")', + \ ' print("こんにちは、世界")', + \ ' print("a β c")', + \ ' print("δ")', + \ '}' + \ ] + Assert Equals(got, want) + End + +End diff --git a/test/lsp/internal/diagnostics/document_diagnostics_command.vimspec b/test/lsp/internal/diagnostics/document_diagnostics_command.vimspec new file mode 100644 index 00000000..d808e086 --- /dev/null +++ b/test/lsp/internal/diagnostics/document_diagnostics_command.vimspec @@ -0,0 +1,88 @@ +function! s:sort_item_by_text(lhs, rhs) abort + if a:lhs['text'] <# a:rhs['text'] + return -1 + endif + if a:lhs['text'] ># a:rhs['text'] + return 1 + endif + return 0 +endfunction + +function! s:add_end_pos(loc) abort + let a:loc['end_lnum'] = 0 + let a:loc['end_col'] = 0 + return a:loc +endfunction + +Describe lsp#internal#diagnostics#document_diagnostics_command + Before + %bwipeout! + let g:lsp_diagnostics_enabled = 1 + call lsp#internal#diagnostics#state#_disable() + call lsp#internal#diagnostics#state#_enable() + End + + After all + %bwipeout! + let g:lsp_diagnostics_enabled = 0 + call lsp#internal#diagnostics#state#_disable() + End + + It should be able to show document diagnostics for a buffer + normal! m' + execute printf('keepalt keepjumps edit %s', lsp#test#projectdir('rust') . '/src/documentdiagnostics.rs') + let l:uri = lsp#utils#get_buffer_uri() + let l:uri2 = l:uri . '2' + let l:bufnr = bufnr('%') + + let l:server1_response1 = {'method': 'textDocument/publishDiagnostics', 'params': {'uri': l:uri, 'diagnostics': [ + \ {'severity': 1, 'message': 'm1', 'range': { 'start': { 'line': 1, 'character': 1, 'end': { 'line': 1, 'character': 1 } } }}, + \ {'severity': 1, 'message': 'm2', 'range': { 'start': { 'line': 1, 'character': 2, 'end': { 'line': 1, 'character': 3 } } }}, + \ ]}} + + let l:server1_response2 = {'method': 'textDocument/publishDiagnostics', 'params': {'uri': l:uri2, 'diagnostics': [ + \ {'severity': 2, 'message': 'm3', 'range': { 'start': { 'line': 2, 'character': 1, 'end': { 'line': 1, 'character': 1 } } }}, + \ {'severity': 2, 'message': 'm4', 'range': { 'start': { 'line': 3, 'character': 2, 'end': { 'line': 1, 'character': 3 } } }}, + \ ]}} + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response1 }) + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response2 }) + + execute ':LspDocumentDiagnostics' + let l:result = getloclist(0) + + " NOTE Older version of Vim does not sort results, and does not include module. + call sort(l:result, function('s:sort_item_by_text')) + call map(l:result, {_, v -> extend(v, {'module': ''})}) + + let l:expected = [ + \ {'bufnr': l:bufnr, 'lnum': 2, 'col': 2, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': 0, 'type': 'E', 'module': '', 'text': 'Error:m1'}, + \ {'bufnr': l:bufnr, 'lnum': 2, 'col': 3, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': 0, 'type': 'E', 'module': '', 'text': 'Error:m2'}, + \ ] + if has_key(l:result[0], 'end_lnum') " feature detection + call map(l:expected, { _, v -> s:add_end_pos(v) }) + endif + Assert Equals(l:result, l:expected) + execute ':lclose' + + execute ':LspDocumentDiagnostics --buffers=*' + let l:result = getloclist(0) + + " NOTE Older version of Vim does not sort results, and does not include module. + call sort(l:result, function('s:sort_item_by_text')) + call map(l:result, {_, v -> extend(v, {'module': ''})}) + + " +2 for bufnr because original bufnr + loclist + new unopened file + let l:expected = [ + \ {'bufnr': l:bufnr, 'lnum': 2, 'col': 2, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': 0, 'type': 'E', 'module': '', 'text': 'Error:m1'}, + \ {'bufnr': l:bufnr, 'lnum': 2, 'col': 3, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': 0, 'type': 'E', 'module': '', 'text': 'Error:m2'}, + \ {'bufnr': l:bufnr + 2, 'lnum': 3, 'col': 2, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': 0, 'type': 'W', 'module': '', 'text': 'Warning:m3'}, + \ {'bufnr': l:bufnr + 2, 'lnum': 4, 'col': 3, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': 0, 'type': 'W', 'module': '', 'text': 'Warning:m4'}, + \ ] + if has_key(l:result[0], 'end_lnum') " feature detection + call map(l:expected, { _, v -> s:add_end_pos(v) }) + endif + Assert Equals(l:result, l:expected) + execute ':lclose' + End +End diff --git a/test/lsp/internal/diagnostics/state.vimspec b/test/lsp/internal/diagnostics/state.vimspec new file mode 100644 index 00000000..9c3195e5 --- /dev/null +++ b/test/lsp/internal/diagnostics/state.vimspec @@ -0,0 +1,70 @@ +Describe lsp#internal#diagnostics#state + Before + %bwipeout! + let g:lsp_diagnostics_enabled = 1 + call lsp#internal#diagnostics#state#_disable() + call lsp#internal#diagnostics#state#_enable() + End + + After all + %bwipeout! + let g:lsp_diagnostics_enabled = 0 + call lsp#internal#diagnostics#state#_disable() + End + + It should be able to subscribe to textDocument/publishDiagnostics stream and update state correctly + let l:uri = 'file://some/uri' + + let l:server1_response1 = {'method': 'textDocument/publishDiagnostics', 'params': {'uri': l:uri, 'diagnostics': [ + \ {'severity': 1, 'message': 'm1', 'range': { 'start': { 'line': 1, 'character': 1, 'end': { 'line': 1, 'character': 1 } } }}, + \ {'severity': 1, 'message': 'm2', 'range': { 'start': { 'line': 1, 'character': 2, 'end': { 'line': 1, 'character': 3 } } }}, + \ ]}} + let l:server2_response1 = {'method': 'textDocument/publishDiagnostics', 'params': {'uri': l:uri, 'diagnostics': [ + \ {'severity': 1, 'message': 's2', 'range': { 'start': { 'line': 2, 'character': 3, 'end': { 'line': 4, 'character': 5 } } }}, + \ ]}} + let l:server1_response2 = {'method': 'textDocument/publishDiagnostics', 'params': {'uri': l:uri, 'diagnostics': [ + \ {'severity': 1, 'message': 'm2', 'range': { 'start': { 'line': 1, 'character': 2, 'end': { 'line': 1, 'character': 3 } } }}, + \ ]}} + + Assert Equals(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri), {}) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response1 }) + Assert Equals(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri), { 'server1': l:server1_response1 }) + let l:want = {} + let l:want[l:uri] = { 'server1': l:server1_response1 } + Assert Equals(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server(), l:want) + + call lsp#stream(1, { 'server': 'server2', 'response': l:server2_response1 }) + Assert Equals(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri), { 'server1': l:server1_response1, 'server2': l:server2_response1 }) + let l:want = {} + let l:want[l:uri] = { 'server1': l:server1_response1, 'server2': l:server2_response1 } + Assert Equals(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server(), l:want) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response2 }) + Assert Equals(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(l:uri), { 'server1': l:server1_response2, 'server2': l:server2_response1 }) + let l:want = {} + let l:want[l:uri] = { 'server1': l:server1_response2, 'server2': l:server2_response1 } + Assert Equals(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server(), l:want) + + call lsp#stream(1, { 'server': '$vimlsp', + \ 'response': { 'method': '$/vimlsp/lsp_server_exit', 'params': { 'server': 'server1' } } }) + let l:want = {} + let l:want[l:uri] = { 'server2': l:server2_response1 } + Assert Equals(lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_uri_and_server(), l:want) + End + + + It should correctly return the state of buffer + call lsp#internal#diagnostics#state#_disable() + call lsp#internal#diagnostics#state#_enable() + + let l:bufnr = bufnr('%') + Assert True(lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr)) + + call lsp#internal#diagnostics#state#_disable_for_buffer(l:bufnr) + Assert False(lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr)) + + call lsp#internal#diagnostics#state#_enable_for_buffer(l:bufnr) + Assert True(lsp#internal#diagnostics#state#_is_enabled_for_buffer(l:bufnr)) + End +End diff --git a/test/lsp/internal/show_message.vimspec b/test/lsp/internal/show_message.vimspec new file mode 100644 index 00000000..d2397fe6 --- /dev/null +++ b/test/lsp/internal/show_message.vimspec @@ -0,0 +1,92 @@ +let s:Error = 1 +let s:Warn = 2 +let s:Info = 3 +let s:Log = 4 + +function! s:response(type, message) abort + return { + \ 'server': 'server1', + \ 'response': { + \ 'method': 'window/showMessage', + \ 'params': { + \ 'type': a:type, + \ 'message': a:message + \ } + \ } + \ } +endfunction + +Describe lsp#internal#show_message + Before + %bwipeout! + let g:lsp_show_message_log_level = 'warning' + call lsp#internal#show_message#_disable() + call lsp#internal#show_message#_enable() + End + + After all + %bwipeout! + let g:lsp_show_message_log_level = 'none' + call lsp#internal#show_message#_disable() + End + + It should show all messages when 'log' is set to g:lsp_show_message_log_level + let g:lsp_show_message_log_level = 'log' + + redir => message_area + call lsp#stream(1, s:response(s:Error, 'error message')) + call lsp#stream(1, s:response(s:Warn, 'warn message')) + call lsp#stream(1, s:response(s:Info, 'info message')) + call lsp#stream(1, s:response(s:Log, 'log message')) + call lsp#stream(1, s:response(s:Info, 'info message2')) + call lsp#stream(1, s:response(s:Info, 'info message3')) + redir END + + Assert Match(message_area, 'server1: error: error message') + Assert Match(message_area, 'server1: warning: warn message') + Assert Match(message_area, 'server1: info: info message') + Assert Match(message_area, 'server1: log: log message') + Assert Match(message_area, 'server1: info: info message2') + Assert Match(message_area, 'server1: info: info message3') + End + + It should filter shown messages by log level set to g:lsp_show_message_log_level + let g:lsp_show_message_log_level = 'warning' + + redir => message_area + call lsp#stream(1, s:response(s:Error, 'error message')) + call lsp#stream(1, s:response(s:Warn, 'warn message')) + call lsp#stream(1, s:response(s:Info, 'info message')) + call lsp#stream(1, s:response(s:Log, 'log message')) + call lsp#stream(1, s:response(s:Info, 'info message2')) + call lsp#stream(1, s:response(s:Info, 'info message3')) + redir END + + Assert Match(message_area, 'server1: error: error message') + Assert Match(message_area, 'server1: warning: warn message') + Assert NotMatch(message_area, 'server1: info: info message') + Assert NotMatch(message_area, 'server1: log: log message') + Assert NotMatch(message_area, 'server1: info: info message2') + Assert NotMatch(message_area, 'server1: info: info message3') + End + + It should show no message when 'none' is set to g:lsp_show_message_log_level + let g:lsp_show_message_log_level = 'none' + + redir => message_area + call lsp#stream(1, s:response(s:Error, 'error message')) + call lsp#stream(1, s:response(s:Warn, 'warn message')) + call lsp#stream(1, s:response(s:Info, 'info message')) + call lsp#stream(1, s:response(s:Log, 'log message')) + call lsp#stream(1, s:response(s:Info, 'info message2')) + call lsp#stream(1, s:response(s:Info, 'info message3')) + redir END + + Assert NotMatch(message_area, 'server1: error: error message') + Assert NotMatch(message_area, 'server1: warning: warn message') + Assert NotMatch(message_area, 'server1: info: info message') + Assert NotMatch(message_area, 'server1: log: log message') + Assert NotMatch(message_area, 'server1: info: info message2') + Assert NotMatch(message_area, 'server1: info: info message3') + End +End diff --git a/test/lsp/omni.vimspec b/test/lsp/omni.vimspec new file mode 100644 index 00000000..b74e3c79 --- /dev/null +++ b/test/lsp/omni.vimspec @@ -0,0 +1,501 @@ +Describe lsp#omni + + let g:lsp_get_vim_completion_item_set_kind = 1 + + Before each + call lsp#omni#_clear_managed_user_data_map() + End + + Describe lsp#omni#get_vim_completion_items + It should return item with proper kind + let item = { + \ 'label': 'my-label', + \ 'documentation': 'my documentation.', + \ 'detail': 'my-detail', + \ 'kind': '3' + \} + + let options = { + \ 'server': { 'name': 'dummy-server' }, + \ 'position': lsp#get_position(), + \ 'response': { 'result': [item] }, + \} + + let want = { + \ 'items': [{ + \ 'word': 'my-label', + \ 'abbr': 'my-label', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"0"}', + \ }], + \ 'incomplete': 0, + \ 'startcol': lsp#utils#position#lsp_character_to_vim('%', lsp#get_position()), + \} + + Assert Equals(lsp#omni#get_vim_completion_items(options), want) + End + + It should get user_data by the item + if !has('patch-8.0.1493') + Skip This test requires 'patch-8.0.1493' + endif + + let item = { + \ 'label': 'my-label', + \ 'documentation': 'my documentation.', + \ 'detail': 'my-detail', + \ 'kind': '3', + \ 'textEdit': { + \ 'range': { + \ 'start': {'line': 5, 'character': 0}, + \ 'end': {'line': 5, 'character': 5} + \ }, + \ 'newText': 'yyy' + \ } + \} + + let options = { + \ 'server': { 'name': 'dummy-server' }, + \ 'position': { 'line': 1, 'character': 1 }, + \ 'response': { 'result': [item] }, + \} + + let want = { + \ 'items': [{ + \ 'word': 'yyy', + \ 'abbr': 'my-label', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"0"}', + \ }], + \ 'incomplete': 0, + \ 'startcol': lsp#utils#position#lsp_character_to_vim('%', {'line': 1, 'character': 0}), + \} + + let got = lsp#omni#get_vim_completion_items(options) + Assert Equals(got, want) + Assert Equals(lsp#omni#get_managed_user_data_from_completed_item(got['items'][0]), { + \ 'server_name': 'dummy-server', + \ 'completion_item': item, + \ 'complete_position': { 'line': 1, 'character': 1 }, + \ 'start_character': 0, + \ 'complete_word': 'yyy', + \ }) + End + + It should not raise errors + let item = { + \ 'label': 'my-label', + \ 'textEdit': v:null, + \} + + let options = { + \ 'server': { 'name': 'dummy-server' }, + \ 'position': lsp#get_position(), + \ 'response': { 'result': [item] }, + \} + + let want = { + \ 'items': [{ + \ 'word': 'my-label', + \ 'abbr': 'my-label', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': '', + \ 'user_data': '{"vim-lsp/key":"0"}', + \ }], + \ 'incomplete': 0, + \ 'startcol': lsp#utils#position#lsp_character_to_vim('%', lsp#get_position()), + \} + + let got = lsp#omni#get_vim_completion_items(options) + Assert Equals(got, want) + + let item = { + \ 'label': 'my-label', + \ 'textEdit': v:null, + \ 'insertText': v:null, + \} + + let options = { + \ 'server': { 'name': 'dummy-server' }, + \ 'position': lsp#get_position(), + \ 'response': { 'result': [item] }, + \} + + let want = { + \ 'items': [{ + \ 'word': 'my-label', + \ 'abbr': 'my-label', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': '', + \ 'user_data': '{"vim-lsp/key":"1"}', + \ }], + \ 'incomplete': 0, + \ 'startcol': lsp#utils#position#lsp_character_to_vim('%', lsp#get_position()), + \} + + let got = lsp#omni#get_vim_completion_items(options) + Assert Equals(got, want) + End + + It should return correct items for snippets + if !has('patch-8.0.1493') + Skip This test requires 'patch-8.0.1493' + endif + + let item = { + \ "label": "sysout", + \ "insertText": "System.out.println(${0});", + \ "kind": 15, + \ "insertTextFormat": 2, + \ "documentation": "System.out.println();", + \ "detail": "print to standard out" + \ } + + let options = { + \ 'server': { 'name': 'dummy-server' }, + \ 'position': { 'line': 1, 'character': 1 }, + \ 'response': { 'result': [item] }, + \} + + let want = { + \ 'items': [{ + \ 'word': 'System.out.println', + \ 'abbr': 'sysout~', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'snippet', + \ 'user_data': '{"vim-lsp/key":"0"}', + \ }], + \ 'incomplete': 0, + \ 'startcol': lsp#utils#position#lsp_character_to_vim('%', { 'line': 1, 'character': 1 }), + \} + + let got = lsp#omni#get_vim_completion_items(options) + Assert Equals(got, want) + Assert Equals(lsp#omni#get_managed_user_data_from_completed_item(got['items'][0]), { + \ 'server_name': 'dummy-server', + \ 'completion_item': item, + \ 'complete_position': { 'line': 1, 'character': 1 }, + \ 'start_character': 0, + \ 'complete_word': 'System.out.println(${0});', + \ }) + End + + It should sort by sortText + let items = [{ + \ 'label': 'my-label1', + \ 'kind': '3', + \ 'sortText': 'c' + \}, + \{ + \ 'label': 'my-label2', + \ 'kind': '3', + \ 'sortText': 'a' + \}, + \{ + \ 'label': 'my-label3', + \ 'kind': '3', + \ 'sortText': 'b' + \}] + + let options = { + \ 'server': { + \ 'name': 'dummy-server', + \ 'config': { + \ 'sort': { 'max': 100 }, + \ }, + \ }, + \ 'position': lsp#get_position(), + \ 'response': { 'result': items }, + \} + + let want = { + \ 'items': [{ + \ 'word': 'my-label2', + \ 'abbr': 'my-label2', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"0"}', + \ }, + \ { + \ 'word': 'my-label3', + \ 'abbr': 'my-label3', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"1"}', + \ }, + \ { + \ 'word': 'my-label1', + \ 'abbr': 'my-label1', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"2"}', + \ }], + \ 'incomplete': 0, + \ 'startcol': lsp#utils#position#lsp_character_to_vim('%', lsp#get_position()), + \} + + Assert Equals(lsp#omni#get_vim_completion_items(options), want) + End + + It should not sort over max + let items = [{ + \ 'label': 'my-label3', + \ 'kind': '3', + \ 'sortText': '3' + \}, + \{ + \ 'label': 'my-label1', + \ 'kind': '3', + \ 'sortText': '1' + \}, + \{ + \ 'label': 'my-label2', + \ 'kind': '3', + \ 'sortText': '2' + \}] + + let options = { + \ 'server': { + \ 'name': 'dummy-server', + \ 'config': { + \ 'sort': { 'max': 2 }, + \ }, + \ }, + \ 'position': lsp#get_position(), + \ 'response': { 'result': items }, + \} + + let want = { + \ 'items': [{ + \ 'word': 'my-label3', + \ 'abbr': 'my-label3', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"0"}', + \ }, + \ { + \ 'word': 'my-label1', + \ 'abbr': 'my-label1', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"1"}', + \ }, + \ { + \ 'word': 'my-label2', + \ 'abbr': 'my-label2', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"2"}', + \ }], + \ 'incomplete': 0, + \ 'startcol': lsp#utils#position#lsp_character_to_vim('%', lsp#get_position()), + \} + Assert Equals(lsp#omni#get_vim_completion_items(options), want) + End + + It should sort by label(sortText not exists) + let items = [{ + \ 'label': 'my-label3', + \ 'kind': '3', + \}, + \{ + \ 'label': 'my-label1', + \ 'kind': '3', + \}, + \{ + \ 'label': 'my-label2', + \ 'kind': '3', + \}] + + let options = { + \ 'server': { + \ 'name': 'dummy-server', + \ 'config': { + \ 'sort': { 'max': 10 }, + \ }, + \ }, + \ 'position': lsp#get_position(), + \ 'response': { 'result': items }, + \} + + let want = { + \ 'items': [{ + \ 'word': 'my-label1', + \ 'abbr': 'my-label1', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"0"}', + \ }, + \ { + \ 'word': 'my-label2', + \ 'abbr': 'my-label2', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"1"}', + \ }, + \ { + \ 'word': 'my-label3', + \ 'abbr': 'my-label3', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"2"}', + \ }], + \ 'incomplete': 0, + \ 'startcol': lsp#utils#position#lsp_character_to_vim('%', lsp#get_position()), + \} + Assert Equals(lsp#omni#get_vim_completion_items(options), want) + End + + It should sort by label(empty sortText) + let items = [{ + \ 'label': 'my-label3', + \ 'kind': '3', + \ 'sortText': '' + \}, + \{ + \ 'label': 'my-label1', + \ 'kind': '3', + \ 'sortText': '', + \}, + \{ + \ 'label': 'my-label2', + \ 'kind': '3', + \ 'sortText': '', + \}] + + let options = { + \ 'server': { + \ 'name': 'dummy-server', + \ 'config': { + \ 'sort': { 'max': 10 }, + \ }, + \ }, + \ 'position': lsp#get_position(), + \ 'response': { 'result': items }, + \} + + let want = { + \ 'items': [{ + \ 'word': 'my-label1', + \ 'abbr': 'my-label1', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"0"}', + \ }, + \ { + \ 'word': 'my-label2', + \ 'abbr': 'my-label2', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"1"}', + \ }, + \ { + \ 'word': 'my-label3', + \ 'abbr': 'my-label3', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"2"}', + \ }], + \ 'incomplete': 0, + \ 'startcol': lsp#utils#position#lsp_character_to_vim('%', lsp#get_position()), + \} + Assert Equals(lsp#omni#get_vim_completion_items(options), want) + End + + Describe g:lsp_ignorecase + Before all + let saved_ignorecase = get(g:, 'lsp_ignorecase', v:null) + End + + After all + if saved_ignorecase isnot v:null + let g:lsp_ignorecase = saved_ignorecase + endif + End + + It should sort completion items case-insensitive when true is set + let g:lsp_ignorecase = v:true + + " 'B' < 'a' but 'a' < 'b' + let result = [{ + \ 'label': 'my-label1', + \ 'kind': '3', + \ 'sortText': 'B' + \}, + \{ + \ 'label': 'my-label2', + \ 'kind': '3', + \ 'sortText': 'a' + \}] + + let options = { + \ 'server': { + \ 'name': 'dummy-server', + \ 'config': { + \ 'sort': { 'max': 10 }, + \ }, + \ }, + \ 'position': lsp#get_position(), + \ 'response': { 'result': result }, + \} + + let want = [{ + \ 'word': 'my-label2', + \ 'abbr': 'my-label2', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"0"}', + \}, + \{ + \ 'word': 'my-label1', + \ 'abbr': 'my-label1', + \ 'icase': 1, + \ 'dup': 1, + \ 'empty': 1, + \ 'kind': 'function', + \ 'user_data': '{"vim-lsp/key":"1"}', + \}] + + Assert Equals(lsp#omni#get_vim_completion_items(options).items, want) + End + End + End +End diff --git a/test/lsp/ui/vim/code_lens.vimspec b/test/lsp/ui/vim/code_lens.vimspec new file mode 100644 index 00000000..aa6f36fd --- /dev/null +++ b/test/lsp/ui/vim/code_lens.vimspec @@ -0,0 +1,134 @@ +Describe lsp#uivim#code_lens + Describe lsp#ui#vim#code_lens#_get_subtitle + It should generate subtitle from response of rust-analyzer + " Example response of Code Lens extracted from #1118 + let item = { + \ 'codelens': { + \ 'command': { + \ 'arguments': [ + \ { + \ 'args': { + \ 'cargoArgs': ['test', '--package', 'tmp', '--lib'], + \ 'cargoExtraArgs': [], + \ 'executableArgs': ['tests::it_works', '--exact', '--nocapture'], + \ 'overrideCargo': v:null, + \ 'workspaceRoot': '/tmp' + \ }, + \ 'kind': 'cargo', + \ 'label': 'test tests::it_works', + \ 'location': { + \ 'targetRange': {'end': {'character': 5, 'line': 14}, 'start': {'character': 4, 'line': 11}}, + \ 'targetSelectionRange': {'end': {'character': 15, 'line': 12}, 'start': {'character': 7, 'line': 12}}, + \ 'targetUri': 'file:////tmp/src/lib.rs' + \ } + \ } + \ ], + \ 'command': 'rust-analyzer.runSingle', + \ 'title': '▶︎ Run Test' + \ }, + \ 'range': {'end': {'character': 5, 'line': 14}, 'start': {'character': 4, 'line': 11}} + \ }, + \ 'server': 'rust-analyzer' + \ } + + let subtitle = lsp#ui#vim#code_lens#_get_subtitle(item) + Assert Equals(subtitle, ': test tests::it_works') + End + + It should generate subtitle from multiple labels of command arguments + let item = { + \ 'codelens': { + \ 'command': { + \ 'arguments': [ + \ { + \ 'args': {}, + \ 'kind': 'kind1', + \ 'label': 'do command1', + \ 'location': {} + \ }, + \ { + \ 'args': {}, + \ 'kind': 'kind2', + \ 'label': 'do command2', + \ 'location': {} + \ } + \ ], + \ 'command': 'server.someCommand', + \ 'title': 'lens title' + \ }, + \ 'range': {'end': {'character': 5, 'line': 14}, 'start': {'character': 4, 'line': 11}} + \ }, + \ 'server': 'rust-analyzer' + \ } + + let subtitle = lsp#ui#vim#code_lens#_get_subtitle(item) + Assert Equals(subtitle, ': do command1 > do command2') + End + + It should return empty string when 'arguments' field is not found + let item = { + \ 'codelens': { + \ 'command': { + \ 'command': 'server.someCommand', + \ 'title': 'lens title' + \ }, + \ 'range': {'end': {'character': 5, 'line': 14}, 'start': {'character': 4, 'line': 11}} + \ }, + \ 'server': 'rust-analyzer' + \ } + + let subtitle = lsp#ui#vim#code_lens#_get_subtitle(item) + Assert Equals(subtitle, '') + End + + It should return empty string when 'arguments' field is not an object + let item = { + \ 'codelens': { + \ 'command': { + \ 'arguments': [ + \ 'command1', + \ 'command2', + \ 'command3' + \ ], + \ 'command': 'server.someCommand', + \ 'title': 'lens title' + \ }, + \ 'range': {'end': {'character': 5, 'line': 14}, 'start': {'character': 4, 'line': 11}} + \ }, + \ 'server': 'rust-analyzer' + \ } + + let subtitle = lsp#ui#vim#code_lens#_get_subtitle(item) + Assert Equals(subtitle, '') + End + + It should return empty string when at least one of elements in 'arguments' field does not have 'label' field + let item = { + \ 'codelens': { + \ 'command': { + \ 'arguments': [ + \ { + \ 'args': {}, + \ 'kind': 'kind1', + \ 'label': 'do command1', + \ 'location': {} + \ }, + \ { + \ 'args': {}, + \ 'kind': 'kind2', + \ 'location': {} + \ } + \ ], + \ 'command': 'server.someCommand', + \ 'title': 'lens title' + \ }, + \ 'range': {'end': {'character': 5, 'line': 14}, 'start': {'character': 4, 'line': 11}} + \ }, + \ 'server': 'rust-analyzer' + \ } + + let subtitle = lsp#ui#vim#code_lens#_get_subtitle(item) + Assert Equals(subtitle, '') + End + End +End diff --git a/test/lsp/ui/vim/completion.vimspec b/test/lsp/ui/vim/completion.vimspec new file mode 100644 index 00000000..5f9b2c47 --- /dev/null +++ b/test/lsp/ui/vim/completion.vimspec @@ -0,0 +1,37 @@ +Describe lsp#uivim#completion + + Before each + %delete _ + setlocal filetype=html + setlocal omnifunc=lsp#omni#complete + End + + It should expand simple snippet with multibyte chars + Skip This test needs asynchronous process and snippetSupport=true + + call setline(1, ['
']) + execute "normal! 'gg$ha id\\\\'" + + " wait for feedkeys. + + Assert Equals(getline(1), '
') + Assert Equals(getpos('.')[1 : 2], [1, 30]) + End + + It should expand when textEdit.start.character is less than completion start col + Skip This test needs asynchronous process and snippetSupport=true + + call setline(1, [ + \ '', + \ '
', + \ ' ', + \ '']) + execute "normal! ':gg2j$ha\\\\'" + + " wait for feedkeys. + + Assert Equals(getline(3), '
') + Assert Equals(getpos('.')[1 : 2], [1, 8]) + End + +End diff --git a/test/lsp/utils.vimspec b/test/lsp/utils.vimspec new file mode 100644 index 00000000..0425e9f4 --- /dev/null +++ b/test/lsp/utils.vimspec @@ -0,0 +1,316 @@ +Describe lsp#utils + + Describe lsp#utils#empty_complete + It should return empty complete + let items = lsp#utils#empty_complete() + Assert type(items) == type([]) + Assert len(items) == 0 + End + End + + Describe lsp#utils#uri_to_path + It should return path from uri (Windows) + if !has('win32') + Skip This tests is not for UNIX + endif + let tests = [ + \ {'uri': 'file:///C:/path/to/the/file.txt', 'path': 'C:\path\to\the\file.txt'}, + \ {'uri': 'file:///C:/path/to/the/file+name.txt', 'path': 'C:\path\to\the\file+name.txt'}, + \ {'uri': 'file:///C:/path/to/the/file%2Bname.txt', 'path': 'C:\path\to\the\file+name.txt'}, + \ {'uri': 'file:///C:/path/to/the/file%20name.txt', 'path': 'C:\path\to\the\file name.txt'}, + \ {'uri': 'file:///C:/path+name?query=your+value', 'path': 'C:\path+name'}, + \ {'uri': 'file:///C:/path+name#hash', 'path': 'C:\path+name'}, + \] + for test in tests + let path = lsp#utils#uri_to_path(test.uri) + Assert Equals(path, test.path) + endfor + End + + It should return path from uri (UNIX) + if has('win32') + Skip This tests is not for Windows + endif + let tests = [ + \ {'uri': 'file:///path/to/the/file.txt', 'path': '/path/to/the/file.txt'}, + \ {'uri': 'file:///path/to/the/file+name.txt', 'path': '/path/to/the/file+name.txt'}, + \ {'uri': 'file:///path/to/the/file%2Bname.txt', 'path': '/path/to/the/file+name.txt'}, + \ {'uri': 'file:///path/to/the/file%20name.txt', 'path': '/path/to/the/file name.txt'}, + \ {'uri': 'file:///path+name?query=your+value', 'path': '/path+name'}, + \ {'uri': 'file:///path+name#hash', 'path': '/path+name'}, + \] + for test in tests + let path = lsp#utils#uri_to_path(test.uri) + Assert Equals(path, test.path) + endfor + End + End + + Describe lsp#utils#path_to_uri + It should return uri from path (Windows) + if !has('win32') + Skip This tests is not for UNIX + endif + let tests = [ + \ {'path': 'C:\path\to\the\file.txt', 'uri': 'file:///C:/path/to/the/file.txt'}, + \ {'path': 'C:\path\to\the\file+name.txt', 'uri': 'file:///C:/path/to/the/file%2Bname.txt'}, + \ {'path': 'C:\path\to\the\file name.txt', 'uri': 'file:///C:/path/to/the/file%20name.txt'}, + \ {'path': 'http://foo/bar.txt', 'uri': 'http://foo/bar.txt'}, + \] + for test in tests + let uri = lsp#utils#path_to_uri(test.path) + Assert Equals(uri, test.uri) + endfor + End + + It should return uri from path (UNIX) + if has('win32') + Skip This tests is not for Windows + endif + let tests = [ + \ {'path': '/path/to/the/file.txt', 'uri': 'file:///path/to/the/file.txt'}, + \ {'path': '/path/to/the/file+name.txt', 'uri': 'file:///path/to/the/file%2Bname.txt'}, + \ {'path': '/path/to/the/file name.txt', 'uri': 'file:///path/to/the/file%20name.txt'}, + \ {'path': 'http://foo/bar.txt', 'uri': 'http://foo/bar.txt'}, + \] + for test in tests + let uri = lsp#utils#path_to_uri(test.path) + Assert Equals(uri, test.uri) + endfor + End + End + + Describe lsp#utils#normalize_uri + It should return normalized uri (Windows) + if !has('win32') + Skip This tests is not for UNIX + endif + let tests = [ + \ {'path': 'file:///C:\path\to\the\file.txt', 'uri': 'file:///c:\path\to\the\file.txt'}, + \ {'path': 'file:///c:\path\to\the\file.txt', 'uri': 'file:///c:\path\to\the\file.txt'}, + \ {'path': 'file:///C%3A\path\to\the\file.txt', 'uri': 'file:///c:\path\to\the\file.txt'}, + \ {'path': 'file:///c%3a\path\to\the\file.txt', 'uri': 'file:///c:\path\to\the\file.txt'}, + \ {'path': 'http://foo/bar.txt', 'uri': 'http://foo/bar.txt'}, + \] + for test in tests + let uri = lsp#utils#normalize_uri(test.path) + Assert Equals(uri, test.uri) + endfor + End + + It should return normalized uri (UNIX) + if has('win32') + Skip This tests is not for Windows + endif + let tests = [ + \ {'path': 'file:///path/to/the/file.txt', 'uri': 'file:///path/to/the/file.txt'}, + \] + for test in tests + let uri = lsp#utils#normalize_uri(test.path) + Assert Equals(uri, test.uri) + endfor + End + End + + Describe lsp#utils#find_nearest_parent_file_directory + It should return the root directory if it is found + let tests = [ + \ {'from': './test/testproject/src/main.cpp', 'target': ['.ccls', 'compile_commands.json', 'README.md', 'git/'], 'root': './test/testproject'}, + \ {'from': './test/testproject/src/main.cpp', 'target': ['.ccls', 'build/', 'CMakeLists.txt', 'git/'], 'root': './test/testproject/src'}, + \ {'from': './test/testproject/src/main.cpp', 'target': '.ccls', 'root': './test/testproject'}, + \ {'from': './test/testproject/src/main.cpp', 'target': 'git/', 'root': './test/testproject'}, + \ {'from': './test/testproject/src/main.cpp', 'target': 'CMakeLists.txt', 'root': './test/testproject/src'}, + \ {'from': './test/testproject/README.md', 'target': ['.ccls', 'compile_commands.json', 'README.md', 'git/'], 'root': './test/testproject'}, + \ {'from': './test/testproject/README.md', 'target': ['.ccls', 'build/', 'CMakeLists.txt', 'git/'], 'root': './test/testproject'}, + \ {'from': './test/testproject/README.md', 'target': '.ccls', 'root': './test/testproject'}, + \ {'from': './test/testproject/README.md', 'target': 'git/', 'root': './test/testproject'}, + \ {'from': './test/testproject/README.md', 'target': 'CMakeLists.txt', 'root': './test/testproject'}, + \] + for test in tests + let path = lsp#utils#find_nearest_parent_file_directory(fnamemodify(test.from, ':p:h'), test.target) + Assert Equals(path, fnamemodify(test.root, ':p:h')) + endfor + End + + It should return an empty string if not target has been found + let tests = [ + \ {'from': './test/testproject/src/main.cpp', 'target': ['fdrvbws/', 'asbr/', 'bgdf/', 'abfrb.txt', 'ngo.c']}, + \ {'from': './test/testproject/src/main.cpp', 'target': 'asbr/'}, + \ {'from': './test/testproject/src/main.cpp', 'target': 'btr.c'}, + \ {'from': './test/testproject/.gitignore', 'target': ['fdrvbws/', 'asbr/', 'bgdf/', 'abfrb.txt', 'ngo.c']}, + \ {'from': './test/testproject/.gitignore', 'target': 'asbr/'}, + \ {'from': './test/testproject/.gitignore', 'target': 'btr.c'}, + \] + for test in tests + let path = lsp#utils#find_nearest_parent_file_directory(fnamemodify(test.from, ':p:h'), test.target) + Assert Empty(path) + endfor + End + End + + Describe lsp#utils#to_char + It should return the character-index from the given byte-index on a buffer + call setline(1, ['a β c', 'δ', '']) + Assert lsp#utils#to_char('%', 1, 1) == 0 + Assert lsp#utils#to_char('%', 1, 2) == 1 + Assert lsp#utils#to_char('%', 1, 3) == 2 + Assert lsp#utils#to_char('%', 1, 5) == 3 + Assert lsp#utils#to_char('%', 1, 6) == 4 + Assert lsp#utils#to_char('%', 1, 7) == 5 + Assert lsp#utils#to_char('%', 2, 1) == 0 + Assert lsp#utils#to_char('%', 2, 3) == 1 + Assert lsp#utils#to_char('%', 3, 1) == 0 + %delete + End + + It should return the character-index from the given byte-index in an unloaded file + Assert lsp#utils#to_char('./test/testfiles/multibyte.txt', 1, 1) == 0 + Assert lsp#utils#to_char('./test/testfiles/multibyte.txt', 1, 2) == 1 + Assert lsp#utils#to_char('./test/testfiles/multibyte.txt', 1, 3) == 2 + Assert lsp#utils#to_char('./test/testfiles/multibyte.txt', 1, 5) == 3 + Assert lsp#utils#to_char('./test/testfiles/multibyte.txt', 1, 6) == 4 + Assert lsp#utils#to_char('./test/testfiles/multibyte.txt', 1, 7) == 5 + Assert lsp#utils#to_char('./test/testfiles/multibyte.txt', 2, 1) == 0 + Assert lsp#utils#to_char('./test/testfiles/multibyte.txt', 2, 3) == 1 + Assert lsp#utils#to_char('./test/testfiles/multibyte.txt', 3, 1) == 0 + End + End + + Describe lsp#utils#_get_before_line + It should return line before cursor on col=1 + enew! + call setline(1, ['123456789']) + call cursor(1, 1) + Assert Equals(lsp#utils#_get_before_line(), '') + End + + It should return line before cursor on col=$ + let l:saved_virtualedit = &virtualedit + let &virtualedit = 'all' + enew! + call setline(1, ['123456789']) + call cursor(1, 10) + Assert Equals(lsp#utils#_get_before_line(), '123456789') + let &virtualedit = l:saved_virtualedit + End + + It should return line before cursor with multibyte + enew! + call setline(1, ['あいうえおabc']) + call cursor(1, 18) + Assert Equals(lsp#utils#_get_before_line(), 'あいうえおab') + End + End + + Describe lsp#utils#_get_before_char_skip_white + It should return before char in above of line + enew! + call setline(1, ['(', '']) + call cursor(2, 1) + Assert Equals(lsp#utils#_get_before_char_skip_white(), '(') + End + It should return before char with multibyte + enew! + call setline(1, ['あいうえお( ']) + call cursor(1, 21) + Assert Equals(lsp#utils#_get_before_char_skip_white(), '(') + End + End + + Describe lsp#utils#base64_decode + It should decode basic string correctly + Assert Equals(lsp#utils#base64_decode('TWFu'), [77, 97, 110]) + End + + It should decode multiple groups correctly + Assert Equals(lsp#utils#base64_decode('TWFuIHRlc3R4'), [77, 97, 110, 32, 116, 101, 115, 116, 120]) + End + + It should handle padding (one octet) + Assert Equals(lsp#utils#base64_decode('TQ=='), [77]) + End + + It should handle padding (two octets) + Assert Equals(lsp#utils#base64_decode('TWE='), [77, 97]) + End + + It should handle more complex string + Assert Equals(lsp#utils#base64_decode('AAAAEgAJABYAAAAIAAQAFw=='), [0, 0, 0, 18, 0, 9, 0, 22, 0, 0, 0, 8, 0, 4, 0, 23]) + End + End + + Describe lsp#utils#make_valid_word + It should make valid word + Assert Equals(lsp#utils#make_valid_word('my-word'), 'my-word') + Assert Equals(lsp#utils#make_valid_word("my\nword"), 'my') + Assert Equals(lsp#utils#make_valid_word('my-word: description'), 'my-word') + Assert Equals(lsp#utils#make_valid_word('my-word : description'), 'my-word') + Assert Equals(lsp#utils#make_valid_word('my-word is word'), 'my-word') + Assert Equals(lsp#utils#make_valid_word('my-func()'), 'my-func') + Assert Equals(lsp#utils#make_valid_word('my-name::space'), 'my-name::space') + Assert Equals(lsp#utils#make_valid_word('my-name#space'), 'my-name#space') + Assert Equals(lsp#utils#make_valid_word('my-name.space'), 'my-name.space') + Assert Equals(lsp#utils#make_valid_word('my-name.space: foo'), 'my-name.space') + Assert Equals(lsp#utils#make_valid_word('my-name%space: foo'), 'my-name%space') + Assert Equals(lsp#utils#make_valid_word('my-name&space: foo'), 'my-name&space') + Assert Equals(lsp#utils#make_valid_word('my-array[0]'), 'my-array') + Assert Equals(lsp#utils#make_valid_word('my-array'), 'my-array') + Assert Equals(lsp#utils#make_valid_word("my-name\tdescription"), 'my-name') + End + End + + Describe lsp#utils#_split_by_eol + + It should split text by \r\n + Assert Equals(lsp#utils#_split_by_eol("あいうえお\r\nかきくけこ"), ['あいうえお', 'かきくけこ']) + End + + It should split text by \r + Assert Equals(lsp#utils#_split_by_eol("あいうえお\rかきくけこ"), ['あいうえお', 'かきくけこ']) + End + + It should split text by \r\n\r + Assert Equals(lsp#utils#_split_by_eol("あいうえお\r\n\rかきくけこ"), ['あいうえお', '', 'かきくけこ']) + End + + It should split text by \r\n\n\r\r\n + Assert Equals(lsp#utils#_split_by_eol("あいうえお\r\n\n\r\r\nかきくけこ"), ['あいうえお', '', '', '', 'かきくけこ']) + End + + End + + Describe lsp#utils#_compare_nearest_path + It should return looong since it is longest + Assert Equals(lsp#utils#_nearest_path( + \ {'/path/to/looong': 1, '/path/to/short': 1,} + \), '/path/to/looong') + End + + It should return loong since they are both longest but loong matches mostly + Assert Equals(lsp#utils#_nearest_path( + \ {'/path/to/loong': 2, '/path/to/short': 1,} + \), '/path/to/loong') + End + + It should return not long since it is not longest + Assert Equals(lsp#utils#_nearest_path( + \ {'/path/to/long': 2, '/path/to/short': 1,} + \), '/path/to/short') + End + End + + Describe lsp#utils#iteratable + It should return empty list if non-list is given + Assert Equals(lsp#utils#iteratable(''), []) + Assert Equals(lsp#utils#iteratable(1), []) + Assert Equals(lsp#utils#iteratable(2.3), []) + Assert Equals(lsp#utils#iteratable(v:false), []) + Assert Equals(lsp#utils#iteratable(v:true), []) + Assert Equals(lsp#utils#iteratable({}), []) + End + + It should return the list it self if list is given + Assert Equals(lsp#utils#iteratable([1,2,3]), [1,2,3]) + End + End +End diff --git a/test/lsp/utils/buffer.vimspec b/test/lsp/utils/buffer.vimspec new file mode 100644 index 00000000..b6986fa1 --- /dev/null +++ b/test/lsp/utils/buffer.vimspec @@ -0,0 +1,101 @@ +Describe lsp#utils#buffer + + Before each + % delete _ + 0put ='foo' + 1put ='bar' + 3delete _ + End + + After all + % delete _ + End + + Describe lsp#utils#buffer#_get_lines + It adds a blank line when nobinary and fixendofline are set + if !exists('+fixendofline') + Skip This test requires 'fixendofline' + endif + setl nobinary + setl fixendofline + Assert Equals(lsp#utils#buffer#_get_lines(bufnr('$')), ['foo', 'bar', '']) + End + + It adds a blank line when nobinary, nofixendofline, and endofline are set + if !exists('+fixendofline') + Skip This test requires 'fixendofline' + endif + setl nobinary + setl nofixendofline + setl endofline + Assert Equals(lsp#utils#buffer#_get_lines(bufnr('$')), ['foo', 'bar', '']) + End + + It adds a blank line when binary and endofline are set + if !exists('+fixendofline') + Skip This test requires 'fixendofline' + endif + setl binary + setl endofline + Assert Equals(lsp#utils#buffer#_get_lines(bufnr('$')), ['foo', 'bar', '']) + End + + It does not add a blank line when nobinary, nofixendofline, and noendofline are set + if !exists('+fixendofline') + Skip This test requires 'fixendofline' + endif + setl nobinary + setl nofixendofline + setl noendofline + Assert Equals(lsp#utils#buffer#_get_lines(bufnr('$')), ['foo', 'bar']) + End + + It does not add a blank line when binary and noendofline are set + if !exists('+fixendofline') + Skip This test requires 'fixendofline' + endif + setl binary + setl noendofline + Assert Equals(lsp#utils#buffer#_get_lines(bufnr('$')), ['foo', 'bar']) + End + + It adds a blank line when nobinary is set + if exists('+fixendofline') + Skip This test is not for 'fixendofline' + endif + setl nobinary + Assert Equals(lsp#utils#buffer#_get_lines(bufnr('$')), ['foo', 'bar', '']) + End + + It adds a blank line when binary and endofline are set + if exists('+fixendofline') + Skip This test is not for 'fixendofline' + endif + setl binary + setl endofline + Assert Equals(lsp#utils#buffer#_get_lines(bufnr('$')), ['foo', 'bar', '']) + End + + It does not add a blank line when binary and noendofline are set + if exists('+fixendofline') + Skip This test is not for 'fixendofline' + endif + setl binary + setl noendofline + Assert Equals(lsp#utils#buffer#_get_lines(bufnr('$')), ['foo', 'bar']) + End + End + + Describe lsp#utils#buffer#get_indent_size + It gets shiftwidth if set + setl shiftwidth=4 + setl tabstop=8 + Assert Equals(lsp#utils#buffer#get_indent_size(bufnr('$')), 4) + End + It gets tabstop if shiftwidth not set + setl shiftwidth=0 + setl tabstop=12 + Assert Equals(lsp#utils#buffer#get_indent_size(bufnr('$')), 12) + End + End +End diff --git a/test/lsp/utils/diff.vimspec b/test/lsp/utils/diff.vimspec new file mode 100644 index 00000000..8bdfeea6 --- /dev/null +++ b/test/lsp/utils/diff.vimspec @@ -0,0 +1,86 @@ +Describe lsp#utils#diff + + Describe lsp#utils#diff#compute + It should return diff of one letter + let lines1 = [ + \ 'foo', + \ 'bar', + \ 'baz', + \] + let lines2 = [ + \ 'foo', + \ 'baR', + \ 'baz', + \] + let want = { + \ 'range': { + \ 'start': { 'line': 1, 'character': 2 }, + \ 'end': { 'line': 1, 'character': 3 }, + \ }, + \ 'text': 'R', + \ 'rangeLength': 1 + \} + let got = lsp#utils#diff#compute(lines1, lines2) + Assert Equals(got, want) + End + + It should return diff of multi-lines + let lines1 = [ + \ 'foo', + \ 'bar', + \ 'baz', + \] + let lines2 = [ + \ 'Foo', + \ 'baR', + \ 'baz', + \] + let want = { + \ 'range': { + \ 'start': { 'line': 0, 'character': 0 }, + \ 'end': { 'line': 1, 'character': 3, } + \ }, + \ 'text': "Foo\nbaR", + \ "rangeLength": 7 + \} + let got = lsp#utils#diff#compute(lines1, lines2) + Assert Equals(got, want) + End + + It should return diff for empty list + let lines1 = [] + let lines2 = [ + \ 'foo', + \ 'bar', + \ 'baz', + \] + let want = { + \ 'range': { + \ 'start': { 'line': 0, 'character': 0 }, + \ 'end': { 'line': 0, 'character': 0, } + \ }, + \ 'text': "foo\nbar\nbaz\n", + \ "rangeLength": 0 + \} + let got = lsp#utils#diff#compute(lines1, lines2) + Assert Equals(got, want) + + let lines1 = [ + \ 'foo', + \ 'bar', + \ 'baz', + \] + let lines2 = [] + let want = { + \ 'range': { + \ 'start': { 'line': 0, 'character': 0 }, + \ 'end': { 'line': 3, 'character': 0, } + \ }, + \ 'text': '', + \ "rangeLength": 12 + \} + let got = lsp#utils#diff#compute(lines1, lines2) + Assert Equals(got, want) + End + End +End diff --git a/test/lsp/utils/position.vimspec b/test/lsp/utils/position.vimspec new file mode 100644 index 00000000..98928973 --- /dev/null +++ b/test/lsp/utils/position.vimspec @@ -0,0 +1,65 @@ +Describe lsp#utils#position + + Before each + % delete _ + End + + After all + % delete _ + End + + Describe lsp#utils#position#lsp_to_vim + + It should return the byte-index from the given character-index on a buffer + call setline(1, ['a β c', 'δ', '']) + Assert Equals(lsp#utils#position#lsp_to_vim('%', { 'line': 0, 'character': 0 }), [1, 1]) + Assert Equals(lsp#utils#position#lsp_to_vim('%', { 'line': 0, 'character': 1 }), [1, 2]) + Assert Equals(lsp#utils#position#lsp_to_vim('%', { 'line': 0, 'character': 2 }), [1, 3]) + Assert Equals(lsp#utils#position#lsp_to_vim('%', { 'line': 0, 'character': 3 }), [1, 5]) + Assert Equals(lsp#utils#position#lsp_to_vim('%', { 'line': 0, 'character': 4 }), [1, 6]) + Assert Equals(lsp#utils#position#lsp_to_vim('%', { 'line': 1, 'character': 0 }), [2, 1]) + Assert Equals(lsp#utils#position#lsp_to_vim('%', { 'line': 1, 'character': 1 }), [2, 3]) + Assert Equals(lsp#utils#position#lsp_to_vim('%', { 'line': 2, 'character': 0 }), [3, 1]) + End + + It should return the byte-index from the given character-index in an unloaded file + Assert Equals(lsp#utils#position#lsp_to_vim('./test/testfiles/multibyte.txt', { 'line': 0, 'character': 0 }), [1, 1]) + Assert Equals(lsp#utils#position#lsp_to_vim('./test/testfiles/multibyte.txt', { 'line': 0, 'character': 1 }), [1, 2]) + Assert Equals(lsp#utils#position#lsp_to_vim('./test/testfiles/multibyte.txt', { 'line': 0, 'character': 2 }), [1, 3]) + Assert Equals(lsp#utils#position#lsp_to_vim('./test/testfiles/multibyte.txt', { 'line': 0, 'character': 3 }), [1, 5]) + Assert Equals(lsp#utils#position#lsp_to_vim('./test/testfiles/multibyte.txt', { 'line': 0, 'character': 4 }), [1, 6]) + Assert Equals(lsp#utils#position#lsp_to_vim('./test/testfiles/multibyte.txt', { 'line': 1, 'character': 0 }), [2, 1]) + Assert Equals(lsp#utils#position#lsp_to_vim('./test/testfiles/multibyte.txt', { 'line': 1, 'character': 1 }), [2, 3]) + Assert Equals(lsp#utils#position#lsp_to_vim('./test/testfiles/multibyte.txt', { 'line': 2, 'character': 0 }), [3, 1]) + End + + End + + Describe lsp#utils#position#vim_to_lsp + + It should return the character-index from the given byte-index on a buffer + call setline(1, ['a β c', 'δ', '']) + Assert Equals({ 'line': 0, 'character': 0 }, lsp#utils#position#vim_to_lsp('%', [1, 1])) + Assert Equals({ 'line': 0, 'character': 1 }, lsp#utils#position#vim_to_lsp('%', [1, 2])) + Assert Equals({ 'line': 0, 'character': 2 }, lsp#utils#position#vim_to_lsp('%', [1, 3])) + Assert Equals({ 'line': 0, 'character': 3 }, lsp#utils#position#vim_to_lsp('%', [1, 5])) + Assert Equals({ 'line': 0, 'character': 4 }, lsp#utils#position#vim_to_lsp('%', [1, 6])) + Assert Equals({ 'line': 1, 'character': 0 }, lsp#utils#position#vim_to_lsp('%', [2, 1])) + Assert Equals({ 'line': 1, 'character': 1 }, lsp#utils#position#vim_to_lsp('%', [2, 3])) + Assert Equals({ 'line': 2, 'character': 0 }, lsp#utils#position#vim_to_lsp('%', [3, 1])) + End + + It should return the character-index from the given byte-index in an unloaded file + Assert Equals({ 'line': 0, 'character': 0 }, lsp#utils#position#vim_to_lsp('./test/testfiles/multibyte.txt', [1, 1])) + Assert Equals({ 'line': 0, 'character': 1 }, lsp#utils#position#vim_to_lsp('./test/testfiles/multibyte.txt', [1, 2])) + Assert Equals({ 'line': 0, 'character': 2 }, lsp#utils#position#vim_to_lsp('./test/testfiles/multibyte.txt', [1, 3])) + Assert Equals({ 'line': 0, 'character': 3 }, lsp#utils#position#vim_to_lsp('./test/testfiles/multibyte.txt', [1, 5])) + Assert Equals({ 'line': 0, 'character': 4 }, lsp#utils#position#vim_to_lsp('./test/testfiles/multibyte.txt', [1, 6])) + Assert Equals({ 'line': 1, 'character': 0 }, lsp#utils#position#vim_to_lsp('./test/testfiles/multibyte.txt', [2, 1])) + Assert Equals({ 'line': 1, 'character': 1 }, lsp#utils#position#vim_to_lsp('./test/testfiles/multibyte.txt', [2, 3])) + Assert Equals({ 'line': 2, 'character': 0 }, lsp#utils#position#vim_to_lsp('./test/testfiles/multibyte.txt', [3, 1])) + End + + End + +End diff --git a/test/lsp/utils/range.vimspec b/test/lsp/utils/range.vimspec new file mode 100644 index 00000000..af810247 --- /dev/null +++ b/test/lsp/utils/range.vimspec @@ -0,0 +1,61 @@ +Describe lsp#utils#range + + Before each + % delete _ + End + + Describe lsp#utils#range#_get_recent_visual_range + + It should return single line visual selection + call setline(1, ['あいうえお']) + normal! gg0llvly + Assert Equals(lsp#utils#range#_get_recent_visual_range(), { + \ 'start': { + \ 'line': 0, + \ 'character': 2 + \ }, + \ 'end': { + \ 'line': 0, + \ 'character': 4 + \ } + \ }) + End + + It should return multi line visual selection + call setline(1, ['あいうえお', 'かきくけこ']) + normal! gg0llvjly + Assert Equals(lsp#utils#range#_get_recent_visual_range(), { + \ 'start': { + \ 'line': 0, + \ 'character': 2 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 4 + \ } + \ }) + End + + End + + Describe lsp#utils#range#_get_current_line_range + + It should return current line range + call setline(1, ['あいうえお', 'かきくけこ', 'さしすせそ']) + call cursor(2, 1) + Assert Equals(lsp#utils#range#_get_current_line_range(), { + \ 'start': { + \ 'line': 1, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 5 + \ } + \ }) + End + + End + +End + diff --git a/test/lsp/utils/tagstack.vimspec b/test/lsp/utils/tagstack.vimspec new file mode 100644 index 00000000..e8ac9529 --- /dev/null +++ b/test/lsp/utils/tagstack.vimspec @@ -0,0 +1,75 @@ +Describe lsp#utils#tagstack + + Before each + % delete _ + if exists('*settagstack') + call settagstack(1, {'items' : []}) + endif + End + + After all + % delete _ + End + + Describe lsp#utils#tagstack#_update() + It should correctly preserve the tag stack + let l:bufnr = bufnr('%') + + 0put = 'foo' + 1put = 'bar' + 2put = 'baz' + + call cursor(1, 1) + call lsp#utils#tagstack#_update() + + call cursor(1, 2) + call lsp#utils#tagstack#_update() + + call cursor(3, 3) + call lsp#utils#tagstack#_update() + + call cursor(2, 1) + call lsp#utils#tagstack#_update() + + if !exists('*gettagstack') || !exists('*settagstack') + " calling lsp#utils#tagstack#_update() should be safe if + " gettagstack and settagstack doesn't exist + return + endif + + let l:tagstack = gettagstack() + + Assert Equals(l:tagstack['length'], 4) + Assert Equals(l:tagstack['curidx'], 5) + + Assert Equals(l:tagstack['items'][0], { + \ 'bufnr': l:bufnr, + \ 'tagname': 'foo', + \ 'from': [l:bufnr, 1, 1, 0], + \ 'matchnr': 1, + \ }) + + Assert Equals(l:tagstack['items'][1], { + \ 'bufnr': l:bufnr, + \ 'tagname': 'foo', + \ 'from': [l:bufnr, 1, 2, 0], + \ 'matchnr': 1, + \ }) + + Assert Equals(l:tagstack['items'][2], { + \ 'bufnr': l:bufnr, + \ 'tagname': 'baz', + \ 'from': [l:bufnr, 3, 3, 0], + \ 'matchnr': 1, + \ }) + + Assert Equals(l:tagstack['items'][3], { + \ 'bufnr': l:bufnr, + \ 'tagname': 'bar', + \ 'from': [l:bufnr, 2, 1, 0], + \ 'matchnr': 1, + \ }) + End + End + +End diff --git a/test/lsp/utils/text_edit.vimspec b/test/lsp/utils/text_edit.vimspec new file mode 100644 index 00000000..494c0708 --- /dev/null +++ b/test/lsp/utils/text_edit.vimspec @@ -0,0 +1,687 @@ +function! s:set_text(lines) + % delete _ + put =a:lines + execute 'normal ggdd' + execute 'file my-file' +endfunction + +function! s:get_text() + return lsp#utils#buffer#_get_lines(bufnr('$')) +endfunction + +Describe lsp#utils#text_edit + + Before all + let s:endofline_backup = &endofline + set endofline + End + + After all + let &endofline = s:endofline_backup + End + + Before each + enew! + End + + Describe lsp#utils#text_edit#apply_text_edits + It insert newText + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 1 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': 'baz' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', 'bbazar', '']) + End + + It insert empty newText + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 1 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', 'bar', '']) + End + + It replace range string to newText + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': 'replaced' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', 'replacedar', '']) + End + + It replace range string to empty newText + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', 'ar', '']) + End + + It single line start character is -1 + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': -1 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 3 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', '', '']) + End + + It single line start character is 0 + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 3 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', '', '']) + End + + It single line start character is 1 + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 1 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 3 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', 'b', '']) + End + + It single line end character is `len(getline('.')) - 1` + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 2 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', 'r', '']) + End + + It single line end character is `len(getline('.'))` + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 3 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', '', '']) + End + + It single line end character is `len(getline('.')) + 1` + call s:set_text(['foo', 'bar']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 4 + \ } + \ }, + \ 'newText': '' + \ }]) + + " if newline character deleting, need end position is next line zero character. + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['foo', '', '']) + End + + It replace range string to empty newText, multiline top to top + call s:set_text(['foo', 'bar', 'baz']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 0 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['bar', 'baz', '']) + End + + It replace range string to empty newText, multiline top to tail + call s:set_text(['foo', 'bar', 'baz']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 2 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['r', 'baz', '']) + + call s:set_text(['foo', 'bar', 'baz']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 3 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['', 'baz', '']) + + call s:set_text(['foo', 'bar', 'baz']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 4 + \ } + \ }, + \ 'newText': '' + \ }]) + + " if newline character deleting, need end position is next line zero character. + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['', 'baz', '']) + End + + It replace range string to empty newText, multiline middle to middle + call s:set_text(['foo', 'bar', 'baz']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 1 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 2 + \ } + \ }, + \ 'newText': '' + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['fr', 'baz', '']) + End + + It replaces entire buffer correctly + call s:set_text(['foo', 'bar', 'baz']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 3, + \ 'character': 0 + \ } + \ }, + \ 'newText': "x\ny\nz\n" + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['x', 'y', 'z', '']) + End + + It multiple textEdit with inserting \r. + " Add some text to buffer + call s:set_text(['class ABC {', ' private:', ' ', 'int a;};', ]) + + " Format + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [ + \ { + \ "range": { + \ "end": { + \ "character": 2, + \ "line": 1 + \ }, + \ "start": { + \ "character": 11, + \ "line": 0 + \ } + \ }, + \ "newText": "\n" + \ }, + \ { + \ "range": { + \ "end": { + \ "character": 0, + \ "line": 3 + \ }, + \ "start": { + \ "character": 10, + \ "line": 1 + \ } + \ }, + \ "newText": "\n " + \ }, + \ { + \ "range": { + \ "end": { + \ "character": 6, + \ "line": 3 + \ }, + \ "start": { + \ "character": 6, + \ "line": 3 + \ } + \ }, + \ "newText": "\n" + \ } + \ ]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['class ABC {', 'private:', ' int a;', '};', '']) + End + + It preserves v:completed_item + " Add some text to buffer + call s:set_text(['foo', 'bar']) + + " Go to end of file and invoke completion + execute "normal Gof\\" + + " Make sure that v:completed_item is set + Assert Equals(v:completed_item["word"], "foo") + let l:old_completed_item = v:completed_item + + " Perform some text edits + + " Insert + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 1 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': 'baz' + \ }]) + + " Insert empty + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 1 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': '' + \ }]) + + " Replace + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': 'replaced' + \ }]) + + " Delete + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 1, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': '' + \ }]) + + " Make sure v:completed_item is not changed + Assert Equals(v:completed_item, l:old_completed_item) + End + + It replaces entire buffer correctly when end column is 1 + call s:set_text(['foo', 'b']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': "x\ny\nz\n" + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['x', 'y', 'z', '', '']) + End + + It should apply edit that contains \r\n + call s:set_text(['foo', 'b']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0 + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 1 + \ } + \ }, + \ 'newText': "x\r\ny\r\nz\r\n" + \ }]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['x', 'y', 'z', '', '']) + End + + It adds imports correctly + call s:set_text(['package main', '', 'import java.util.ArrayList;', '', 'public class Main {}']) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ [{ + \ "range": { + \ "start": { + \ "character": 0, + \ "line": 2 + \ }, + \ "end": { + \ "character": 0, + \ "line": 3 + \ } + \ }, + \ "newText": "" + \ }, + \ { + \ "range": { + \ "start": { + \ "character": 0, + \ "line": 2 + \ }, + \ "end": { + \ "character": 0, + \ "line": 2 + \ } + \ }, + \ "newText": "import java.util.ArrayList;\n" + \ } + \ ]) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['package main', '', 'import java.util.ArrayList;', '', 'public class Main {}', '']) + End + + It adds null + let l:text = ['package main', '', 'import java.util.ArrayList;', '', 'public class Main {}'] + call s:set_text(l:text) + + call lsp#utils#text_edit#apply_text_edits( + \ expand('%'), + \ v:null) + + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, l:text + ['']) + End + + It should apply edits to unloaded file + let l:target = globpath(&runtimepath, 'test/lsp/utils/text_edit.vimspec') + call themis#log(l:target) + call lsp#utils#text_edit#apply_text_edits(lsp#utils#path_to_uri(l:target), [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0, + \ }, + \ 'end': { + \ 'line': 0, + \ 'character': 0, + \ } + \ }, + \ 'newText': "aiueo\n" + \ }]) + + Assert Equals(getbufline(l:target, 1), ['aiueo']) + End + + It should apply edits to buffer and unloaded file + let l:text = ['plop'] + call s:set_text(l:text) + let l:buffer_text = s:get_text() + Assert Equals(l:buffer_text, ['plop', '']) + let l:target = globpath(&runtimepath, 'test/lsp/utils/text_edit.vimspec') + call lsp#utils#text_edit#apply_text_edits( + \ lsp#utils#path_to_uri(expand('%')), + \ [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0, + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 0, + \ } + \ }, + \ 'newText': "buffer\n" + \ }]) + call lsp#utils#text_edit#apply_text_edits(lsp#utils#path_to_uri(l:target), [{ + \ 'range': { + \ 'start': { + \ 'line': 0, + \ 'character': 0, + \ }, + \ 'end': { + \ 'line': 1, + \ 'character': 0, + \ } + \ }, + \ 'newText': "unloaded\n" + \ }]) + + Assert Equals(getbufline(l:target, 1), ['unloaded']) + Assert Equals(getbufline(expand('%'), 1), ['buffer']) + End + End +End + diff --git a/test/lsp/utils/work_done_progress.vimspec b/test/lsp/utils/work_done_progress.vimspec new file mode 100644 index 00000000..526612df --- /dev/null +++ b/test/lsp/utils/work_done_progress.vimspec @@ -0,0 +1,92 @@ +Describe lsp#internal#work_done_progress + Before each + let g:lsp_work_done_progress_enabled = 1 + call lsp#internal#work_done_progress#_enable() + End + + After each + let g:lsp_work_done_progress_enabled = 0 + call lsp#internal#work_done_progress#_disable() + End + + It should be able to subscribe to $progress stream + let l:server1_response1 = {'method': '$/progress', 'params':{'token':'token text','value':{'kind':'begin', 'title':'title'}}} + let l:server1_response2 = {'method': '$/progress', 'params':{'token':'token text','value':{'percentage':50,'message':'test message','kind':'report'}}} + let l:server1_response3 = {'method': '$/progress', 'params':{'token':'token text','value':{'kind':'end'}}} + + Assert Equals(lsp#internal#work_done_progress#get_progress(), []) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response1 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': '', 'token': 'token text', 'title': 'title', 'server': 'server1'}]) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response2 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': 'test message', 'token': 'token text', 'percentage': 50, 'title': 'title', 'server': 'server1'}]) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response3 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), []) + End + + It should be able to subscribe to multi $progress stream + let l:server1_response1 = {'method': '$/progress', 'params':{'token':'token text','value':{'kind':'begin', 'title':'title1'}}} + let l:server1_response2 = {'method': '$/progress', 'params':{'token':'token text','value':{'percentage':50,'message':'msg1','kind':'report'}}} + let l:server1_response3 = {'method': '$/progress', 'params':{'token':'token text','value':{'percentage':90,'message':'msg1','kind':'report'}}} + let l:server1_response4 = {'method': '$/progress', 'params':{'token':'token text','value':{'kind':'end'}}} + let l:server2_response1 = {'method': '$/progress', 'params':{'token':'server2_token','value':{'kind':'begin', 'title':'title2'}}} + let l:server2_response2 = {'method': '$/progress', 'params':{'token':'server2_token','value':{'percentage':0,'message':'msg2','kind':'report'}}} + let l:server2_response3 = {'method': '$/progress', 'params':{'token':'server2_token','value':{'kind':'end'}}} + + Assert Equals(lsp#internal#work_done_progress#get_progress(), []) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response1 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': '', 'token': 'token text', 'title': 'title1', 'server': 'server1'}]) + + call lsp#stream(1, { 'server': 'server2', 'response': l:server2_response1 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': '', 'token': 'server2_token', 'title': 'title2', 'server': 'server2'}, + \ {'message': '', 'token': 'token text', 'title': 'title1', 'server': 'server1'}]) + + call lsp#stream(1, { 'server': 'server2', 'response': l:server2_response2 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': 'msg2', 'token': 'server2_token', 'percentage':0, 'title': 'title2', 'server': 'server2'}, + \ {'message': '', 'token': 'token text', 'title': 'title1', 'server': 'server1'}]) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response2 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': 'msg1', 'token': 'token text', 'percentage':50, 'title': 'title1', 'server': 'server1'}, + \ {'message': 'msg2', 'token': 'server2_token', 'percentage':0, 'title': 'title2', 'server': 'server2'}]) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response3 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': 'msg1', 'token': 'token text', 'percentage':90, 'title': 'title1', 'server': 'server1'}, + \ {'message': 'msg2', 'token': 'server2_token', 'percentage':0, 'title': 'title2', 'server': 'server2'}]) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response4 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': 'msg2', 'token': 'server2_token', 'percentage':0, 'title': 'title2', 'server': 'server2'}]) + + call lsp#stream(1, { 'server': 'server2', 'response': l:server2_response3 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), []) + End + + It should be returned correctly even if percentage and message do not exist. + let l:server1_response1 = {'method': '$/progress', 'params':{'token':'token text','value':{'kind':'begin', 'title':'title'}}} + let l:server1_response2 = {'method': '$/progress', 'params':{'token':'token text','value':{'kind':'report'}}} + let l:server1_response3 = {'method': '$/progress', 'params':{'token':'token text','value':{'kind':'end'}}} + + Assert Equals(lsp#internal#work_done_progress#get_progress(), []) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response1 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': '', 'token': 'token text', 'title': 'title', 'server': 'server1'}]) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response2 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), + \ [{'message': '', 'token': 'token text', 'title': 'title', 'server': 'server1'}]) + + call lsp#stream(1, { 'server': 'server1', 'response': l:server1_response3 }) + Assert Equals(lsp#internal#work_done_progress#get_progress(), []) + End +End diff --git a/test/lsp/utils/workspace_config.vimspec b/test/lsp/utils/workspace_config.vimspec new file mode 100644 index 00000000..f6de9336 --- /dev/null +++ b/test/lsp/utils/workspace_config.vimspec @@ -0,0 +1,183 @@ +Describe lsp#utils#workspace_config + + Describe lsp#utils#workspace_config#get + It should return the workspace config, when it is a dict + let l:name = 'Unit Test Server' + + call lsp#register_server({ + \ 'name': l:name, + \ 'workspace_config': { + \ 'a': { + \ 'a1': v:true, + \ 'a2': { + \ 'a21': 'disabled', + \ }, + \ }, + \ 'b': 'path/to/file', + \ } + \ }) + + let l:config = lsp#utils#workspace_config#get(l:name) + + Assert Equals(l:config['a']['a1'], v:true) + Assert Equals(l:config['a']['a2']['a21'], 'disabled') + Assert Equals(l:config['b'], 'path/to/file') + end + + It should return the workspace config, produced by a callback + let l:name = 'Unit Test Server' + + let l:callResult = {} + + call lsp#register_server({ + \ 'name': l:name, + \ 'workspace_config': {server_info->l:callResult}, + \ }) + + let l:config = lsp#utils#workspace_config#get(l:name) + Assert Equals(l:config, {}) + + let l:callResult = { + \ 'a': { + \ 'a1': v:true, + \ 'a2': { + \ 'a21': 'disabled', + \ }, + \ }, + \ 'b': 'path/to/file' + \ } + + let l:config = lsp#utils#workspace_config#get(l:name) + Assert Equals(l:config['a']['a1'], v:true) + Assert Equals(l:config['a']['a2']['a21'], 'disabled') + Assert Equals(l:config['b'], 'path/to/file') + end + End + + Describe lsp#utils#workspace_config#project + It should return a projection of a dictionary + let l:config = { + \ 'a': { + \ 'a1': v:true, + \ 'a2': { + \ 'a21': 'disabled', + \ }, + \ }, + \ 'b': 'path/to/file', + \ } + + let l:config_a_a1 = lsp#utils#workspace_config#projection( + \ l:config, + \ { 'section': 'a.a1' }, + \ ) + let l:config_a_a2_a21 = lsp#utils#workspace_config#projection( + \ l:config, + \ { 'section': 'a.a2.a21' }, + \ ) + let l:config_b = lsp#utils#workspace_config#projection( + \ l:config, + \ { 'section': 'b' }, + \ ) + let l:config_c = lsp#utils#workspace_config#projection( + \ l:config, + \ { 'section': 'c' }, + \ ) + + Assert Equals(l:config_a_a1, v:true) + Assert Equals(l:config_a_a2_a21, 'disabled') + Assert Equals(l:config_b, 'path/to/file') + Assert Equals(l:config_c, v:null) + end + End + + Describe lsp#utils#workspace_config#get_value + It should return a projection of the workspace config, when it is a dict + let l:name = 'Unit Test Server' + + call lsp#register_server({ + \ 'name': l:name, + \ 'workspace_config': { + \ 'a': { + \ 'a1': v:true, + \ 'a2': { + \ 'a21': 'disabled', + \ }, + \ }, + \ 'b': "path/to/file", + \ } + \ }) + + let l:config_a_a1 = lsp#utils#workspace_config#get_value( + \ l:name, + \ { 'section': 'a.a1' }, + \ ) + let l:config_a_a2_a21 = lsp#utils#workspace_config#get_value( + \ l:name, + \ { 'section': 'a.a2.a21' }, + \ ) + let l:config_b = lsp#utils#workspace_config#get_value( + \ l:name, + \ { 'section': 'b' }, + \ ) + let l:config_c = lsp#utils#workspace_config#get_value( + \ l:name, + \ { 'section': 'c' }, + \ ) + + Assert Equals(l:config_a_a1, v:true) + Assert Equals(l:config_a_a2_a21, 'disabled') + Assert Equals(l:config_b, 'path/to/file') + Assert Equals(l:config_c, v:null) + end + + It should return a projection of the workspace config, produced by a callback + let l:name = 'Unit Test Server' + + let l:callResult = {} + + call lsp#register_server({ + \ 'name': l:name, + \ 'workspace_config': {server_info->l:callResult}, + \ }) + + let l:config = lsp#utils#workspace_config#get_value( + \ l:name, + \ { 'section': '' }, + \ ) + Assert Equals(l:config, {}) + + let l:callResult = { + \ 'a': { + \ 'a1': v:true, + \ 'a2': { + \ 'a21': 'disabled', + \ }, + \ }, + \ 'b': "path/to/file", + \ } + + let l:config_a_a1 = lsp#utils#workspace_config#get_value( + \ l:name, + \ { 'section': 'a.a1' }, + \ ) + let l:config_a_a2_a21 = lsp#utils#workspace_config#get_value( + \ l:name, + \ { 'section': 'a.a2.a21' }, + \ ) + let l:config_b = lsp#utils#workspace_config#get_value( + \ l:name, + \ { 'section': 'b' }, + \ ) + let l:config_c = lsp#utils#workspace_config#get_value( + \ l:name, + \ { 'section': 'c' }, + \ ) + + Assert Equals(l:config_a_a1, v:true) + Assert Equals(l:config_a_a2_a21, 'disabled') + Assert Equals(l:config_b, 'path/to/file') + Assert Equals(l:config_c, v:null) + end + End +End + diff --git a/test/lsp/utils/workspace_edit.vimspec b/test/lsp/utils/workspace_edit.vimspec new file mode 100644 index 00000000..5b627fee --- /dev/null +++ b/test/lsp/utils/workspace_edit.vimspec @@ -0,0 +1,81 @@ +Describe lsp#utils#workspace_edit + + Describe lsp#utils#text_edit#apply_workspace_edit + It populates location list with changes + let g:lsp_show_workspace_edits = 1 + + call lsp#utils#workspace_edit#apply_workspace_edit({ + \ 'documentChanges': [{ + \ 'textDocument': { 'uri': 'file:///path/to/file' }, + \ 'edits': [ + \ { + \ "range": { + \ "start": { + \ "character": 0, + \ "line": 1 + \ }, + \ "end": { + \ "character": 0, + \ "line": 1 + \ } + \ }, + \ "newText": "import java.util.LinkedList;" + \ }, + \ { + \ "range": { + \ "start": { + \ "character": 0, + \ "line": 0 + \ }, + \ "end": { + \ "character": 0, + \ "line": 0 + \ } + \ }, + \ "newText": "import java.util.ArrayList;" + \ } + \ ] + \ }]}) + + let l:loclist = getloclist(0) + + Assert Equals(len(l:loclist), 2) + + Assert Equals(l:loclist[0]['lnum'], 2) + Assert Equals(l:loclist[0]['col'], 1) + Assert Equals(l:loclist[0]['text'], 'import java.util.LinkedList;') + + Assert Equals(l:loclist[1]['lnum'], 1) + Assert Equals(l:loclist[1]['col'], 1) + Assert Equals(l:loclist[1]['text'], 'import java.util.ArrayList;') + + end + + It should not set location list if not enabled + let g:lsp_show_workspace_edits = 0 + + call lsp#utils#workspace_edit#apply_workspace_edit({ + \ 'documentChanges': [{ + \ 'textDocument': { 'uri': 'file:///path/to/file' }, + \ 'edits': [ + \ { + \ "range": { + \ "start": { + \ "character": 0, + \ "line": 3 + \ }, + \ "end": { + \ "character": 0, + \ "line": 3 + \ } + \ }, + \ "newText": "import java.util.LinkedList;" + \ } + \ ] + \ }]}) + + Assert Equals(len(getloclist(0)), 0) + End + End +End + diff --git a/test/testfiles/multibyte.txt b/test/testfiles/multibyte.txt new file mode 100644 index 00000000..eb30fd12 --- /dev/null +++ b/test/testfiles/multibyte.txt @@ -0,0 +1,3 @@ +a β c +δ + diff --git a/test/testproject-go/documentformat.go b/test/testproject-go/documentformat.go new file mode 100644 index 00000000..dc41250d --- /dev/null +++ b/test/testproject-go/documentformat.go @@ -0,0 +1,7 @@ +package main +func main() { +print("hello, world!") +print("こんにちは、世界") +print("a β c") +print("δ") +} diff --git a/test/testproject-rust/.gitignore b/test/testproject-rust/.gitignore new file mode 100644 index 00000000..2f7896d1 --- /dev/null +++ b/test/testproject-rust/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/test/testproject-rust/Cargo.lock b/test/testproject-rust/Cargo.lock new file mode 100644 index 00000000..9ebd5ce5 --- /dev/null +++ b/test/testproject-rust/Cargo.lock @@ -0,0 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "testproject-rust" +version = "0.1.0" diff --git a/test/testproject-rust/Cargo.toml b/test/testproject-rust/Cargo.toml new file mode 100644 index 00000000..a93cd843 --- /dev/null +++ b/test/testproject-rust/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "testproject-rust" +version = "0.1.0" +authors = ["Prabir Shrestha "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/test/testproject-rust/src/calc/add.rs b/test/testproject-rust/src/calc/add.rs new file mode 100644 index 00000000..6eec06a1 --- /dev/null +++ b/test/testproject-rust/src/calc/add.rs @@ -0,0 +1,3 @@ +pub fn add(a: u32, b: u32) -> u32 { + a + b +} diff --git a/test/testproject-rust/src/calc/mod.rs b/test/testproject-rust/src/calc/mod.rs new file mode 100644 index 00000000..cced7b48 --- /dev/null +++ b/test/testproject-rust/src/calc/mod.rs @@ -0,0 +1 @@ +pub mod add; diff --git a/test/testproject-rust/src/documentdefinition.rs b/test/testproject-rust/src/documentdefinition.rs new file mode 100644 index 00000000..fbf0234f --- /dev/null +++ b/test/testproject-rust/src/documentdefinition.rs @@ -0,0 +1,13 @@ +use crate::calc::add::add; + +fn document_definition_same_file() { + func1(); +} + +fn func1() { + unimplemented!(); +} + +fn document_definition_different_file() { + add(1, 2); +} diff --git a/test/testproject-rust/src/documentdiagnostics.rs b/test/testproject-rust/src/documentdiagnostics.rs new file mode 100644 index 00000000..78e77a50 --- /dev/null +++ b/test/testproject-rust/src/documentdiagnostics.rs @@ -0,0 +1,11 @@ +fn document_diagnostics() { + unimplemented!(); +} + +fn broken1( + print +} + +fn broken2() + broken(1); +} diff --git a/test/testproject-rust/src/documentformat.rs b/test/testproject-rust/src/documentformat.rs new file mode 100644 index 00000000..4dee881c --- /dev/null +++ b/test/testproject-rust/src/documentformat.rs @@ -0,0 +1,3 @@ +fn format () { + unimplemented!(); +} diff --git a/test/testproject-rust/src/main.rs b/test/testproject-rust/src/main.rs new file mode 100644 index 00000000..7a8622df --- /dev/null +++ b/test/testproject-rust/src/main.rs @@ -0,0 +1,8 @@ +mod calc; + +mod documentdefinition; +mod documentformat; + +fn main() { + println!("Hello, world!"); +} diff --git a/test/testproject/.ccls b/test/testproject/.ccls new file mode 100644 index 00000000..e69de29b diff --git a/test/testproject/CMakeLists.txt b/test/testproject/CMakeLists.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/testproject/README.md b/test/testproject/README.md new file mode 100644 index 00000000..e69de29b diff --git a/test/testproject/compile_commands.json b/test/testproject/compile_commands.json new file mode 100644 index 00000000..e69de29b diff --git a/test/testproject/git/.gitignore b/test/testproject/git/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/test/testproject/src/CMakeLists.txt b/test/testproject/src/CMakeLists.txt new file mode 100644 index 00000000..e69de29b diff --git a/test/testproject/src/main.cpp b/test/testproject/src/main.cpp new file mode 100644 index 00000000..e69de29b diff --git a/test/utils/autoload/lsp/test.vim b/test/utils/autoload/lsp/test.vim new file mode 100644 index 00000000..57c524e6 --- /dev/null +++ b/test/utils/autoload/lsp/test.vim @@ -0,0 +1,50 @@ +function! lsp#test#projectdir(name) abort + if a:name ==# 'rust' + return expand('%:p:h') .'/test/testproject-rust' + elseif a:name ==# 'go' + return expand('%:p:h') .'/test/testproject-go' + else + throw 'projectdir not supported for ' . a:name + endif +endfunction + +function! lsp#test#openproject(name, options) abort + if a:name ==# 'go' + filetype on + + call lsp#register_server({ + \ 'name': 'gopls', + \ 'cmd': ['gopls'], + \ 'allowlist': ['go'], + \ }) + + call lsp#enable() + + " open .go file to trigger gopls then close it + execute printf('keepalt keepjumps edit %s', lsp#test#projectdir(a:name) . '/documentformat.go') + " wait for server starting + call lsp#test#wait(10000, {-> lsp#get_server_status('gopls') ==# 'running' }) + + %bwipeout! + else + throw 'open project not not supported for ' . a:name + endif +endfunction + +function! lsp#test#closeproject(name) abort + if lsp#test#hasproject(a:name) + silent! call lsp#stop_sserver(a:name) + endif +endfunction + +function! lsp#test#hasproject(name) abort + if a:name ==# 'go' && executable('gopls') + return 1 + else + return 0 + endif +endfunction + +function! lsp#test#wait(timeout, condition) abort + call lsp#utils#_wait(a:timeout, a:condition) +endfunction