Page MenuHomePhorge

No OneTemporary

diff --git a/lua/ivy/libivy.lua b/lua/ivy/libivy.lua
index f9f8857..36946dc 100644
--- a/lua/ivy/libivy.lua
+++ b/lua/ivy/libivy.lua
@@ -1,43 +1,71 @@
local library_path = (function()
local root = string.sub(debug.getinfo(1).source, 2, #"/libivy.lua" * -1)
local release_path = root .. "../../target/release"
return package.searchpath("libivyrs", release_path .. "/?.so;" .. release_path .. "/?.dylib;")
end)()
local ffi = require "ffi"
local ok, ivy_c = pcall(ffi.load, library_path)
if not ok then
vim.api.nvim_err_writeln(
"libivyrs.so not found! Please ensure you have complied the shared library."
.. " For more info refer to the documentation, https://github.com/AdeAttwood/ivy.nvim#compiling"
)
return
end
ffi.cdef [[
void ivy_init(const char*);
char* ivy_cwd();
int ivy_match(const char*, const char*);
char* ivy_files(const char*, const char*);
+
+ int ivy_files_iter(const char*, const char*);
+ int ivy_files_iter_len(int);
+ char* ivy_files_iter_at(int, int);
+ void ivy_files_iter_delete(int);
]]
+local iter_mt = {
+ __len = function(self)
+ return self.length
+ end,
+ __index = function(self, index)
+ -- Pass in our index -1. This will map lua's one based indexing to zero
+ -- based indexing that we are using in the rust lib.
+ local item = ffi.string(ivy_c.ivy_files_iter_at(self.id, index - 1))
+ return { content = item }
+ end,
+ __newindex = function(_, _, _)
+ error("attempt to update a read-only table", 2)
+ end,
+ __gc = function(self)
+ ivy_c.ivy_files_iter_delete(self.id)
+ end,
+}
+
local libivy = {}
libivy.ivy_init = function(dir)
ivy_c.ivy_init(dir)
end
libivy.ivy_cwd = function()
return ffi.string(ivy_c.ivy_cwd())
end
libivy.ivy_match = function(pattern, text)
return ivy_c.ivy_match(pattern, text)
end
libivy.ivy_files = function(pattern, base_dir)
- return ffi.string(ivy_c.ivy_files(pattern, base_dir))
+ local iter_id = ivy_c.ivy_files_iter(pattern, base_dir)
+ local iter_len = ivy_c.ivy_files_iter_len(iter_id)
+ local iter = { id = iter_id, length = iter_len }
+ setmetatable(iter, iter_mt)
+
+ return iter
end
return libivy
diff --git a/lua/ivy/libivy_test.lua b/lua/ivy/libivy_test.lua
index c7fe89b..fe18455 100644
--- a/lua/ivy/libivy_test.lua
+++ b/lua/ivy/libivy_test.lua
@@ -1,27 +1,46 @@
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 matches = libivy.ivy_files(".github/workflows/ci.yml", current_dir)
+ local results = libivy.ivy_files(".github/workflows/ci.yml", current_dir)
- local results = {}
- for line in string.gmatch(matches, "[^\r\n]+") do
- table.insert(results, line)
+ if results.length ~= 2 then
+ t.error("Incorrect number of results found " .. results.length)
end
- if #results ~= 2 then
- t.error "Incorrect number of results"
+ 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 results[2] ~= ".github/workflows/ci.yml" then
- t.error("Invalid matches: " .. results[2])
+ if type(mt["__len"]) ~= "function" then
+ t.error "The iterator does not have an __len metamethod"
end
end)
diff --git a/lua/ivy/window.lua b/lua/ivy/window.lua
index c53d6e1..087e94b 100644
--- a/lua/ivy/window.lua
+++ b/lua/ivy/window.lua
@@ -1,147 +1,167 @@
-- 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 string_to_table(lines)
local matches = {}
for line in lines:gmatch "[^\r\n]+" do
table.insert(matches, { content = line })
end
return matches
end
+local function get_items_length(items)
+ local mt = getmetatable(items)
+ if mt ~= nil and mt.__len ~= nil then
+ return mt.__len(items)
+ end
+
+ return #items
+end
+
+local function call_gc(items)
+ local mt = getmetatable(items)
+ if mt ~= nil and mt.__gc ~= nil then
+ return mt.__gc(items)
+ end
+end
+
local window = {}
window.index = 0
window.origin = nil
window.window = nil
window.buffer = nil
window.origin_buffer = nil
window.initialize = function()
window.make_buffer()
end
window.make_buffer = function()
window.origin = vim.api.nvim_get_current_win()
window.origin_buffer = vim.api.nvim_win_get_buf(0)
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], "<cmd>lua vim.ivy.input('" .. char .. "')<CR>", opts)
end
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<C-c>", "<cmd>lua vim.ivy.destroy()<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<C-u>", "<cmd>lua vim.ivy.search('')<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<C-n>", "<cmd>lua vim.ivy.next()<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<C-p>", "<cmd>lua vim.ivy.previous()<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<C-M-n>", "<cmd>lua vim.ivy.next(); vim.ivy.checkpoint()<CR>", opts)
vim.api.nvim_buf_set_keymap(
window.buffer,
"n",
"<C-M-p>",
"<cmd>lua vim.ivy.previous(); vim.ivy.checkpoint()<CR>",
opts
)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<CR>", "<cmd>lua vim.ivy.complete(vim.ivy.action.EDIT)<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<C-v>", "<cmd>lua vim.ivy.complete(vim.ivy.action.VSPLIT)<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<C-s>", "<cmd>lua vim.ivy.complete(vim.ivy.action.SPLIT)<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<BS>", "<cmd>lua vim.ivy.input('BACKSPACE')<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<Left>", "<cmd>lua vim.ivy.input('LEFT')<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<Right>", "<cmd>lua vim.ivy.input('RIGHT')<CR>", opts)
vim.api.nvim_buf_set_keymap(window.buffer, "n", "<C-w>", "<cmd>lua vim.ivy.input('DELETE_WORD')<CR>", 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()
vim.api.nvim_win_set_cursor(window.window, { window.index + 1, 0 })
end
window.set_items = function(items)
if type(items) == "string" then
items = string_to_table(items)
end
+ local items_length = get_items_length(items)
+
-- TODO(ade): Validate the items are in the correct format. This also need to
-- come with some descriptive messages and possible help.
-- Display no items text if there are no items to dispaly
- if #items == 0 then
+ if items_length == 0 then
+ items_length = 1
items = { { content = "-- No Items --" } }
end
- local items_length = #items
window.index = items_length - 1
for index = 1, items_length do
vim.api.nvim_buf_set_lines(window.buffer, index - 1, -1, false, { items[index].content })
end
-- Limit the results window size to 10 so when there are lots of results the
-- window does not take up the hole terminal
local line_count = items_length
if line_count > 10 then
line_count = 10
end
vim.api.nvim_win_set_height(window.window, line_count)
window.update()
+
+ call_gc(items)
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.origin = nil
window.origin_buffer = nil
window.index = 0
end
return window
diff --git a/lua/ivy/window_test.lua b/lua/ivy/window_test.lua
index c42b380..dcad637 100644
--- a/lua/ivy/window_test.lua
+++ b/lua/ivy/window_test.lua
@@ -1,23 +1,33 @@
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/rust/lib.rs b/rust/lib.rs
index 6d9ba73..a4ac000 100644
--- a/rust/lib.rs
+++ b/rust/lib.rs
@@ -1,107 +1,179 @@
mod finder;
mod matcher;
mod sorter;
use std::collections::HashMap;
use std::ffi::CStr;
use std::ffi::CString;
use std::os::raw::{c_char, c_int};
use std::sync::Mutex;
use std::sync::OnceLock;
+// A store to the singleton instance of the ivy struct. This must not be accessed directly it must
+// be use via the Ivy::global() function. Accessing this directly may cause a panic if its been
+// initialized correctly.
+static INSTANCE: OnceLock<Mutex<Ivy>> = OnceLock::new();
+
struct Ivy {
+ // The file cache so we don't have to keep iterating the filesystem. The map key is the root
+ // directory that has been search and the value an a vector containing all of the files that as
+ // in the root. The value will be relative from the root.
pub file_cache: HashMap<String, Vec<String>>,
+ // The sequence number of the last iterator created. This will use as a pointer value to the
+ // iterator so we can access it though lua and rust without having to copy strings.
+ pub iter_sequence: i32,
+ // A store of all the iterators that have been created. The key is the sequence number and the
+ // value is the vector of matches that were matched in the search.
+ pub iter_map: HashMap<i32, Vec<CString>>,
}
-static INSTANCE: OnceLock<Mutex<Ivy>> = OnceLock::new();
-
impl Ivy {
- pub fn new() -> Self {
- Self {
- file_cache: HashMap::new(),
- }
- }
-
+ // Get the global instance of the ivy struct. This will initialize the struct if it has not
+ // initialized yet.
pub fn global() -> &'static Mutex<Ivy> {
- INSTANCE.get_or_init(|| Mutex::new(Ivy::new()))
+ INSTANCE.get_or_init(|| {
+ Mutex::new(Ivy {
+ file_cache: HashMap::new(),
+ iter_sequence: 0,
+ iter_map: HashMap::new(),
+ })
+ })
}
}
fn to_string(input: *const c_char) -> String {
unsafe { CStr::from_ptr(input) }
.to_str()
.unwrap()
.to_string()
}
fn get_files(directory: &String) -> Vec<String> {
let mut ivy = Ivy::global().lock().unwrap();
if !ivy.file_cache.contains_key(directory) {
let finder_options = finder::Options {
directory: directory.clone(),
};
ivy.file_cache
.insert(directory.clone(), finder::find_files(finder_options));
}
return ivy.file_cache.get(directory).unwrap().to_vec();
}
#[no_mangle]
pub extern "C" fn ivy_init(c_base_dir: *const c_char) {
let directory = to_string(c_base_dir);
get_files(&directory);
}
#[no_mangle]
pub extern "C" fn ivy_cwd() -> *const c_char {
return CString::new(std::env::current_dir().unwrap().to_str().unwrap())
.unwrap()
.into_raw();
}
#[no_mangle]
pub extern "C" fn ivy_match(c_pattern: *const c_char, c_text: *const c_char) -> c_int {
let pattern = to_string(c_pattern);
let text = to_string(c_text);
inner_match(pattern, text)
}
pub fn inner_match(pattern: String, text: String) -> i32 {
let m = matcher::Matcher::new(pattern);
m.score(text.as_str()) as i32
}
+// Create a new iterator that will iterate over all the files in the given directory that match a
+// pattern. It will return the pointer to the iterator so it can be retrieve later. The iterator
+// can be deleted with `ivy_files_iter_delete`
+#[no_mangle]
+pub extern "C" fn ivy_files_iter(c_pattern: *const c_char, c_base_dir: *const c_char) -> i32 {
+ let directory = to_string(c_base_dir);
+ let pattern = to_string(c_pattern);
+
+ let files = get_files(&directory);
+
+ let mut ivy = Ivy::global().lock().unwrap();
+
+ // Convert the matches into CStrings so we can pass the pointers out while still maintaining
+ // ownership. If we didn't do this the CString would be dropped and the pointer would be freed
+ // while its being used externally.
+ let sorter_options = sorter::Options::new(pattern);
+ let matches = sorter::sort_strings(sorter_options, files)
+ .into_iter()
+ .map(|m| CString::new(m.content.as_str()).unwrap())
+ .collect::<Vec<CString>>();
+
+ ivy.iter_sequence += 1;
+ let new_sequence = ivy.iter_sequence;
+ ivy.iter_map.insert(new_sequence, matches);
+
+ new_sequence
+}
+
+// Delete the iterator with the given id. This will free the memory used by the iterator that was
+// created with `ivy_files_iter`
+#[no_mangle]
+pub extern "C" fn ivy_files_iter_delete(iter_id: i32) {
+ let mut ivy = Ivy::global().lock().unwrap();
+ ivy.iter_map.remove(&iter_id);
+}
+
+// Returns the length of a given iterator. This will return the number of items that were matched
+// when the iterator was created with `ivy_files_iter`
+#[no_mangle]
+pub extern "C" fn ivy_files_iter_len(iter_id: i32) -> i32 {
+ let ivy = Ivy::global().lock().unwrap();
+
+ let items = ivy.iter_map.get(&iter_id).unwrap();
+ items.len() as i32
+}
+
+// Returns the item at the given index in the iterator. This will return the full match that was
+// given in the iterator. This will return a pointer to the string so it can be used in lua.
+#[no_mangle]
+pub extern "C" fn ivy_files_iter_at(iter_id: i32, index: i32) -> *const c_char {
+ let ivy = Ivy::global().lock().unwrap();
+
+ let items = ivy.iter_map.get(&iter_id).unwrap();
+ let item = items.get(index as usize).unwrap();
+
+ item.as_ptr()
+}
+
#[no_mangle]
pub extern "C" fn ivy_files(c_pattern: *const c_char, c_base_dir: *const c_char) -> *const c_char {
let pattern = to_string(c_pattern);
let directory = to_string(c_base_dir);
let output = inner_files(pattern, directory);
CString::new(output).unwrap().into_raw()
}
pub fn inner_files(pattern: String, base_dir: String) -> String {
let mut output = String::new();
// Bail out early if the pattern is empty; it's never going to find anything
if pattern.is_empty() {
return output;
}
let files = get_files(&base_dir);
let sorter_options = sorter::Options::new(pattern);
let files = sorter::sort_strings(sorter_options, files);
for file in files.iter() {
output.push_str(&file.content);
output.push('\n');
}
output
}

File Metadata

Mime Type
text/x-diff
Expires
Wed, Sep 10, 5:22 PM (7 h, 25 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
9039
Default Alt Text
(16 KB)

Event Timeline