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 @@ -20,8 +20,31 @@ ffi.cdef [[ 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) @@ -37,7 +60,12 @@ libivy.ivy_match = function(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 @@ -10,18 +10,37 @@ 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" - end - - if results[2] ~= ".github/workflows/ci.yml" then - t.error("Invalid matches: " .. results[2]) + 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/window.lua b/lua/ivy/window.lua index c53d6e1..087e94b 100644 --- a/lua/ivy/window.lua +++ b/lua/ivy/window.lua @@ -21,6 +21,22 @@ local function string_to_table(lines) 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 @@ -106,15 +122,17 @@ window.set_items = function(items) 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 @@ -130,6 +148,8 @@ window.set_items = function(items) vim.api.nvim_win_set_height(window.window, line_count) window.update() + + call_gc(items) end window.destroy = function() 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 @@ -21,3 +21,13 @@ it("can set items", function(t) 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 @@ -9,21 +9,35 @@ use std::os::raw::{c_char, c_int}; use std::sync::Mutex; use std::sync::OnceLock; -struct Ivy { - pub file_cache: HashMap>, -} - +// 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> = OnceLock::new(); -impl Ivy { - pub fn new() -> Self { - Self { - file_cache: HashMap::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>, + // 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>, +} +impl Ivy { + // Get the global instance of the ivy struct. This will initialize the struct if it has not + // initialized yet. pub fn global() -> &'static Mutex { - 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(), + }) + }) } } @@ -75,6 +89,64 @@ pub fn inner_match(pattern: String, text: String) -> i32 { 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::>(); + + 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);