Skip to content
This repository has been archived by the owner on Aug 12, 2023. It is now read-only.

Commit

Permalink
Merge pull request #339 from jose-elias-alvarez/on-save-handling
Browse files Browse the repository at this point in the history
feat: migrate to diagnostic API and support linting on save (feat. golangci-lint)
  • Loading branch information
Jose Alvarez committed Nov 14, 2021
2 parents a3ded9b + ba8dfd9 commit 8b2a232
Show file tree
Hide file tree
Showing 17 changed files with 518 additions and 250 deletions.
7 changes: 5 additions & 2 deletions README.md
Expand Up @@ -24,8 +24,9 @@ for external processes.
null-ls is in **beta status**. Please see below for steps to follow if something
doesn't work the way you expect (or doesn't work at all).

At the moment, null-is is compatible with Neovim 0.5 (stable) and 0.6 (head),
but you'll get the best experience from the latest version you can run.
At the moment, null-is is compatible with Neovim 0.5.1 (stable) and 0.6 (head),
but some features and performance improvements are exclusive to the latest
version.

Note that null-ls development takes place primarily on macOS and Linux and may
not work as expected (or at all) on Windows. Contributions to expand Windows
Expand Down Expand Up @@ -287,6 +288,8 @@ The test suite includes unit and integration tests and depends on plenary.nvim.
Run `make test` in the root of the project to run the suite or
`FILE=filename_spec.lua make test-file` to test an individual file.

E2E tests expect the latest Neovim master.

## Alternatives

