Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/Project.zig
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,82 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query_: []c
return @min(max, matches.items.len);
}

pub fn query_tree_files(self: *Self, from: tp.pid_ref, max: usize, query_: []const u8) RequestError!usize {
const query = try self.strip_non_search_chars(query_);
defer self.allocator.free(query);
if (query.len < 3)
return self.simple_query_tree_files(from, max, query);
defer from.send(.{ "PRJ", "tree_search_done", self.longest_file_path, query, self.files.items.len }) catch {};

var searcher = try fuzzig.Ascii.init(
self.allocator,
4096,
4096,
.{ .case_sensitive = false },
);
defer searcher.deinit();

const Match = struct {
path: []const u8,
type: []const u8,
icon: []const u8,
color: u24,
score: i32,
matches: []const usize,
};
var matches: std.ArrayList(Match) = .empty;

for (self.files.items) |file| {
const match = searcher.scoreMatches(file.path, query);
if (match.score) |score| {
(try matches.addOne(self.allocator)).* = .{
.path = file.path,
.type = file.type,
.icon = file.icon,
.color = file.color,
.score = score,
.matches = try self.allocator.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|
from.send(.{ "PRJ", "tree_search", self.longest_file_path, match.path, match.type, match.icon, match.color, match.matches }) catch |e| {
std.log.err("send tree_search failed: {t}", .{e});
return error.InvalidQueryRecentFilesRequest;
};
return @min(max, matches.items.len);
}

fn simple_query_tree_files(self: *Self, from: tp.pid_ref, max: usize, query: []const u8) RequestError!usize {
var i: usize = 0;
defer from.send(.{ "PRJ", "tree_search_done", self.longest_file_path, query, self.files.items.len }) catch {};
for (self.files.items) |file| {
if (file.path.len < query.len) continue;
if (std.mem.indexOf(u8, file.path, query)) |idx| {
var matches = try self.allocator.alloc(usize, query.len);
defer self.allocator.free(matches);
var n: usize = 0;
while (n < query.len) : (n += 1) matches[n] = idx + n;
from.send(.{ "PRJ", "tree_search", self.longest_file_path, file.path, file.type, file.icon, file.color, matches }) catch |e| {
std.log.err("send tree_search failed: {t}", .{e});
return error.InvalidRecentFilesRequest;
};
i += 1;
if (i >= max) return i;
}
}
return i;
}

fn walk_tree_entry_callback(parent: tp.pid_ref, root_path: []const u8, file_path: []const u8, mtime_high: i64, mtime_low: i64) error{Exit}!void {
const file_type: []const u8, const file_icon: []const u8, const file_color: u24 = guess_file_type(file_path);
try parent.send(.{ "walk_tree_entry", root_path, file_path, mtime_high, mtime_low, file_type, file_icon, file_color });
Expand Down
18 changes: 18 additions & 0 deletions src/project_manager.zig
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ pub fn query_new_or_modified_files(max: usize, query: []const u8) (ProjectManage
return send(.{ "query_new_or_modified_files", project, max, query });
}

pub fn query_tree_files(max: usize, query: []const u8) (ProjectManagerError || ProjectError)!void {
const project = tp.env.get().str("project");
if (project.len == 0)
return error.NoProject;
return send(.{ "query_tree_files", project, max, query });
}

pub fn request_path_files(max: usize, path: []const u8) (ProjectManagerError || ProjectError)!void {
const project = tp.env.get().str("project");
if (project.len == 0)
Expand Down Expand Up @@ -488,6 +495,8 @@ const Process = struct {
self.query_recent_files(from, project_directory, max, query) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "query_new_or_modified_files", tp.extract(&project_directory), tp.extract(&max), tp.extract(&query) })) {
self.query_new_or_modified_files(from, project_directory, max, query) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "query_tree_files", tp.extract(&project_directory), tp.extract(&max), tp.extract(&query) })) {
self.query_tree_files(from, project_directory, max, query) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "request_path_files", tp.extract(&project_directory), tp.extract(&max), tp.extract(&path) })) {
self.request_path_files(from, project_directory, max, path) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "request_tasks", tp.extract(&project_directory) })) {
Expand Down Expand Up @@ -682,6 +691,15 @@ const Process = struct {
self.logger.print("query \"{s}\" matched {d}/{d} in {d} ms", .{ query, matched, project.files.items.len, query_time });
}

fn query_tree_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize, query: []const u8) (ProjectError || Project.RequestError)!void {
const project = self.projects.get(project_directory) orelse return error.NoProject;
const start_time = root.get_now().toMilliseconds();
const matched = try project.query_tree_files(from, max, query);
const query_time = root.get_now().toMilliseconds() - start_time;
if (query_time > 250)
self.logger.print("tree query \"{s}\" matched {d}/{d} in {d} ms", .{ query, matched, project.files.items.len, query_time });
}

fn request_path_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize, path: []const u8) (ProjectError || SpawnError || std.Io.Dir.OpenError)!void {
const project = self.projects.get(project_directory) orelse return error.NoProject;
var buf: std.ArrayList(u8) = .empty;
Expand Down
145 changes: 136 additions & 9 deletions src/tui/mode/overlay/file_tree_palette.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub const description = "file tree";
pub const icon = " ";

const max_path_entries = 1024;
const max_tree_search_results: usize = 100;

pub const NodeType = enum {
file,
Expand Down Expand Up @@ -75,6 +76,11 @@ pub const ValueType = struct {
root_node: ?*Node = null,
pending_node: ?*Node = null,
follow_path: ?[]const u8 = null,
is_searching: bool = false,
search_query_pending: bool = false,
search_need_reset: bool = false,
search_total: usize = 0,
search_project_total: usize = 0,
};
pub const defaultValue: ValueType = .{};

Expand Down Expand Up @@ -126,19 +132,93 @@ fn receive_project_manager(palette: *Type, _: tp.pid_ref, m: tp.message) Message
const pending = palette.value.pending_node;
palette.value.pending_node = null;
if (pending) |p| if (p.children) |*children| sort_children(children);
palette.entries.clearRetainingCapacity();
if (palette.value.root_node) |root| try build_visible_list(palette, root, 0);
palette.longest_hint = max_entry_overhead(palette);
try follow_path(palette, pending);
palette.start_query(0) catch {};
tui.need_render(@src());
if (!palette.value.is_searching) {
palette.entries.clearRetainingCapacity();
if (palette.value.root_node) |root| try build_visible_list(palette, root, 0);
palette.longest_hint = max_entry_overhead(palette);
try follow_path(palette, pending);
palette.start_query(0) catch {};
tui.need_render(@src());
}
} else if (try cbor.match(m.buf, .{ "PRJ", "path_error", tp.any, tp.any, tp.any })) {
palette.value.pending_node = null;
} else {
try receive_search_messages(palette, m);
}

return true;
}

fn receive_search_messages(palette: *Type, m: tp.message) MessageFilter.Error!void {
var file_path: []const u8 = undefined;
var file_type: []const u8 = undefined;
var file_icon: []const u8 = undefined;
var file_color: u24 = undefined;
var longest: usize = undefined;
var matches: []const u8 = undefined;
var query: []const u8 = undefined;

if (try cbor.match(m.buf, .{ // Accumulate results
"PRJ",
"tree_search",
tp.extract(&longest),
tp.extract(&file_path),
tp.extract(&file_type),
tp.extract(&file_icon),
tp.extract(&file_color),
tp.extract_cbor(&matches),
})) {
if (!palette.value.is_searching) return;
if (palette.value.search_need_reset) {
palette.menu.reset_items();
palette.items = 0;
palette.total_items = 0;
palette.value.search_need_reset = false;
}
palette.longest = @max(palette.longest, longest);
try add_search_item(palette, file_path, file_icon, file_color, matches);
palette.value.search_total += 1;
palette.total_items += 1;
palette.resize();
tui.need_render(@src());
} else if (try cbor.match(m.buf, .{ // Render results
"PRJ",
"tree_search_done",
tp.extract(&longest),
tp.extract(&query),
tp.extract(&palette.value.search_project_total),
})) {
palette.longest = @max(palette.longest, longest);
palette.value.search_query_pending = false;
update_search_count_hint(palette);
if (palette.value.is_searching and !std.mem.eql(u8, palette.inputbox.text.items, query))
try on_query_changed(palette, palette.inputbox.text.items);
palette.resize();
tui.need_render(@src());
}
}

fn add_search_item(palette: *Type, file_path: []const u8, file_icon: []const u8, file_color: u24, matches_cbor: []const u8) !void {
var value: std.Io.Writer.Allocating = .init(palette.allocator);
defer value.deinit();
const writer = &value.writer;
try cbor.writeValue(writer, file_path);
try cbor.writeValue(writer, file_icon);
try cbor.writeValue(writer, file_color);
try cbor.writeValue(writer, "");
_ = try writer.write(matches_cbor);
try palette.menu.add_item_with_handler(value.written(), select_search_result);
palette.items += 1;
}

fn update_search_count_hint(palette: *Type) void {
palette.inputbox.hint.clearRetainingCapacity();
palette.inputbox.hint.print(palette.inputbox.allocator, "{d}/{d}", .{
palette.value.search_total,
palette.value.search_project_total,
}) catch {};
}

fn sort_children(children: *std.ArrayList(Node)) void {
const less_fn = struct {
fn less_fn(_: void, lhs: Node, rhs: Node) bool {
Expand Down Expand Up @@ -251,7 +331,47 @@ pub fn load_entries(palette: *Type) !usize {
return max_entry_overhead(palette);
}

pub fn should_clear_on_query(palette: *Type) bool {
return !palette.value.is_searching;
}

pub fn on_query_changed(palette: *Type, query: []const u8) !void {
if (query.len > 0) {
palette.value.is_searching = true;
palette.value.search_need_reset = true;
palette.value.search_total = 0;
palette.total_items = 0;
if (palette.value.search_query_pending) return;
palette.value.search_query_pending = true;
try project_manager.query_tree_files(max_tree_search_results, query);
} else {
if (palette.value.is_searching) {
palette.value.is_searching = false;
palette.value.search_query_pending = false;
palette.value.search_total = 0;
palette.inputbox.hint.clearRetainingCapacity();
palette.entries.clearRetainingCapacity();
if (palette.value.root_node) |root| try build_visible_list(palette, root, 0);
palette.longest_hint = max_entry_overhead(palette);
}
}
}

pub fn on_render_menu(_: *Type, button: *Type.ButtonType, theme: *const Widget.Theme, selected: bool) bool {
var iter = button.opts.label;
var first_str: []const u8 = undefined;
if (!(cbor.matchString(&iter, &first_str) catch false)) return false;

var peek = iter;
var second_str: []const u8 = undefined;
if (cbor.matchString(&peek, &second_str) catch false) { // Search result: string - string - ...
return tui.render_file_item_cbor(&button.plane, button.opts.label, button.active, selected, button.hover, theme);
}

return render_tree_item(button, theme, selected, first_str, iter); // Tree item: string - int - ...
}

fn render_tree_item(button: *Type.ButtonType, theme: *const Widget.Theme, selected: bool, label_str: []const u8, iter_after_label: []const u8) bool {
const style_base = theme.editor_widget;
const style_label = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget;
const style_hint = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_label;
Expand All @@ -263,14 +383,12 @@ pub fn on_render_menu(_: *Type, button: *Type.ButtonType, theme: *const Widget.T
button.plane.fill(" ");
button.plane.home();
}
var label_str: []const u8 = undefined;
var file_icon: []const u8 = undefined;
var indent: usize = 0;
var entry_index: usize = 0;
var icon_color: u24 = 0;

var iter = button.opts.label;
if (!(cbor.matchString(&iter, &label_str) catch false)) return false;
var iter = iter_after_label;
if (!(cbor.matchInt(usize, &iter, &entry_index) catch false)) return false;
if (!(cbor.matchInt(usize, &iter, &indent) catch false)) return false;
if (!(cbor.matchString(&iter, &file_icon) catch false)) return false;
Expand Down Expand Up @@ -341,6 +459,15 @@ fn select(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void {
}
}

fn select_search_result(menu: **Type.MenuType, button: *Type.ButtonType, _: Type.Pos) void {
const palette = menu.*.opts.ctx;
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| palette.logger.err(module_name, e);
tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch |e| palette.logger.err(module_name, e);
}

pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void {
var value: std.Io.Writer.Allocating = .init(palette.allocator);
defer value.deinit();
Expand Down
16 changes: 13 additions & 3 deletions src/tui/mode/overlay/palette.zig
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ pub fn Create(options: type) type {
self.after_resize();
}

pub fn resize(self: *Self) void {
const padding = tui.get_widget_style(widget_type).padding;
self.do_resize(padding);
}

fn get_view_rows(screen: Widget.Box) usize {
var h = screen.h;
if (h > 0) h = h / 5 * 4;
Expand Down Expand Up @@ -352,9 +357,12 @@ pub fn Create(options: type) type {
pub fn start_query(self: *Self, n: usize) !void {
defer tui.reset_hover(@src());
defer self.update_count_hint();
self.items = 0;
self.menu.reset_items();
self.menu.selected = null;
const should_clear = !@hasDecl(options, "should_clear_on_query") or options.should_clear_on_query(self);
if (should_clear) {
self.items = 0;
self.menu.reset_items();
self.menu.selected = null;
}
self.longest = self.inputbox.text.items.len;
for (self.entries.items) |entry|
self.longest = @max(self.longest, entry.label.len);
Expand All @@ -369,6 +377,8 @@ pub fn Create(options: type) type {
if (self.items < self.view_rows)
try options.add_menu_entry(self, entry, null);
}
} else if (@hasDecl(options, "on_query_changed")) {
try options.on_query_changed(self, self.inputbox.text.items);
} else {
_ = try self.query_entries(self.inputbox.text.items);
}
Expand Down