diff --git a/README.md b/README.md index b8036627e..c1456f0aa 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/doc/BUILTINS.md b/doc/BUILTINS.md index ed4f4a206..9e0e2ff60 100644 --- a/doc/BUILTINS.md +++ b/doc/BUILTINS.md @@ -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) @@ -2008,7 +2034,7 @@ following snippet: runtime_condit ``` -#### Vsnip +#### [vim-vsnip](https://github.com/hrsh7th/vim-vsnip) ##### About @@ -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). diff --git a/lua/null-ls/builtins/diagnostics/golangci_lint.lua b/lua/null-ls/builtins/diagnostics/golangci_lint.lua new file mode 100644 index 000000000..9f0e6ea3f --- /dev/null +++ b/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, +}) diff --git a/lua/null-ls/config.lua b/lua/null-ls/config.lua index 9ddb644dc..8af3d534d 100644 --- a/lua/null-ls/config.lua +++ b/lua/null-ls/config.lua @@ -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) diff --git a/lua/null-ls/diagnostics.lua b/lua/null-ls/diagnostics.lua index b9a76a992..1a66f4baf 100644 --- a/lua/null-ls/diagnostics.lua +++ b/lua/null-ls/diagnostics.lua @@ -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) @@ -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 @@ -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 = {} @@ -57,11 +92,16 @@ 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({ @@ -69,6 +109,7 @@ M.handler = function(original_params) 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) @@ -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 diff --git a/lua/null-ls/formatting.lua b/lua/null-ls/formatting.lua index b6d51aa3c..6c9a670c5 100644 --- a/lua/null-ls/formatting.lua +++ b/lua/null-ls/formatting.lua @@ -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) diff --git a/lua/null-ls/generators.lua b/lua/null-ls/generators.lua index 508e3496f..a8ccac199 100644 --- a/lua/null-ls/generators.lua +++ b/lua/null-ls/generators.lua @@ -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() @@ -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) @@ -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 @@ -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) @@ -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 diff --git a/lua/null-ls/handlers.lua b/lua/null-ls/handlers.lua index 95f161d97..146332d0a 100644 --- a/lua/null-ls/handlers.lua +++ b/lua/null-ls/handlers.lua @@ -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 diff --git a/lua/null-ls/lspconfig.lua b/lua/null-ls/lspconfig.lua index c534e0f13..6d738aa0d 100644 --- a/lua/null-ls/lspconfig.lua +++ b/lua/null-ls/lspconfig.lua @@ -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 diff --git a/lua/null-ls/methods.lua b/lua/null-ls/methods.lua index 5f37b55f2..421b8bbc8 100644 --- a/lua/null-ls/methods.lua +++ b/lua/null-ls/methods.lua @@ -10,6 +10,7 @@ local lsp_methods = { DID_CHANGE = "textDocument/didChange", DID_OPEN = "textDocument/didOpen", DID_CLOSE = "textDocument/didClose", + DID_SAVE = "textDocument/didSave", HOVER = "textDocument/hover", COMPLETION = "textDocument/completion", } @@ -18,6 +19,8 @@ vim.tbl_add_reverse_lookup(lsp_methods) local internal_methods = { CODE_ACTION = "NULL_LS_CODE_ACTION", DIAGNOSTICS = "NULL_LS_DIAGNOSTICS", + DIAGNOSTICS_ON_OPEN = "NULL_LS_DIAGNOSTICS_ON_OPEN", + DIAGNOSTICS_ON_SAVE = "NULL_LS_DIAGNOSTICS_ON_SAVE", FORMATTING = "NULL_LS_FORMATTING", RANGE_FORMATTING = "NULL_LS_RANGE_FORMATTING", HOVER = "NULL_LS_HOVER", @@ -29,15 +32,24 @@ local lsp_to_internal_map = { [lsp_methods.CODE_ACTION] = internal_methods.CODE_ACTION, [lsp_methods.FORMATTING] = internal_methods.FORMATTING, [lsp_methods.RANGE_FORMATTING] = internal_methods.RANGE_FORMATTING, - [lsp_methods.DID_OPEN] = internal_methods.DIAGNOSTICS, [lsp_methods.DID_CHANGE] = internal_methods.DIAGNOSTICS, + [lsp_methods.DID_SAVE] = internal_methods.DIAGNOSTICS_ON_SAVE, + [lsp_methods.DID_OPEN] = internal_methods.DIAGNOSTICS_ON_OPEN, [lsp_methods.HOVER] = internal_methods.HOVER, [lsp_methods.COMPLETION] = internal_methods.COMPLETION, } +local overrides = { + [internal_methods.DIAGNOSTICS_ON_OPEN] = { + [internal_methods.DIAGNOSTICS] = true, + [internal_methods.DIAGNOSTICS_ON_SAVE] = true, + }, +} + local readable_map = { [internal_methods.CODE_ACTION] = "Code actions", [internal_methods.DIAGNOSTICS] = "Diagnostics", + [internal_methods.DIAGNOSTICS_ON_SAVE] = "Diagnostics on save", [internal_methods.FORMATTING] = "Formatting", [internal_methods.RANGE_FORMATTING] = "Range formatting", [internal_methods.HOVER] = "Hover", @@ -49,5 +61,6 @@ M.lsp = lsp_methods M.internal = internal_methods M.map = lsp_to_internal_map M.readable = readable_map +M.overrides = overrides return M diff --git a/lua/null-ls/rpc.lua b/lua/null-ls/rpc.lua index e67a23dcc..382cd726b 100644 --- a/lua/null-ls/rpc.lua +++ b/lua/null-ls/rpc.lua @@ -20,6 +20,7 @@ local capabilities = { textDocumentSync = { change = 1, -- prompt LSP client to send full document text on didOpen and didChange openClose = true, + save = true, }, } diff --git a/lua/null-ls/sources.lua b/lua/null-ls/sources.lua index 6bd21314b..ff649705a 100644 --- a/lua/null-ls/sources.lua +++ b/lua/null-ls/sources.lua @@ -1,3 +1,6 @@ +local methods = require("null-ls.methods") +local u = require("null-ls.utils") + local validate = vim.validate local registered = { @@ -28,11 +31,21 @@ M.is_available = function(source, filetype, method) return false end - return ( - not filetype - or (source.filetypes[filetype] == nil and source.filetypes["_all"]) - or source.filetypes[filetype] - ) and (not method or source.methods[method]) + local filetype_matches = not filetype + or source.filetypes["_all"] and source.filetypes[filetype] == nil + or source.filetypes[filetype] == true + + local method_matches = not method or source.methods[method] == true + if not method_matches and methods.overrides[method] then + for m in pairs(methods.overrides[method]) do + if source.methods[m] then + method_matches = true + break + end + end + end + + return filetype_matches and method_matches end M.get_available = function(filetype, method) @@ -93,7 +106,7 @@ M.validate_and_transform = function(source) local generator, name = source.generator, source.name or "anonymous source" generator.opts = generator.opts or {} - local methods = type(source.method) == "table" and source.method or { source.method } + local source_methods = type(source.method) == "table" and source.method or { source.method } local filetypes, disabled_filetypes = source.filetypes, source.disabled_filetypes validate({ @@ -101,12 +114,11 @@ M.validate_and_transform = function(source) filetypes = { filetypes, "table" }, disabled_filetypes = { disabled_filetypes, "table", true }, name = { name, "string" }, - methods = { methods, "table" }, fn = { generator.fn, "function" }, opts = { generator.opts, "table" }, async = { generator.async, "boolean", true }, method = { - methods, + source_methods, function(m) return not vim.tbl_isempty(m), "at least one method" end, @@ -129,7 +141,7 @@ M.validate_and_transform = function(source) end end - for _, method in ipairs(methods) do + for _, method in ipairs(source_methods) do validate({ method = { method, @@ -142,6 +154,11 @@ M.validate_and_transform = function(source) method_map[method] = true end + if method_map[methods.internal.DIAGNOSTICS_ON_SAVE] and not u.has_version("0.6.0") then + u.echo("WarningMsg", string.format("source %s is not supported on nvim versions < 0.6.0", name)) + return + end + return { name = name, generator = generator, diff --git a/test/spec/diagnostics_spec.lua b/test/spec/diagnostics_spec.lua index 642d3b067..25d7b816a 100644 --- a/test/spec/diagnostics_spec.lua +++ b/test/spec/diagnostics_spec.lua @@ -8,6 +8,7 @@ local s = require("null-ls.state") local c = require("null-ls.config") local lsp = mock(vim.lsp, "true") +local diagnostic_api = mock(vim.diagnostic, "true") describe("diagnostics", function() local diagnostics = require("null-ls.diagnostics") @@ -19,6 +20,13 @@ describe("diagnostics", function() local mock_uri = "file:///mock-file" local mock_client_id = 999 + local mock_handler = stub.new() + local mock_client = { + name = "null-ls", + id = mock_client_id, + handlers = { [methods.lsp.PUBLISH_DIAGNOSTICS] = mock_handler }, + } + local mock_params before_each(function() mock_params = { @@ -27,13 +35,12 @@ describe("diagnostics", function() method = methods.lsp.DID_OPEN, } u.make_params.returns(mock_params) - lsp.get_active_clients.returns({}) + lsp.get_active_clients.returns({ mock_client }) end) after_each(function() - lsp.handlers[methods.lsp.PUBLISH_DIAGNOSTICS]:clear() + mock_handler:clear() s.clear_cache:clear() - generators.run_registered:clear() u.make_params:clear() @@ -60,42 +67,15 @@ describe("diagnostics", function() it("should call make_params with params and method", function() diagnostics.handler(mock_params) - assert.stub(u.make_params).was_called_with(mock_params, methods.internal.DIAGNOSTICS) - end) - - describe("changedtick tracking", function() - it("should call handler on each callback if buffer did not change", function() - diagnostics.handler(mock_params) - diagnostics.handler(mock_params) - - generators.run_registered.calls[1].refs[1].callback("diagnostics") - generators.run_registered.calls[2].refs[1].callback("diagnostics") - - assert.stub(lsp.handlers[methods.lsp.PUBLISH_DIAGNOSTICS]).was_called(2) - end) - - it("should call handler only once if buffer changed in between callbacks", function() - diagnostics.handler(mock_params) - local new_params = vim.deepcopy(mock_params) - new_params.textDocument.version = 9999 - diagnostics.handler(new_params) - - generators.run_registered.calls[1].refs[1].callback("diagnostics") - generators.run_registered.calls[2].refs[1].callback("diagnostics") - - assert.stub(lsp.handlers[methods.lsp.PUBLISH_DIAGNOSTICS]).was_called(1) - end) + assert.stub(u.make_params).was_called_with(mock_params, methods.internal.DIAGNOSTICS_ON_OPEN) end) describe("handler", function() - local has = stub(vim.fn, "has") - after_each(function() - has:clear() - end) - - describe("0.5", function() + describe("LSP handler", function() before_each(function() - has.returns(0) + c._set({ _use_lsp_handler = true }) + generators.run_registered:clear() + mock_handler:clear() end) it("should send results of diagnostic generators to lsp handler", function() @@ -105,39 +85,62 @@ describe("diagnostics", function() local callback = generators.run_registered.calls[1].refs[1].callback callback("diagnostics") - assert.stub(lsp.handlers[methods.lsp.PUBLISH_DIAGNOSTICS]).was_called_with( - nil, - methods.lsp.PUBLISH_DIAGNOSTICS, - { - diagnostics = "diagnostics", - uri = mock_params.textDocument.uri, - }, - mock_client_id, - vim.uri_to_bufnr(mock_uri) - ) + assert.stub(mock_handler).was_called_with(nil, { + diagnostics = "diagnostics", + uri = mock_params.textDocument.uri, + }, { + method = methods.lsp.PUBLISH_DIAGNOSTICS, + client_id = mock_client_id, + bufnr = vim.uri_to_bufnr(mock_uri), + }) + end) + + describe("changedtick tracking", function() + it("should call handler on each callback if buffer did not change", function() + diagnostics.handler(mock_params) + diagnostics.handler(mock_params) + + generators.run_registered.calls[1].refs[1].callback("diagnostics 1") + generators.run_registered.calls[2].refs[1].callback("diagnostics") + + assert.stub(mock_handler).was_called(2) + end) + + it("should call handler only once if buffer changed in between callbacks", function() + diagnostics.handler(mock_params) + local new_params = vim.deepcopy(mock_params) + new_params.textDocument.version = 9999 + diagnostics.handler(new_params) + + generators.run_registered.calls[1].refs[1].callback("diagnostics") + generators.run_registered.calls[2].refs[1].callback("diagnostics") + + assert.stub(mock_handler).was_called(1) + end) end) end) - describe("0.5.1", function() + describe("API handler", function() + local mock_diagnostics = { [1] = "diagnostics", [2] = "more diagnostics" } + local mock_bufnr = vim.uri_to_bufnr(mock_uri) before_each(function() - has.returns(1) + c.reset() end) - it("should send results of diagnostic generators to lsp handler", function() - u.make_params.returns({ uri = mock_params.textDocument.uri }) - + it("should send results of diagnostic generators to API handler", function() diagnostics.handler(mock_params) - local callback = generators.run_registered.calls[1].refs[1].callback - callback("diagnostics") - assert.stub(lsp.handlers[methods.lsp.PUBLISH_DIAGNOSTICS]).was_called_with(nil, { - diagnostics = "diagnostics", - uri = mock_params.textDocument.uri, - }, { - method = methods.lsp.PUBLISH_DIAGNOSTICS, - client_id = mock_client_id, - bufnr = vim.uri_to_bufnr(mock_uri), - }) + local callback = generators.run_registered.calls[1].refs[1].callback + callback(mock_diagnostics) + + assert.stub(diagnostic_api.set).was_called(#mock_diagnostics) + for id in pairs(mock_diagnostics) do + assert.stub(vim.diagnostic.set).was_called_with( + diagnostics.namespaces[id], + mock_bufnr, + mock_diagnostics[id] + ) + end end) end) end) @@ -161,95 +164,198 @@ describe("diagnostics", function() postprocess = generators.run_registered.calls[1].refs[1].postprocess end) - it("should convert range when all positions are defined", function() - local diagnostic = { row = 1, col = 5, end_row = 2, end_col = 6 } + describe("LSP range", function() + before_each(function() + c._set({ _use_lsp_handler = true }) + end) - postprocess(diagnostic, mock_params, mock_generator) + it("should convert range when all positions are defined", function() + local diagnostic = { row = 1, col = 5, end_row = 2, end_col = 6 } - assert.same(diagnostic.range, { - ["end"] = { character = 5, line = 1 }, - start = { character = 4, line = 0 }, - }) - end) + postprocess(diagnostic, mock_params, mock_generator) - it("should convert range when row is missing", function() - local diagnostic = { - row = nil, - col = 5, - end_row = 2, - end_col = 6, - } + assert.same(diagnostic.range, { + ["end"] = { character = 5, line = 1 }, + start = { character = 4, line = 0 }, + }) + end) - postprocess(diagnostic, mock_params, mock_generator) + it("should convert range when row is missing", function() + local diagnostic = { + row = nil, + col = 5, + end_row = 2, + end_col = 6, + } - assert.same(diagnostic.range, { - ["end"] = { character = 5, line = 1 }, - start = { character = 4, line = 0 }, - }) - end) + postprocess(diagnostic, mock_params, mock_generator) - it("should convert range when col is missing", function() - local diagnostic = { - row = 1, - col = nil, - end_row = 2, - end_col = 6, - } + assert.same(diagnostic.range, { + ["end"] = { character = 5, line = 1 }, + start = { character = 4, line = 0 }, + }) + end) - postprocess(diagnostic, mock_params, mock_generator) + it("should convert range when col is missing", function() + local diagnostic = { + row = 1, + col = nil, + end_row = 2, + end_col = 6, + } - assert.same(diagnostic.range, { - ["end"] = { character = 5, line = 1 }, - start = { character = 0, line = 0 }, - }) - end) + postprocess(diagnostic, mock_params, mock_generator) - it("should convert range when end_row is missing", function() - local diagnostic = { - row = 1, - col = 5, - end_row = nil, - end_col = 6, - } + assert.same(diagnostic.range, { + ["end"] = { character = 5, line = 1 }, + start = { character = 0, line = 0 }, + }) + end) - postprocess(diagnostic, mock_params, mock_generator) + it("should convert range when end_row is missing", function() + local diagnostic = { + row = 1, + col = 5, + end_row = nil, + end_col = 6, + } - assert.same(diagnostic.range, { - ["end"] = { character = 5, line = 0 }, - start = { character = 4, line = 0 }, - }) - end) + postprocess(diagnostic, mock_params, mock_generator) - it("should convert range when end_col is missing", function() - local diagnostic = { - row = 1, - col = 5, - end_row = 2, - end_col = nil, - } + assert.same(diagnostic.range, { + ["end"] = { character = 5, line = 0 }, + start = { character = 4, line = 0 }, + }) + end) - postprocess(diagnostic, mock_params, mock_generator) + it("should convert range when end_col is missing", function() + local diagnostic = { + row = 1, + col = 5, + end_row = 2, + end_col = nil, + } - assert.same(diagnostic.range, { - ["end"] = { character = 0, line = 1 }, - start = { character = 4, line = 0 }, - }) + postprocess(diagnostic, mock_params, mock_generator) + + assert.same(diagnostic.range, { + ["end"] = { character = 0, line = 1 }, + start = { character = 4, line = 0 }, + }) + end) + + it("should convert range when all positions are missing", function() + local diagnostic = { + row = nil, + col = nil, + end_row = nil, + end_col = nil, + } + + postprocess(diagnostic, mock_params, mock_generator) + + assert.same(diagnostic.range, { + ["end"] = { character = 0, line = 1 }, + start = { character = 0, line = 0 }, + }) + end) end) - it("should convert range when all positions are missing", function() - local diagnostic = { - row = nil, - col = nil, - end_row = nil, - end_col = nil, - } + describe("API range", function() + before_each(function() + c.reset() + end) - postprocess(diagnostic, mock_params, mock_generator) + it("should convert range when all positions are defined", function() + local diagnostic = { row = 1, col = 5, end_row = 2, end_col = 6 } - assert.same(diagnostic.range, { - ["end"] = { character = 0, line = 1 }, - start = { character = 0, line = 0 }, - }) + postprocess(diagnostic, mock_params, mock_generator) + + assert.equals(diagnostic.lnum, 0) + assert.equals(diagnostic.end_lnum, 1) + assert.equals(diagnostic.col, 4) + assert.equals(diagnostic.end_col, 5) + end) + + it("should convert range when row is missing", function() + local diagnostic = { + row = nil, + col = 5, + end_row = 2, + end_col = 6, + } + + postprocess(diagnostic, mock_params, mock_generator) + + assert.equals(diagnostic.lnum, 0) + assert.equals(diagnostic.end_lnum, 1) + assert.equals(diagnostic.col, 4) + assert.equals(diagnostic.end_col, 5) + end) + + it("should convert range when col is missing", function() + local diagnostic = { + row = 1, + col = nil, + end_row = 2, + end_col = 6, + } + + postprocess(diagnostic, mock_params, mock_generator) + + assert.equals(diagnostic.lnum, 0) + assert.equals(diagnostic.end_lnum, 1) + assert.equals(diagnostic.col, 0) + assert.equals(diagnostic.end_col, 5) + end) + + it("should convert range when end_row is missing", function() + local diagnostic = { + row = 1, + col = 5, + end_row = nil, + end_col = 6, + } + + postprocess(diagnostic, mock_params, mock_generator) + + assert.equals(diagnostic.lnum, 0) + assert.equals(diagnostic.end_lnum, 0) + assert.equals(diagnostic.col, 4) + assert.equals(diagnostic.end_col, 5) + end) + + it("should convert range when end_col is missing", function() + local diagnostic = { + row = 1, + col = 5, + end_row = 2, + end_col = nil, + } + + postprocess(diagnostic, mock_params, mock_generator) + + assert.equals(diagnostic.lnum, 0) + assert.equals(diagnostic.end_lnum, 1) + assert.equals(diagnostic.col, 4) + assert.equals(diagnostic.end_col, 0) + end) + + it("should convert range when all positions are missing", function() + local diagnostic = { + row = nil, + col = nil, + end_row = nil, + end_col = nil, + } + + postprocess(diagnostic, mock_params, mock_generator) + + assert.equals(diagnostic.lnum, 0) + assert.equals(diagnostic.end_lnum, 1) + assert.equals(diagnostic.col, 0) + assert.equals(diagnostic.end_col, 0) + end) end) it("should keep diagnostic source when defined", function() diff --git a/test/spec/e2e_spec.lua b/test/spec/e2e_spec.lua index 2f7e2e3ca..f2327e3bc 100644 --- a/test/spec/e2e_spec.lua +++ b/test/spec/e2e_spec.lua @@ -115,16 +115,16 @@ describe("e2e", function() end) it("should get buffer diagnostics on attach", function() - local buf_diagnostics = lsp.diagnostic.get(0) + local buf_diagnostics = vim.diagnostic.get(0) assert.equals(vim.tbl_count(buf_diagnostics), 1) local write_good_diagnostic = buf_diagnostics[1] assert.equals(write_good_diagnostic.message, '"really" can weaken meaning') assert.equals(write_good_diagnostic.source, "write-good") - assert.same(write_good_diagnostic.range, { - start = { character = 7, line = 0 }, - ["end"] = { character = 13, line = 0 }, - }) + assert.equals(write_good_diagnostic.lnum, 0) + assert.equals(write_good_diagnostic.end_lnum, 0) + assert.equals(write_good_diagnostic.col, 7) + assert.equals(write_good_diagnostic.end_col, 13) end) it("should update buffer diagnostics on text change", function() @@ -132,7 +132,7 @@ describe("e2e", function() api.nvim_buf_set_text(api.nvim_get_current_buf(), 0, 6, 0, 13, {}) lsp_wait() - assert.equals(vim.tbl_count(lsp.diagnostic.get(0)), 0) + assert.equals(vim.tbl_count(vim.diagnostic.get(0)), 0) end) describe("multiple diagnostics", function() @@ -146,7 +146,7 @@ describe("e2e", function() vim.cmd("e") lsp_wait() - local diagnostics = lsp.diagnostic.get(0) + local diagnostics = vim.diagnostic.get(0) assert.equals(vim.tbl_count(diagnostics), 2) local markdownlint_diagnostic, write_good_diagnostic @@ -169,7 +169,7 @@ describe("e2e", function() vim.cmd("e") lsp_wait() - local write_good_diagnostic = lsp.diagnostic.get(0)[1] + local write_good_diagnostic = vim.diagnostic.get(0)[1] assert.equals(write_good_diagnostic.message, '"really" can weaken meaning (write-good)') end) @@ -330,16 +330,16 @@ describe("e2e", function() api.nvim_buf_set_text(api.nvim_get_current_buf(), 0, 52, 0, 53, { ".." }) lsp_wait() - local buf_diagnostics = lsp.diagnostic.get(0) + local buf_diagnostics = vim.diagnostic.get(0) assert.equals(vim.tbl_count(buf_diagnostics), 1) local tl_check_diagnostic = buf_diagnostics[1] assert.equals(tl_check_diagnostic.message, "in return value: got string, expected number") assert.equals(tl_check_diagnostic.source, "tl check") - assert.same(tl_check_diagnostic.range, { - start = { character = 52, line = 0 }, - ["end"] = { character = 0, line = 1 }, - }) + assert.equals(tl_check_diagnostic.lnum, 0) + assert.equals(tl_check_diagnostic.end_lnum, 1) + assert.equals(tl_check_diagnostic.col, 52) + assert.equals(tl_check_diagnostic.end_col, 0) end) end) diff --git a/test/spec/formatting_spec.lua b/test/spec/formatting_spec.lua index 9ed9f5881..126c700b8 100644 --- a/test/spec/formatting_spec.lua +++ b/test/spec/formatting_spec.lua @@ -113,49 +113,17 @@ describe("formatting", function() end) describe("handler", function() - local has = stub(vim.fn, "has") - after_each(function() - has:clear() - end) - - describe("0.5", function() - before_each(function() - has.returns(0) - end) - - it("should call lsp_handler with text edit response", function() - formatting.handler(methods.lsp.FORMATTING, mock_params, handler) - - local callback = generators.run_registered_sequentially.calls[1].refs[1].callback - callback(mock_edits, mock_params) - - assert.stub(lsp_handler).was_called_with( - nil, - mock_params.lsp_method, - { mock_diffed }, - mock_params.client_id, - mock_params.bufnr - ) - end) - end) - - describe("0.5.1", function() - before_each(function() - has.returns(1) - end) - - it("should call lsp_handler with text edit response", function() - formatting.handler(methods.lsp.FORMATTING, mock_params, handler) + it("should call lsp_handler with text edit response", function() + formatting.handler(methods.lsp.FORMATTING, mock_params, handler) - local callback = generators.run_registered_sequentially.calls[1].refs[1].callback - callback(mock_edits, mock_params) + local callback = generators.run_registered_sequentially.calls[1].refs[1].callback + callback(mock_edits, mock_params) - assert.stub(lsp_handler).was_called_with(nil, { mock_diffed }, { - method = mock_params.lsp_method, - client_id = mock_params.client_id, - bufnr = mock_params.bufnr, - }) - end) + assert.stub(lsp_handler).was_called_with(nil, { mock_diffed }, { + method = mock_params.lsp_method, + client_id = mock_params.client_id, + bufnr = mock_params.bufnr, + }) end) end) end) diff --git a/test/spec/generators_spec.lua b/test/spec/generators_spec.lua index e1d531c51..20909a683 100644 --- a/test/spec/generators_spec.lua +++ b/test/spec/generators_spec.lua @@ -283,10 +283,33 @@ describe("generators", function() generators.run_registered(mock_opts) assert.stub(run).was_called_with( - { sync_generator }, + generators.get_available(mock_opts.filetype, mock_opts.method), mock_opts.params, mock_opts.postprocess, - mock_opts.callback + mock_opts.callback, + nil + ) + end) + + it("should call run with available generators indexed by id", function() + register(method, sync_generator, { "lua" }) + local mock_opts = { + filetype = mock_params.ft, + method = mock_params.method, + params = mock_params, + postprocess = postprocess, + callback = callback, + index_by_id = true, + } + + generators.run_registered(mock_opts) + + assert.stub(run).was_called_with( + generators.get_available(mock_opts.filetype, mock_opts.method, mock_opts.index_by_id), + mock_opts.params, + mock_opts.postprocess, + mock_opts.callback, + true ) end) end) @@ -361,6 +384,20 @@ describe("generators", function() assert.same(available, {}) end) + + it("should index generators by id if index_by_id is true", function() + register(method, sync_generator, { "lua" }) + + local available = generators.get_available("lua", method, true) + + local generator_id + for id in pairs(available) do + generator_id = id + break + end + + assert.same(available, { [generator_id] = sync_generator }) + end) end) describe("can_run", function() diff --git a/test/spec/sources_spec.lua b/test/spec/sources_spec.lua index ed25a802b..d23eb5a0d 100644 --- a/test/spec/sources_spec.lua +++ b/test/spec/sources_spec.lua @@ -1,6 +1,8 @@ local stub = require("luassert.stub") +local mock = require("luassert.mock") local methods = require("null-ls.methods") +local u = mock(require("null-ls.utils"), true) describe("sources", function() local sources = require("null-ls.sources") @@ -74,6 +76,14 @@ describe("sources", function() assert.truthy(is_available) end) + it("should return true if method has override", function() + mock_source.methods = { [methods.internal.DIAGNOSTICS_ON_SAVE] = true } + + local is_available = sources.is_available(mock_source, nil, methods.internal.DIAGNOSTICS_ON_OPEN) + + assert.truthy(is_available) + end) + it("should return true if filetype and method match", function() local is_available = sources.is_available(mock_source, "lua", methods.internal.FORMATTING) @@ -262,6 +272,7 @@ describe("sources", function() describe("validate_and_transform", function() local mock_source before_each(function() + u.has_version.returns(true) mock_source = { generator = { fn = function() end, opts = {}, async = false }, name = "mock generator", @@ -270,6 +281,10 @@ describe("sources", function() } end) + after_each(function() + u.has_version:clear() + end) + it("should validate and return transformed source", function() local validated = sources.validate_and_transform(mock_source) @@ -336,6 +351,15 @@ describe("sources", function() assert.falsy(validated) end) + it("should return nil if nvim version does not support method", function() + mock_source.method = methods.internal.DIAGNOSTICS_ON_SAVE + u.has_version.returns(false) + + local validated = sources.validate_and_transform(mock_source) + + assert.falsy(validated) + end) + it("should throw if generator is invalid", function() mock_source.generator = nil