diff --git a/.luacheckrc b/.luacheckrc index 337ac8e..2d46e75 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,18 +1,21 @@ -- Rerun tests only if their modification time changed. cache = true std = luajit codes = true self = false -- Global objects defined by the C code read_globals = { "vim", - "it", "after", "after_each", + "assert", "before", "before_each", + "describe", + "it", + "spy", } diff --git a/lua/ivy/controller_spec.lua b/lua/ivy/controller_spec.lua new file mode 100644 index 0000000..6556cee --- /dev/null +++ b/lua/ivy/controller_spec.lua @@ -0,0 +1,57 @@ +local window = require "ivy.window" +local controller = require "ivy.controller" + +describe("controller", function() + before_each(function() + vim.cmd "highlight IvyMatch cterm=bold gui=bold" + window.initialize() + end) + + after_each(function() + controller.destroy() + end) + + it("will run the completion", function() + controller.run("Testing", function() + return { { content = "Some content" } } + end, function() + return {} + end) + + -- Run all the scheduled tasks + vim.wait(0) + + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true) + assert.is_equal(#lines, 1) + assert.is_equal(lines[1], "Some content") + end) + + it("will not try and highlight the buffer if there is nothing to highlight", function() + spy.on(vim, "cmd") + + controller.items = function() + return { { content = "Hello" } } + end + + controller.update "" + + vim.wait(0) + + assert.spy(vim.cmd).was_called_with "syntax clear IvyMatch" + assert.spy(vim.cmd).was_not_called_with "syntax match IvyMatch '[H]'" + end) + + it("will escape a - when passing it to be highlighted", function() + spy.on(vim, "cmd") + + controller.items = function() + return { { content = "Hello" } } + end + + controller.update "some-file" + + vim.wait(0) + + assert.spy(vim.cmd).was_called_with "syntax match IvyMatch '[some\\-file]'" + end) +end) diff --git a/lua/ivy/libivy_spec.lua b/lua/ivy/libivy_spec.lua new file mode 100644 index 0000000..8faf503 --- /dev/null +++ b/lua/ivy/libivy_spec.lua @@ -0,0 +1,36 @@ +require "busted.runner"() + +local libivy = require "ivy.libivy" + +describe("libivy", function() + it("should run a simple match", function() + local score = libivy.ivy_match("term", "I am a serch term") + + assert.is_true(score > 0) + end) + + it("should find a dot file", function() + local current_dir = libivy.ivy_cwd() + local results = libivy.ivy_files(".github/workflows/ci.yml", current_dir) + + assert.is_equal(2, results.length, "Incorrect number of results found") + assert.is_equal(".github/workflows/ci.yml", results[2].content, "Invalid matches") + end) + + it("will allow you to access the length via the metatable", function() + local current_dir = libivy.ivy_cwd() + local results = libivy.ivy_files(".github/workflows/ci.yml", current_dir) + + local mt = getmetatable(results) + + assert.is_equal(results.length, mt.__len(results), "The `length` property does not match the __len metamethod") + end) + + it("will create an iterator", function() + local iter = libivy.ivy_files(".github/workflows/ci.yml", libivy.ivy_cwd()) + local mt = getmetatable(iter) + + assert.is_equal(type(mt["__index"]), "function") + assert.is_equal(type(mt["__len"]), "function") + end) +end) diff --git a/lua/ivy/matcher_spec.lua b/lua/ivy/matcher_spec.lua new file mode 100644 index 0000000..7128a2c --- /dev/null +++ b/lua/ivy/matcher_spec.lua @@ -0,0 +1,29 @@ +local libivy = require "ivy.libivy" + +-- Helper function to test a that string `one` has a higher match score than +-- string `two`. If string `one` has a lower score than string `two` a string +-- will be returned that can be used in body of an error. If not then `nil` is +-- returned and all is good. +local match_test = function(term, one, two) + local score_one = libivy.ivy_match(term, one) + local score_two = libivy.ivy_match(term, two) + + assert.is_true( + score_one > score_two, + ("The score of %s (%d) ranked higher than %s (%d)"):format(one, score_one, two, score_two) + ) +end + +describe("ivy matcher", function() + it("should match path separator", function() + match_test("file", "some/file.lua", "somefile.lua") + end) + + -- it("should match pattern with spaces", function() + -- match_test("so fi", "some/file.lua", "somefile.lua") + -- end) + + it("should match the start of a string", function() + match_test("file", "file.lua", "somefile.lua") + end) +end) diff --git a/lua/ivy/prompt_spec.lua b/lua/ivy/prompt_spec.lua new file mode 100644 index 0000000..452a091 --- /dev/null +++ b/lua/ivy/prompt_spec.lua @@ -0,0 +1,91 @@ +local prompt = require "ivy.prompt" + +-- Input a list of strings into the prompt +local input = function(input_table) + for index = 1, #input_table do + prompt.input(input_table[index]) + end +end + +describe("prompt", function() + before_each(function() + prompt.destroy() + end) + + it("starts with empty text", function() + assert.is_same(prompt.text(), "") + end) + + it("can input some text", function() + input { "A", "d", "e" } + assert.is_same(prompt.text(), "Ade") + end) + + it("can delete a char", function() + input { "A", "d", "e", "BACKSPACE" } + assert.is_same(prompt.text(), "Ad") + end) + + it("will reset the text", function() + input { "A", "d", "e" } + prompt.set "New" + assert.is_same(prompt.text(), "New") + end) + + it("can move around the a word", function() + input { "P", "r", "o", "p", "t", "LEFT", "LEFT", "LEFT", "RIGHT", "m" } + assert.is_same(prompt.text(), "Prompt") + end) + + it("can delete a word", function() + prompt.set "Ade Attwood" + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "Ade ") + end) + + it("can delete a word in the middle and leave the cursor at that word", function() + prompt.set "Ade middle A" + input { "LEFT", "LEFT", "DELETE_WORD", "a" } + + assert.is_same(prompt.text(), "Ade a A") + end) + + it("will delete the space and the word if the last word is single space", function() + prompt.set "some.thing " + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "some.") + end) + + it("will only delete one word from path", function() + prompt.set "some/nested/path" + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "some/nested/") + end) + + it("will delete tailing space", function() + prompt.set "word " + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "") + end) + + it("will leave a random space", function() + prompt.set "some word " + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), "some ") + end) + + local special_characters = { ".", "/", "^" } + for _, char in ipairs(special_characters) do + it(string.format("will stop at a %s", char), function() + prompt.set(string.format("key%sValue", char)) + input { "DELETE_WORD" } + + assert.is_same(prompt.text(), string.format("key%s", char)) + end) + end +end) diff --git a/lua/ivy/utils.lua b/lua/ivy/utils.lua index 910e57f..40489a8 100644 --- a/lua/ivy/utils.lua +++ b/lua/ivy/utils.lua @@ -1,111 +1,113 @@ local utils = {} -- A list of all of the actions defined by ivy. The callback function can -- implement as many of them as necessary. As a minimum it should implement the -- "EDIT" action that is called on the default complete. utils.actions = { EDIT = "EDIT", CHECKPOINT = "CHECKPOINT", VSPLIT = "VSPLIT", SPLIT = "SPLIT", } utils.command_map = { [utils.actions.EDIT] = "edit", [utils.actions.CHECKPOINT] = "edit", [utils.actions.VSPLIT] = "vsplit", [utils.actions.SPLIT] = "split", } utils.existing_command_map = { [utils.actions.EDIT] = "buffer", [utils.actions.CHECKPOINT] = "buffer", [utils.actions.VSPLIT] = "vsplit | buffer", [utils.actions.SPLIT] = "split | buffer", } utils.command_finder = function(command, min) if min == nil then min = 3 end return function(input) -- Dont run the commands unless we have somting to search that wont -- return a ton of results or on some commands the command files with -- no search term if #input < min then return "-- Please type more than " .. min .. " chars --" end -- TODO(ade): Think if we want to start escaping the command here. I -- dont know if its causing issues while trying to use regex especially -- with word boundaries `input:gsub("'", "\\'"):gsub('"', '\\"')` local handle = io.popen(command .. " " .. input .. " 2>&1 || true") if handle == nil then return {} end local results = {} for line in handle:lines() do table.insert(results, { content = line }) end handle:close() return results end end utils.vimgrep_action = function() return function(item, action) -- Match file and line form vimgrep style commands local file = item:match "([^:]+):" local line = item:match ":(%d+):" -- Cant do anything if we cant find a file to go to if file == nil then return end utils.file_action()(file, action) if line ~= nil then vim.cmd(line) end end end utils.file_action = function() return function(file, action) if file == nil then return end local buffer_number = vim.fn.bufnr(file) local command if buffer_number > -1 then command = utils.existing_command_map[action] else command = utils.command_map[action] end if command == nil then vim.api.nvim_err_writeln("[IVY] The file action is unable the handel the action " .. action) return end vim.cmd(command .. " " .. utils.escape_file_name(file)) end end utils.line_action = function() return function(item) local line = item:match "^%s+(%d+):" - vim.cmd(line) + if line ~= nil then + vim.cmd(line) + end end end utils.escape_file_name = function(input) local file, _ = string.gsub(input, "([$%]\\[])", "\\%1") return file end return utils diff --git a/lua/ivy/utils_escape_spec.lua b/lua/ivy/utils_escape_spec.lua new file mode 100644 index 0000000..7a47331 --- /dev/null +++ b/lua/ivy/utils_escape_spec.lua @@ -0,0 +1,11 @@ +local utils = require "ivy.utils" + +it("will escape a dollar in the file name", function() + local result = utils.escape_file_name "/path/to/$file/$name.lua" + assert.is_same(result, "/path/to/\\$file/\\$name.lua") +end) + +it("will escape a brackets in the file name", function() + local result = utils.escape_file_name "/path/to/[file]/[name].lua" + assert.is_same(result, "/path/to/\\[file\\]/\\[name\\].lua") +end) diff --git a/lua/ivy/utils_line_action_spec.lua b/lua/ivy/utils_line_action_spec.lua new file mode 100644 index 0000000..8e4484e --- /dev/null +++ b/lua/ivy/utils_line_action_spec.lua @@ -0,0 +1,28 @@ +local utils = require "ivy.utils" +local line_action = utils.line_action() + +describe("utils line_action", function() + before_each(function() + spy.on(vim, "cmd") + end) + + it("will run the line command", function() + line_action " 4: Some text" + + assert.is_equal(#vim.cmd.calls, 1, "The `vim.cmd` function should be called once") + assert.spy(vim.cmd).was_called_with "4" + end) + + it("will run with more numbers", function() + line_action " 44: Some text" + + assert.is_equal(#vim.cmd.calls, 1, "The `vim.cmd` function should be called once") + assert.spy(vim.cmd).was_called_with "44" + end) + + it("dose not run any action if no line is found", function() + line_action "Some text" + + assert.spy(vim.cmd).was_not_called() + end) +end) diff --git a/lua/ivy/utils_vimgrep_action_spec.lua b/lua/ivy/utils_vimgrep_action_spec.lua new file mode 100644 index 0000000..51cb55d --- /dev/null +++ b/lua/ivy/utils_vimgrep_action_spec.lua @@ -0,0 +1,56 @@ +local utils = require "ivy.utils" +local vimgrep_action = utils.vimgrep_action() + +local test_data = { + { + it = "will edit some file and goto the line", + completion = "some/file.lua:2: This is some text", + action = utils.actions.EDIT, + commands = { + "edit some/file.lua", + "2", + }, + }, + { + it = "will skip the line if its not matched", + completion = "some/file.lua: This is some text", + action = utils.actions.EDIT, + commands = { "buffer some/file.lua" }, + }, + { + it = "will run the vsplit command", + completion = "some/file.lua: This is some text", + action = utils.actions.VSPLIT, + commands = { "vsplit | buffer some/file.lua" }, + }, + { + it = "will run the split command", + completion = "some/file.lua: This is some text", + action = utils.actions.SPLIT, + commands = { "split | buffer some/file.lua" }, + }, +} + +describe("utils vimgrep_action", function() + before_each(function() + spy.on(vim, "cmd") + end) + + after_each(function() + vim.cmd:revert() + end) + + for i = 1, #test_data do + local data = test_data[i] + it(data.it, function() + assert.is_true(#data.commands > 0, "You must assert that at least one command is run") + + vimgrep_action(data.completion, data.action) + assert.is_equal(#vim.cmd.calls, #data.commands, "The `vim.cmd` function should be called once") + + for j = 1, #data.commands do + assert.spy(vim.cmd).was_called_with(data.commands[j]) + end + end) + end +end) diff --git a/lua/ivy/window_spec.lua b/lua/ivy/window_spec.lua new file mode 100644 index 0000000..873504a --- /dev/null +++ b/lua/ivy/window_spec.lua @@ -0,0 +1,32 @@ +local window = require "ivy.window" +local controller = require "ivy.controller" + +describe("window", function() + before_each(function() + vim.cmd "highlight IvyMatch cterm=bold gui=bold" + window.initialize() + end) + + after_each(function() + controller.destroy() + end) + + it("can initialize and destroy the window", function() + assert.is_equal(vim.api.nvim_get_current_buf(), window.buffer) + + window.destroy() + assert.is_equal(nil, window.buffer) + end) + + it("can set items", function() + window.set_items { { content = "Line one" } } + assert.is_equal("Line one", window.get_current_selection()) + end) + + it("will set the items when a string is passed in", function() + local items = table.concat({ "One", "Two", "Three" }, "\n") + window.set_items(items) + + assert.is_equal(items, table.concat(vim.api.nvim_buf_get_lines(window.buffer, 0, -1, true), "\n")) + end) +end) diff --git a/scripts/busted.lua b/scripts/busted.lua new file mode 100755 index 0000000..753c0e6 --- /dev/null +++ b/scripts/busted.lua @@ -0,0 +1,8 @@ +-- Script to run the busted cli tool. You can use this under nvim using be +-- below command. Any arguments can be passed in the same as the busted cli. +-- +-- ```bash +-- nvim -l scripts/busted.lua +-- ``` +vim.opt.rtp:append(vim.fn.getcwd()) +require "busted.runner" { standalone = false }