Compare commits

...

8 commits

Author SHA1 Message Date
61de5d89d7
refactor: rename open_changed_files to show_vcs_status
This fits a bit better as we will add commands to do more than just
open the files in the status palette.
2025-10-28 22:21:16 +01:00
Igor Támara
7f7f12a441 feat: update vcs changed files info when click on branch status 2025-10-28 22:20:36 +01:00
Igor Támara
fdf2b6d376 feat: allow to update vcs changed files info for current project 2025-10-28 22:20:36 +01:00
Igor Támara
8d0fa9a355 feat: [hx] space g bound to open changed and new files reported by git 2025-10-28 22:20:36 +01:00
Igor Támara
b6010767f2 feat: Add open changed files palette to commands
The palette shows modified and new files for the current project
allowing to open one by one
2025-10-28 22:20:36 +01:00
Igor Támara
81a6fef3b5 feat: Project manager exposes new and modified files for the current project 2025-10-28 22:20:36 +01:00
Igor Támara
52609ab198 feat: Project makes available modified and untracked files
Project exposes functions to query and return modified and
untracked files informed by git.
2025-10-28 22:20:36 +01:00
Igor Támara
9c1d1cb557 feat: untracked and modified files are identified by git
Git service offers untracked files and modified ones
staged or not filtering out any other statuses and marking
if new or modified.  This includes renamed files.
2025-10-28 22:20:36 +01:00
7 changed files with 736 additions and 0 deletions

View file

