Compare commits

...

2 commits

7 changed files with 185 additions and 30 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String, Vec<String>>,
}
// 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();
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<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>>,
}
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<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(),
})
})
}
}
@ -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::<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);

View file

@ -17,8 +17,7 @@ impl Matcher {
pub fn score(&self, text: &str) -> i64 {
self.matcher
.fuzzy_indices(text, &self.pattern)
.map(|(score, _indices)| score)
.fuzzy_match(text, &self.pattern)
.unwrap_or_default()
}
}

View file

@ -25,12 +25,19 @@ pub fn sort_strings(options: Options, strings: Vec<String>) -> Vec<Match> {
let mut matches = strings
.into_par_iter()
.map(|candidate| Match {
score: matcher.score(candidate.as_str()),
content: candidate,
.filter_map(|candidate| {
let score = matcher.score(candidate.as_str());
if score > options.minimum_score {
return None;
}
Some(Match {
score,
content: candidate,
})
})
.filter(|m| m.score > options.minimum_score)
.collect::<Vec<Match>>();
matches.par_sort_unstable_by(|a, b| a.score.cmp(&b.score));
matches
}