- [efm-langserver](https://github.com/mattn/efm-langserver) and
Expand Down
32 changes: 30 additions & 2 deletions doc/BUILTINS.md
Expand Up @@ -1888,6 +1888,32 @@ local sources = { null_ls.builtins.diagnostics.yamllint }
- `command = "yamllint"`
- `args = { "--format", "parsable", "-" }`

### Diagnostics on save

**NOTE**: These sources depend on Neovim version 0.6.0 and are not compatible
with previous versions.

These sources run **only** on save, meaning that the diagnostics you see will
not reflect changes to the buffer until you write the changes to the disk.

#### [golangci-lint](https://golangci-lint.run/)

##### About

A Go linter aggregator.

##### Usage

```lua
local sources = { null_ls.builtins.diagnostics.golangci_lint }
```

##### Defaults

- `filetypes = { "go" }`
- `command = "golangci-lint"`
- `args = { "run", "--fix=false", "--fast", "--out-format=json", "$DIRNAME", "--path-prefix", "$ROOT" }`

### Code actions

#### [gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim)
Expand Down Expand Up @@ -2008,7 +2034,7 @@ following snippet:
runtime_condit
```

#### Vsnip
#### [vim-vsnip](https://github.com/hrsh7th/vim-vsnip)

##### About

Expand All @@ -2020,4 +2046,6 @@ Snippets managed by [vim-vsnip](https://github.com/hrsh7th/vim-vsnip).
local sources = { null_ls.builtins.completion.vsnip }
```

Registering this source will show available snippets in the completion list, but vim-vsnip is responsible for expanding them. See [vim-vsnip's documentation for setup instructions](https://github.com/hrsh7th/vim-vsnip#2-setting).
Registering this source will show available snippets in the completion list, but
vim-vsnip is in charge of expanding them. See [vim-vsnip's documentation for
setup instructions](https://github.com/hrsh7th/vim-vsnip#2-setting).
44 changes: 44 additions & 0 deletions lua/null-ls/builtins/diagnostics/golangci_lint.lua
@@ -0,0 +1,44 @@
local h = require("null-ls.helpers")
local methods = require("null-ls.methods")

local DIAGNOSTICS_ON_SAVE = methods.internal.DIAGNOSTICS_ON_SAVE

return h.make_builtin({
method = DIAGNOSTICS_ON_SAVE,
filetypes = { "go" },
generator_opts = {
command = "golangci-lint",
to_stdin = true,
from_stderr = false,
args = {
"run",
"--fix=false",
"--fast",
"--out-format=json",
"$DIRNAME",
"--path-prefix",
"$ROOT",
},
format = "json",
check_exit_code = function(code)
return code <= 2
end,
on_output = function(params)
local diags = {}
local issues = params.output["Issues"]
if type(issues) == "table" then
for _, d in ipairs(issues) do
if d.Pos.Filename == params.bufname then
table.insert(diags, {
row = d.Pos.Line,
col = d.Pos.Column,
message = d.Text,
})
end
end
end
return diags
end,
},
factory = h.generator_factory,
})
2 changes: 2 additions & 0 deletions lua/null-ls/config.lua
Expand Up @@ -7,6 +7,8 @@ local defaults = {
debug = false,
-- prevent double setup
_setup = false,
-- force using LSP handler, even when native API is available (e.g diagnostics)
_use_lsp_handler = false,
}

local config = vim.deepcopy(defaults)
Expand Down
62 changes: 46 additions & 16 deletions lua/null-ls/diagnostics.lua
Expand Up @@ -5,8 +5,16 @@ local methods = require("null-ls.methods")

local api = vim.api

local should_use_diagnostic_api = function()
return vim.diagnostic and not c.get()._use_lsp_handler
end

local namespaces = {}

local M = {}

M.namespaces = namespaces

-- assume 1-indexed ranges
local convert_range = function(diagnostic)
local row = tonumber(diagnostic.row or 1)
Expand All @@ -23,7 +31,17 @@ local convert_range = function(diagnostic)
end

local postprocess = function(diagnostic, _, generator)
diagnostic.range = convert_range(diagnostic)
local range = convert_range(diagnostic)
-- the diagnostic API requires 0-indexing, so we can repurpose the LSP range
if should_use_diagnostic_api() then
diagnostic.lnum = range["start"].line
diagnostic.end_lnum = range["end"].line
diagnostic.col = range["start"].character
diagnostic.end_col = range["end"].character
else
diagnostic.range = range
end

diagnostic.source = diagnostic.source or generator.opts.name or generator.opts.command or "null-ls"

local formatted = generator and generator.opts.diagnostics_format or c.get().diagnostics_format
Expand All @@ -38,6 +56,23 @@ local postprocess = function(diagnostic, _, generator)
diagnostic.message = formatted
end

local handle_diagnostics = function(diagnostics, uri, bufnr, client_id)
if should_use_diagnostic_api() then
for id, by_id in pairs(diagnostics) do
namespaces[id] = namespaces[id] or api.nvim_create_namespace("NULL_LS_SOURCE_" .. id)
vim.diagnostic.set(namespaces[id], bufnr, by_id)
end
return
end

local handler = u.resolve_handler(methods.lsp.PUBLISH_DIAGNOSTICS)
handler(nil, { diagnostics = diagnostics, uri = uri }, {
method = methods.lsp.PUBLISH_DIAGNOSTICS,
client_id = client_id,
bufnr = bufnr,
})
end

-- track last changedtick to only send most recent diagnostics
local last_changedtick = {}

Expand All @@ -57,18 +92,24 @@ M.handler = function(original_params)
s.clear_cache(uri)
end

local params = u.make_params(original_params, methods.map[method])
local handler = u.resolve_handler(methods.lsp.PUBLISH_DIAGNOSTICS)
local bufnr = vim.uri_to_bufnr(uri)

local changedtick = original_params.textDocument.version or api.nvim_buf_get_changedtick(bufnr)

if method == methods.lsp.DID_SAVE and changedtick == last_changedtick[uri] then
u.debug_log("buffer unchanged; ignoring didSave notification")
return
end

local params = u.make_params(original_params, methods.map[method])

last_changedtick[uri] = changedtick

require("null-ls.generators").run_registered({
filetype = params.ft,
method = methods.map[method],
params = params,
postprocess = postprocess,
index_by_id = should_use_diagnostic_api(),
callback = function(diagnostics)
u.debug_log("received diagnostics from generators")
u.debug_log(diagnostics)
Expand All @@ -81,18 +122,7 @@ M.handler = function(original_params)
return
end

if u.has_version("0.5.1") then
handler(nil, { diagnostics = diagnostics, uri = uri }, {
method = methods.lsp.PUBLISH_DIAGNOSTICS,
client_id = original_params.client_id,
bufnr = bufnr,
})
else
handler(nil, methods.lsp.PUBLISH_DIAGNOSTICS, {
diagnostics = diagnostics,
uri = uri,
}, original_params.client_id, bufnr)
end
handle_diagnostics(diagnostics, uri, bufnr, original_params.client_id)
end,
})
end
Expand Down
7 changes: 1 addition & 6 deletions lua/null-ls/formatting.lua
Expand Up @@ -66,12 +66,7 @@ M.apply_edits = function(edits, params)

local marks, views = save_win_data(bufnr)

if u.has_version("0.5.1") then
handler(nil, diffed_edits, { method = params.lsp_method, client_id = params.client_id, bufnr = bufnr })
else
---@diagnostic disable-next-line: redundant-parameter
handler(nil, params.lsp_method, diffed_edits, params.client_id, bufnr)
end
handler(nil, diffed_edits, { method = params.lsp_method, client_id = params.client_id, bufnr = bufnr })

vim.schedule(function()
restore_win_data(marks, views, bufnr)
Expand Down
29 changes: 20 additions & 9 deletions lua/null-ls/generators.lua
Expand Up @@ -2,7 +2,7 @@ local u = require("null-ls.utils")

local M = {}

M.run = function(generators, params, postprocess, callback)
M.run = function(generators, params, postprocess, callback, should_index)
local a = require("plenary.async")

local runner = function()
Expand All @@ -14,7 +14,14 @@ M.run = function(generators, params, postprocess, callback)
end

local futures, all_results = {}, {}
for _, generator in ipairs(generators) do
local iterator = should_index and pairs or ipairs
for index, generator in iterator(generators) do
local to_insert = all_results
if should_index then
all_results[index] = {}
to_insert = all_results[index]
end

table.insert(futures, function()
local copied_params = vim.deepcopy(params)

Expand Down Expand Up @@ -50,7 +57,7 @@ M.run = function(generators, params, postprocess, callback)
postprocess(result, copied_params, generator)
end

table.insert(all_results, result)
table.insert(to_insert, result)
end
end)
end
Expand Down Expand Up @@ -90,11 +97,11 @@ M.run_sequentially = function(generators, make_params, postprocess, callback, af
end

M.run_registered = function(opts)
local filetype, method, params, postprocess, callback =
opts.filetype, opts.method, opts.params, opts.postprocess, opts.callback
local generators = M.get_available(filetype, method)
local filetype, method, params, postprocess, callback, index_by_id =
opts.filetype, opts.method, opts.params, opts.postprocess, opts.callback, opts.index_by_id
local generators = M.get_available(filetype, method, index_by_id)

M.run(generators, params, postprocess, callback)
M.run(generators, params, postprocess, callback, index_by_id)
end

M.run_registered_sequentially = function(opts)
Expand All @@ -105,10 +112,14 @@ M.run_registered_sequentially = function(opts)
M.run_sequentially(generators, make_params, postprocess, callback, after_all)
end

M.get_available = function(filetype, method)
M.get_available = function(filetype, method, index_by_id)
local available = {}
for _, source in ipairs(require("null-ls.sources").get_available(filetype, method)) do
table.insert(available, source.generator)
if index_by_id then
available[source.id] = source.generator
else
table.insert(available, source.generator)
end
end
return available
end
Expand Down
23 changes: 6 additions & 17 deletions lua/null-ls/handlers.lua
Expand Up @@ -15,33 +15,22 @@ end
-- this will override a handler, batch results and debounce them
function M.combine(method, ms)
ms = ms or 100
local orig = u.resolve_handler(method)
local is_new = u.has_version("0.5.1")

local orig = u.resolve_handler(method)
local all_results = {}

local handler = u.debounce(ms, function()
if #all_results > 0 then
if is_new then
pcall(orig, nil, all_results)
else
pcall(orig, nil, nil, all_results)
end
pcall(orig, nil, all_results)
all_results = {}
end
end)

if is_new then
vim.lsp.handlers[method] = function(_, results)
vim.list_extend(all_results, results or {})
handler()
end
else
vim.lsp.handlers[method] = function(_, _, results)
vim.list_extend(all_results, results or {})
handler()
end
vim.lsp.handlers[method] = function(_, results)
vim.list_extend(all_results, results or {})
handler()
end

return vim.lsp.handlers[method]
end

Expand Down
2 changes: 1 addition & 1 deletion lua/null-ls/lspconfig.lua
Expand Up @@ -19,7 +19,7 @@ local should_attach = function(bufnr)

local ft = api.nvim_buf_get_option(bufnr, "filetype")
-- writing and immediately deleting a buffer (e.g. :wq from a git commit)
-- triggers a bug on 0.5 which is fixed on master
-- triggers a bug on 0.5.1 which is fixed on master
if ft == "gitcommit" and not u.has_version("0.6.0") then
return false
end
Expand Down

0 comments on commit 8b2a232

Please sign in to comment.