diff --git a/lua/ivy/libivy.lua b/lua/ivy/libivy.lua index 0b00c2f..cc162ad 100644 --- a/lua/ivy/libivy.lua +++ b/lua/ivy/libivy.lua @@ -1,29 +1,32 @@ local library_path = (function() local dirname = string.sub(debug.getinfo(1).source, 2, #"/fzf_lib.lua" * -1) return dirname .. "/../../target/release/libivyrs.so" end)() local ffi = require "ffi" local ivy_c = ffi.load(library_path) ffi.cdef [[ + typedef struct { int score; const char* content; } match; + typedef struct { int len; match* matches; } match_list; + void ivy_init(const char*); int ivy_match(const char*, const char*); - char* ivy_files(const char*, const char*); + match_list* ivy_files(const char*, const char*); ]] local libivy = {} libivy.ivy_init = function(dir) ivy_c.ivy_init(dir) 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)) + return ivy_c.ivy_files(pattern, base_dir) end return libivy diff --git a/lua/ivy/libivy_test.lua b/lua/ivy/libivy_test.lua index 28fb527..9ecf50a 100644 --- a/lua/ivy/libivy_test.lua +++ b/lua/ivy/libivy_test.lua @@ -1,9 +1,10 @@ local libivy = require "ivy.libivy" +local ffi = require "ffi" 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) diff --git a/lua/ivy/window.lua b/lua/ivy/window.lua index addefc1..53a2ca2 100644 --- a/lua/ivy/window.lua +++ b/lua/ivy/window.lua @@ -1,144 +1,152 @@ +local ffi = require "ffi" + -- 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 items = {} for line in lines:gmatch "[^\r\n]+" do table.insert(items, line) end return items end local function set_items_string(buffer, lines) - vim.api.nvim_buf_set_lines(buffer, 0, 9999, false, string_to_table(lines)) + vim.api.nvim_buf_set_lines(buffer, 0, -1, false, string_to_table(lines)) end local function set_items_array(buffer, lines) if type(lines[1]) == "string" then - vim.api.nvim_buf_set_lines(buffer, 0, 9999, false, lines) + vim.api.nvim_buf_set_lines(buffer, 0, -1, false, lines) else for i = 1, #lines do vim.api.nvim_buf_set_lines(buffer, i - 1, 9999, false, { lines[i][2] }) end end end local window = {} window.index = 0 window.origin = nil window.window = nil window.buffer = nil window.initialize = function() window.make_buffer() end window.make_buffer = function() window.origin = 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) vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.input('LEFT')", opts) vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.input('RIGHT')", opts) vim.api.nvim_buf_set_keymap(window.buffer, "n", "", "lua vim.ivy.input('DELETE_WORD')", 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 #items == 0 then + vim.api.nvim_buf_set_lines(window.get_buffer(), 0, -1, false, {}) + + if items.len ~= nil then + for i = 0, items.len - 1 do + vim.api.nvim_buf_set_lines(window.get_buffer(), i - 1, -1, false, { ffi.string(items.matches[i].content) }) + end + elseif #items == 0 then vim.api.nvim_buf_set_lines(window.get_buffer(), 0, 9999, false, { "-- No Items --" }) elseif type(items) == "string" then set_items_string(window.get_buffer(), items) elseif type(items) == "table" then set_items_array(window.get_buffer(), items) end local line_count = vim.api.nvim_buf_line_count(window.buffer) window.index = line_count - 1 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.origin = nil window.index = 0 end return window diff --git a/rust/lib.rs b/rust/lib.rs index e2aca28..5ca802d 100644 --- a/rust/lib.rs +++ b/rust/lib.rs @@ -1,86 +1,90 @@ 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::mem; + +#[repr(C)] +pub struct FFiMatchList { + len: c_int, + matches: *mut sorter::Match +} #[macro_use] extern crate lazy_static; lazy_static! { static ref GLOBAL_FILE_CACHE: Mutex>> = Mutex::new(HashMap::new()); } fn to_string(input: *const c_char) -> String { unsafe { CStr::from_ptr(input) } .to_str() .unwrap() .to_string() } +fn to_ffi_match_list(mut list: Vec) -> *const FFiMatchList { + list.shrink_to_fit(); + let matches = list.as_mut_ptr(); + let len: c_int = list.len().try_into().unwrap(); + mem::forget(list); + + return Box::into_raw(Box::new(FFiMatchList { len, matches })) +} + fn get_files(directory: &String) -> Vec { let mut cache = GLOBAL_FILE_CACHE.lock().unwrap(); if !cache.contains_key(directory) { let finder_options = finder::Options { directory: directory.clone(), }; cache.insert(directory.clone(), finder::find_files(finder_options)); } return 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_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 } #[no_mangle] -pub extern "C" fn ivy_files(c_pattern: *const c_char, c_base_dir: *const c_char) -> *const c_char { +pub extern "C" fn ivy_files(c_pattern: *const c_char, c_base_dir: *const c_char) -> *const FFiMatchList { 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() + return inner_files(pattern, directory); } -pub fn inner_files(pattern: String, base_dir: String) -> String { - let mut output = String::new(); - +pub fn inner_files(pattern: String, base_dir: String) -> *const FFiMatchList { // Bail out early if the pattern is empty; it's never going to find anything if pattern.is_empty() { - return output; + return to_ffi_match_list(Vec::new()); } 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 + to_ffi_match_list(sorter::sort_strings(sorter_options, files)) } diff --git a/rust/sorter.rs b/rust/sorter.rs index f81f622..3009830 100644 --- a/rust/sorter.rs +++ b/rust/sorter.rs @@ -1,36 +1,43 @@ use super::matcher; use rayon::prelude::*; +use std::os::raw::{c_char, c_int}; +use std::ffi::CString; +#[repr(C)] pub struct Match { - pub score: i64, - pub content: String, + pub score: c_int, + pub content: *const c_char + // pub score: i64, + // pub content: String, } +unsafe impl Send for Match {} + pub struct Options { pub pattern: String, pub minimum_score: i64, } impl Options { pub fn new(pattern: String) -> Self { Self { pattern, minimum_score: 25, } } } pub fn sort_strings(options: Options, strings: Vec) -> Vec { let matcher = matcher::Matcher::new(options.pattern); let mut matches = strings .into_par_iter() .map(|candidate| Match { - score: matcher.score(candidate.as_str()), - content: candidate, + score: matcher.score(candidate.as_str()) as i32, + content: CString::new(candidate.clone().to_string()).unwrap().into_raw(), }) - .filter(|m| m.score > options.minimum_score) + .filter(|m| m.score > options.minimum_score as i32) .collect::>(); matches.par_sort_unstable_by(|a, b| a.score.cmp(&b.score)); matches }