@ -19,8 +19,10 @@ const walk_tree = @import("walk_tree.zig");
allocator: std.mem.Allocator,
name: []const u8,
files: std.ArrayListUnmanaged(File) = .empty,
new_or_modified_files: std.ArrayListUnmanaged(FileVcsStatus) = .empty,
pending: std.ArrayListUnmanaged(File) = .empty,
longest_file_path: usize = 0,
longest_new_or_modified_file_path: usize = 0,
open_time: i64,
language_servers: std.StringHashMap(*const LSP),
file_language_server_name: std.StringHashMap([]const u8),
@ -41,6 +43,7 @@ state: struct {
current_branch: State = .none,
workspace_files: State = .none,
status: State = .none,
vcs_new_or_modified_files: State = .none,
} = .{},
status: VcsStatus = .{},
@ -66,6 +69,14 @@ const File = struct {
visited: bool = false,
};
const FileVcsStatus = struct {
path: []const u8,
type: []const u8,
icon: []const u8,
color: u24,
vcs_status: u8,
};
pub const FilePos = struct {
row: usize = 0,
col: usize = 0,
@ -105,6 +116,8 @@ pub fn deinit(self: *Self) void {
self.allocator.free(p.key_ptr.*);
p.value_ptr.*.term();
}
for (self.new_or_modified_files.items) |file| self.allocator.free(file.path);
self.new_or_modified_files.deinit(self.allocator);
for (self.files.items) |file| self.allocator.free(file.path);
self.files.deinit(self.allocator);
self.pending.deinit(self.allocator);
@ -372,6 +385,84 @@ pub fn request_recent_files(self: *Self, from: tp.pid_ref, max: usize) ClientErr
}
}
fn simple_query_new_or_modified_files(self: *Self, from: tp.pid_ref, max: usize, query: []const u8) ClientError!usize {
var i: usize = 0;
defer from.send(.{ "PRJ", "new_or_modified_files_done", self.longest_file_path, query }) catch {};
for (self.new_or_modified_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", "new_or_modified_files", self.longest_new_or_modified_file_path, file.path, file.type, file.icon, file.color, file.vcs_status, matches }) catch return error.ClientFailed;
i += 1;
if (i >= max) return i;
}
}
return i;
}
pub fn query_new_or_modified_files(self: *Self, from: tp.pid_ref, max: usize, query: []const u8) ClientError!usize {
if (query.len < 3)
return self.simple_query_new_or_modified_files(from, max, query);
defer from.send(.{ "PRJ", "new_or_modified_files_done", self.longest_file_path, query }) catch {};
var searcher = try fuzzig.Ascii.init(
self.allocator,
4096, // haystack max size
4096, // needle max size
.{ .case_sensitive = false },
);
defer searcher.deinit();
const Match = struct {
path: []const u8,
type: []const u8,
icon: []const u8,
color: u24,
vcs_status: u8,
score: i32,
matches: []const usize,
};
var matches: std.ArrayList(Match) = .empty;
for (self.new_or_modified_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,
.vcs_status = file.vcs_status,
.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", "new_or_modified_files", self.longest_new_or_modified_file_path, match.path, match.type, match.icon, match.color, match.vcs_status, match.matches }) catch return error.ClientFailed;
return @min(max, matches.items.len);
}
pub fn request_new_or_modified_files(self: *Self, from: tp.pid_ref, max: usize) ClientError!void {
defer from.send(.{ "PRJ", "new_or_modified_files_done", self.longest_new_or_modified_file_path, "" }) catch {};
for (self.new_or_modified_files.items, 0..) |file, i| {
from.send(.{ "PRJ", "new_or_modified_files", self.longest_new_or_modified_file_path, file.path, file.type, file.icon, file.color, file.vcs_status }) catch return error.ClientFailed;
if (i >= max) return;
}
}
fn simple_query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []const u8) ClientError!usize {
var i: usize = 0;
defer from.send(.{ "PRJ", "recent_done", self.longest_file_path, query }) catch {};
@ -544,6 +635,8 @@ fn loaded(self: *Self, parent: tp.pid_ref) OutOfMemoryError!void {
std.time.milliTimestamp() - self.open_time,
});
self.logger.print("vcs outstanding files: {d}", .{self.new_or_modified_files.items.len});
parent.send(.{ "PRJ", "open_done", self.name, self.longest_file_path, self.files.items.len }) catch {};
}
@ -2080,6 +2173,13 @@ pub fn query_git(self: *Self) void {
git.status(@intFromPtr(self)) catch {
self.state.status = .failed;
};
// TODO: This needs to be invoked when there are identified changes in the fs
for (self.new_or_modified_files.items) |file| self.allocator.free(file.path);
self.new_or_modified_files.clearRetainingCapacity();
self.state.vcs_new_or_modified_files = .running;
git.new_or_modified_files(@intFromPtr(self)) catch {
self.state.vcs_new_or_modified_files = .failed;
};
}
fn start_walker(self: *Self) void {
@ -2093,6 +2193,7 @@ fn start_walker(self: *Self) void {
pub fn process_git(self: *Self, parent: tp.pid_ref, m: tp.message) (OutOfMemoryError || error{Exit})!void {
var value: []const u8 = undefined;
var path: []const u8 = undefined;
var vcs_status: u8 = undefined;
if (try m.match(.{ tp.any, tp.any, "status", tp.more })) {
return self.process_status(m);
} else if (try m.match(.{ tp.any, tp.any, "workspace_path", tp.null_ })) {
@ -2131,6 +2232,19 @@ pub fn process_git(self: *Self, parent: tp.pid_ref, m: tp.message) (OutOfMemoryE
} else if (try m.match(.{ tp.any, tp.any, "workspace_files", tp.null_ })) {
self.state.workspace_files = .done;
try self.loaded(parent);
} else if (try m.match(.{ tp.any, tp.any, "new_or_modified_files", tp.null_ })) {
self.state.vcs_new_or_modified_files = .done;
try self.loaded(parent);
} else if (try m.match(.{ tp.any, tp.any, "new_or_modified_files", tp.extract(&vcs_status), tp.extract(&path) })) {
self.longest_new_or_modified_file_path = @max(self.longest_new_or_modified_file_path, path.len);
const file_type: []const u8, const file_icon: []const u8, const file_color: u24 = guess_file_type(path);
(try self.new_or_modified_files.addOne(self.allocator)).* = .{
.path = try self.allocator.dupe(u8, path),
.type = file_type,
.icon = file_icon,
.color = file_color,
.vcs_status = vcs_status,
};
} else {
self.logger_git.err("git", tp.unexpected(m));
}

View file

@ -174,6 +174,105 @@ pub fn status(context_: usize) Error!void {
}.result, exit_null(tag));
}
pub fn new_or_modified_files(context_: usize) Error!void {
const tag = @src().fn_name;
try git_err(context_, .{
"--no-optional-locks",
"status",
"--porcelain=v2",
"--null",
}, struct {
fn result(context: usize, parent: tp.pid_ref, output: []const u8) void {
var it_ = std.mem.splitScalar(u8, output, 0);
var counter: u8 = 0;
while (it_.next()) |line| {
var it = std.mem.splitScalar(u8, line, ' ');
const rec_type = if (it.next()) |type_tag|
std.meta.stringToEnum(StatusRecordType, type_tag) orelse {
if (type_tag.len > 0)
std.log.debug("found {s}, it happens when a file is renamed and not modified. Check `git --no-optional-locks status --porcelain=v2`", .{type_tag});
continue;
}
else
return;
switch (rec_type) {
.@"1" => { // ordinary file: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
const sub = it.next() orelse return;
const mH = it.next() orelse return;
var vcs_status: u8 = undefined;
if (sub[0] == 'A') {
// New staged file is shown as new
vcs_status = '+';
} else if (sub[0] == 'M' or sub[1] == 'M') {
if (mH[0] == 'S') {
// We do not handle submodules, yet
continue;
}
vcs_status = '~';
} else {
// We will not edit deleted files
continue;
}
for (0..5) |_| {
_ = it.next() orelse return;
}
var path: std.ArrayListUnmanaged(u8) = .empty;
defer path.deinit(allocator);
while (it.next()) |path_part| {
if (path.items.len > 0) path.append(allocator, ' ') catch return;
path.appendSlice(allocator, path_part) catch return;
}
parent.send(.{ module_name, context, tag, vcs_status, path.items }) catch {};
counter += 1;
},
.@"2" => {
const sub = it.next() orelse return;
if (sub[0] != 'R') {
continue;
}
// An staged file is editable
// renamed: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <rn> <path>
for (0..7) |_| {
_ = it.next() orelse return;
}
var path: std.ArrayListUnmanaged(u8) = .empty;
defer path.deinit(allocator);
while (it.next()) |path_part| {
if (path.items.len > 0) path.append(allocator, ' ') catch return;
path.appendSlice(allocator, path_part) catch return;
}
parent.send(.{ module_name, context, tag, '+', path.items }) catch {};
counter += 1;
},
.@"?" => { // untracked file: <path>
var path: std.ArrayListUnmanaged(u8) = .empty;
defer path.deinit(allocator);
while (it.next()) |path_part| {
if (path.items.len > 0) path.append(allocator, ' ') catch return;
path.appendSlice(allocator, path_part) catch return;
}
parent.send(.{ module_name, context, tag, '+', path.items }) catch {};
counter += 1;
},
else => {
// Omit showing other statuses
},
}
}
std.log.info("git: {} changed files", .{counter});
}
}.result, struct {
fn result(_: usize, _: tp.pid_ref, output: []const u8) void {
var it = std.mem.splitScalar(u8, output, '\n');
while (it.next()) |line| if (line.len > 0)
std.log.err("{s}: {s}", .{ module_name, line });
}
}.result, exit_null(tag));
}
fn git_line_output(context_: usize, comptime tag: []const u8, cmd: anytype) Error!void {
try git_err(context_, cmd, struct {
fn result(context: usize, parent: tp.pid_ref, output: []const u8) void {

View file

@ -228,6 +228,7 @@
["space R", "replace_selections_with_clipboard"],
["space ?", "open_command_palette"],
["space f", "find_file"],
["space g", "show_vcs_status"],
["space b", "switch_buffers"],
["space j", "jumplist_picker"],
["space s", "symbol_picker"],

View file

@ -98,6 +98,20 @@ pub fn request_recent_files(max: usize) (ProjectManagerError || ProjectError)!vo
return send(.{ "request_recent_files", project, max });
}
pub fn request_new_or_modified_files(max: usize) (ProjectManagerError || ProjectError)!void {
const project = tp.env.get().str("project");
if (project.len == 0)
return error.NoProject;
return send(.{ "request_new_or_modified_files", project, max });
}
pub fn request_sync_with_vcs() (ProjectManagerError || ProjectError)!void {
const project = tp.env.get().str("project");
if (project.len == 0)
return error.NoProject;
return send(.{ "sync_with_vcs", project });
}
pub fn request_recent_projects() (ProjectManagerError || ProjectError)!void {
const project = tp.env.get().str("project");
return send(.{ "request_recent_projects", project });
@ -110,6 +124,13 @@ pub fn query_recent_files(max: usize, query: []const u8) (ProjectManagerError ||
return send(.{ "query_recent_files", project, max, query });
}
pub fn query_new_or_modified_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_new_or_modified_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)
@ -370,10 +391,16 @@ const Process = struct {
self.request_n_most_recent_file(from, project_directory, n) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "request_recent_files", tp.extract(&project_directory), tp.extract(&max) })) {
self.request_recent_files(from, project_directory, max) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "request_new_or_modified_files", tp.extract(&project_directory), tp.extract(&max) })) {
self.request_new_or_modified_files(from, project_directory, max) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "sync_with_vcs", tp.extract(&project_directory) })) {
self.request_sync_with_vcs(from, project_directory) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "request_recent_projects", tp.extract(&project_directory) })) {
self.request_recent_projects(from, project_directory) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "query_recent_files", tp.extract(&project_directory), tp.extract(&max), tp.extract(&query) })) {
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, .{ "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) })) {
@ -468,6 +495,16 @@ const Process = struct {
return project.request_recent_files(from, max);
}
fn request_sync_with_vcs(self: *Process, _: tp.pid_ref, project_directory: []const u8) (ProjectError || Project.ClientError)!void {
const project = self.projects.get(project_directory) orelse return error.NoProject;
return project.query_git();
}
fn request_new_or_modified_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize) (ProjectError || Project.ClientError)!void {
const project = self.projects.get(project_directory) orelse return error.NoProject;
return project.request_new_or_modified_files(from, max);
}
fn request_recent_projects(self: *Process, from: tp.pid_ref, project_directory: []const u8) (ProjectError || Project.ClientError)!void {
var recent_projects: std.ArrayList(RecentProject) = .empty;
defer recent_projects.deinit(self.allocator);
@ -498,6 +535,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_new_or_modified_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize, query: []const u8) (ProjectError || Project.ClientError)!void {
const project = self.projects.get(project_directory) orelse return error.NoProject;
const start_time = std.time.milliTimestamp();
const matched = try project.query_new_or_modified_files(from, max, query);
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 request_path_files(self: *Process, from: tp.pid_ref, project_directory: []const u8, max: usize, path: []const u8) (ProjectError || SpawnError || std.fs.Dir.OpenError)!void {
const project = self.projects.get(project_directory) orelse return error.NoProject;
var buf: std.ArrayList(u8) = .empty;

View file

@ -0,0 +1,391 @@
const std = @import("std");
const tp = @import("thespian");
const log = @import("log");
const cbor = @import("cbor");
const file_type_config = @import("file_type_config");
const Plane = @import("renderer").Plane;
const input = @import("input");
const keybind = @import("keybind");
const project_manager = @import("project_manager");
const command = @import("command");
const EventHandler = @import("EventHandler");
const BufferManager = @import("Buffer").Manager;
const tui = @import("../../tui.zig");
const MessageFilter = @import("../../MessageFilter.zig");
const Button = @import("../../Button.zig");
const InputBox = @import("../../InputBox.zig");
const Menu = @import("../../Menu.zig");
const Widget = @import("../../Widget.zig");
const ModalBackground = @import("../../ModalBackground.zig");
const Self = @This();
const max_recent_files: usize = 25;
const widget_type: Widget.Type = .palette;
allocator: std.mem.Allocator,
f: usize = 0,
modal: *ModalBackground.State(*Self),
menu: *MenuType,
inputbox: *InputBox.State(*Self),
logger: log.Logger,
query_pending: bool = false,
need_reset: bool = false,
need_select_first: bool = true,
longest: usize,
commands: Commands = undefined,
buffer_manager: ?*BufferManager,
const inputbox_label = "Changed or untracked files";
const MenuType = Menu.Options(*Self).MenuType;
const ButtonType = MenuType.ButtonType;
pub fn create(allocator: std.mem.Allocator) !tui.Mode {
const mv = tui.mainview() orelse return error.NotFound;
const self = try allocator.create(Self);
errdefer allocator.destroy(self);
self.* = .{
.allocator = allocator,
.modal = try ModalBackground.create(*Self, allocator, tui.mainview_widget(), .{ .ctx = self }),
.menu = try Menu.create(*Self, allocator, tui.plane(), .{
.ctx = self,
.style = widget_type,
.on_render = on_render_menu,
.prepare_resize = prepare_resize_menu,
}),
.logger = log.logger(@typeName(Self)),
.inputbox = (try self.menu.add_header(try InputBox.create(*Self, self.allocator, self.menu.menu.parent, .{
.ctx = self,
.label = inputbox_label,
.padding = 2,
.icon = "󰈞 ",
}))).dynamic_cast(InputBox.State(*Self)) orelse unreachable,
.buffer_manager = tui.get_buffer_manager(),
.longest = inputbox_label.len,
};
try self.commands.init(self);
try tui.message_filters().add(MessageFilter.bind(self, receive_project_manager));
self.query_pending = true;
try project_manager.request_new_or_modified_files(max_recent_files);
self.do_resize();
try mv.floating_views.add(self.modal.widget());
try mv.floating_views.add(self.menu.container_widget);
var mode = try keybind.mode("overlay/palette", allocator, .{
.insert_command = "overlay_insert_bytes",
});
mode.event_handler = EventHandler.to_owned(self);
mode.name = " status";
return mode;
}
pub fn deinit(self: *Self) void {
self.commands.deinit();
tui.message_filters().remove_ptr(self);
if (tui.mainview()) |mv| {
mv.floating_views.remove(self.menu.container_widget);
mv.floating_views.remove(self.modal.widget());
}
self.logger.deinit();
self.allocator.destroy(self);
}
inline fn menu_width(self: *Self) usize {
return @max(@min(self.longest + 3, max_menu_width()) + 5, inputbox_label.len + 2);
}
inline fn menu_pos_x(self: *Self) usize {
const screen_width = tui.screen().w;
const width = self.menu_width();
return if (screen_width <= width) 0 else (screen_width - width) / 2;
}
inline fn max_menu_width() usize {
const width = tui.screen().w;
return @max(15, width - (width / 5));
}
fn on_render_menu(_: *Self, button: *ButtonType, theme: *const Widget.Theme, selected: bool) bool {
return tui.render_file_vcs_item_cbor(&button.plane, button.opts.label, button.active, selected, button.hover, theme);
}
fn prepare_resize_menu(self: *Self, _: *MenuType, _: Widget.Box) Widget.Box {
return self.prepare_resize();
}
fn prepare_resize(self: *Self) Widget.Box {
const w = self.menu_width();
const x = self.menu_pos_x();
const h = self.menu.menu.widgets.items.len;
return .{ .y = 0, .x = x, .w = w, .h = h };
}
fn do_resize(self: *Self) void {
self.menu.resize(self.prepare_resize());
}
fn menu_action_open_file(menu: **MenuType, button: *ButtonType, _: Widget.Pos) 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 = file_path } }) catch |e| menu.*.opts.ctx.logger.err("navigate", e);
}
fn add_item(
self: *Self,
file_name: []const u8,
file_icon: []const u8,
file_color: u24,
vcs_status: u8,
indicator: []const u8,
matches: ?[]const u8,
) !void {
var label: std.Io.Writer.Allocating = .init(self.allocator);
defer label.deinit();
const writer = &label.writer;
try cbor.writeValue(writer, file_name);
try cbor.writeValue(writer, file_icon);
try cbor.writeValue(writer, file_color);
try cbor.writeValue(writer, indicator);
try cbor.writeValue(writer, vcs_status);
if (matches) |cb| _ = try writer.write(cb) else try cbor.writeValue(writer, &[_]usize{});
try self.menu.add_item_with_handler(label.written(), menu_action_open_file);
}
fn receive_project_manager(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool {
if (cbor.match(m.buf, .{ "PRJ", tp.more }) catch false) {
try self.process_project_manager(m);
return true;
}
return false;
}
fn process_project_manager(self: *Self, m: tp.message) MessageFilter.Error!void {
var file_name: []const u8 = undefined;
var file_type: []const u8 = undefined;
var file_icon: []const u8 = undefined;
var file_color: u24 = undefined;
var vcs_status: u8 = undefined;
var matches: []const u8 = undefined;
var query: []const u8 = undefined;
if (try cbor.match(m.buf, .{
"PRJ",
"new_or_modified_files",
tp.extract(&self.longest),
tp.extract(&file_name),
tp.extract(&file_type),
tp.extract(&file_icon),
tp.extract(&file_color),
tp.extract(&vcs_status),
tp.extract_cbor(&matches),
})) {
if (self.need_reset) self.reset_results();
const indicator = if (self.buffer_manager) |bm| tui.get_file_state_indicator(bm, file_name) else "";
try self.add_item(file_name, file_icon, file_color, vcs_status, indicator, matches);
self.do_resize();
if (self.need_select_first) {
self.menu.select_down();
self.need_select_first = false;
}
tui.need_render();
} else if (try cbor.match(m.buf, .{
"PRJ",
"new_or_modified_files",
tp.extract(&self.longest),
tp.extract(&file_name),
tp.extract(&file_type),
tp.extract(&file_icon),
tp.extract(&file_color),
tp.extract(&vcs_status),
})) {
if (self.need_reset) self.reset_results();
const indicator = if (self.buffer_manager) |bm| tui.get_file_state_indicator(bm, file_name) else "";
try self.add_item(file_name, file_icon, file_color, vcs_status, indicator, null);
self.do_resize();
if (self.need_select_first) {
self.menu.select_down();
self.need_select_first = false;
}
tui.need_render();
} else if (try cbor.match(m.buf, .{ "PRJ", "new_or_modified_files_done", tp.extract(&self.longest), tp.extract(&query) })) {
self.query_pending = false;
self.need_reset = true;
if (!std.mem.eql(u8, self.inputbox.text.items, query))
try self.start_query();
} else if (try cbor.match(m.buf, .{ "PRJ", "open_done", tp.string, tp.extract(&self.longest), tp.any })) {
self.query_pending = false;
self.need_reset = true;
try self.start_query();
} else {
self.logger.err("receive", tp.unexpected(m));
}
}
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
var text: []const u8 = undefined;
if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
self.insert_bytes(text) catch |e| return tp.exit_error(e, @errorReturnTrace());
}
return false;
}
fn reset_results(self: *Self) void {
self.need_reset = false;
self.menu.reset_items();
self.menu.selected = null;
self.need_select_first = true;
}
fn start_query(self: *Self) MessageFilter.Error!void {
if (self.query_pending) return;
self.query_pending = true;
try project_manager.query_new_or_modified_files(max_recent_files, self.inputbox.text.items);
}
fn delete_word(self: *Self) !void {
if (std.mem.lastIndexOfAny(u8, self.inputbox.text.items, "/\\. -_")) |pos| {
self.inputbox.text.shrinkRetainingCapacity(pos);
} else {
self.inputbox.text.shrinkRetainingCapacity(0);
}
self.inputbox.cursor = tui.egc_chunk_width(self.inputbox.text.items, 0, 8);
return self.start_query();
}
fn delete_code_point(self: *Self) !void {
if (self.inputbox.text.items.len > 0) {
self.inputbox.text.shrinkRetainingCapacity(self.inputbox.text.items.len - tui.egc_last(self.inputbox.text.items).len);
self.inputbox.cursor = tui.egc_chunk_width(self.inputbox.text.items, 0, 8);
}
return self.start_query();
}
fn insert_code_point(self: *Self, c: u32) !void {
var buf: [6]u8 = undefined;
const bytes = try input.ucs32_to_utf8(&[_]u32{c}, &buf);
try self.inputbox.text.appendSlice(self.allocator, buf[0..bytes]);
self.inputbox.cursor = tui.egc_chunk_width(self.inputbox.text.items, 0, 8);
return self.start_query();
}
fn insert_bytes(self: *Self, bytes: []const u8) !void {
try self.inputbox.text.appendSlice(self.allocator, bytes);
self.inputbox.cursor = tui.egc_chunk_width(self.inputbox.text.items, 0, 8);
return self.start_query();
}
fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result {
try command.executeName(name_, ctx);
}
fn msg(_: *Self, text: []const u8) tp.result {
return tp.self_pid().send(.{ "log", "home", text });
}
fn cmd_async(_: *Self, name_: []const u8) tp.result {
return tp.self_pid().send(.{ "cmd", name_ });
}
const Commands = command.Collection(cmds);
const cmds = struct {
pub const Target = Self;
const Ctx = command.Context;
const Meta = command.Metadata;
const Result = command.Result;
pub fn palette_menu_down(self: *Self, _: Ctx) Result {
self.menu.select_down();
}
pub const palette_menu_down_meta: Meta = .{};
pub fn palette_menu_up(self: *Self, _: Ctx) Result {
self.menu.select_up();
}
pub const palette_menu_up_meta: Meta = .{};
pub fn palette_menu_pagedown(self: *Self, _: Ctx) Result {
self.menu.select_last();
}
pub const palette_menu_pagedown_meta: Meta = .{};
pub fn palette_menu_pageup(self: *Self, _: Ctx) Result {
self.menu.select_first();
}
pub const palette_menu_pageup_meta: Meta = .{};
pub fn palette_menu_bottom(self: *Self, _: Ctx) Result {
self.menu.select_last();
}
pub const palette_menu_bottom_meta: Meta = .{};
pub fn palette_menu_top(self: *Self, _: Ctx) Result {
self.menu.select_first();
}
pub const palette_menu_top_meta: Meta = .{};
pub fn palette_menu_activate(self: *Self, _: Ctx) Result {
self.menu.activate_selected();
}
pub const palette_menu_activate_meta: Meta = .{};
pub fn palette_menu_activate_quick(self: *Self, _: Ctx) Result {
if (self.menu.selected orelse 0 > 0) self.menu.activate_selected();
}
pub const palette_menu_activate_quick_meta: Meta = .{};
pub fn palette_menu_cancel(self: *Self, _: Ctx) Result {
try self.cmd("exit_overlay_mode", .{});
}
pub const palette_menu_cancel_meta: Meta = .{};
pub fn overlay_delete_word_left(self: *Self, _: Ctx) Result {
self.delete_word() catch |e| return tp.exit_error(e, @errorReturnTrace());
}
pub const overlay_delete_word_left_meta: Meta = .{ .description = "Delete word to the left" };
pub fn overlay_delete_backwards(self: *Self, _: Ctx) Result {
self.delete_code_point() catch |e| return tp.exit_error(e, @errorReturnTrace());
}
pub const overlay_delete_backwards_meta: Meta = .{ .description = "Delete backwards" };
pub fn overlay_insert_code_point(self: *Self, ctx: Ctx) Result {
var egc: u32 = 0;
if (!try ctx.args.match(.{tp.extract(&egc)}))
return error.InvalidOpenRecentInsertCodePointArgument;
self.insert_code_point(egc) catch |e| return tp.exit_error(e, @errorReturnTrace());
}
pub const overlay_insert_code_point_meta: Meta = .{ .arguments = &.{.integer} };
pub fn overlay_insert_bytes(self: *Self, ctx: Ctx) Result {
var bytes: []const u8 = undefined;
if (!try ctx.args.match(.{tp.extract(&bytes)}))
return error.InvalidOpenRecentInsertBytesArgument;
self.insert_bytes(bytes) catch |e| return tp.exit_error(e, @errorReturnTrace());
}
pub const overlay_insert_bytes_meta: Meta = .{ .arguments = &.{.string} };
pub fn overlay_toggle_panel(self: *Self, _: Ctx) Result {
return self.cmd_async("toggle_panel");
}
pub const overlay_toggle_panel_meta: Meta = .{};
pub fn overlay_toggle_inputview(self: *Self, _: Ctx) Result {
return self.cmd_async("toggle_inputview");
}
pub const overlay_toggle_inputview_meta: Meta = .{};
pub fn overlay_next_widget_style(self: *Self, _: Ctx) Result {
tui.set_next_style(widget_type);
self.do_resize();
tui.need_render();
try tui.save_config();
}
pub const overlay_next_widget_style_meta: Meta = .{};
pub fn mini_mode_paste(self: *Self, ctx: Ctx) Result {
return overlay_insert_bytes(self, ctx);
}
pub const mini_mode_paste_meta: Meta = .{ .arguments = &.{.string} };
};

