diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..68cf318 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,12 @@ +-- 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", +} diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..ecb6dca --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,6 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferDouble" +call_parentheses = "None" diff --git a/README.md b/README.md new file mode 100644 index 0000000..14b5ab0 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +
+ +# ivy.nvim + +An [ivy-mode](https://github.com/abo-abo/swiper#ivy) port to neovim. Ivy is a +generic completion mechanism for ~~Emacs~~ Nvim + +
+ +## Installation + +### Manually + +```sh +git clone https://github.com/AdeAttwood/ivy.nvim ~/.config/nvim/pack/bundle/start/ivy.nvim +``` + +### Plugin managers + +TODO: Add docs in the plugin managers I don't use any + +## Features + +### Commands + +A command can be run that will launch the completion UI + +| Command | Key Map | Description | +| ---------- | ----------- | ------------------------------------------------------ | +| IvyFd | \p | Find files in your project with the fd cli file finder | +| IvyAg | \/ | Find content in files using the silver searcher | +| IvyBuffers | | Search though open buffers | + +### 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 | + +## API + +```lua + vim.ivy.run( + -- Call back function to get all the candidates that will be displayed in + -- the results window, The `input` will be passed in, so you can filter + -- your results with the value from the prompt + function(input) return { "One", "Two", Three } end, + -- Action callback that will be called on the completion or peek actions. + The currently selected item is passed in as the result. + function(result) vim.cmd("edit " .. result) end + ) +``` + +## Other stuff you might like + +- [ivy-mode](https://github.com/abo-abo/swiper#ivy) - An emacs package that was the inspiration for this nvim plugin +- [Command-T](https://github.com/wincent/command-t) - Vim plugin I used before I started this one +- [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) - Another competition plugin, lots of people are using diff --git a/lua/ivy/controller.lua b/lua/ivy/controller.lua new file mode 100644 index 0000000..57b34fe --- /dev/null +++ b/lua/ivy/controller.lua @@ -0,0 +1,56 @@ +local window = require "ivy.window" +local prompt = require "ivy.prompt" + +local controller = {} + +controller.items = nil +controller.callback = nil + +controller.run = function(items, callback) + controller.callback = callback + controller.items = items + + window.initialize() + controller.input "" +end + +controller.input = function(char) + prompt.input(char) + window.set_items(controller.items(prompt.text())) +end + +controller.search = function(value) + prompt.set(value) + window.set_items(controller.items(prompt.text())) +end + +controller.complete = function() + controller.checkpoint() + controller.destroy() +end + +controller.checkpoint = function() + vim.api.nvim_set_current_win(window.previous) + controller.callback(window.get_current_selection()) + vim.api.nvim_set_current_win(window.window) +end + +controller.next = function() + window.index = window.index + 1 + window.update() +end + +controller.previous = function() + window.index = window.index - 1 + window.update() +end + +controller.destroy = function() + controller.items = nil + controller.callback = nil + + window.destroy() + prompt.destroy() +end + +return controller diff --git a/lua/ivy/prompt.lua b/lua/ivy/prompt.lua new file mode 100644 index 0000000..264cc1b --- /dev/null +++ b/lua/ivy/prompt.lua @@ -0,0 +1,37 @@ +-- The prefix that will be before the search text for the user +local prompt_prefix = ">> " + +local prompt = {} + +prompt.value = "" + +prompt.text = function() + return prompt.value +end +prompt.update = function() + vim.notify(prompt_prefix .. prompt.text()) +end + +prompt.input = function(char) + if char == "BACKSPACE" then + prompt.value = string.sub(prompt.value, 0, -2) + elseif char == "\\\\" then + prompt.value = prompt.value .. "\\" + else + prompt.value = prompt.value .. char + end + + prompt.update() +end + +prompt.set = function(value) + prompt.value = value + prompt.update() +end + +prompt.destroy = function() + prompt.value = "" + vim.notify "" +end + +return prompt diff --git a/lua/ivy/utils.lua b/lua/ivy/utils.lua new file mode 100644 index 0000000..c087167 --- /dev/null +++ b/lua/ivy/utils.lua @@ -0,0 +1,57 @@ +local utils = {} + +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") + if handle == nil then + return {} + end + local result = handle:read "*a" + handle:close() + + return result + end +end + +utils.vimgrep_action = function() + return function(item) + -- 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 + + vim.cmd("edit " .. file) + if line ~= nil then + vim.cmd(line) + end + end +end + +utils.file_action = function() + return function(file) + if file == nil then + return + end + vim.cmd("edit " .. file) + end +end + +return utils diff --git a/lua/ivy/window.lua b/lua/ivy/window.lua new file mode 100644 index 0000000..1c0c7c1 --- /dev/null +++ b/lua/ivy/window.lua @@ -0,0 +1,139 @@ +-- Constent options that will be used for the keymaps +local opts = { noremap = true, silent = true, nowait = true } + +-- All of the base chars that will be used for an "input" operation on the +-- prompt +-- stylua: ignore +local chars = { + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", + "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", + "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "<", ">", "`", "@", "#", "~", "!", + "\"", "$", "%", "^", "&", "/", "(", ")", "=", "+", "*", "-", "_", ".", ",", ";", ":", "?", "\\", "|", "'", "{", "}", + "[", "]", " ", +} + +local function parse_lines(lines) + local items = {} + for line in lines:gmatch "[^\r\n]+" do + table.insert(items, line) + end + + return items +end + +local function parse_array(arr) + return arr +end + +local window = {} + +window.index = 0 +window.previous = nil +window.window = nil +window.buffer = nil + +window.initialize = function() + window.make_buffer() +end + +window.make_buffer = function() + window.previous = vim.api.nvim_get_current_win() + + vim.api.nvim_command "botright split new" + window.buffer = vim.api.nvim_win_get_buf(0) + window.window = vim.api.nvim_get_current_win() + + vim.api.nvim_win_set_option(window.window, "number", false) + vim.api.nvim_win_set_option(window.window, "relativenumber", false) + vim.api.nvim_win_set_option(window.window, "signcolumn", "no") + + vim.api.nvim_buf_set_option(window.buffer, "filetype", "ivy") + vim.api.nvim_buf_set_var(window.buffer, "bufftype", "nofile") + + for index = 1, #chars do + local char = chars[index] + if char == "'" then + char = "\\'" + end + if char == "\\" then + char = "\\\\\\\\" + end + vim.api.nvim_buf_set_keymap(window.buffer, "n", chars[index], "lua vim.ivy.input('" .. char .. "')", opts) + end + + vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.destroy()", opts) + vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.search('')", opts) + vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.next()", opts) + vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.previous()", opts) + vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.next(); vim.ivy.checkpoint()", opts) + vim.api.nvim_buf_set_keymap( + window.buffer, + "n", + "", + "lua vim.ivy.previous(); vim.ivy.checkpoint()", + opts + ) + vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.complete()", opts) + vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.input('BACKSPACE')", opts) +end + +window.get_current_selection = function() + local line = vim.api.nvim_buf_get_lines(window.buffer, window.index, window.index + 1, true) + if line == nil then + line = { "" } + end + + return line[1] +end + +window.get_buffer = function() + if window.buffer == nil then + window.make_buffer() + end + + return window.buffer +end + +window.update = function() + -- TODO(ade): Add a guard in so we can not go out of range on the results buffer + vim.api.nvim_win_set_cursor(window.window, { window.index + 1, 0 }) +end + +window.set_items = function(items) + local lines = {} + + if type(items) == "string" then + lines = parse_lines(items) + elseif type(items) == "table" then + lines = parse_array(items) + end + + if #lines == 0 then + lines = { "-- No Items --" } + end + + vim.api.nvim_buf_set_lines(window.get_buffer(), 0, 9999, false, lines) + + local line_count = #lines + window.index = 0 + + if line_count > 10 then + line_count = 10 + end + vim.api.nvim_win_set_height(window.window, line_count) + + window.update() +end + +window.destroy = function() + if type(window.buffer) == "number" then + vim.api.nvim_buf_delete(window.buffer, { force = true }) + end + + window.buffer = nil + window.window = nil + window.previous = nil + window.index = 0 +end + +return window diff --git a/plugin/ivy.lua b/plugin/ivy.lua new file mode 100644 index 0000000..96bdf5f --- /dev/null +++ b/plugin/ivy.lua @@ -0,0 +1,33 @@ +local controller = require "ivy.controller" +local utils = require "ivy.utils" + +-- Put the controller in to the vim global so we can access it in mappings +-- better without requires. You can call controller commands like `vim.ivy.xxx`. +vim.ivy = controller + +vim.api.nvim_create_user_command("IvyAg", function() + vim.ivy.run(utils.command_finder "ag", utils.vimgrep_action()) +end, { bang = true, desc = "Run ag to search for content in files" }) + +vim.api.nvim_create_user_command("IvyFd", function() + vim.ivy.run(utils.command_finder("fd --hidden --type f --exclude .git", 0), utils.file_action()) +end, { bang = true, desc = "Find files in the project" }) + +vim.api.nvim_create_user_command("IvyBuffers", function() + vim.ivy.run(function(input) + local list = {} + local buffers = vim.api.nvim_list_bufs() + for index = 1, #buffers do + local buffer = buffers[index] + local buffer_name = vim.api.nvim_buf_get_name(buffer) + if vim.api.nvim_buf_is_loaded(buffer) and #buffer_name > 0 then + table.insert(list, buffer_name) + end + end + + return list + end, utils.file_action()) +end, { bang = true, desc = "List all of the current open buffers" }) + +vim.api.nvim_set_keymap("n", "p", "IvyFd", { nowait = true, silent = true }) +vim.api.nvim_set_keymap("n", "/", "IvyAg", { nowait = true, silent = true })