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,
|
.optimize = dependency_optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fuzzig_dep = b.dependency("fuzzig", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = dependency_optimize,
|
||||||
|
});
|
||||||
|
|
||||||
const tracy_dep = if (tracy_enabled) b.dependency("tracy", .{
|
const tracy_dep = if (tracy_enabled) b.dependency("tracy", .{
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = dependency_optimize,
|
.optimize = dependency_optimize,
|
||||||
|
@ -147,6 +152,7 @@ pub fn build(b: *std.Build) void {
|
||||||
.{ .name = "tracy", .module = tracy_mod },
|
.{ .name = "tracy", .module = tracy_mod },
|
||||||
.{ .name = "syntax", .module = syntax_dep.module("syntax") },
|
.{ .name = "syntax", .module = syntax_dep.module("syntax") },
|
||||||
.{ .name = "dizzy", .module = dizzy_dep.module("dizzy") },
|
.{ .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",
|
.url = "https://github.com/neurocyte/flow-syntax/archive/9f60a6d13b61511de6202bf96b4e85d1caae981e.tar.gz",
|
||||||
.hash = "122089ff4be8879c8be3d66bc73d745cc0b32cb27c1b70cd6265dc27461072b60485",
|
.hash = "122089ff4be8879c8be3d66bc73d745cc0b32cb27c1b70cd6265dc27461072b60485",
|
||||||
},
|
},
|
||||||
|
.fuzzig = .{
|
||||||
|
.url = "https://github.com/fjebaker/fuzzig/archive/c6a0e0ca1a24e55ebdce51c83b918d4325ca7032.tar.gz",
|
||||||
|
.hash = "1220214dfb9a0806d9c8a059beb9e3b07811fd138cd5baeb9d1da432588920a084bf",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
.paths = .{
|
.paths = .{
|
||||||
"include",
|
"include",
|
||||||
|
|
|
@ -4,6 +4,7 @@ const cbor = @import("cbor");
|
||||||
const root = @import("root");
|
const root = @import("root");
|
||||||
const dizzy = @import("dizzy");
|
const dizzy = @import("dizzy");
|
||||||
const Buffer = @import("Buffer");
|
const Buffer = @import("Buffer");
|
||||||
|
const fuzzig = @import("fuzzig");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
const LSP = @import("LSP.zig");
|
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;
|
var i: usize = 0;
|
||||||
defer from.send(.{ "PRJ", "recent_done", query }) catch {};
|
defer from.send(.{ "PRJ", "recent_done", query }) catch {};
|
||||||
for (self.files.items) |file| {
|
for (self.files.items) |file| {
|
||||||
if (file.path.len < query.len) continue;
|
if (file.path.len < query.len) continue;
|
||||||
if (std.mem.indexOf(u8, file.path, query)) |_| {
|
if (std.mem.indexOf(u8, file.path, query)) |idx| {
|
||||||
try from.send(.{ "PRJ", "recent", file.path });
|
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;
|
i += 1;
|
||||||
if (i >= max) return i;
|
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;
|
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 {
|
pub fn update_mru(self: *Self, file_path: []const u8, row: usize, col: usize) !void {
|
||||||
defer self.sort_files_by_mtime();
|
defer self.sort_files_by_mtime();
|
||||||
try self.update_mru_internal(file_path, std.time.nanoTimestamp(), row, col);
|
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 {
|
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 project = if (self.projects.get(project_directory)) |p| p else return tp.exit("No project");
|
||||||
// const start_time = std.time.milliTimestamp();
|
const start_time = std.time.milliTimestamp();
|
||||||
// project.sort_files_by_mtime();
|
|
||||||
const matched = try project.query_recent_files(from, max, query);
|
const matched = try project.query_recent_files(from, max, query);
|
||||||
_ = matched;
|
const query_time = std.time.milliTimestamp() - start_time;
|
||||||
// self.logger.print("queried: {s} for {s} match {d} in {d} ms", .{ project_directory, query, matched, 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 {
|
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 nc = @import("notcurses");
|
||||||
const tp = @import("thespian");
|
const tp = @import("thespian");
|
||||||
const log = @import("log");
|
const log = @import("log");
|
||||||
|
const cbor = @import("cbor");
|
||||||
|
|
||||||
const tui = @import("../../tui.zig");
|
const tui = @import("../../tui.zig");
|
||||||
const command = @import("../../command.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);
|
try tui.set_base_style_alpha(button.plane, " ", style_base, nc.ALPHA_OPAQUE, nc.ALPHA_OPAQUE);
|
||||||
button.plane.erase();
|
button.plane.erase();
|
||||||
button.plane.home();
|
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 " ";
|
const pointer = if (selected) "⏵" else " ";
|
||||||
var buf: [max_menu_width]u8 = undefined;
|
var buf: [max_menu_width]u8 = undefined;
|
||||||
_ = button.plane.print("{s}{s} ", .{
|
_ = button.plane.print("{s}{s} ", .{
|
||||||
pointer,
|
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 {};
|
}) 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;
|
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 {
|
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);
|
const w = @min(box.w, @min(self.longest, max_menu_width) + 2);
|
||||||
self.menu.resize(.{
|
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 {
|
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", "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 {
|
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();
|
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 {
|
fn receive_project_manager(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
|
||||||
if (try m.match(.{ "PRJ", tp.more })) {
|
if (try m.match(.{ "PRJ", tp.more })) {
|
||||||
try self.process_project_manager(m);
|
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 {
|
fn process_project_manager(self: *Self, m: tp.message) tp.result {
|
||||||
var file_name: []const u8 = undefined;
|
var file_name: []const u8 = undefined;
|
||||||
|
var matches: []const u8 = undefined;
|
||||||
var query: []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();
|
if (self.need_reset) self.reset_results();
|
||||||
self.longest = @max(self.longest, file_name.len);
|
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 });
|
self.menu.resize(.{ .y = 0, .x = 25, .w = @min(self.longest, max_menu_width) + 2 });
|
||||||
if (self.need_select_first) {
|
if (self.need_select_first) {
|
||||||
self.menu.select_down();
|
self.menu.select_down();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue