diff --git a/build.zig b/build.zig index 023f96f..ad3088b 100644 --- a/build.zig +++ b/build.zig @@ -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") }, }, }); diff --git a/build.zig.zon b/build.zig.zon index 3c402b4..4eee855 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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", diff --git a/src/Project.zig b/src/Project.zig index 2499b09..996f05e 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -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); diff --git a/src/project_manager.zig b/src/project_manager.zig index 7fc6526..3fbef18 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -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 { diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index ddf4d11..a68fa76 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -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();