View file

@ -56,6 +56,7 @@ pub fn ctx_deinit(self: *Self) void {
fn on_click(self: *Self, _: *ButtonType, _: Widget.Pos) void {
self.refresh_vcs_status();
tui.sync_with_vcs() catch {};
command.executeName("show_vcs_status", .{}) catch {};
}

View file

@ -1032,6 +1032,11 @@ const cmds = struct {
}
pub const open_recent_meta: Meta = .{ .description = "Open recent" };
pub fn show_vcs_status(self: *Self, _: Ctx) Result {
return self.enter_overlay_mode(@import("mode/overlay/vcs_status.zig"));
}
pub const show_vcs_status_meta: Meta = .{ .description = "Show git status" };
pub fn open_recent_project(_: *Self, _: Ctx) Result {
try project_manager.request_recent_projects();
}
@ -1365,6 +1370,10 @@ pub fn mainview() ?*MainView {
return if (current().mainview_) |*mv| mv.dynamic_cast(MainView) else null;
}
pub fn sync_with_vcs() !void {
try project_manager.request_sync_with_vcs();
}
pub fn mainview_widget() Widget {
return current().mainview_ orelse @panic("tui main view not found");
}
@ -1679,6 +1688,15 @@ pub fn render_pointer(self: *renderer.Plane, selected: bool) void {
_ = self.print("{s}", .{pointer}) catch {};
}
pub fn render_pointer_vcs(self: *renderer.Plane, vcs_status: u8, selected: bool) void {
const pointer = "";
if (selected) {
_ = self.print("{s}{c}", .{ pointer, vcs_status }) catch {};
} else {
_ = self.print("{c} ", .{vcs_status}) catch {};
}
}
pub fn render_file_item(
self: *renderer.Plane,
file_path_: []const u8,
@ -1742,6 +1760,72 @@ pub fn render_file_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8,
return render_file_item(self, file_path_, icon, color, indicator, matches_cbor, active, selected, hover, theme_);
}
pub fn render_file_vcs_item(
self: *renderer.Plane,
file_path_: []const u8,
icon: []const u8,
color: u24,
indicator: []const u8,
vcs_status: u8,
matches_cbor: []const u8,
active: bool,
selected: bool,
hover: bool,
theme_: *const Widget.Theme,
) bool {
const style_base = theme_.editor_widget;
const style_label = if (active) theme_.editor_cursor else if (hover or selected) theme_.editor_selection else theme_.editor_widget;
const style_hint = if (find_scope_style(theme_, "entity.name")) |sty| sty.style else style_label;
self.set_base_style(style_base);
self.erase();
self.home();
self.set_style(style_label);
if (active or hover or selected) {
self.fill(" ");
self.home();
}
self.set_style(style_hint);
render_pointer_vcs(self, vcs_status, selected);
const icon_width = render_file_icon(self, icon, color);
self.set_style(style_label);
_ = self.print("{s} ", .{file_path_}) catch {};
self.set_style(style_hint);
_ = self.print_aligned_right(0, "{s} ", .{indicator}) catch {};
var iter = matches_cbor;
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_match_cell(self, 0, index + 2 + icon_width, theme_) catch break;
} else break;
}
return false;
}
pub fn render_file_vcs_item_cbor(self: *renderer.Plane, file_item_cbor: []const u8, active: bool, selected: bool, hover: bool, theme_: *const Widget.Theme) bool {
var iter = file_item_cbor;
var file_path_: []const u8 = undefined;
var icon: []const u8 = undefined;
var color: u24 = undefined;
var indicator: []const u8 = undefined;
var vcs_status: u8 = undefined;
var matches_cbor: []const u8 = undefined;
if (!(cbor.matchString(&iter, &file_path_) catch false)) @panic("invalid buffer file path");
if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid buffer file type icon");
if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid buffer file type color");
if (!(cbor.matchString(&iter, &indicator) catch false)) indicator = "";
if (!(cbor.matchInt(u8, &iter, &vcs_status) catch false)) indicator = " ";
if (!(cbor.matchValue(&iter, cbor.extract_cbor(&matches_cbor)) catch false)) @panic("invalid matches cbor");
return render_file_vcs_item(self, file_path_, icon, color, indicator, vcs_status, matches_cbor, active, selected, hover, theme_);
}
fn get_or_create_theme_file(self: *Self, allocator: std.mem.Allocator) ![]const u8 {
const theme_name = self.current_theme().name;
if (root.read_theme(allocator, theme_name)) |content| {