diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index 8048e58..7687edc 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -39,5 +39,23 @@ jobs: - name: test run: | - luacheck lua - + luacheck lua specs + + tests: + strategy: + fail-fast: false + matrix: + version: + - stable + - nightly + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Neovim + uses: rhysd/action-setup-vim@v1 + id: neovim + with: + neovim: true + version: ${{ matrix.version }} + - name: Run tests + run: make specs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/.luacheckrc b/.luacheckrc index 35b522b..7d82b11 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,2 +1,2 @@ -globals = { "vim", "_" } +globals = { "vim", "_", "assert", "describe", "before_each", "after_each", "it" } max_line_length = false diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f34e254 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +ifndef VERBOSE +.SILENT: +endif + +specs: dependencies + @echo "Running lsp-format specs..." + timeout 300 nvim -e \ + --headless \ + -u specs/minimal_init.vim \ + -c "PlenaryBustedDirectory specs/features {minimal_init = 'specs/minimal_init.vim'}" + +dependencies: + if [ ! -d vendor ]; then \ + git clone --depth 1 \ + https://github.com/nvim-lua/plenary.nvim \ + vendor/pack/vendor/start/plenary.nvim; \ + git clone --depth 1 \ + https://github.com/neovim/nvim-lspconfig \ + vendor/pack/vendor/start/nvim-lspconfig; \ + fi diff --git a/README.md b/README.md index 0ea58d7..5b4cea7 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ formatting. `sync` turns on synchronous formatting. The editor will block until the formatting is done. +#### `force` format option + +`force` will write the format result to the buffer, even if the buffer changed after the format request started. + ## Notes #### Make sure you remove any old format on save code diff --git a/doc/format.txt b/doc/format.txt index 4e72aae..e83f9a7 100644 --- a/doc/format.txt +++ b/doc/format.txt @@ -2,7 +2,7 @@ Author: Lukas Reineke -Version: 2.3.3 +Version: 2.4.0 ============================================================================== CONTENTS *lsp-format* @@ -87,6 +87,20 @@ sync *lsp-format-sync* } } +------------------------------------------------------------------------------ +force *lsp-format-force* + + `force` is a boolean flag. When on the format result will always be written + to the buffer, even if the buffer changed. + + Example: > + + require "lsp-format".setup { + go = { + force = true + } + } + ============================================================================== 4. COMMANDS *lsp-format-commands* @@ -121,6 +135,9 @@ sync *lsp-format-sync* ============================================================================== 5. CHANGELOG *lsp-format-changelog* +2.4.0 + Add `force` option + 2.3.3 Use "vim.lsp.log" for logging Don't attach servers that don't support formatting diff --git a/lua/lsp-format/init.lua b/lua/lsp-format/init.lua index 8bcddf2..6449b83 100644 --- a/lua/lsp-format/init.lua +++ b/lua/lsp-format/init.lua @@ -29,6 +29,27 @@ M.setup = function(format_options) ) end +M._parse_value = function(key, value) + if not value then + return true + end + if key == "order" or key == "exclude" then + return vim.split(value, ",") + end + local int_value = tonumber(value) + if int_value then + return int_value + end + if value == "false" then + return false + end + if value == "true" then + return true + end + + return value +end + M.format = function(options) if vim.b.format_saving or M.disabled or M.disabled_filetypes[vim.bo.filetype] then return @@ -38,10 +59,7 @@ M.format = function(options) local format_options = vim.deepcopy(M.format_options[vim.bo.filetype] or {}) for _, option in ipairs(options.fargs or {}) do local key, value = unpack(vim.split(option, "=")) - if key == "order" or key == "exclude" then - value = vim.split(value, ",") - end - format_options[key] = value or true + format_options[key] = M._parse_value(key, value) end local clients = vim.tbl_values(vim.lsp.buf_get_clients()) @@ -144,10 +162,14 @@ M._handler = function(err, result, ctx) if not vim.api.nvim_buf_is_loaded(ctx.bufnr) then vim.fn.bufload(ctx.bufnr) vim.api.nvim_buf_set_var(ctx.bufnr, "format_changedtick", vim.api.nvim_buf_get_var(ctx.bufnr, "changedtick")) - elseif - vim.api.nvim_buf_get_var(ctx.bufnr, "format_changedtick") - ~= vim.api.nvim_buf_get_var(ctx.bufnr, "changedtick") - or vim.startswith(vim.api.nvim_get_mode().mode, "i") + end + if + not ctx.params.options.force + and ( + vim.api.nvim_buf_get_var(ctx.bufnr, "format_changedtick") + ~= vim.api.nvim_buf_get_var(ctx.bufnr, "changedtick") + or vim.startswith(vim.api.nvim_get_mode().mode, "i") + ) then M._next() return diff --git a/specs/features/main_spec.lua b/specs/features/main_spec.lua new file mode 100644 index 0000000..57494ce --- /dev/null +++ b/specs/features/main_spec.lua @@ -0,0 +1,198 @@ +local mock = require "luassert.mock" +local match = require "luassert.match" +local spy = require "luassert.spy" +local f = require "lsp-format" + +local mock_client = { + id = 1, + name = "lsp-client-test", + request = function(_, _, _, _) end, + request_sync = function(_, _, _, _) end, + supports_method = function(_) end, + setup = function() end, +} + +vim.lsp.buf_get_clients = function() + local clients = {} + clients[mock_client.name] = mock_client + return clients +end + +describe("lsp-format", function() + local c + local api + + before_each(function() + c = mock(mock_client, true) + api = mock(vim.api) + c.supports_method = function(_) + return true + end + f.setup {} + f.on_attach(c) + end) + + after_each(function() + mock.revert(c) + mock.revert(api) + end) + + it("sends a valid format request", function() + f.format {} + assert.stub(c.request).was_called(1) + assert.stub(c.request).was_called_with("textDocument/formatting", { + options = { + insertSpaces = false, + tabSize = 8, + }, + textDocument = { + uri = "file://", + }, + }, match.is_ref(f._handler), 1) + end) + + it("FormatToggle prevent/allow formatting", function() + f.toggle { args = "" } + f.format {} + assert.stub(c.request).was_called(0) + + f.toggle { args = "" } + f.format {} + assert.stub(c.request).was_called(1) + end) + + it("FormatDisable/Enable prevent/allow formatting", function() + f.disable { args = "" } + f.format {} + assert.stub(c.request).was_called(0) + + f.enable { args = "" } + f.format {} + assert.stub(c.request).was_called(1) + end) + + it("sends default format options", function() + f.setup { + lua = { + bool_test = true, + int_test = 1, + string_test = "string", + }, + } + vim.bo.filetype = "lua" + f.format {} + assert.stub(c.request).was_called(1) + assert.stub(c.request).was_called_with("textDocument/formatting", { + options = { + insertSpaces = false, + tabSize = 8, + bool_test = true, + int_test = 1, + string_test = "string", + }, + textDocument = { + uri = "file://", + }, + }, match.is_ref(f._handler), 1) + end) + + it("sends format options", function() + f.format { + fargs = { "bool_test", "int_test=1", "string_test=string" }, + } + assert.stub(c.request).was_called(1) + assert.stub(c.request).was_called_with("textDocument/formatting", { + options = { + insertSpaces = false, + tabSize = 8, + bool_test = true, + int_test = 1, + string_test = "string", + }, + textDocument = { + uri = "file://", + }, + }, match.is_ref(f._handler), 1) + end) + + it("overwrites default format options", function() + f.setup { + lua = { + bool_test = true, + int_test = 1, + string_test = "string", + }, + } + vim.bo.filetype = "lua" + f.format { + fargs = { "bool_test=false", "int_test=2", "string_test=another_string" }, + } + assert.stub(c.request).was_called(1) + assert.stub(c.request).was_called_with("textDocument/formatting", { + options = { + insertSpaces = false, + tabSize = 8, + bool_test = false, + int_test = 2, + string_test = "another_string", + }, + textDocument = { + uri = "file://", + }, + }, match.is_ref(f._handler), 1) + end) + + it("does not overwrite changes", function() + local apply_text_edits = spy.on(vim.lsp.util, "apply_text_edits") + c.request = function(_, params, handler, bufnr) + api.nvim_buf_get_var = function(_, var) + if var == "format_changedtick" then + return 9999 + end + return 1 + end + handler(nil, {}, { bufnr = bufnr, params = params }) + end + f.format {} + assert.spy(apply_text_edits).was.called(0) + end) + + it("does overwrite changes with force", function() + local apply_text_edits = spy.on(vim.lsp.util, "apply_text_edits") + c.request = function(_, params, handler, bufnr) + api.nvim_buf_get_var = function(_, var) + if var == "format_changedtick" then + return 9999 + end + return 1 + end + handler(nil, {}, { bufnr = bufnr, params = params }) + end + f.format { fargs = { "force=true" } } + assert.spy(apply_text_edits).was.called(1) + end) + + it("does not overwrite when in insert mode", function() + local apply_text_edits = spy.on(vim.lsp.util, "apply_text_edits") + c.request = function(_, params, handler, bufnr) + api.nvim_get_mode = function() + return "insert" + end + handler(nil, {}, { bufnr = bufnr, params = params }) + end + f.format {} + assert.spy(apply_text_edits).was.called(0) + end) + + it("does overwrite when in insert mode with force", function() + local apply_text_edits = spy.on(vim.lsp.util, "apply_text_edits") + c.request = function(_, params, handler, bufnr) + api.nvim_get_mode = function() + return "insert" + end + handler(nil, {}, { bufnr = bufnr, params = params }) + end + f.format { fargs = { "force=true" } } + assert.spy(apply_text_edits).was.called(1) + end) +end) diff --git a/specs/minimal_init.vim b/specs/minimal_init.vim new file mode 100644 index 0000000..3507561 --- /dev/null +++ b/specs/minimal_init.vim @@ -0,0 +1,13 @@ +set rtp-=~/.config/nvim +set rtp-=~/.local/share/nvim/site +set rtp+=. +set noswapfile + +let $lsp_format = getcwd() +let $specs = getcwd() .. "/specs" +let $vendor = getcwd() .. "/vendor" + +set rtp+=$lsp_format,$specs +set packpath=$vendor + +packloadall