]> git.madduck.net Git - etc/vim.git/commitdiff

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Squashed '.vim/bundle/vim-lsp-ale/' content from commit db0f9a8a
authormartin f. krafft <madduck@madduck.net>
Tue, 8 Apr 2025 15:04:25 +0000 (17:04 +0200)
committermartin f. krafft <madduck@madduck.net>
Tue, 8 Apr 2025 15:04:25 +0000 (17:04 +0200)
git-subtree-dir: .vim/bundle/vim-lsp-ale
git-subtree-split: db0f9a8a33c0480988dc420cd2fba8a07743e4a4

26 files changed:
.codecov.yml [new file with mode: 0644]
.coveragerc [new file with mode: 0644]
.github/workflows/ci.yml [new file with mode: 0644]
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
autoload/lsp/ale.vim [new file with mode: 0644]
doc/vim-lsp-ale.txt [new file with mode: 0644]
plugin/lsp_ale.vim [new file with mode: 0644]
test/README.md [new file with mode: 0644]
test/integ/.gitignore [new file with mode: 0644]
test/integ/.themisrc [new file with mode: 0644]
test/integ/deps/.gitkeep [new file with mode: 0644]
test/integ/project/.gitignore [new file with mode: 0644]
test/integ/project/Cargo.lock [new file with mode: 0644]
test/integ/project/Cargo.toml [new file with mode: 0644]
test/integ/project/src/lib.rs [new file with mode: 0644]
test/integ/test.vimspec [new file with mode: 0644]
test/unit/.themisrc [new file with mode: 0644]
test/unit/runtime/autoload/ale/other_source.vim [new file with mode: 0644]
test/unit/runtime/autoload/lsp.vim [new file with mode: 0644]
test/unit/runtime/autoload/lsp/callbag.vim [new file with mode: 0644]
test/unit/runtime/autoload/lsp/internal/diagnostics/state.vim [new file with mode: 0644]
test/unit/runtime/autoload/lsp/ui/vim/utils.vim [new file with mode: 0644]
test/unit/runtime/autoload/lsp/utils.vim [new file with mode: 0644]
test/unit/test.vimspec [new file with mode: 0644]

diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644 (file)
index 0000000..53b2086
--- /dev/null
@@ -0,0 +1,8 @@
+coverage:
+  status:
+    project:
+      default:
+        target: 0%
+    patch:
+      default:
+        target: 0%
diff --git a/.coveragerc b/.coveragerc
new file mode 100644 (file)
index 0000000..8cddf91
--- /dev/null
@@ -0,0 +1,3 @@
+[run]
+plugins = covimerage
+data_file = .coverage_covimerage
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644 (file)
index 0000000..2e4e3a4
--- /dev/null
@@ -0,0 +1,101 @@
+name: CI
+on: [push, pull_request]
+
+jobs:
+  vint:
+    name: Run vint
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+      - run: pip install vim-vint
+      - run: vint --warning --verbose --enable-neovim ./autoload ./plugin
+
+  unit-test:
+    name: Unit tests
+    strategy:
+      matrix:
+        os: [macos-latest, ubuntu-latest, windows-latest]
+        neovim: [true, false]
+      fail-fast: false
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v2
+      - name: Checkout themis.vim
+        uses: actions/checkout@v2
+        with:
+          repository: thinca/vim-themis
+          path: vim-themis
+      - uses: rhysd/action-setup-vim@v1
+        id: vim
+        with:
+          neovim: ${{ matrix.neovim }}
+      - name: Run unit tests
+        env:
+          THEMIS_VIM: ${{ steps.vim.outputs.executable }}
+          THEMIS_PROFILE: profile.txt
+        run: ./vim-themis/bin/themis ./test/unit
+      - uses: actions/setup-python@v2
+        if: matrix.os != 'windows-latest'
+      - name: Report coverage
+        if: matrix.os != 'windows-latest'
+        run: |
+          # https://github.com/Vimjas/covimerage/issues/95
+          pip install 'click<8.0.0'
+          pip install covimerage
+          covimerage write_coverage profile.txt
+          coverage report
+          coverage xml
+      - name: Upload coverage to codecov
+        if: matrix.os != 'windows-latest'
+        uses: codecov/codecov-action@v1
+        with:
+          file: ./coverage.xml
+
+  integ-test:
+    name: Integration tests
+    strategy:
+      matrix:
+        neovim: [true, false]
+      fail-fast: false
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Checkout themis.vim
+        uses: actions/checkout@v2
+        with:
+          repository: thinca/vim-themis
+          path: vim-themis
+      - uses: rhysd/action-setup-vim@v1
+        id: vim
+        with:
+          neovim: ${{ matrix.neovim }}
+      - name: Checkout vim-lsp
+        uses: actions/checkout@v2
+        with:
+          repository: prabirshrestha/vim-lsp
+          path: test/integ/deps/vim-lsp
+      - name: Checkout ale
+        uses: actions/checkout@v2
+        with:
+          repository: dense-analysis/ale
+          path: test/integ/deps/ale
+      - name: Install rust-analyzer
+        run: |
+          mkdir ~/bin
+          curl --fail -L https://github.com/rust-analyzer/rust-analyzer/releases/latest/download/rust-analyzer-x86_64-unknown-linux-gnu.gz -o rust-analyzer.gz
+          gunzip ./rust-analyzer.gz
+          chmod +x ./rust-analyzer
+          mv ./rust-analyzer ~/bin
+          echo "$HOME/bin" >> $GITHUB_PATH
+      - name: Run integration tests
+        env:
+          THEMIS_VIM: ${{ steps.vim.outputs.executable }}
+        run: ./vim-themis/bin/themis ./test/integ
+      - name: Show runtime information
+        if: ${{ failure() }}
+        run: |
+          echo 'integ_messages.txt'
+          [ -f test/integ/integ_messages.txt ] && cat test/integ/integ_messages.txt
+          echo 'lsp-log.txt'
+          [ -f test/integ/lsp-log.txt ] && cat test/integ/lsp-log.txt
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..0a56e3f
--- /dev/null
@@ -0,0 +1 @@
+/doc/tags
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..b0687ad
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2021 rhysd
+
+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 (file)
index 0000000..2734b07
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+[vim-lsp][] + [ALE][]
+=====================
+[![Build Status][ci-badge]][ci]
+[![Coverage Status][codecov-badge]][codecov]
+
+[vim-lsp-ale][] is a Vim plugin for bridge between [vim-lsp][] and [ALE][]. Diagnostics results received
+by vim-lsp are shown in ALE's interface.
+
+When simply using ALE and vim-lsp, both plugins run LSP servers respectively. Running multiple server processes
+consume resources and may cause some issues. And showing lint results from multiple plugins is confusing.
+vim-lsp-ale solves the problem.
+
+<img alt="screencast" src="https://github.com/rhysd/ss/blob/master/vim-lsp-ale/main.gif?raw=true" width="582" height="316"/>
+
+## Installation
+
+Install [vim-lsp][], [ale][ALE], [vim-lsp-ale][] with your favorite package manager or `:packadd` in your `.vimrc`.
+
+An example with [vim-plug](https://github.com/junegunn/vim-plug):
+
+```viml
+Plug 'dense-analysis/ale'
+Plug 'prabirshrestha/vim-lsp'
+Plug 'rhysd/vim-lsp-ale'
+```
+
+## Usage
+
+Register LSP servers you want to use with `lsp#register_server` and set `vim-lsp` linter to `g:ale_linters`
+for filetypes you want to check with vim-lsp.
+
+The following example configures `gopls` to check Go sources.
+
+```vim
+" LSP configurations for vim-lsp
+if executable('gopls')
+    autocmd User lsp_setup call lsp#register_server({
+        \   'name': 'gopls',
+        \   'cmd': ['gopls'],
+        \   'allowlist': ['go', 'gomod'],
+        \ })
+endif
+
+" Set 'vim-lsp' linter
+let g:ale_linters = {
+    \   'go': ['golint'], " vim-lsp is implicitly active
+    \ }
+```
+
+This plugin configures vim-lsp and ALE automatically. You don't need to setup various variables.
+
+When opening a source code including some lint errors, vim-lsp will receive the errors from language server
+and ALE will report the errors in the buffer.
+
+ALE supports also many external programs. All errors can be seen in one place. The above example enables
+vim-lsp and golint.
+
+For more details, see [the documentation](./doc/vim-lsp-ale.txt).
+
+## Testing
+
+There are unit tests and integration tests. CI runs on GitHub Actions.
+See [test/README.md](./test/README.md) for more details.
+
+## License
+
+Licensed under [the MIT license](./LICENSE).
+
+[vim-lsp]: https://github.com/prabirshrestha/vim-lsp
+[ALE]: https://github.com/dense-analysis/ale
+[vim-lsp-ale]: https://github.com/rhysd/vim-lsp-ale
+[ci-badge]: https://github.com/rhysd/vim-lsp-ale/workflows/CI/badge.svg?branch=master&event=push
+[ci]: https://github.com/rhysd/vim-lsp-ale/actions?query=workflow%3ACI+branch%3Amaster
+[codecov-badge]: https://codecov.io/gh/rhysd/vim-lsp-ale/branch/master/graph/badge.svg
+[codecov]: https://codecov.io/gh/rhysd/vim-lsp-ale
diff --git a/autoload/lsp/ale.vim b/autoload/lsp/ale.vim
new file mode 100644 (file)
index 0000000..04db135
--- /dev/null
@@ -0,0 +1,203 @@
+" DiagnosticSeverity
+let s:ERROR = 1
+let s:WARN = 2
+let s:INFO = 3
+let s:HINT = 4
+
+let s:Dispose = v:null
+
+function! s:severity_threshold() abort
+    let s = g:lsp_ale_diagnostics_severity
+    if s ==? 'error'
+        return s:ERROR
+    elseif s ==? 'warning' || s ==? 'warn'
+        return s:WARN
+    elseif s ==? 'information' || s ==? 'info'
+        return s:INFO
+    elseif s ==? 'hint'
+        return s:HINT
+    else
+        throw 'vim-lsp-ale: Unexpected severity "' . s . '". Severity must be one of "error", "warning", "information", "hint"'
+    endif
+endfunction
+
+function! s:get_loc_type(severity) abort
+    if a:severity == s:ERROR
+        return 'E'
+    elseif a:severity == s:WARN
+        return 'W'
+    elseif a:severity == s:INFO
+        return 'I'
+    elseif a:severity == s:HINT
+        return 'H'
+    else
+        throw 'vim-lsp-ale: Unexpected severity: ' . a:severity
+    endif
+endfunction
+
+let s:prev_num_diags = {}
+function! lsp#ale#_reset_prev_num_diags() abort
+    let s:prev_num_diags = {}
+endfunction
+
+function! s:can_skip_diags(server, uri, diags) abort
+    if !has_key(s:prev_num_diags, a:server)
+        let s:prev_num_diags[a:server] = {}
+    endif
+    let prev = s:prev_num_diags[a:server]
+
+    let num_diags = len(a:diags)
+    if num_diags == 0 && get(prev, a:uri, -1) == 0
+        " Some language servers send diagnostics notifications even if the
+        " results are not changed from previous. It's hard to check the
+        " notifications are perfectly the same as previous. Here only checks
+        " emptiness and skip if both previous ones and current ones are
+        " empty.
+        " I believe programmers usually try to keep no lint errors in the
+        " source code they are writing :)
+        return v:true
+    endif
+
+    let prev[a:uri] = num_diags
+    return v:false
+endfunction
+
+function! s:can_skip_all_diags(uri, all_diags) abort
+    for [server, diags] in items(a:all_diags)
+        if !s:can_skip_diags(server, a:uri, diags.params.diagnostics)
+            return v:false
+        endif
+    endfor
+    return v:true
+endfunction
+
+function! s:is_active_linter() abort
+    if g:lsp_ale_auto_enable_linter
+        return v:true
+    endif
+    let active_linters = get(b:, 'ale_linters', get(g:ale_linters, &filetype, []))
+    return index(active_linters, 'vim-lsp') >= 0
+endf
+
+function! lsp#ale#on_ale_want_results(bufnr) abort
+    " Note: Checking lsp#internal#diagnostics#state#_is_enabled_for_buffer here. If previous lint
+    " errors remain in a buffer, they won't be updated when vim-lsp is disabled for the buffer.
+    if s:Dispose is v:null || !lsp#internal#diagnostics#state#_is_enabled_for_buffer(a:bufnr)
+        return
+    endif
+
+    let uri = lsp#utils#get_buffer_uri(a:bufnr)
+    let all_diags = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(uri)
+    if empty(all_diags) || s:can_skip_all_diags(uri, all_diags)
+        " Do nothing when no diagnostics results
+        return
+    endif
+
+    if s:is_active_linter()
+        call ale#other_source#StartChecking(a:bufnr, 'vim-lsp')
+        " Avoid the issue that sign and highlight are not set
+        " https://github.com/dense-analysis/ale/issues/3690
+        call timer_start(0, {-> s:notify_diag_to_ale(a:bufnr, all_diags) })
+    endif
+endfunction
+
+function! s:notify_diag_to_ale(bufnr, diags) abort
+    try
+        let threshold = s:severity_threshold()
+        let results = []
+        for [server, diag] in items(a:diags)
+            " Note: Do not filter `diag` destructively since the object is also used by vim-lsp
+            let locs = lsp#ui#vim#utils#diagnostics_to_loc_list({'response': diag})
+            let idx = 0
+            for loc in locs
+                let severity = get(diag.params.diagnostics[idx], 'severity', s:ERROR)
+                if severity > threshold
+                    continue
+                endif
+                let loc.text = '[' . server . '] ' . loc.text
+                let loc.type = s:get_loc_type(severity)
+                let results += [loc]
+                let idx += 1
+            endfor
+        endfor
+    catch
+        " Since ale#other_source#StartChecking() was already called, ale#other_source#ShowResults()
+        " needs to be called to notify ALE that checking was done.
+        call ale#other_source#ShowResults(a:bufnr, 'vim-lsp', [])
+        let msg = v:exception . ' at ' . v:throwpoint
+        if msg !~# '^vim-lsp-ale: '
+            " Avoid E608 on rethrowing exceptions from Vim script runtime
+            let msg = 'vim-lsp-ale: Error while notifying results to ALE: ' . msg
+        endif
+        throw msg
+    endtry
+    call ale#other_source#ShowResults(a:bufnr, 'vim-lsp', results)
+endfunction
+
+function! s:notify_diag_to_ale_for_buf(bufnr) abort
+    if !s:is_active_linter()
+        return
+    endif
+
+    let uri = lsp#utils#get_buffer_uri(a:bufnr)
+    let diags = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(uri)
+    call s:notify_diag_to_ale(a:bufnr, diags)
+endfunction
+
+function! s:on_diagnostics(res) abort
+    let uri = a:res.response.params.uri
+    if s:can_skip_diags(a:res.server, uri, a:res.response.params.diagnostics)
+        return
+    endif
+
+    let path = lsp#utils#uri_to_path(uri)
+    let bufnr = bufnr('^' . path . '$')
+    if bufnr == -1
+        " This branch is reachable when vim-lsp receives some notifications
+        " but the buffer for them was already deleted. This can happen since
+        " notifications are asynchronous
+        return
+    endif
+
+    call ale#other_source#StartChecking(bufnr, 'vim-lsp')
+    " Use timer_start to ensure calling s:notify_diag_to_ale after all
+    " subscribers handled the publishDiagnostics event.
+    " lsp_setup is hooked before vim-lsp sets various internal hooks. So this
+    " function is called before the response is not handled by vim-lsp yet.
+    call timer_start(0, {-> s:notify_diag_to_ale_for_buf(bufnr) })
+endfunction
+
+function! s:is_diagnostics_response(item) abort
+    if !has_key(a:item, 'server') || !has_key(a:item, 'response')
+        return v:false
+    endif
+    let res = a:item.response
+    if !has_key(res, 'method')
+        return v:false
+    endif
+    return res.method ==# 'textDocument/publishDiagnostics'
+endfunction
+
+function! lsp#ale#enable() abort
+    if s:Dispose isnot v:null
+        return
+    endif
+
+    let s:Dispose = lsp#callbag#pipe(
+            \   lsp#stream(),
+            \   lsp#callbag#filter(funcref('s:is_diagnostics_response')),
+            \   lsp#callbag#subscribe({ 'next': funcref('s:on_diagnostics') }),
+            \ )
+endfunction
+
+function! lsp#ale#disable() abort
+    if s:Dispose is v:null
+        return
+    endif
+    call s:Dispose()
+    let s:Dispose = v:null
+endfunction
+
+function! lsp#ale#enabled() abort
+    return s:Dispose isnot v:null
+endfunction
diff --git a/doc/vim-lsp-ale.txt b/doc/vim-lsp-ale.txt
new file mode 100644 (file)
index 0000000..1e4e578
--- /dev/null
@@ -0,0 +1,213 @@
+*vim-lsp-ale.txt*                   Bridge between vim-lsp and ALE
+
+Author: rhysd <https://rhysd.github.io>
+
+CONTENTS                            *vim-lsp-ale-contents*
+
+Introduction                        |vim-lsp-ale-introduction|
+Install                             |vim-lsp-ale-install|
+Usage                               |vim-lsp-ale-usage|
+Variables                           |vim-lsp-ale-variables|
+Functions                           |vim-lsp-ale-functions|
+Issues                              |vim-lsp-ale-issues|
+License                             |vim-lsp-ale-license|
+
+
+==============================================================================
+INTRODUCTION                                          *vim-lsp-ale-introduction*
+
+*vim-lsp-ale* is a plugin for bridge between |vim-lsp| and |ale|.
+
+When using ALE and vim-lsp, both plugins run language server process
+respectively. It's resource consuming and may cause some issues due to
+multiple server processes running at the same time.
+
+|vim-lsp-ale| solves the problem by getting diagnostics results from vim-lsp
+and by sending them to ALE. It means vim-lsp can be handled as one of linters
+supported by ALE.
+
+It's also possible to disable LSP features in ALE and use both ALE and
+vim-lsp's |:LspDocumentDiagnostics| together. But managing linter results
+with multiple plugins is complicated and confusing since behaviors of each
+plugins are not persistent.
+
+Screencast: https://github.com/rhysd/ss/blob/master/vim-lsp-ale/main.gif
+
+==============================================================================
+INSTALL                                                    *vim-lsp-ale-install*
+
+Install |ale|, |vim-lsp| and |vim-lsp-ale| with your favorite plugin manager
+or using |:packadd|.
+The following is an example using vim-plug.
+
+>
+  Plug 'dense-analysis/ale'
+  Plug 'prabirshrestha/vim-lsp'
+  Plug 'rhysd/vim-lsp-ale'
+<
+Repositories:
+
+- ALE: https://github.com/dense-analysis/ale/
+- vim-lsp: https://github.com/prabirshrestha/vim-lsp
+- vim-lsp-ale: https://github.com/rhysd/vim-lsp-ale
+
+
+==============================================================================
+USAGE                                                        *vim-lsp-ale-usage*
+
+Register LSP servers you want to use with |lsp#register_server()| and set
+"vim-lsp" linter to |g:ale_linters| for filetypes you want to check with
+vim-lsp.
+
+The following example configures gopls and golint to check Go sources. ALE
+will automatically reports diagnostics results from gopls and lint errrors
+from golint when you're writing Go source code.
+>
+  if executable('gopls')
+      autocmd User lsp_setup call lsp#register_server({
+          \ 'name': 'gopls',
+          \ 'cmd': ['gopls'],
+          \ 'allowlist': ['go', 'gomod'],
+          \ })
+  endif
+  let g:ale_linters = {
+      \   'go': ['vim-lsp', 'golint'],
+      \ }
+<
+gopls: https://github.com/golang/tools/tree/master/gopls
+
+vim-lsp-ale configures vim-lsp and ALE automatically. You don't need to setup
+various variables for them.
+
+vim-lsp-ale automatically does:
+
+- disable showing diagnostics results from vim-lsp since ALE will show the
+  results
+- disable LSP support of ALE since vim-lsp handles all LSP requests/responses
+
+If you don't want them to be done automatically, see
+|g:lsp_ale_auto_config_vim_lsp| and |g:lsp_ale_auto_config_ale|
+
+
+==============================================================================
+VARIABLES                                                *vim-lsp-ale-variables*
+
+Behavior of vim-lsp-ale can be customized with some global variables.
+
+------------------------------------------------------------------------------
+*g:lsp_ale_auto_config_vim_lsp* (Default: |v:true|)
+
+When |v:true| is set, vim-lsp-ale automatically sets several variables
+for configuring vim-lsp not to show diagnostics results in vim-lsp side.
+
+At the time of writing, setting |v:true| is the same as:
+>
+  let g:lsp_diagnostics_enabled = 1
+  let g:lsp_diagnostics_echo_cursor = 0
+  let g:lsp_diagnostics_float_cursor = 0
+  let g:lsp_diagnostics_highlights_enabled = 0
+  let g:lsp_diagnostics_signs_enabled = 0
+  let g:lsp_diagnostics_virtual_text_enabled = 0
+<
+When |v:false| is set, vim-lsp-ale does not set any variables to configure
+vim-lsp so that you can configure them by yourself.
+
+------------------------------------------------------------------------------
+*g:lsp_ale_auto_config_ale* (Default: |v:true|)
+
+When |v:true| is set, vim-lsp-ale automatically sets several variables
+for configuring ALE not to start LSP server process.
+
+At the time of writing, setting |v:true| is the same as:
+>
+  let g:ale_disable_lsp = 1
+<
+When |v:false| is set, vim-lsp-ale does not set any variables to configure
+ALE so that you can configure them by yourself.
+
+------------------------------------------------------------------------------
+*g:lsp_ale_auto_enable_linter* (Default: |v:true|)
+
+When |v:true| is set, vim-lsp-ale automatically enables itself as a linter for
+all filetypes. It does not modify |g:ale_linters|.
+
+When |v:false| is set, vim-lsp-ale is only active when configured as a linter
+for a filetype:
+>
+  let g:ale_linters = {
+      \   'go':     ['vim-lsp'],
+      \   'lua':    ['vim-lsp'],
+      \   'python': ['vim-lsp'],
+      \ }
+<
+------------------------------------------------------------------------------
+*g:lsp_ale_diagnostics_severity* (Default: "information")
+
+Severity level of reported diagnostics results. Possible values are "error",
+"warning", "information", "hint". Diagnostics results will be filtered by the
+severity set to this variable.
+For example, when "warning" is set, "error"/"warning" results are shown
+and "information"/"hint" results are not shown.
+
+
+==============================================================================
+FUNCTIONS                                                *vim-lsp-ale-functions*
+
+------------------------------------------------------------------------------
+lsp#ale#enable()                                                *lsp#ale#enable*
+
+Enables bridge between vim-lsp and ALE. This function is automatically called
+when |lsp_setup| autocmd event is triggered by vim-lsp. So basically you don't
+need to call this function.
+
+------------------------------------------------------------------------------
+lsp#ale#disable()                                              *lsp#ale#disable*
+
+Disables bridge between vim-lsp and ALE. After this function is called,
+diagnostics results will no longer be sent to ALE until |lsp#ale#enable| is
+called again.
+
+------------------------------------------------------------------------------
+lsp#ale#enabled()                                              *lsp#ale#enabled*
+
+Returns whether bridge between vim-lsp and ALE is enabled.
+
+
+==============================================================================
+ISSUES                                                      *vim-lsp-ale-issues*
+
+When you find some issues or you have some feature requests to vim-lsp-ale,
+visit GitHub repository page and make a new issue:
+
+https://github.com/rhysd/vim-lsp-ale/issues/new
+
+
+==============================================================================
+LICENSE                                                    *vim-lsp-ale-license*
+
+vim-lsp-ale is distributed under the MIT license.
+>
+  The MIT License (MIT)
+
+  Copyright (c) 2021 rhysd
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+  of the Software, and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+  PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
+  THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+<
+
+==============================================================================
+vim:tw=78:ts=8:ft=help:norl:et:fen:fdl=0:
diff --git a/plugin/lsp_ale.vim b/plugin/lsp_ale.vim
new file mode 100644 (file)
index 0000000..74ea929
--- /dev/null
@@ -0,0 +1,28 @@
+if (exists('g:loaded_lsp_ale') && g:loaded_lsp_ale) || &cp
+    finish
+endif
+let g:loaded_lsp_ale = 1
+
+let g:lsp_ale_diagnostics_severity = get(g:, 'lsp_ale_diagnostics_severity', 'information')
+let g:lsp_ale_auto_enable_linter = get(g:, 'lsp_ale_auto_enable_linter', v:true)
+
+if get(g:, 'lsp_ale_auto_config_vim_lsp', v:true)
+    " Enable diagnostics and disable all functionalities to show error
+    " messages by vim-lsp
+    let g:lsp_diagnostics_enabled = 1
+    let g:lsp_diagnostics_echo_cursor = 0
+    let g:lsp_diagnostics_float_cursor = 0
+    let g:lsp_diagnostics_highlights_enabled = 0
+    let g:lsp_diagnostics_signs_enabled = 0
+    let g:lsp_diagnostics_virtual_text_enabled = 0
+endif
+if get(g:, 'lsp_ale_auto_config_ale', v:true)
+    " Disable ALE's LSP integration
+    let g:ale_disable_lsp = 1
+endif
+
+augroup plugin-lsp-ale
+    autocmd!
+    autocmd User lsp_setup call lsp#ale#enable()
+    autocmd User ALEWantResults call lsp#ale#on_ale_want_results(g:ale_want_results_buffer)
+augroup END
diff --git a/test/README.md b/test/README.md
new file mode 100644 (file)
index 0000000..e566c2a
--- /dev/null
@@ -0,0 +1,103 @@
+Tests
+=====
+
+## Directory structure
+
+- [`test/unit`](./unit): Unit tests
+  - [`test/unit/test.vimspec`](./unit/test.vimspec): Unit test cases
+  - [`test/unit/runtime`](./unit/runtime): Runtime directory loaded on running unit tests. They mocks several external APIs like `ale#*` or `lsp#*`
+- [`test/integ`](./integ): Integration tests
+  - [`test/integ/test.vimspec`](./integ/test.vimspec): Integration test cases
+  - [`test/integ/deps`](./integ/deps): Dependant plugins
+
+## Unit tests
+
+Unit tests confirm vim-lsp-ale works as intended.
+
+### Prerequisites
+
+Unit tests can be run with no dependency because they mock every external API.
+
+[vim-themis](https://github.com/thinca/vim-themis) is used as test runner.
+
+By default, it runs tests with `vim` command. When running tests with Neovim, set `THEMIS_VIM=nvim` environment variable.
+
+### Run unit tests
+
+```sh
+cd path/to/vim-lsp-ale
+git clone https://github.com/thinca/vim-themis.git
+
+# Run tests with Vim
+./vim-themis/bin/themis ./test/unit/
+
+# Run tests with NeoVim
+THEMIS_VIM=nvim ./vim-themis/bin/themis ./test/unit/
+```
+
+### Measure unit test coverage
+
+[covimerage](https://github.com/Vimjas/covimerage) is used to measure test coverage. Install it by `pip install covimerage`.
+
+Set a file path to `THEMIS_PROFILE` environment variable and run unit tests. Vim will store the profile data to the file.
+`covimerage` command will convert the profile data into coverage data for `coverage` command provided by Python.
+
+```sh
+cd path/to/vim-lsp-ale
+git clone https://github.com/thinca/vim-themis.git
+
+# Run test case with $THEMIS_PROFILE environment variable
+THEMIS_PROFILE=profile.txt ./vim-themis/bin/themis ./test/unit
+
+# Store coverage data at .coverage_covimerage converted from the profile data
+covimerage write_coverage profile.txt
+
+# Show coverage report by `coverage` command
+coverage report
+```
+
+## Integration tests
+
+Integration tests confirm integrity among vim-lsp, ALE, vim-lsp-ale and a language server.
+
+### Prerequisites
+
+Integration tests require all dependencies are installed in [deps](./integ/deps) directory.
+
+```sh
+cd path/to/vim-lsp-ale
+git clone https://github.com/prabirshrestha/vim-lsp.git test/integ/deps/vim-lsp
+git clone https://github.com/dense-analysis/ale.git test/integ/deps/ale
+```
+
+[rust-analyzer](https://rust-analyzer.github.io/) is used as language server to run integration tests.
+Download the binary following [the instruction](https://rust-analyzer.github.io/manual.html#rust-analyzer-language-server-binary)
+and put the binary in `$PATH` directory.
+
+And [vim-themis](https://github.com/thinca/vim-themis) is used as test runner.
+
+Note that integration tests were not confirmed on Windows.
+
+### Run integration tests
+
+```sh
+cd path/to/vim-lsp-ale
+git clone https://github.com/thinca/vim-themis.git
+./vim-themis/bin/themis ./test/integ/
+```
+
+### Log files
+
+When some integration tests fail, the following log files would be useful to investigate the failure.
+
+- `test/integ/integ_messages.txt`: Messages in `:message` area while running the tests
+- `test/integ/lsp-log.txt`: Log information of vim-lsp. It records communications between vim-lsp and a language server
+
+## CI
+
+Tests are run continuously on GitHub Actions.
+
+https://github.com/rhysd/vim-lsp-ale/actions?query=workflow%3ACI
+
+- Unit tests are run on Linux, macOS and Windows with Vim and Neovim
+- Integration tests are run on Linux with Vim and Neovim
diff --git a/test/integ/.gitignore b/test/integ/.gitignore
new file mode 100644 (file)
index 0000000..d864a6f
--- /dev/null
@@ -0,0 +1,4 @@
+/deps/vim-lsp
+/deps/ale
+/lsp-log.txt
+/integ_messages.txt
diff --git a/test/integ/.themisrc b/test/integ/.themisrc
new file mode 100644 (file)
index 0000000..fb5c9f0
--- /dev/null
@@ -0,0 +1,44 @@
+call themis#option('exclude', 'test/README.md')
+
+let s:dir = expand('<sfile>:p:h')
+let s:sep = has('win32') ? '\' : '/'
+let s:vim_lsp_dir = join([s:dir, 'deps', 'vim-lsp'], s:sep)
+let s:ale_dir = join([s:dir, 'deps', 'ale'], s:sep)
+
+if !isdirectory(s:vim_lsp_dir)
+    throw 'vim-lsp is not cloned at ' . s:vim_lsp_dir
+endif
+
+if !isdirectory(s:ale_dir)
+    throw 'ALE is not cloned at ' . s:ale_dir
+endif
+
+function! IntegTestRootDir() abort
+    return s:dir
+endfunction
+
+execute 'set rtp+=' . s:vim_lsp_dir
+execute 'set rtp+=' . s:ale_dir
+filetype plugin indent on
+
+let g:lsp_log_file = 'lsp-log.txt'
+autocmd User lsp_setup call lsp#register_server({
+        \ 'name': 'rust-analyzer',
+        \ 'cmd': { server_info -> ['rust-analyzer'] },
+        \ 'allowlist': ['rust'],
+        \ })
+
+let g:ale_linters = { 'rust': ['vim-lsp'] }
+
+runtime plugin/lsp_ale.vim
+runtime plugin/lsp.vim
+runtime plugin/ale.vim
+
+" This is called automatically at VimEnter, but our tests load vim-lsp
+" after the event. So manually call it here
+call lsp#enable()
+
+let s:helper = themis#helper('assert')
+call themis#helper('command').with(s:helper)
+
+" vim: set ft=vim:
diff --git a/test/integ/deps/.gitkeep b/test/integ/deps/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/test/integ/project/.gitignore b/test/integ/project/.gitignore
new file mode 100644 (file)
index 0000000..53eaa21
--- /dev/null
@@ -0,0 +1,2 @@
+/target
+**/*.rs.bk
diff --git a/test/integ/project/Cargo.lock b/test/integ/project/Cargo.lock
new file mode 100644 (file)
index 0000000..52257f6
--- /dev/null
@@ -0,0 +1,5 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "project"
+version = "0.1.0"
diff --git a/test/integ/project/Cargo.toml b/test/integ/project/Cargo.toml
new file mode 100644 (file)
index 0000000..5217cb0
--- /dev/null
@@ -0,0 +1,9 @@
+[package]
+name = "project"
+version = "0.1.0"
+authors = ["rhysd <lin90162@yahoo.co.jp>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
diff --git a/test/integ/project/src/lib.rs b/test/integ/project/src/lib.rs
new file mode 100644 (file)
index 0000000..8f97f82
--- /dev/null
@@ -0,0 +1,3 @@
+pub fn do_something() {
+    let this_variable_is_unused = 42;
+}
diff --git a/test/integ/test.vimspec b/test/integ/test.vimspec
new file mode 100644 (file)
index 0000000..5ae295b
--- /dev/null
@@ -0,0 +1,103 @@
+let s:SEP = has('win32') ? '\' : '/'
+
+function! s:get_debug_info(bufnr) abort
+    let uri = lsp#utils#get_buffer_uri(a:bufnr)
+    let all_diags = lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(uri)
+    return "\nall diags: " . string(all_diags)
+       \ . "\nlocation list: " . string(ale#engine#GetLoclist(a:bufnr))
+       \ . "\nserver_status: " . lsp#get_server_status()
+       \ . "\ncurrent lines: " . string(getline(1, '$'))
+endfunction
+
+Describe rust-analyzer
+    Before all
+        if !executable('rust-analyzer')
+            throw 'rust-analyzer command is not found. It must be installed for running integration tests'
+        endif
+
+        let dir = IntegTestRootDir()
+        execute 'cd' dir
+        let file = join([dir, 'project', 'src', 'lib.rs'], s:SEP)
+
+        " Note: It might be better to write lib.rs here and delete in `After all` hook rather than
+        " modifying a file committed to repository directly.
+        let lib_rs_contents = readfile(file)
+    End
+
+    After all
+        " Restore contents of lib.rs since it was modified by test case
+        call writefile(lib_rs_contents, file)
+
+        redir! > integ_messages.txt
+            if exists(':LspStatus')
+                LspStatus
+            else
+                echom 'No :LspStatus command is defined'
+            endif
+            message
+        redir END
+    End
+
+    Before each
+        execute 'edit!' file
+    End
+
+    After each
+        bwipeout!
+    End
+
+    It shows diagnostics results with ALE through vim-lsp
+        Assert lsp#ale#enabled()
+
+        let bufnr = bufnr('')
+
+        let elapsed = 0 " in seconds
+        let timeout = 120 " in seconds
+        let counts = ale#statusline#Count(bufnr)
+        while elapsed <= timeout
+            if counts.total > 0
+                break
+            endif
+            sleep 1
+            let elapsed += 1
+            let counts = ale#statusline#Count(bufnr)
+        endwhile
+
+        let info = s:get_debug_info(bufnr)
+        Assert True(counts.total > 0, 'No error found after ' . elapsed . ' seconds' . info)
+
+        let loclist = ale#engine#GetLoclist(bufnr)
+        Assert NotEmpty(loclist, 'Location list from ALE is empty after ' . elapsed . ' seconds. ' . info)
+
+        let item = loclist[0]
+        let item_str = string(item)
+        Assert Equals(item.linter_name, 'vim-lsp', item_str . info)
+        Assert True(item.from_other_source, item_str . info)
+        Assert Match(item.filename, 'lib\.rs$', item_str . info)
+        Assert Match(item.text, 'this_variable_is_unused', item_str . info)
+
+        " Fix the problem
+        normal! ggjdd
+        write
+
+        let elapsed = 0 " in seconds
+        let counts = ale#statusline#Count(bufnr)
+        while elapsed <= timeout
+            if counts.total == 0
+                break
+            endif
+            sleep 1
+            let elapsed += 1
+            let counts = ale#statusline#Count(bufnr)
+        endwhile
+
+        let info = s:get_debug_info(bufnr)
+        Assert True(counts.total == 0, 'Error found after ' . elapsed . ' seconds' . info)
+
+        " Check the error was removed from location list since it'd been fixed
+        let loclist = ale#engine#GetLoclist(bufnr)
+        Assert Empty(loclist, 'Location list from ALE is not empty after ' . elapsed . ' seconds. ' . info)
+    End
+End
+
+" vim: set ft=vim:
diff --git a/test/unit/.themisrc b/test/unit/.themisrc
new file mode 100644 (file)
index 0000000..f04b082
--- /dev/null
@@ -0,0 +1,18 @@
+call themis#option('exclude', 'test/README.md')
+
+let s:dir = expand('<sfile>:p:h')
+let s:sep = has('win32') ? '\' : '/'
+let s:runtime_dir = join([s:dir, 'runtime'], s:sep)
+
+execute 'set rtp+=' . s:runtime_dir
+
+let s:helper = themis#helper('assert')
+call themis#helper('command').with(s:helper)
+
+if $THEMIS_PROFILE !=# ''
+  execute 'profile' 'start' $THEMIS_PROFILE
+  profile! file ./autoload/lsp/ale.vim
+  profile! file ./plugin/lsp_ale.vim
+endif
+
+" vim: set ft=vim:
diff --git a/test/unit/runtime/autoload/ale/other_source.vim b/test/unit/runtime/autoload/ale/other_source.vim
new file mode 100644 (file)
index 0000000..dda4fae
--- /dev/null
@@ -0,0 +1,62 @@
+function! ale#other_source#StartChecking(bufnr, name) abort
+    let s:start_checking_called = [a:bufnr, a:name]
+endfunction
+
+function! ale#other_source#ShowResults(bufnr, name, results) abort
+    let s:show_results_called = [a:bufnr, a:name, a:results]
+endfunction
+
+function! ale#other_source#last_start_checking() abort
+    return s:start_checking_called
+endfunction
+
+function! ale#other_source#last_show_results() abort
+    return s:show_results_called
+endfunction
+
+function! WaitUntil(func, ...) abort
+    let timeout = get(a:, 1, 1) " 1sec by default
+    let total = 0
+    while !a:func()
+        sleep 100m
+        let total += 0.1
+        if total >= timeout
+            " Note: v:true/v:false are not supported by themis.vim
+            " https://github.com/thinca/vim-themis/pull/56
+            return 0
+        endif
+    endwhile
+    return 1
+endfunction
+
+function! ale#other_source#wait_until_show_results() abort
+    let timeout = 1
+    let total = 0
+    while s:show_results_called is v:null
+        let total += 0.1
+        if total > timeout
+            throw 'ale#other_source#ShowResults() was not called while 1 second'
+        endif
+        sleep 100m
+    endwhile
+endfunction
+
+function! ale#other_source#check_show_no_result() abort
+    let timeout = 1
+    let total = 0
+    while s:show_results_called is v:null
+        let total += 0.1
+        if total > timeout
+            return
+        endif
+        sleep 100m
+    endwhile
+    throw 'ale#other_source#ShowResults() was called within 1 second: ' . string(s:show_results_called)
+endfunction
+
+function! ale#other_source#reset() abort
+    let s:start_checking_called = v:null
+    let s:show_results_called = v:null
+endfunction
+
+call ale#other_source#reset()
diff --git a/test/unit/runtime/autoload/lsp.vim b/test/unit/runtime/autoload/lsp.vim
new file mode 100644 (file)
index 0000000..64d078b
--- /dev/null
@@ -0,0 +1,2 @@
+function! lsp#stream() abort
+endfunction
diff --git a/test/unit/runtime/autoload/lsp/callbag.vim b/test/unit/runtime/autoload/lsp/callbag.vim
new file mode 100644 (file)
index 0000000..9d3b35f
--- /dev/null
@@ -0,0 +1,35 @@
+function! lsp#callbag#pipe(source, filter, sink) abort
+    let s:Filter = a:filter
+    let s:Next = a:sink.next
+    return {-> extend(s:, {'disposed': v:true})}
+endfunction
+
+function! lsp#callbag#filter(pred) abort
+    return a:pred
+endfunction
+
+function! lsp#callbag#subscribe(sink) abort
+    return a:sink
+endfunction
+
+" Functions for tests
+
+function! lsp#callbag#piped() abort
+    return s:Filter isnot v:null && s:Next isnot v:null
+endfunction
+
+function! lsp#callbag#disposed() abort
+    return s:disposed
+endfunction
+
+function! lsp#callbag#reset() abort
+    let s:Filter = v:null
+    let s:Next = v:null
+    let s:disposed = v:false
+endfunction
+
+function! lsp#callbag#mock_receive(res) abort
+    if s:Filter(a:res)
+        call s:Next(a:res)
+    endif
+endfunction
diff --git a/test/unit/runtime/autoload/lsp/internal/diagnostics/state.vim b/test/unit/runtime/autoload/lsp/internal/diagnostics/state.vim
new file mode 100644 (file)
index 0000000..e492cf2
--- /dev/null
@@ -0,0 +1,7 @@
+function! lsp#internal#diagnostics#state#_is_enabled_for_buffer(bufnr) abort
+    return a:bufnr == g:lsp_ale_test_mock_bufnr
+endfunction
+
+function! lsp#internal#diagnostics#state#_get_all_diagnostics_grouped_by_server_for_uri(uri) abort
+    return g:lsp_ale_test_mock_diags
+endfunction
diff --git a/test/unit/runtime/autoload/lsp/ui/vim/utils.vim b/test/unit/runtime/autoload/lsp/ui/vim/utils.vim
new file mode 100644 (file)
index 0000000..3236f17
--- /dev/null
@@ -0,0 +1,19 @@
+function! lsp#ui#vim#utils#diagnostics_to_loc_list(res) abort
+    if len(s:loclists) == 0
+        return []
+    endif
+
+    let ret = s:loclists[0]
+    let s:loclists = s:loclists[1:]
+    return ret
+endfunction
+
+function! lsp#ui#vim#utils#mock_diagnostics_to_loc_list(loclists) abort
+    let s:loclists = copy(a:loclists)
+endfunction
+
+function! lsp#ui#vim#utils#reset() abort
+    let s:loclists = []
+endfunction
+
+call lsp#ui#vim#utils#reset()
diff --git a/test/unit/runtime/autoload/lsp/utils.vim b/test/unit/runtime/autoload/lsp/utils.vim
new file mode 100644 (file)
index 0000000..c8dcd35
--- /dev/null
@@ -0,0 +1,11 @@
+function! lsp#utils#get_buffer_uri(bufnr) abort
+    return 'file://' . s:bufname
+endfunction
+
+function! lsp#utils#uri_to_path(uri) abort
+    return s:bufname
+endfunction
+
+function! lsp#utils#mock_buf_name(name) abort
+    let s:bufname = a:name
+endfunction
diff --git a/test/unit/test.vimspec b/test/unit/test.vimspec
new file mode 100644 (file)
index 0000000..cbe57d3
--- /dev/null
@@ -0,0 +1,471 @@
+function! s:test_diags() abort
+    return {
+        \ 'gopls': {
+        \   'method': 'textDocument/publishDiagnostics',
+        \   'jsonrpc': '2.0',
+        \   'params': {
+        \       'uri': 'file:///path/to/dummy.txt',
+        \       'diagnostics': [
+        \           {
+        \               'source': 'compiler',
+        \               'range': {
+        \                   'end': {'character': 4, 'line': 4},
+        \                   'start': {'character': 1, 'line': 4}
+        \               },
+        \               'message': 'error message 1',
+        \               'severity': 1
+        \           },
+        \           {
+        \               'source': 'compiler',
+        \               'range': {
+        \                   'end': {'character': 4, 'line': 4},
+        \                   'start': {'character': 1, 'line': 4}
+        \               },
+        \               'message': 'warning message 1',
+        \               'severity': 2
+        \           }
+        \       ]
+        \   }
+        \ }
+        \}
+endfunction
+
+function! s:test_locs() abort
+    return [[
+        \   {
+        \       'lnum': 5,
+        \       'col': 2,
+        \       'filename': '/path/to/dummy.txt',
+        \       'text': 'compiler:Error:error message 1'
+        \   },
+        \   {
+        \       'lnum': 5,
+        \       'col': 2,
+        \       'filename': '/path/to/dummy.txt',
+        \       'text': 'compiler:Warning:warning message 1'
+        \   }
+        \ ]]
+endfunction
+
+function! s:test_diags_all_severities() abort
+    let diags = s:test_diags()
+    let diag = diags.gopls.params.diagnostics[0]
+    let diags.gopls.params.diagnostics = []
+    for [sev, name] in [[1, 'error'], [2, 'warning'], [3, 'info'], [4, 'hint']]
+        let d = copy(diag)
+        let d.severity = sev
+        let d.message = name . ' message'
+        let diags.gopls.params.diagnostics += [d]
+    endfor
+    return diags
+endfunction
+
+function! s:test_locs_all_severities() abort
+    let loc = s:test_locs()[0][0]
+    let locs = []
+    for [sev, name] in [[1, 'Error'], [2, 'Warning'], [3, 'Info'], [4, 'Hint']]
+        let l = copy(loc)
+        let l.text = 'compiler:' . name . ':' . tolower(name) . ' message'
+        let locs += [l]
+    endfor
+    return [locs]
+endfunction
+
+function! s:test_expected_locs_all_severities() abort
+    let loc = s:test_locs()[0][0]
+    let locs = []
+    for [sev, name, type] in [[1, 'Error', 'E'], [2, 'Warning', 'W'], [3, 'Info', 'I'], [4, 'Hint', 'H']]
+        let l = copy(loc)
+        let l.type = type
+        let l.text = '[gopls] compiler:' . name . ':' . tolower(name) . ' message'
+        let locs += [l]
+    endfor
+    return locs
+endfunction
+
+function! s:modify_loc_item(item, type) abort
+    let a:item.type = a:type
+    let a:item.text = '[gopls] ' . a:item.text
+    return a:item
+endfunction
+
+Describe vim-lsp-ale
+    Before all
+        " Set bufffer name to 'foo'
+        file /path/to/dummy.txt
+    End
+
+    Before each
+        call lsp#ale#_reset_prev_num_diags()
+        call lsp#callbag#reset()
+        call ale#other_source#reset()
+        call lsp#ui#vim#utils#reset()
+        call lsp#utils#mock_buf_name('/path/to/dummy.txt')
+        let g:lsp_ale_test_mock_diags = {}
+        let g:lsp_ale_test_mock_bufnr = bufnr('')
+        doautocmd User lsp_setup
+        let g:ale_want_results_buffer = bufnr('')
+    End
+
+    After each
+        call lsp#ale#disable()
+    End
+
+    It enables vim-lsp's diagnostics and disables to output diagnostics
+        Assert True(g:lsp_diagnostics_enabled)
+        Assert False(g:lsp_diagnostics_highlights_enabled)
+        Assert False(g:lsp_diagnostics_signs_enabled)
+        Assert False(g:lsp_diagnostics_echo_cursor)
+        Assert False(g:lsp_diagnostics_virtual_text_enabled)
+    End
+
+    It disables ALE's LSP support
+        Assert True(g:ale_disable_lsp)
+    End
+
+    It defines plugin-lsp-ale autocmd group
+        Assert True(exists('g:loaded_lsp_ale'))
+
+        redir => autocmds
+            autocmd plugin-lsp-ale
+        redir END
+
+        Assert True(stridx(autocmds, 'lsp_setup') >= 0, autocmds)
+        Assert True(stridx(autocmds, 'ALEWantResults') >= 0, autocmds)
+    End
+
+    It subscribes notification stream on lsp_setup autocmd event
+        Assert True(lsp#callbag#piped())
+    End
+
+    It stops subscribing stream when lsp#ale#disable() is called
+        call lsp#ale#enable()
+        Assert True(lsp#ale#enabled())
+        call lsp#ale#disable()
+        Assert False(lsp#ale#enabled())
+        Assert True(lsp#callbag#disposed())
+        call lsp#ale#disable()
+        Assert False(lsp#ale#enabled())
+    End
+
+    Context ALEWantResults
+        It does not notify results when vim-lsp-ale is disabled
+            call lsp#ale#disable()
+            doautocmd User ALEWantResults
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+        End
+
+        It does not notify results when the buffer disables LSP
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+
+            let g:lsp_ale_test_mock_bufnr = -1
+            doautocmd User ALEWantResults
+
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+        End
+
+        It does not notify on no diagnostics error
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+            Assert Equals(ale#other_source#last_show_results(), v:null)
+
+            let g:lsp_ale_test_mock_diags = {}
+
+            let bufnr = bufnr('')
+            doautocmd User ALEWantResults
+
+            call ale#other_source#check_show_no_result()
+
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+            Assert Equals(ale#other_source#last_show_results(), v:null)
+        End
+
+        It notifies location list items converted from diagnostics results
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+            Assert Equals(ale#other_source#last_show_results(), v:null)
+
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs())
+            let g:lsp_ale_test_mock_diags = s:test_diags()
+
+            let bufnr = bufnr('')
+            doautocmd User ALEWantResults
+
+            call ale#other_source#wait_until_show_results()
+
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+
+            let [show_bufnr, show_name, loclist] = ale#other_source#last_show_results()
+            Assert Equals(show_bufnr, bufnr)
+            Assert Equals(show_name, 'vim-lsp')
+
+            let expected_locs = s:test_locs()[0]
+            call s:modify_loc_item(expected_locs[0], 'E')
+            call s:modify_loc_item(expected_locs[1], 'W')
+
+            Assert Equals(loclist, expected_locs)
+        End
+
+        It filters diagnostics results by severity
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+            Assert Equals(ale#other_source#last_show_results(), v:null)
+
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs())
+            let actual_diags = s:test_diags()
+            " Set 'hint' severity. Default threshold is 'info'
+            let actual_diags.gopls.params.diagnostics[1].severity = 4
+            let g:lsp_ale_test_mock_diags = actual_diags
+
+            let bufnr = bufnr('')
+            doautocmd User ALEWantResults
+
+            call ale#other_source#wait_until_show_results()
+
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+
+            let [show_bufnr, show_name, loclist] = ale#other_source#last_show_results()
+            Assert Equals(show_bufnr, bufnr)
+            Assert Equals(show_name, 'vim-lsp')
+
+            Assert Equals(len(loclist), 1, string(loclist))
+        End
+
+        It skips notifying results to ALE when no error continues to happen
+            let bufnr = bufnr('')
+
+            " Prepare empty results
+            let diags = s:test_diags()
+            let diags.gopls.params.diagnostics = []
+            let g:lsp_ale_test_mock_diags = diags
+
+            " First notification
+            doautocmd User ALEWantResults
+
+            call ale#other_source#wait_until_show_results()
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+            Assert Equals(ale#other_source#last_show_results(), [bufnr, 'vim-lsp', []])
+
+            call ale#other_source#reset()
+
+            " Second notification
+            doautocmd User ALEWantResults
+
+            call ale#other_source#check_show_no_result()
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+        End
+    End
+
+    Context textDocument/publishDiagnostics notification
+        It notifies diagnostics results to ALE
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+            Assert Equals(ale#other_source#last_show_results(), v:null)
+
+            let bufnr = bufnr('')
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs())
+            let g:lsp_ale_test_mock_diags = s:test_diags()
+
+            let response = { 'response': s:test_diags()['gopls'], 'server': 'gopls' }
+            call lsp#callbag#mock_receive(response)
+
+            call ale#other_source#wait_until_show_results()
+
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+
+            let [show_bufnr, show_name, loclist] = ale#other_source#last_show_results()
+            Assert Equals(show_bufnr, bufnr)
+            Assert Equals(show_name, 'vim-lsp')
+
+            let expected_locs = s:test_locs()[0]
+            call s:modify_loc_item(expected_locs[0], 'E')
+            call s:modify_loc_item(expected_locs[1], 'W')
+
+            Assert Equals(loclist, expected_locs)
+        End
+
+        It does nothing when receiving notification other than textDocument/publishDiagnostics
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs())
+            let g:lsp_ale_test_mock_diags = s:test_diags()
+            let response = {
+                \   'server': 'gopls',
+                \   'response': {
+                \       'method': 'something/doSomethihg',
+                \       'jsonrpc': '2.0',
+                \       'params': {},
+                \   }
+                \ }
+            call lsp#callbag#mock_receive(response)
+
+            call ale#other_source#check_show_no_result()
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+        End
+
+        It does nothing when method or server is missing in the notification
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs())
+            let g:lsp_ale_test_mock_diags = s:test_diags()
+
+            for response in [
+                    \   {
+                    \       'response': {
+                    \           'method': 'something/doSomethihg',
+                    \           'jsonrpc': '2.0',
+                    \           'params': {},
+                    \       }
+                    \   },
+                    \   {
+                    \       'server': 'gopls',
+                    \       'response': {
+                    \           'jsonrpc': '2.0',
+                    \           'params': {},
+                    \       }
+                    \   }
+                    \ ]
+                call lsp#callbag#mock_receive(response)
+            endfor
+
+            call ale#other_source#check_show_no_result()
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+        End
+
+        It does nothing when received notification is for buffer which doesn't exist
+            let bufnr = bufnr('')
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs())
+            let g:lsp_ale_test_mock_diags = s:test_diags()
+            call lsp#utils#mock_buf_name('/path/to/somewhere/else.txt')
+
+            let response = { 'response': s:test_diags()['gopls'], 'server': 'gopls' }
+            call lsp#callbag#mock_receive(response)
+
+            call ale#other_source#check_show_no_result()
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+        End
+
+        It notifies empty list when notification says no lint error was found
+            let bufnr = bufnr('')
+            let response = { 'response': s:test_diags()['gopls'], 'server': 'gopls' }
+            let response.response.params.diagnostics = []
+
+            call lsp#callbag#mock_receive(response)
+
+            call ale#other_source#wait_until_show_results()
+
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+            Assert Equals(ale#other_source#last_show_results(), [bufnr, 'vim-lsp', []])
+        End
+
+        It skips sending results to ALE when no error continues to happen
+            let bufnr = bufnr('')
+            let diags = s:test_diags()
+            let diags.gopls.params.diagnostics = []
+
+            " First notification
+            let response = { 'response': diags.gopls, 'server': 'gopls' }
+            call lsp#callbag#mock_receive(response)
+
+            call ale#other_source#wait_until_show_results()
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+            Assert Equals(ale#other_source#last_show_results(), [bufnr, 'vim-lsp', []])
+
+            call ale#other_source#reset()
+
+            " Second notification
+            call lsp#callbag#mock_receive(response)
+
+            call ale#other_source#check_show_no_result()
+            Assert Equals(ale#other_source#last_start_checking(), v:null)
+        End
+    End
+
+    Describe g:lsp_ale_diagnostics_severity
+        Before
+            let saved_diagnostics_severity = g:lsp_ale_diagnostics_severity
+        End
+
+        After
+            let g:lsp_ale_diagnostics_severity = saved_diagnostics_severity
+        End
+
+        It filters results by severity 'error'
+            let g:lsp_ale_diagnostics_severity = 'error'
+
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs_all_severities())
+            let g:lsp_ale_test_mock_diags = s:test_diags_all_severities()
+
+            let bufnr = bufnr('')
+            doautocmd User ALEWantResults
+            call ale#other_source#wait_until_show_results()
+
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+            let expected = s:test_expected_locs_all_severities()
+            call filter(expected, {_, l -> l.type ==# 'E'})
+            Assert Equals(ale#other_source#last_show_results(), [bufnr, 'vim-lsp', expected])
+        End
+
+        It filters results by severity 'warning'
+            let g:lsp_ale_diagnostics_severity = 'warning'
+
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs_all_severities())
+            let g:lsp_ale_test_mock_diags = s:test_diags_all_severities()
+
+            let bufnr = bufnr('')
+            doautocmd User ALEWantResults
+            call ale#other_source#wait_until_show_results()
+
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+            let expected = s:test_expected_locs_all_severities()
+            call filter(expected, {_, l -> l.type =~# 'E\|W'})
+            Assert Equals(ale#other_source#last_show_results(), [bufnr, 'vim-lsp', expected])
+        End
+
+        It filters results by severity 'information'
+            let g:lsp_ale_diagnostics_severity = 'information'
+
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs_all_severities())
+            let g:lsp_ale_test_mock_diags = s:test_diags_all_severities()
+
+            let bufnr = bufnr('')
+            doautocmd User ALEWantResults
+            call ale#other_source#wait_until_show_results()
+
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+            let expected = s:test_expected_locs_all_severities()
+            call filter(expected, {_, l -> l.type =~# 'E\|W\|I'})
+            Assert Equals(ale#other_source#last_show_results(), [bufnr, 'vim-lsp', expected])
+        End
+
+        It filters results by severity 'hint'
+            let g:lsp_ale_diagnostics_severity = 'hint'
+
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs_all_severities())
+            let g:lsp_ale_test_mock_diags = s:test_diags_all_severities()
+
+            let bufnr = bufnr('')
+            doautocmd User ALEWantResults
+            call ale#other_source#wait_until_show_results()
+
+            Assert Equals(ale#other_source#last_start_checking(), [bufnr, 'vim-lsp'])
+            let expected = s:test_expected_locs_all_severities()
+            Assert Equals(ale#other_source#last_show_results(), [bufnr, 'vim-lsp', expected])
+        End
+
+        It throws an error when invalid value is set
+            let g:lsp_ale_diagnostics_severity = 'invalid!'
+            let bufnr = bufnr('')
+
+            call lsp#ui#vim#utils#mock_diagnostics_to_loc_list(s:test_locs())
+            let g:lsp_ale_test_mock_diags = s:test_diags()
+
+            call lsp#callbag#mock_receive({ 'response': s:test_diags().gopls, 'server': 'gopls' })
+
+            if has('nvim')
+                Throws /^vim-lsp-ale: Unexpected severity/ ale#other_source#wait_until_show_results()
+            else
+                " XXX: No way to catch exception thrown while sleeping. Indirectly
+                " check the error was handled correctly by checking the result is
+                " set to empty.
+                call ale#other_source#wait_until_show_results()
+                Assert Equals(ale#other_source#last_show_results(), [bufnr, 'vim-lsp', []])
+            endif
+        End
+    End
+
+End
+
+" vim: set ft=vim: