feat: add fuzzy matching to recent files list with fuzzig

Many thanks to @fjebaker
This commit is contained in:
CJ van den Berg 2024-04-15 18:59:23 +02:00
parent 5375e1449e
commit 0f5f41751e
5 changed files with 113 additions and 11 deletions

View file

@ -66,6 +66,11 @@ pub fn build(b: *std.Build) void {
.optimize = dependency_optimize,
});
const fuzzig_dep = b.dependency("fuzzig", .{
.target = target,
.optimize = dependency_optimize,
});
const tracy_dep = if (tracy_enabled) b.dependency("tracy", .{
.target = target,
.optimize = dependency_optimize,
@ -147,6 +152,7 @@ pub fn build(b: *std.Build) void {
.{ .name = "tracy", .module = tracy_mod },
.{ .name = "syntax", .module = syntax_dep.module("syntax") },
.{ .name = "dizzy", .module = dizzy_dep.module("dizzy") },
.{ .name = "fuzzig", .module = fuzzig_dep.module("fuzzig") },
},
});

View file

@ -31,6 +31,10 @@
.url = "https://github.com/neurocyte/flow-syntax/archive/9f60a6d13b61511de6202bf96b4e85d1caae981e.tar.gz",
.hash = "122089ff4be8879c8be3d66bc73d745cc0b32cb27c1b70cd6265dc27461072b60485",
},
.fuzzig = .{
.url = "https://github.com/fjebaker/fuzzig/archive/c6a0e0ca1a24e55ebdce51c83b918d4325ca7032.tar.gz",
.hash = "1220214dfb9a0806d9c8a059beb9e3b07811fd138cd5baeb9d1da432588920a084bf",
},
},
.paths = .{
"include",

View file

@ -4,6 +4,7 @@ const cbor = @import("cbor");
const root = @import("root");
const dizzy = @import("dizzy");
const Buffer = @import("Buffer");
const fuzzig = @import("fuzzig");
const builtin = @import("builtin");
const LSP = @import("LSP.zig");
@ -129,13 +130,17 @@ pub fn request_recent_files(self: *Self, from: tp.pid_ref, max: usize) error{ Ou
}
}
pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []const u8) error{ OutOfMemory, Exit }!usize {
fn simple_query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []const u8) error{ OutOfMemory, Exit }!usize {
var i: usize = 0;
defer from.send(.{ "PRJ", "recent_done", query }) catch {};
for (self.files.items) |file| {
if (file.path.len < query.len) continue;
if (std.mem.indexOf(u8, file.path, query)) |_| {
try from.send(.{ "PRJ", "recent", file.path });
if (std.mem.indexOf(u8, file.path, query)) |idx| {
switch (query.len) {
1 => try from.send(.{ "PRJ", "recent", file.path, .{idx} }),
2 => try from.send(.{ "PRJ", "recent", file.path, .{ idx, idx + 1 } }),
else => try from.send(.{ "PRJ", "recent", file.path }),
}
i += 1;
if (i >= max) return i;
}
@ -143,6 +148,50 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co
return i;
}
pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []const u8) error{ OutOfMemory, Exit }!usize {
if (query.len < 3)
return self.simple_query_recent_files(from, max, query);
defer from.send(.{ "PRJ", "recent_done", query }) catch {};
var searcher = try fuzzig.Ascii.init(
self.a,
std.fs.max_path_bytes, // haystack max size
std.fs.max_path_bytes, // needle max size
.{ .case_sensitive = false },
);
defer searcher.deinit();
const Match = struct {
path: []const u8,
score: i32,
matches: []const usize,
};
var matches = std.ArrayList(Match).init(self.a);
for (self.files.items) |file| {
const match = searcher.scoreMatches(file.path, query);
if (match.score) |score| {
(try matches.addOne()).* = .{
.path = file.path,
.score = score,
.matches = try self.a.dupe(usize, match.matches),
};
}
}
if (matches.items.len == 0) return 0;
const less_fn = struct {
fn less_fn(_: void, lhs: Match, rhs: Match) bool {
return lhs.score > rhs.score;
}
}.less_fn;
std.mem.sort(Match, matches.items, {}, less_fn);
for (matches.items[0..@min(max, matches.items.len)]) |match|
try from.send(.{ "PRJ", "recent", match.path, match.matches });
return @min(max, matches.items.len);
}
pub fn update_mru(self: *Self, file_path: []const u8, row: usize, col: usize) !void {
defer self.sort_files_by_mtime();
try self.update_mru_internal(file_path, std.time.nanoTimestamp(), row, col);

View file

@ -237,11 +237,11 @@ const Process = struct {
fn query_recent_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize, query: []const u8) error{ OutOfMemory, Exit }!void {
const project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project");
// const start_time = std.time.milliTimestamp();
// project.sort_files_by_mtime();
const start_time = std.time.milliTimestamp();
const matched = try project.query_recent_files(from, max, query);
_ = matched;
// self.logger.print("queried: {s} for {s} match {d} in {d} ms", .{ project_directory, query, matched, std.time.milliTimestamp() - start_time });
const query_time = std.time.milliTimestamp() - start_time;
if (query_time > 250)
self.logger.print("query \"{s}\" matched {d}/{d} in {d} ms", .{ query, matched, project.files.items.len, query_time });
}
fn did_open(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, file_type: []const u8, language_server: []const u8, version: usize, text: []const u8) tp.result {

View file

@ -2,6 +2,7 @@ const std = @import("std");
const nc = @import("notcurses");
const tp = @import("thespian");
const log = @import("log");
const cbor = @import("cbor");
const tui = @import("../../tui.zig");
const command = @import("../../command.zig");
@ -68,15 +69,34 @@ fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *c
try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_OPAQUE, nc.ALPHA_OPAQUE);
button.plane.erase();
button.plane.home();
var file_path: []const u8 = undefined;
var iter = button.opts.label; // label contains cbor, first the file name, then multiple match indexes
if (!(cbor.matchString(&iter, &file_path) catch false))
file_path = "#ERROR#";
const pointer = if (selected) "" else " ";
var buf: [max_menu_width]u8 = undefined;
_ = button.plane.print("{s}{s} ", .{
pointer,
if (button.opts.label.len > max_menu_width - 2) shorten_path(&buf, button.opts.label) else button.opts.label,
if (file_path.len > max_menu_width - 2) shorten_path(&buf, file_path) else file_path,
}) catch {};
var index: usize = 0;
var len = cbor.decodeArrayHeader(&iter) catch return false;
while (len > 0) : (len -= 1) {
if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) {
render_cell(button.plane, 0, index + 1, theme.editor_match) catch break;
} else break;
}
return false;
}
fn render_cell(plane: nc.Plane, y: usize, x: usize, style: Widget.Theme.Style) !void {
plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return;
var cell = plane.cell_init();
_ = plane.at_cursor_cell(&cell) catch return;
tui.set_cell_style(&cell, style);
_ = plane.putc(&cell) catch {};
}
fn on_resize_menu(self: *Self, state: *Menu.State(*Self), box: Widget.Box) void {
const w = @min(box.w, @min(self.longest, max_menu_width) + 2);
self.menu.resize(.{
@ -88,8 +108,11 @@ fn on_resize_menu(self: *Self, state: *Menu.State(*Self), box: Widget.Box) void
}
fn menu_action_open_file(menu: **Menu.State(*Self), button: *Button.State(*Menu.State(*Self))) void {
var file_path: []const u8 = undefined;
var iter = button.opts.label;
if (!(cbor.matchString(&iter, &file_path) catch false)) return;
tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("navigate", e);
tp.self_pid().send(.{ "cmd", "navigate", .{ .file = button.label.items } }) catch |e| menu.*.opts.ctx.logger.err("navigate", e);
tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| menu.*.opts.ctx.logger.err("navigate", e);
}
fn shorten_path(buf: []u8, path: []const u8) []const u8 {
@ -112,6 +135,15 @@ fn shorten_path(buf: []u8, path: []const u8) []const u8 {
return stream.getWritten();
}
fn add_item(self: *Self, file_name: []const u8, matches: ?[]const u8) !void {
var label = std.ArrayList(u8).init(self.a);
defer label.deinit();
const writer = label.writer();
try cbor.writeValue(writer, file_name);
if (matches) |cb| _ = try writer.write(cb);
try self.menu.add_item_with_handler(label.items, menu_action_open_file);
}
fn receive_project_manager(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "PRJ", tp.more })) {
try self.process_project_manager(m);
@ -122,11 +154,22 @@ fn receive_project_manager(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit
fn process_project_manager(self: *Self, m: tp.message) tp.result {
var file_name: []const u8 = undefined;
var matches: []const u8 = undefined;
var query: []const u8 = undefined;
if (try m.match(.{ "PRJ", "recent", tp.extract(&file_name) })) {
if (try m.match(.{ "PRJ", "recent", tp.extract(&file_name), tp.extract_cbor(&matches) })) {
if (self.need_reset) self.reset_results();
self.longest = @max(self.longest, file_name.len);
self.menu.add_item_with_handler(file_name, menu_action_open_file) catch |e| return tp.exit_error(e);
self.add_item(file_name, matches) catch |e| return tp.exit_error(e);
self.menu.resize(.{ .y = 0, .x = 25, .w = @min(self.longest, max_menu_width) + 2 });
if (self.need_select_first) {
self.menu.select_down();
self.need_select_first = false;
}
tui.need_render();
} else if (try m.match(.{ "PRJ", "recent", tp.extract(&file_name) })) {
if (self.need_reset) self.reset_results();
self.longest = @max(self.longest, file_name.len);
self.add_item(file_name, null) catch |e| return tp.exit_error(e);
self.menu.resize(.{ .y = 0, .x = 25, .w = @min(self.longest, max_menu_width) + 2 });
if (self.need_select_first) {
self.menu.select_down();