feat: add fuzzy matching to recent files list with fuzzig
Many thanks to @fjebaker
This commit is contained in:
parent
5375e1449e
commit
0f5f41751e
5 changed files with 113 additions and 11 deletions
|
@ -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") },
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Reference in a new issue