diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2163ad4..7422053 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v4
- name: Run stylua
- uses: JohnnyMorganz/stylua-action@v3.0.0
+ uses: JohnnyMorganz/stylua-action@v4.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: latest
@@ -52,6 +52,9 @@ jobs:
test:
name: Build and test
+ strategy:
+ matrix:
+ nvim-version: ["v0.9.5", "stable", "nightly"]
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -61,10 +64,25 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
- run: sudo apt update && sudo apt install -y luajit build-essential
+ run: sudo apt update && sudo apt install -y build-essential luarocks
+
+ - name: Install busted
+ run: sudo luarocks install busted
+
+ - name: Install neovim
+ run: |
+ test -d _neovim || {
+ mkdir -p _neovim
+ curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.nvim-version }}/nvim-linux64.tar.gz" | tar xzf - --strip-components=1 -C "${PWD}/_neovim"
+ }
- name: Build
run: cargo build --release
- - name: Test
- run: find lua -name "*_test.lua" | xargs luajit scripts/test.lua
+ - name: Run tests
+ run: |
+ export PATH="${PWD}/_neovim/bin:${PATH}"
+ export VIM="${PWD}/_neovim/share/nvim/runtime"
+
+ nvim --version
+ nvim -l ./scripts/busted.lua -o TAP ./lua/ivy/ 2> /dev/null
diff --git a/.luacheckrc b/.luacheckrc
index 337ac8e..2d46e75 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -10,9 +10,12 @@ self = false
read_globals = {
"vim",
- "it",
"after",
"after_each",
+ "assert",
"before",
"before_each",
+ "describe",
+ "it",
+ "spy",
}
diff --git a/Cargo.lock b/Cargo.lock
index 60d34ec..17ec5d4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -429,9 +429,9 @@ dependencies = [
[[package]]
name = "rayon"
-version = "1.8.1"
+version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
diff --git a/Cargo.toml b/Cargo.toml
index cf70aca..d36ee36 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@ path = "rust/lib.rs"
[dependencies]
ignore = "0.4.22"
fuzzy-matcher = "0.3.7"
-rayon = "1.8.1"
+rayon = "1.10.0"
[dev-dependencies]
criterion = "0.5.1"
diff --git a/README.md b/README.md
index e9e8d1e..f048609 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-

+
@@ -20,7 +20,47 @@ git clone https://github.com/AdeAttwood/ivy.nvim ~/.config/nvim/pack/bundle/star
### Plugin managers
-TODO: Add docs in the plugin managers I don't use any
+Using [lazy.nvim](https://github.com/folke/lazy.nvim)
+```lua
+{
+ "AdeAttwood/ivy.nvim",
+ build = "cargo build --release",
+},
+```
+
+TODO: Add more plugin managers
+
+### Setup / Configuration
+
+Ivy can be configured with minimal config that will give you all the defaults
+provided by Ivy.
+
+```lua
+require('ivy').setup()
+```
+
+With Ivy you can configure your own backends.
+
+```lua
+require('ivy').setup {
+ backends = {
+ -- A backend module that will be registered
+ "ivy.backends.buffers",
+ -- Using a table so you can configure a custom keymap overriding the
+ -- default one.
+ { "ivy.backends.files", { keymap = "
" } }
+ },
+}
+```
+
+The `setup` function can only be called once, if its called a second time any
+backends or config will not be used. Ivy does expose the `register_backend`
+function, this can be used to load backends before or after the setup function
+is called.
+
+```lua
+require('ivy').register_backend("ivy.backends.files")
+```
### Compiling
@@ -56,28 +96,46 @@ cp ./post-merge.sample ./.git/hooks/post-merge
## Features
-### Commands
+### Backends
-A command can be run that will launch the completion UI
+A backend is a module that will provide completion candidates for the UI to
+show. It will also provide functionality when actions are taken. The Command
+and Key Map are the default options provided by the backend, they can be
+customized when you register it.
-| Command | Key Map | Description |
-| ------------------ | ----------- | ----------------------------------------------------------- |
-| IvyFd | \p | Find files in your project with a custom rust file finder |
-| IvyAg | \/ | Find content in files using the silver searcher |
-| IvyBuffers | \b | Search though open buffers |
-| IvyLines | | Search the lines in the current buffer |
-| IvyWorkspaceSymbol | | Search for workspace symbols using the lsp workspace/symbol |
+| Module | Command | Key Map | Description |
+| ------------------------------------ | ------------------ | ----------- | ----------------------------------------------------------- |
+| `ivy.backends.files` | IvyFd | \p | Find files in your project with a custom rust file finder |
+| `ivy.backends.ag` | IvyAg | \/ | Find content in files using the silver searcher |
+| `ivy.backends.rg` | IvyRg | \/ | Find content in files using ripgrep cli tool |
+| `ivy.backends.buffers` | IvyBuffers | \b | Search though open buffers |
+| `ivy.backends.lines` | IvyLines | | Search the lines in the current buffer |
+| `ivy.backends.lsp-workspace-symbols` | IvyWorkspaceSymbol | | Search for workspace symbols using the lsp workspace/symbol |
### Actions
Action can be run on selected candidates provide functionality
-| Action | Description |
-| -------------- | ------------------------------------------------------------------------------ |
-| Complete | Run the completion function, usually this will be opening a file |
-| Peek | Run the completion function on a selection, but don't close the results window |
-| Vertical Split | Run the completion function in a new vertical split |
-| Split | Run the completion function in a new split |
+| Action | Key Map | Description |
+| -------------- | ----------- | ------------------------------------------------------------------------------ |
+| Complete | \ |Run the completion function, usually this will be opening a file |
+| Vertical Split | \ |Run the completion function in a new vertical split |
+| Split | \ |Run the completion function in a new split |
+| Destroy | \ |Close the results window |
+| Clear | \ |Clear the results window |
+| Delete word | \ |Delete the word under the cursor |
+| Next | \ |Move to the next candidate |
+| Previous | \ |Move to the previous candidate |
+| Next Checkpoint| \ |Move to the next candidate and keep Ivy open and focussed |
+| Previous Checkpoint| \|Move to the previous candidate and keep Ivy open and focussed |
+
+Add your own keymaps for an action by adding a `ftplugin/ivy.lua` file in your config.
+Just add a simple keymap like this:
+
+```lua
+vim.api.nvim_set_keymap( "n", "", "lua vim.ivy.destroy()", { noremap = true, silent = true, nowait = true })
+```
+
## API
@@ -130,7 +188,7 @@ vertical split action it will open the buffer in a new `vsplit`
{ content = "Three" },
}
end,
- -- Action callback that will be called on the completion or peek actions.
+ -- Action callback that will be called on the completion or checkpoint actions.
-- The currently selected item is passed in as the result.
function(result) vim.cmd("edit " .. result) end
)
diff --git a/lua/ivy/config.lua b/lua/ivy/config.lua
new file mode 100644
index 0000000..8ff1e6a
--- /dev/null
+++ b/lua/ivy/config.lua
@@ -0,0 +1,33 @@
+local config_mt = {}
+config_mt.__index = config_mt
+
+function config_mt:get_in(config, key_table)
+ local current_value = config
+ for _, key in ipairs(key_table) do
+ if current_value == nil then
+ return nil
+ end
+
+ current_value = current_value[key]
+ end
+
+ return current_value
+end
+
+function config_mt:get(key_table)
+ return self:get_in(self.user_config, key_table) or self:get_in(self.default_config, key_table)
+end
+
+local config = { user_config = {} }
+
+config.default_config = {
+ backends = {
+ "ivy.backends.buffers",
+ "ivy.backends.files",
+ "ivy.backends.lines",
+ "ivy.backends.rg",
+ "ivy.backends.lsp-workspace-symbols",
+ },
+}
+
+return setmetatable(config, config_mt)
diff --git a/lua/ivy/config_spec.lua b/lua/ivy/config_spec.lua
new file mode 100644
index 0000000..7ef8cd8
--- /dev/null
+++ b/lua/ivy/config_spec.lua
@@ -0,0 +1,27 @@
+local config = require "ivy.config"
+
+describe("config", function()
+ before_each(function()
+ config.user_config = {}
+ end)
+
+ it("gets the first item when there is only default values", function()
+ local first_backend = config:get { "backends", 1 }
+ assert.is_equal("ivy.backends.buffers", first_backend)
+ end)
+
+ it("returns nil if we access a key that is not a valid config item", function()
+ assert.is_nil(config:get { "not", "a", "thing" })
+ end)
+
+ it("returns the users overridden config value", function()
+ config.user_config = { backends = { "ivy.my.backend" } }
+ local first_backend = config:get { "backends", 1 }
+ assert.is_equal("ivy.my.backend", first_backend)
+ end)
+
+ it("returns a nested value", function()
+ config.user_config = { some = { nested = "value" } }
+ assert.is_equal(config:get { "some", "nested" }, "value")
+ end)
+end)
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/controller_test.lua b/lua/ivy/controller_test.lua
deleted file mode 100644
index 9865142..0000000
--- a/lua/ivy/controller_test.lua
+++ /dev/null
@@ -1,51 +0,0 @@
-local vim_mock = require "ivy.vim_mock"
-local window = require "ivy.window"
-local controller = require "ivy.controller"
-
--- The number of the mock buffer where all the test completions gets put
-local buffer_number = 10
-
-before_each(function()
- vim_mock.reset()
- window.initialize()
-end)
-
-after_each(function()
- controller.destroy()
-end)
-
-it("will run", function(t)
- controller.run("Testing", function()
- return { { content = "Some content" } }
- end, function()
- return {}
- end)
-
- local lines = vim_mock.get_lines()
- local completion_lines = lines[buffer_number]
-
- t.assert_equal(#completion_lines, 1)
- t.assert_equal(completion_lines[1], "Some content")
-end)
-
-it("will not try and highlight the buffer if there is nothing to highlight", function(t)
- controller.items = function()
- return { { content = "Hello" } }
- end
-
- controller.update ""
- local commands = vim_mock.get_commands()
- t.assert_equal(#commands, 1)
-end)
-
-it("will escape a - when passing it to be highlighted", function(t)
- controller.items = function()
- return { { content = "Hello" } }
- end
-
- controller.update "some-file"
- local commands = vim_mock.get_commands()
- local syntax_command = commands[2]
-
- t.assert_equal("syntax match IvyMatch '[some\\-file]'", syntax_command)
-end)
diff --git a/lua/ivy/init.lua b/lua/ivy/init.lua
new file mode 100644
index 0000000..58672ff
--- /dev/null
+++ b/lua/ivy/init.lua
@@ -0,0 +1,32 @@
+local controller = require "ivy.controller"
+local config = require "ivy.config"
+local register_backend = require "ivy.register_backend"
+
+local ivy = {}
+ivy.run = controller.run
+ivy.register_backend = register_backend
+
+-- Private variable to check if ivy has been setup, this is to prevent multiple
+-- setups of ivy. This is only exposed for testing purposes.
+---@private
+ivy.has_setup = false
+
+---@class IvySetupOptions
+---@field backends (IvyBackend | { ["1"]: string, ["2"]: IvyBackendOptions} | string)[]
+
+---@param user_config IvySetupOptions
+function ivy.setup(user_config)
+ if ivy.has_setup then
+ return
+ end
+
+ config.user_config = user_config or {}
+
+ for _, backend in ipairs(config:get { "backends" } or {}) do
+ register_backend(backend)
+ end
+
+ ivy.has_setup = true
+end
+
+return ivy
diff --git a/lua/ivy/init_spec.lua b/lua/ivy/init_spec.lua
new file mode 100644
index 0000000..f8ea91b
--- /dev/null
+++ b/lua/ivy/init_spec.lua
@@ -0,0 +1,30 @@
+local ivy = require "ivy"
+local config = require "ivy.config"
+
+describe("ivy.setup", function()
+ before_each(function()
+ ivy.has_setup = false
+ config.user_config = {}
+ end)
+
+ it("sets the users config options", function()
+ ivy.setup { backends = { "ivy.backends.files" } }
+ assert.is_equal("ivy.backends.files", config:get { "backends", 1 })
+ end)
+
+ it("will not reconfigure if its called twice", function()
+ ivy.setup { backends = { "ivy.backends.files" } }
+ ivy.setup { backends = { "some.backend" } }
+ assert.is_equal("ivy.backends.files", config:get { "backends", 1 })
+ end)
+
+ it("does not crash if you don't pass in any params to the setup function", function()
+ ivy.setup()
+ assert.is_equal("ivy.backends.buffers", config:get { "backends", 1 })
+ end)
+
+ it("will fallback if the key is not set at all in the users config", function()
+ ivy.setup { some_key = "some_value" }
+ assert.is_equal("ivy.backends.buffers", config:get { "backends", 1 })
+ 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/libivy_test.lua b/lua/ivy/libivy_test.lua
deleted file mode 100644
index fe18455..0000000
--- a/lua/ivy/libivy_test.lua
+++ /dev/null
@@ -1,46 +0,0 @@
-local libivy = require "ivy.libivy"
-
-it("should run a simple match", function(t)
- local score = libivy.ivy_match("term", "I am a serch term")
-
- if score <= 0 then
- t.error("Score should not be less than 0 found " .. score)
- end
-end)
-
-it("should find a dot file", function(t)
- local current_dir = libivy.ivy_cwd()
- local results = libivy.ivy_files(".github/workflows/ci.yml", current_dir)
-
- if results.length ~= 2 then
- t.error("Incorrect number of results found " .. results.length)
- end
-
- if results[2].content ~= ".github/workflows/ci.yml" then
- t.error("Invalid matches: " .. results[2].content)
- end
-end)
-
-it("will allow you to access the length via the metatable", function(t)
- local current_dir = libivy.ivy_cwd()
- local results = libivy.ivy_files(".github/workflows/ci.yml", current_dir)
-
- local mt = getmetatable(results)
-
- if results.length ~= mt.__len(results) then
- t.error "The `length` property does not match the __len metamethod"
- end
-end)
-
-it("will create an iterator", function(t)
- local iter = libivy.ivy_files(".github/workflows/ci.yml", libivy.ivy_cwd())
- local mt = getmetatable(iter)
-
- if type(mt["__index"]) ~= "function" then
- t.error "The iterator does not have an __index metamethod"
- end
-
- if type(mt["__len"]) ~= "function" then
- t.error "The iterator does not have an __len metamethod"
- 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/matcher_test.lua b/lua/ivy/matcher_test.lua
deleted file mode 100644
index f80f565..0000000
--- a/lua/ivy/matcher_test.lua
+++ /dev/null
@@ -1,37 +0,0 @@
-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)
-
- if score_one < score_two then
- return one .. " should be ranked higher than " .. two
- end
-
- return nil
-end
-
-it("sould match path separator", function(t)
- local result = match_test("file", "some/file.lua", "somefile.lua")
- if result then
- t.error(result)
- end
-end)
-
-it("sould match pattern with spaces", function(t)
- local result = match_test("so fi", "some/file.lua", "somefile.lua")
- if result then
- t.error(result)
- end
-end)
-
-it("sould match the start of a string", function(t)
- local result = match_test("file", "file.lua", "somefile.lua")
- if result then
- t.error(result)
- 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/prompt_test.lua b/lua/ivy/prompt_test.lua
deleted file mode 100644
index ee834b6..0000000
--- a/lua/ivy/prompt_test.lua
+++ /dev/null
@@ -1,94 +0,0 @@
-local prompt = require "ivy.prompt"
-local vim_mock = require "ivy.vim_mock"
-
-before_each(function()
- vim_mock.reset()
- prompt.destroy()
-end)
-
--- 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
-
--- Asserts the prompt contains the correct value
-local assert_prompt = function(t, expected)
- local text = prompt.text()
- if text ~= expected then
- t.error("The prompt text should be '" .. expected .. "' found '" .. text .. "'")
- end
-end
-
-it("starts with empty text", function(t)
- if prompt.text() ~= "" then
- t.error "The prompt should start with empty text"
- end
-end)
-
-it("can input some text", function(t)
- input { "A", "d", "e" }
- assert_prompt(t, "Ade")
-end)
-
-it("can delete a char", function(t)
- input { "A", "d", "e", "BACKSPACE" }
- assert_prompt(t, "Ad")
-end)
-
-it("will reset the text", function(t)
- input { "A", "d", "e" }
- prompt.set "New"
- assert_prompt(t, "New")
-end)
-
-it("can move around the a word", function(t)
- input { "P", "r", "o", "p", "t", "LEFT", "LEFT", "LEFT", "RIGHT", "m" }
- assert_prompt(t, "Prompt")
-end)
-
-it("can delete a word", function(t)
- prompt.set "Ade Attwood"
- input { "DELETE_WORD" }
- assert_prompt(t, "Ade ")
-end)
-
-it("can delete a word in the middle", function(t)
- prompt.set "Ade middle A"
- input { "LEFT", "LEFT", "DELETE_WORD" }
- assert_prompt(t, "Ade A")
-end)
-
-it("will delete the space and the word if the last word is single space", function(t)
- prompt.set "some.thing "
- input { "DELETE_WORD" }
- assert_prompt(t, "some.")
-end)
-
-it("will only delete one word from path", function(t)
- prompt.set "some/nested/path"
- input { "DELETE_WORD" }
- assert_prompt(t, "some/nested/")
-end)
-
-it("will delete tailing space", function(t)
- prompt.set "word "
- input { "DELETE_WORD" }
- assert_prompt(t, "")
-end)
-
-it("will leave a random space", function(t)
- prompt.set "some word "
- input { "DELETE_WORD" }
- assert_prompt(t, "some ")
-end)
-
-local special_characters = { ".", "/", "^" }
-for _, char in ipairs(special_characters) do
- it(string.format("will stop at a %s", char), function(t)
- prompt.set(string.format("key%sValue", char))
- input { "DELETE_WORD" }
- assert_prompt(t, string.format("key%s", char))
- end)
-end
diff --git a/lua/ivy/register_backend.lua b/lua/ivy/register_backend.lua
new file mode 100644
index 0000000..f314588
--- /dev/null
+++ b/lua/ivy/register_backend.lua
@@ -0,0 +1,54 @@
+---@class IvyBackend
+---@field command string The command this backend will have
+---@field items fun(input: string): { content: string }[] | string The callback function to get the items to select from
+---@field callback fun(result: string, action: string) The callback function to run when a item is selected
+---@field description string? The description of the backend, this will be used in the keymaps
+---@field name string? The name of the backend, this will fallback to the command if its not set
+---@field keymap string? The keymap to trigger this backend
+
+---@class IvyBackendOptions
+---@field command string The command this backend will have
+---@field keymap string? The keymap to trigger this backend
+
+---Register a new backend
+---
+---This will create all the commands and set all the keymaps for the backend
+---@param backend IvyBackend
+local register_backend_class = function(backend)
+ local user_command_options = { bang = true }
+ if backend.description ~= nil then
+ user_command_options.desc = backend.description
+ end
+
+ local name = backend.name or backend.command
+ vim.api.nvim_create_user_command(backend.command, function()
+ vim.ivy.run(name, backend.items, backend.callback)
+ end, user_command_options)
+
+ if backend.keymap ~= nil then
+ vim.api.nvim_set_keymap("n", backend.keymap, "" .. backend.command .. "", { nowait = true, silent = true })
+ end
+end
+
+---@param backend IvyBackend | { ["1"]: string, ["2"]: IvyBackendOptions} | string The backend or backend module
+---@param options IvyBackendOptions? The options for the backend, that will be merged with the backend
+local register_backend = function(backend, options)
+ if type(backend[1]) == "string" then
+ options = backend[2]
+ backend = require(backend[1])
+ end
+
+ if type(backend) == "string" then
+ backend = require(backend)
+ end
+
+ if options then
+ for key, value in pairs(options) do
+ backend[key] = value
+ end
+ end
+
+ register_backend_class(backend)
+end
+
+return register_backend
diff --git a/lua/ivy/register_backend_spec.lua b/lua/ivy/register_backend_spec.lua
new file mode 100644
index 0000000..ab51e4e
--- /dev/null
+++ b/lua/ivy/register_backend_spec.lua
@@ -0,0 +1,71 @@
+local register_backend = require "ivy.register_backend"
+
+local function get_command(name)
+ local command_iter = vim.api.nvim_get_commands {}
+
+ for _, cmd in pairs(command_iter) do
+ if cmd.name == name then
+ return cmd
+ end
+ end
+
+ return nil
+end
+
+local function get_keymap(mode, rhs)
+ local keymap_iter = vim.api.nvim_get_keymap(mode)
+ for _, keymap in pairs(keymap_iter) do
+ if keymap.rhs == rhs then
+ return keymap
+ end
+ end
+
+ return nil
+end
+
+describe("register_backend", function()
+ after_each(function()
+ vim.api.nvim_del_user_command "IvyFd"
+
+ local keymap = get_keymap("n", "IvyFd")
+ if keymap then
+ vim.api.nvim_del_keymap("n", keymap.lhs)
+ end
+ end)
+
+ it("registers a backend from a string with the default options", function()
+ register_backend "ivy.backends.files"
+
+ local command = get_command "IvyFd"
+ assert.is_not_nil(command)
+
+ local keymap = get_keymap("n", "IvyFd")
+ assert.is_not_nil(keymap)
+ end)
+
+ it("allows you to override the keymap", function()
+ register_backend("ivy.backends.files", { keymap = "" })
+
+ local keymap = get_keymap("n", "IvyFd")
+ assert(keymap ~= nil)
+ assert.are.equal("", keymap.lhs)
+ end)
+
+ it("allows you to pass in a hole backend module", function()
+ register_backend(require "ivy.backends.files")
+
+ local command = get_command "IvyFd"
+ assert.is_not_nil(command)
+
+ local keymap = get_keymap("n", "IvyFd")
+ assert.is_not_nil(keymap)
+ end)
+
+ it("allows you to pass in a hole backend module", function()
+ register_backend { "ivy.backends.files", { keymap = "" } }
+
+ local keymap = get_keymap("n", "IvyFd")
+ assert(keymap ~= nil)
+ assert.are.equal("", keymap.lhs)
+ 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
@@ -99,7 +99,9 @@ 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
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_line_action_test.lua b/lua/ivy/utils_line_action_test.lua
deleted file mode 100644
index f68810e..0000000
--- a/lua/ivy/utils_line_action_test.lua
+++ /dev/null
@@ -1,39 +0,0 @@
-local utils = require "ivy.utils"
-local line_action = utils.line_action()
-local vim_mock = require "ivy.vim_mock"
-
-before_each(function()
- vim_mock.reset()
-end)
-
-it("will run the line command", function(t)
- line_action " 4: Some text"
-
- if #vim_mock.commands ~= 1 then
- t.error "`line_action` command length should be 1"
- end
-
- if vim_mock.commands[1] ~= "4" then
- t.error "`line_action` command should be 4"
- end
-end)
-
-it("will run with more numbers", function(t)
- line_action " 44: Some text"
-
- if #vim_mock.commands ~= 1 then
- t.error "`line_action` command length should be 1"
- end
-
- if vim_mock.commands[1] ~= "44" then
- t.error "`line_action` command should be 44"
- end
-end)
-
-it("dose not run any action if no line is found", function(t)
- line_action "Some text"
-
- if #vim_mock.commands ~= 0 then
- t.error "`line_action` command length should be 1"
- end
-end)
diff --git a/lua/ivy/utils_vimgrep_action_test.lua b/lua/ivy/utils_vimgrep_action_spec.lua
similarity index 50%
rename from lua/ivy/utils_vimgrep_action_test.lua
rename to lua/ivy/utils_vimgrep_action_spec.lua
index 0c08c09..51cb55d 100644
--- a/lua/ivy/utils_vimgrep_action_test.lua
+++ b/lua/ivy/utils_vimgrep_action_spec.lua
@@ -1,10 +1,5 @@
local utils = require "ivy.utils"
local vimgrep_action = utils.vimgrep_action()
-local vim_mock = require "ivy.vim_mock"
-
-before_each(function()
- vim_mock.reset()
-end)
local test_data = {
{
@@ -20,37 +15,42 @@ local test_data = {
it = "will skip the line if its not matched",
completion = "some/file.lua: This is some text",
action = utils.actions.EDIT,
- commands = { "edit some/file.lua" },
+ 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 some/file.lua" },
+ 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 some/file.lua" },
+ commands = { "split | buffer some/file.lua" },
},
}
-for i = 1, #test_data do
- local data = test_data[i]
- it(data.it, function(t)
- vimgrep_action(data.completion, data.action)
-
- if #vim_mock.commands ~= #data.commands then
- t.error("Incorrect number of commands run expected " .. #data.commands .. " but found " .. #vim_mock.commands)
- end
-
- for j = 1, #data.commands do
- if vim_mock.commands[j] ~= data.commands[j] then
- t.error(
- "Incorrect command run expected '" .. data.commands[j] .. "' but found '" .. vim_mock.commands[j] .. "'"
- )
- end
- end
+describe("utils vimgrep_action", function()
+ before_each(function()
+ spy.on(vim, "cmd")
end)
-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/lua/ivy/window_test.lua b/lua/ivy/window_test.lua
deleted file mode 100644
index dcad637..0000000
--- a/lua/ivy/window_test.lua
+++ /dev/null
@@ -1,33 +0,0 @@
-local vim_mock = require "ivy.vim_mock"
-local window = require "ivy.window"
-
-before_each(function()
- vim_mock.reset()
-end)
-
-it("can initialize and destroy the window", function(t)
- window.initialize()
-
- t.assert_equal(10, window.get_buffer())
- t.assert_equal(10, window.buffer)
-
- window.destroy()
- t.assert_equal(nil, window.buffer)
-end)
-
-it("can set items", function(t)
- window.initialize()
-
- window.set_items { { content = "Line one" } }
- t.assert_equal("Line one", window.get_current_selection())
-end)
-
-it("will set the items when a string is passed in", function(t)
- window.initialize()
-
- local items = table.concat({ "One", "Two", "Three" }, "\n")
- window.set_items(items)
-
- local lines = table.concat(vim_mock.get_lines()[window.buffer], "\n")
- t.assert_equal(items, lines)
-end)
diff --git a/plugin/ivy.lua b/plugin/ivy.lua
index 383e866..cab4b9a 100644
--- a/plugin/ivy.lua
+++ b/plugin/ivy.lua
@@ -5,26 +5,6 @@ local controller = require "ivy.controller"
-- luacheck: ignore
vim.ivy = controller
-local register_backend = function(backend)
- assert(backend.command, "The backend must have a command")
- assert(backend.items, "The backend must have a items function")
- assert(backend.callback, "The backend must have a callback function")
-
- local user_command_options = { bang = true }
- if backend.description ~= nil then
- user_command_options.desc = backend.description
- end
-
- local name = backend.name or backend.command
- vim.api.nvim_create_user_command(backend.command, function()
- vim.ivy.run(name, backend.items, backend.callback)
- end, user_command_options)
-
- if backend.keymap ~= nil then
- vim.api.nvim_set_keymap("n", backend.keymap, "" .. backend.command .. "", { nowait = true, silent = true })
- end
-end
-
vim.paste = (function(overridden)
return function(lines, phase)
local file_type = vim.api.nvim_buf_get_option(0, "filetype")
@@ -36,15 +16,4 @@ vim.paste = (function(overridden)
end
end)(vim.paste)
-register_backend(require "ivy.backends.buffers")
-register_backend(require "ivy.backends.files")
-register_backend(require "ivy.backends.lines")
-register_backend(require "ivy.backends.lsp-workspace-symbols")
-
-if vim.fn.executable "rg" then
- register_backend(require "ivy.backends.rg")
-elseif vim.fn.executable "ag" then
- register_backend(require "ivy.backends.ag")
-end
-
vim.cmd "highlight IvyMatch cterm=bold gui=bold"
diff --git a/rust/finder.rs b/rust/finder.rs
index f3baa97..37eff2a 100644
--- a/rust/finder.rs
+++ b/rust/finder.rs
@@ -26,7 +26,11 @@ pub fn find_files(options: Options) -> Vec {
builder.overrides(overrides);
for result in builder.build() {
- let absolute_candidate = result.unwrap();
+ let absolute_candidate = match result {
+ Ok(absolute_candidate) => absolute_candidate,
+ Err(..) => continue,
+ };
+
let candidate_path = absolute_candidate.path().strip_prefix(base_path).unwrap();
if candidate_path.is_dir() {
continue;
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 }