Merge 9bb186d6b0 into sapling-pr-archive-AdeAttwood

This commit is contained in:
Ade Attwood 2024-07-02 20:08:35 +01:00 committed by GitHub
commit 7fd1fe226c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 677 additions and 384 deletions

View file

@ -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

View file

@ -10,9 +10,12 @@ self = false
read_globals = {
"vim",
"it",
"after",
"after_each",
"assert",
"before",
"before_each",
"describe",
"it",
"spy",
}

4
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -1,6 +1,6 @@
<div align="center">
<img src="assets/logo.svg" alt="ivy.vim" />
<img src="assets/logo.svg" alt="ivy.vim" />
<br />
<br />
@ -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 = "<C-p>" } }
},
}
```
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 | \<leader\>p | Find files in your project with a custom rust file finder |
| IvyAg | \<leader\>/ | Find content in files using the silver searcher |
| IvyBuffers | \<leader\>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 | \<leader\>p | Find files in your project with a custom rust file finder |
| `ivy.backends.ag` | IvyAg | \<leader\>/ | Find content in files using the silver searcher |
| `ivy.backends.rg` | IvyRg | \<leader\>/ | Find content in files using ripgrep cli tool |
| `ivy.backends.buffers` | IvyBuffers | \<leader\>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 | \<CR\> |Run the completion function, usually this will be opening a file |
| Vertical Split | \<C-v\> |Run the completion function in a new vertical split |
| Split | \<C-s\> |Run the completion function in a new split |
| Destroy | \<C-c\> |Close the results window |
| Clear | \<C-u\> |Clear the results window |
| Delete word | \<C-w\> |Delete the word under the cursor |
| Next | \<C-n\> |Move to the next candidate |
| Previous | \<C-p\> |Move to the previous candidate |
| Next Checkpoint| \<C-M-n\> |Move to the next candidate and keep Ivy open and focussed |
| Previous Checkpoint| \<C-M-n\>|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", "<esc>", "<cmd>lua vim.ivy.destroy()<CR>", { 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
)

33
lua/ivy/config.lua Normal file
View file

@ -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)

27
lua/ivy/config_spec.lua Normal file
View file

@ -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)

View file

@ -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)

View file

@ -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)

32
lua/ivy/init.lua Normal file
View file

@ -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

30
lua/ivy/init_spec.lua Normal file
View file

@ -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)

36
lua/ivy/libivy_spec.lua Normal file
View file

@ -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)

View file

@ -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)

29
lua/ivy/matcher_spec.lua Normal file
View file

@ -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)

View file

@ -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)

91
lua/ivy/prompt_spec.lua Normal file
View file

@ -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)

View file

@ -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

View file

@ -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, "<cmd>" .. backend.command .. "<CR>", { 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

View file

@ -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", "<Cmd>IvyFd<CR>")
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", "<Cmd>IvyFd<CR>")
assert.is_not_nil(keymap)
end)
it("allows you to override the keymap", function()
register_backend("ivy.backends.files", { keymap = "<C-p>" })
local keymap = get_keymap("n", "<Cmd>IvyFd<CR>")
assert(keymap ~= nil)
assert.are.equal("<C-P>", 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", "<Cmd>IvyFd<CR>")
assert.is_not_nil(keymap)
end)
it("allows you to pass in a hole backend module", function()
register_backend { "ivy.backends.files", { keymap = "<C-p>" } }
local keymap = get_keymap("n", "<Cmd>IvyFd<CR>")
assert(keymap ~= nil)
assert.are.equal("<C-P>", keymap.lhs)
end)
end)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

32
lua/ivy/window_spec.lua Normal file
View file

@ -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)

View file

@ -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)

View file

@ -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, "<cmd>" .. backend.command .. "<CR>", { 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"

View file

@ -26,7 +26,11 @@ pub fn find_files(options: Options) -> Vec<String> {
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;

8
scripts/busted.lua Executable file
View file

@ -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 }