From cf6c9455c7a27b4f2a67eae99651d8ff9a5302ef Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Tue, 29 Jul 2025 17:24:43 +0200 Subject: [PATCH 1/9] feat: update git status on focus_in events --- src/tui/status/branch.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tui/status/branch.zig b/src/tui/status/branch.zig index 9e4ff44..3ad6457 100644 --- a/src/tui/status/branch.zig +++ b/src/tui/status/branch.zig @@ -90,10 +90,17 @@ fn process_event(self: *Self, m: tp.message) error{Exit}!bool { fn receive_git(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool { return if (try match(m.buf, .{ "git", more })) self.process_git(m) + else if (try match(m.buf, .{"focus_in"})) + self.process_focus_in() else false; } +fn process_focus_in(self: *Self) MessageFilter.Error!bool { + self.refresh_git_status(); + return false; +} + fn process_git(self: *Self, m: tp.message) MessageFilter.Error!bool { var value: []const u8 = undefined; if (try match(m.buf, .{ any, any, "workspace_path", null_ })) { From 910c69183d76469655fde210b6dbac1f5ffbbb36 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 30 Jul 2025 18:25:48 +0200 Subject: [PATCH 2/9] feat: add indent_mode config option --- src/config.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/config.zig b/src/config.zig index 9efce2c..7436e84 100644 --- a/src/config.zig +++ b/src/config.zig @@ -26,6 +26,7 @@ limit_auto_save_file_types: ?[]const []const u8 = null, // null means *all* indent_size: usize = 4, tab_width: usize = 8, +indent_mode: IndentMode = .auto, top_bar: []const u8 = "tabs", bottom_bar: []const u8 = "mode file log selection diagnostics keybind branch linenumber clock spacer", @@ -48,3 +49,9 @@ pub const LineNumberMode = enum { relative, absolute, }; + +pub const IndentMode = enum { + auto, + spaces, + tabs, +}; From 196f5167242ba751a74ad0c6e1635504ab5a8ff5 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 30 Jul 2025 18:28:04 +0200 Subject: [PATCH 3/9] feat: save and restore indent_mode to editor state --- src/tui/editor.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index dd4c0b4..9115bda 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -23,6 +23,7 @@ const editor_gutter = @import("editor_gutter.zig"); const Widget = @import("Widget.zig"); const WidgetList = @import("WidgetList.zig"); const tui = @import("tui.zig"); +const IndentMode = @import("config").IndentMode; pub const Cursor = Buffer.Cursor; pub const View = Buffer.View; @@ -318,6 +319,7 @@ pub const Editor = struct { render_whitespace: WhitespaceMode, indent_size: usize, tab_width: usize, + indent_mode: IndentMode, last: struct { root: ?Buffer.Root = null, @@ -370,12 +372,15 @@ pub const Editor = struct { const Result = command.Result; pub fn write_state(self: *const Self, writer: Buffer.MetaWriter) !void { - try cbor.writeArrayHeader(writer, 8); + try cbor.writeArrayHeader(writer, 11); try cbor.writeValue(writer, self.file_path orelse ""); try cbor.writeValue(writer, self.clipboard orelse ""); try cbor.writeValue(writer, self.last_find_query orelse ""); try cbor.writeValue(writer, self.enable_format_on_save); try cbor.writeValue(writer, self.enable_auto_save); + try cbor.writeValue(writer, self.indent_size); + try cbor.writeValue(writer, self.tab_width); + try cbor.writeValue(writer, self.indent_mode); if (self.find_history) |history| { try cbor.writeArrayHeader(writer, history.items.len); for (history.items) |item| @@ -411,6 +416,9 @@ pub const Editor = struct { tp.extract(&last_find_query), tp.extract(&self.enable_format_on_save), tp.extract(&self.enable_auto_save), + tp.extract(&self.indent_size), + tp.extract(&self.tab_width), + tp.extract(&self.indent_mode), tp.extract_cbor(&find_history), tp.extract_cbor(&view_cbor), tp.extract_cbor(&cursels_cbor), @@ -454,11 +462,13 @@ pub const Editor = struct { const frame_rate = tp.env.get().num("frame-rate"); const indent_size = tui.config().indent_size; const tab_width = tui.config().tab_width; + const indent_mode = tui.config().indent_mode; self.* = Self{ .allocator = allocator, .plane = n, .indent_size = indent_size, .tab_width = tab_width, + .indent_mode = indent_mode, .metrics = self.plane.metrics(tab_width), .logger = logger, .file_path = null, From 9774b513d4d65b025da9d199a07a19f828ba7161 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 30 Jul 2025 18:50:42 +0200 Subject: [PATCH 4/9] fix: update buffer file type in set_type This fixes the buffer file type getting lost when switching buffers if the file type was set with set_type. --- src/tui/editor.zig | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 9115bda..5ae958e 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -584,6 +584,7 @@ pub const Editor = struct { if (self.buffer) |_| try self.close(); self.buffer = new_buf; const file_type = file_type_ orelse new_buf.file_type_name; + const buffer_meta = if (self.buffer) |buffer| buffer.get_meta() else null; if (new_buf.root.lines() > root_mod.max_syntax_lines) { self.logger.print("large file threshold {d} lines < file size {d} lines", .{ @@ -623,7 +624,7 @@ pub const Editor = struct { null; }; - if (self.file_type) |ft| { + if (buffer_meta == null) if (self.file_type) |ft| { const frame_ = tracy.initZone(@src(), .{ .name = "did_open" }); defer frame_.deinit(); project_manager.did_open( @@ -634,7 +635,7 @@ pub const Editor = struct { new_buf.is_ephemeral(), ) catch |e| self.logger.print("project_manager.did_open failed: {any}", .{e}); - } + }; break :syntax syn; }; self.syntax_no_render = tp.env.get().is("no-syntax"); @@ -649,11 +650,11 @@ pub const Editor = struct { buffer.file_type_color = ftc; } - if (self.buffer) |buffer| if (buffer.get_meta()) |meta| { + if (buffer_meta) |meta| { const frame_ = tracy.initZone(@src(), .{ .name = "extract_state" }); defer frame_.deinit(); try self.extract_state(meta, .none); - }; + } try self.send_editor_open(file_path, new_buf.file_exists, ftn, fti, ftc); } @@ -5916,9 +5917,14 @@ pub const Editor = struct { self.syntax_no_render = tp.env.get().is("no-syntax"); self.syntax_report_timing = tp.env.get().is("syntax-report-timing"); - const ftn = if (self.file_type) |ft| ft.name else "text"; - const fti = if (self.file_type) |ft| ft.icon orelse "🖹" else "🖹"; - const ftc = if (self.file_type) |ft| ft.color orelse 0x000000 else 0x000000; + const ftn = if (self.file_type) |ft| ft.name else file_type_config.default.name; + const fti = if (self.file_type) |ft| ft.icon orelse file_type_config.default.icon else file_type_config.default.icon; + const ftc = if (self.file_type) |ft| ft.color orelse file_type_config.default.color else file_type_config.default.color; + if (self.buffer) |buffer| { + buffer.file_type_name = ftn; + buffer.file_type_icon = fti; + buffer.file_type_color = ftc; + } const file_exists = if (self.buffer) |b| b.file_exists else false; try self.send_editor_open(self.file_path orelse "", file_exists, ftn, fti, ftc); self.logger.print("file type {s}", .{file_type}); From a74c0ecf4619291e1d02ba5090e3ef0b7fbbf69c Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 30 Jul 2025 19:16:27 +0200 Subject: [PATCH 5/9] feat: add indent_mode detection (auto mode) --- src/tui/editor.zig | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 5ae958e..f9e84d9 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -594,15 +594,19 @@ pub const Editor = struct { self.logger.print("syntax highlighting disabled", .{}); self.syntax_no_render = true; } + + var content = std.ArrayListUnmanaged(u8).empty; + defer content.deinit(std.heap.c_allocator); + { + const frame_ = tracy.initZone(@src(), .{ .name = "store" }); + defer frame_.deinit(); + try new_buf.root.store(content.writer(std.heap.c_allocator), new_buf.file_eol_mode); + } + if (self.indent_mode == .auto) + self.detect_indent_mode(content.items); + self.syntax = syntax: { const lang_override = file_type orelse tp.env.get().str("language"); - var content = std.ArrayListUnmanaged(u8).empty; - defer content.deinit(std.heap.c_allocator); - { - const frame_ = tracy.initZone(@src(), .{ .name = "store" }); - defer frame_.deinit(); - try new_buf.root.store(content.writer(std.heap.c_allocator), new_buf.file_eol_mode); - } self.file_type = blk: { const frame_ = tracy.initZone(@src(), .{ .name = "guess" }); @@ -674,6 +678,19 @@ pub const Editor = struct { self.enable_auto_save = true; } + fn detect_indent_mode(self: *Self, content: []const u8) void { + var it = std.mem.splitScalar(u8, content, '\n'); + while (it.next()) |line| { + if (line.len == 0) continue; + if (line[0] == '\t') { + self.indent_mode = .tabs; + return; + } + } + self.indent_mode = .spaces; + return; + } + fn close(self: *Self) !void { var meta = std.ArrayListUnmanaged(u8).empty; defer meta.deinit(self.allocator); From 3abfd6555e0fb2ca315b42f55ad77a7fabf4eaad Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 30 Jul 2025 19:17:52 +0200 Subject: [PATCH 6/9] feat: make indent_cursor follow indent_mode and insert tabs --- src/tui/editor.zig | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index f9e84d9..5424ab7 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -3679,9 +3679,16 @@ pub const Editor = struct { const space = " "; var cursel: CurSel = .{}; cursel.cursor = cursor; - const cols = self.indent_size - find_first_non_ws(root, cursel.cursor.row, self.metrics) % self.indent_size; try move_cursor_begin(root, &cursel.cursor, self.metrics); - return self.insert(root, &cursel, space[0..cols], allocator) catch return error.Stop; + switch (self.indent_mode) { + .spaces, .auto => { + const cols = self.indent_size - find_first_non_ws(root, cursel.cursor.row, self.metrics) % self.indent_size; + return self.insert(root, &cursel, space[0..cols], allocator) catch return error.Stop; + }, + .tabs => { + return self.insert(root, &cursel, "\t", allocator) catch return error.Stop; + }, + } } fn indent_cursel(self: *Self, root_: Buffer.Root, cursel: *CurSel, allocator: Allocator) error{Stop}!Buffer.Root { From 4100585b03627ab77e211a0bbf9f6782a2fc4814 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 30 Jul 2025 19:37:28 +0200 Subject: [PATCH 7/9] feat: make smart_insert_line and friends follow indent_mode --- src/tui/editor.zig | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 5424ab7..8146541 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4403,16 +4403,39 @@ pub const Editor = struct { } pub const insert_line_meta: Meta = .{ .description = "Insert line" }; + fn generate_leading_ws(self: *Self, writer: anytype, leading_ws: usize) !void { + return switch (self.indent_mode) { + .spaces, .auto => generate_leading_spaces(writer, leading_ws), + .tabs => generate_leading_tabs(writer, leading_ws, self.tab_width), + }; + } + + fn generate_leading_spaces(writer: anytype, leading_ws: usize) !void { + var width = leading_ws; + while (width > 0) : (width -= 1) + try writer.writeByte(' '); + } + + fn generate_leading_tabs(writer: anytype, leading_ws: usize, tab_width: usize) !void { + var width = leading_ws; + while (width > 0) if (width >= tab_width) { + width -= tab_width; + try writer.writeByte('\t'); + } else { + width -= 1; + try writer.writeByte(' '); + }; + } + fn cursel_smart_insert_line(self: *Self, root: Buffer.Root, cursel: *CurSel, b_allocator: std.mem.Allocator) !Buffer.Root { - var leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.metrics), cursel.cursor.col); + const leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.metrics), cursel.cursor.col); var sfa = std.heap.stackFallback(512, self.allocator); const allocator = sfa.get(); var stream = std.ArrayListUnmanaged(u8).empty; defer stream.deinit(allocator); var writer = stream.writer(allocator); _ = try writer.write("\n"); - while (leading_ws > 0) : (leading_ws -= 1) - _ = try writer.write(" "); + try self.generate_leading_ws(&writer, leading_ws); return self.insert(root, cursel, stream.items, b_allocator); } @@ -4466,7 +4489,7 @@ pub const Editor = struct { const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - var leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.metrics), cursel.cursor.col); + const leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.metrics), cursel.cursor.col); try move_cursor_begin(root, &cursel.cursor, self.metrics); root = try self.insert(root, cursel, "\n", b.allocator); try move_cursor_left(root, &cursel.cursor, self.metrics); @@ -4475,8 +4498,7 @@ pub const Editor = struct { var stream = std.ArrayListUnmanaged(u8).empty; defer stream.deinit(allocator); var writer = stream.writer(self.allocator); - while (leading_ws > 0) : (leading_ws -= 1) - _ = try writer.write(" "); + try self.generate_leading_ws(&writer, leading_ws); if (stream.items.len > 0) root = try self.insert(root, cursel, stream.items, b.allocator); }; @@ -4501,7 +4523,7 @@ pub const Editor = struct { const b = try self.buf_for_update(); var root = b.root; for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { - var leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.metrics), cursel.cursor.col); + const leading_ws = @min(find_first_non_ws(root, cursel.cursor.row, self.metrics), cursel.cursor.col); try move_cursor_end(root, &cursel.cursor, self.metrics); var sfa = std.heap.stackFallback(512, self.allocator); const allocator = sfa.get(); @@ -4509,8 +4531,7 @@ pub const Editor = struct { defer stream.deinit(allocator); var writer = stream.writer(allocator); _ = try writer.write("\n"); - while (leading_ws > 0) : (leading_ws -= 1) - _ = try writer.write(" "); + try self.generate_leading_ws(&writer, leading_ws); if (stream.items.len > 0) root = try self.insert(root, cursel, stream.items, b.allocator); }; From ed1fe30e7422ee99baa379cc3813fd798a0777c8 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 30 Jul 2025 20:03:12 +0200 Subject: [PATCH 8/9] feat: make indent_size always equal to tab_width in indent_mode tabs --- src/tui/editor.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 8146541..0aaae44 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -460,9 +460,9 @@ pub const Editor = struct { fn init(self: *Self, allocator: Allocator, n: Plane, buffer_manager: *Buffer.Manager) void { const logger = log.logger("editor"); const frame_rate = tp.env.get().num("frame-rate"); - const indent_size = tui.config().indent_size; const tab_width = tui.config().tab_width; const indent_mode = tui.config().indent_mode; + const indent_size = if (indent_mode == .tabs) tab_width else tui.config().indent_size; self.* = Self{ .allocator = allocator, .plane = n, @@ -683,10 +683,12 @@ pub const Editor = struct { while (it.next()) |line| { if (line.len == 0) continue; if (line[0] == '\t') { + self.indent_size = self.tab_width; self.indent_mode = .tabs; return; } } + self.indent_size = tui.config().indent_size; self.indent_mode = .spaces; return; } From 666d30df3bf61e0ef9e5ecf0d10681cfceacd1ff Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 30 Jul 2025 20:04:26 +0200 Subject: [PATCH 9/9] fix: make unindent_cursor work correctly in indent_mode tabs --- src/tui/editor.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 0aaae44..6b3938e 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -3725,16 +3725,16 @@ pub const Editor = struct { const off = first % self.indent_size; const cols = if (off == 0) self.indent_size else off; const sel = cursel.enable_selection(root, self.metrics) catch return error.Stop; - sel.begin.move_begin(); - try sel.end.move_to(root, sel.end.row, cols, self.metrics); + try sel.begin.move_to(root, sel.begin.row, first, self.metrics); + try sel.end.move_to(root, sel.end.row, first - cols, self.metrics); var saved = false; - if (cursor_protect) |cp| if (cp.row == cursor.row and cp.col < cols) { - cp.col = cols + 1; + if (cursor_protect) |cp| if (cp.row == cursor.row and cp.col < first and cp.col >= first - cols) { + cp.col = first + 1; saved = true; }; newroot = try self.delete_selection(root, &cursel, allocator); if (cursor_protect) |cp| if (saved) { - try cp.move_to(root, cp.row, 0, self.metrics); + try cp.move_to(root, cp.row, first - cols, self.metrics); cp.clamp_to_buffer(newroot, self.metrics); }; return newroot;