diff options
author | Steven Arcangeli <506791+stevearc@users.noreply.github.com> | 2023-08-30 18:34:13 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-30 18:34:13 -0700 |
commit | 92393f02efadfb1d9f97c74c8feb853c1caea9de (patch) | |
tree | 980a43e5f6b70c419e089f82074d177aca9da7aa /tests | |
parent | c100b8548fd7262a1275bdb867186d0cd94e8b45 (diff) |
feat: apply changes as text edits using LSP utils (#18)
* feat: apply changes as text edits using LSP utils
This means we can leverage all of the work that was done in the LSP
client to preserve marks, cursor position, etc
* log: add trace logging to debug performance
* feat: use the same diff -> TextEdit technique for bad LSP servers
Some LSP servers simply return a single TextEdit that replaces the whole
buffer. This is bad for extmarks, cursor, and if the buffer is open in
multiple windows the non-active window will jump to the top. We can
detect that situation and apply the same vim.diff logic to convert it
into more granular TextEdits.
Diffstat (limited to 'tests')
-rw-r--r-- | tests/fuzzer_spec.lua | 122 | ||||
-rw-r--r-- | tests/runner_spec.lua | 70 |
2 files changed, 180 insertions, 12 deletions
diff --git a/tests/fuzzer_spec.lua b/tests/fuzzer_spec.lua new file mode 100644 index 0000000..81c032f --- /dev/null +++ b/tests/fuzzer_spec.lua @@ -0,0 +1,122 @@ +require("plenary.async").tests.add_to_env() +local test_util = require("tests.test_util") +local conform = require("conform") +local runner = require("conform.runner") + +describe("fuzzer", function() + before_each(function() + conform.formatters.test = { + meta = { url = "", description = "" }, + command = "tests/fake_formatter.sh", + } + end) + + after_each(function() + test_util.reset_editor() + end) + + ---@param buf_content string[] + ---@param expected string[] + ---@param opts? table + local function run_formatter(buf_content, expected, opts) + local bufnr = vim.fn.bufadd("testfile") + vim.fn.bufload(bufnr) + vim.api.nvim_set_current_buf(bufnr) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, buf_content) + vim.bo[bufnr].modified = false + runner.apply_format(0, buf_content, expected, nil, false) + -- We expect the last newline to be effectively "swallowed" by the formatter + -- because vim will use that as the EOL at the end of the file. The exception is that we always + -- expect at least one line in the output + if #expected > 1 and expected[#expected] == "" then + table.remove(expected) + end + assert.are.same(expected, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end + + local function make_word() + local chars = {} + for _ = 1, math.random(1, 10) do + table.insert(chars, string.char(math.random(97, 122))) + end + return table.concat(chars, "") + end + + local function make_line() + local words = {} + for _ = 1, math.random(0, 6) do + table.insert(words, make_word()) + end + return table.concat(words, " ") + end + + local function make_file(num_lines) + local lines = {} + for _ = 1, math.random(1, num_lines) do + table.insert(lines, make_line()) + end + return lines + end + + local function do_insert(lines) + local idx = math.random(1, #lines + 1) + for _ = 1, math.random(1, 3) do + table.insert(lines, idx, make_line()) + end + end + + local function do_replace(lines) + local num_lines = math.random(1, math.min(3, #lines)) + local idx = math.random(1, #lines - num_lines + 1) + local replacement = {} + local num_replace = math.random(1, 5) + for _ = 1, num_replace do + table.insert(replacement, make_line()) + end + local col = math.random(1, lines[idx]:len()) + replacement[1] = lines[idx]:sub(1, col) .. replacement[1] + col = math.random(1, lines[idx + num_lines - 1]:len()) + replacement[#replacement] = replacement[#replacement] .. lines[idx + num_lines - 1]:sub(col) + + for _ = 1, num_lines - num_replace do + table.remove(lines, idx) + end + for _ = 1, num_replace - num_lines do + table.insert(lines, idx, "") + end + for i = 1, num_replace do + lines[idx + i - 1] = replacement[i] + end + end + + local function do_delete(lines) + local num_lines = math.random(1, 3) + local idx = math.random(1, #lines - num_lines) + for _ = 1, num_lines do + table.remove(lines, idx) + end + end + + local function make_edits(lines) + lines = vim.deepcopy(lines) + for _ = 1, math.random(0, 3) do + do_insert(lines) + end + for _ = 1, math.random(0, 3) do + do_replace(lines) + end + for _ = 1, math.random(0, 3) do + do_delete(lines) + end + return lines + end + + it("formats correctly", function() + for i = 1, 50000 do + math.randomseed(i) + local content = make_file(20) + local formatted = make_edits(content) + run_formatter(content, formatted) + end + end) +end) diff --git a/tests/runner_spec.lua b/tests/runner_spec.lua index f054d9e..8807d2d 100644 --- a/tests/runner_spec.lua +++ b/tests/runner_spec.lua @@ -124,16 +124,21 @@ describe("runner", function() vim.bo[bufnr].modified = false local expected_lines = vim.split(expected, "\n", { plain = true }) test_util.set_formatter_output(expected_lines) - conform.format(vim.tbl_extend("force", opts or {}, { formatters = { "test" } })) + conform.format(vim.tbl_extend("force", opts or {}, { formatters = { "test" }, quiet = true })) + -- We expect the last newline to be effectively "swallowed" by the formatter + -- because vim will use that as the EOL at the end of the file. The exception is that we always + -- expect at least one line in the output + if #expected_lines > 1 and expected_lines[#expected_lines] == "" then + table.remove(expected_lines) + end return expected_lines end ---@param buf_content string ---@param new_content string - ---@param expected? string[] - local function run_formatter_test(buf_content, new_content, expected) + local function run_formatter_test(buf_content, new_content) local lines = run_formatter(buf_content, new_content) - assert.are.same(expected or lines, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + assert.are.same(lines, vim.api.nvim_buf_get_lines(0, 0, -1, false)) end it("sets the correct output", function() @@ -181,15 +186,18 @@ print("b") print("a") ]] ) - run_formatter_test("hello\ngoodbye", "hello\n\n\ngoodbye", { "hello", "", "", "goodbye" }) - run_formatter_test("hello", "hello\ngoodbye", { "hello", "goodbye" }) - run_formatter_test("", "hello", { "hello" }) - run_formatter_test("\nfoo", "\nhello\nfoo", { "", "hello", "foo" }) - run_formatter_test("hello", "hello\n\n", { "hello", "" }) - run_formatter_test("hello", "hello\n", { "hello" }) + run_formatter_test("hello\ngoodbye", "hello\n\n\ngoodbye") + run_formatter_test("hello", "hello\ngoodbye") + run_formatter_test("hello\ngoodbye", "hello") + run_formatter_test("", "hello") + run_formatter_test("\nfoo", "\nhello\nfoo") + run_formatter_test("hello", "hello\n") + run_formatter_test("hello", "hello\n\n") + run_formatter_test("hello", "hello\n") + -- This should generate no changes to the buffer assert.falsy(vim.bo.modified) - run_formatter_test("hello\n", "hello", { "hello" }) - run_formatter_test("hello\n ", "hello", { "hello" }) + run_formatter_test("hello\n", "hello") + run_formatter_test("hello\n ", "hello") end) it("does not change output if formatter fails", function() @@ -238,5 +246,43 @@ print("a") vim.fn.delete("tests/testfile.txt") assert.are.same({ "goodbye" }, lines) end) + + describe("range formatting", function() + it("applies edits that overlap the range start", function() + run_formatter( + "a\nb\nc", + "d\nb\nd", + { range = { + start = { 1, 0 }, + ["end"] = { 2, 0 }, + } } + ) + assert.are.same({ "d", "b", "c" }, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end) + + it("applies edits that overlap the range end", function() + run_formatter( + "a\nb\nc", + "d\nb\nd", + { range = { + start = { 3, 0 }, + ["end"] = { 3, 1 }, + } } + ) + assert.are.same({ "a", "b", "d" }, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end) + + it("applies edits that are completely contained by the range", function() + run_formatter( + "a\nb\nc", + "a\nd\nc", + { range = { + start = { 1, 0 }, + ["end"] = { 3, 0 }, + } } + ) + assert.are.same({ "a", "d", "c" }, vim.api.nvim_buf_get_lines(0, 0, -1, false)) + end) + end) end) end) |