From 6ee4c3caa6faf42b9825dca39e9130ac83f425a8 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 1 Oct 2026 18:07:32 -0300 Subject: Enter, o and O give the new line the old one's indent as it is, tabs and all, as acme's autoindent does; a level more after an opening delimiter is a tab where the indent is tabs The indent was rebuilt as spaces, partial levels dropped, so a tab-indented line went on with spaces (or with one space, after Enter). Co-Authored-By: Claude Opus 5.5 --- src/edit.zig | 34 ++++++++++++++++++++++++++++++++-- src/modal.zig | 8 +++----- test/snapshots/layout-open.golden | 2 +- test/snapshots/shrinktag.golden | 10 +++++----- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/edit.zig b/src/edit.zig index 3e3ba341..bf65935e 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -899,11 +899,18 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void { t.show(); return; } - const indent = modal.newlineIndentWidth(line, c.col); + // The line's own indent, as it is (tabs stay tabs), and a + // level more after an opening delimiter: a tab where the + // indent is tabs, else spaces. + const lead = modal.indentText(line); + const more = modal.newlineIndentWidth(line, c.col) - lead.len; + const tabbed = std.mem.indexOfScalar(u8, lead, '\t') != null; + const indent = lead.len + (if (more == 0) 0 else if (tabbed) 1 else more); const arena = p.scratch.allocator(); const block = arena.alloc(u8, 1 + indent) catch return; block[0] = '\n'; - @memset(block[1..], ' '); + @memcpy(block[1..][0..lead.len], lead); + @memset(block[1 + lead.len ..], if (tabbed) '\t' else ' '); // a tag keeps its blanks: they part its words, and the last // may be its read-only prefix's const kept = if (t.what == .body) std.mem.trimEnd(u8, before, " \t").len else c.col; @@ -2136,6 +2143,29 @@ test "insert newline adds one indent level after a closing call" { try std.testing.expectEqual(@as(i32, 8), pane.body.cur_col); } +test "Enter in insert mode keeps a tab-indented line's tabs, as o and O do" { + const gpa = std.testing.allocator; + const p = try Pardes.init(gpa, .{ .tty_only = true }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const pane = try p.setTestFile("\t\tvalue = 1;\n\tif (x) {\n"); + pane.body.mode = .insert; + pane.body.cur_col = 12; + p.update(.{ .key = .{ .cp = Key.enter } }); + try std.testing.expectEqualStrings("\t\tvalue = 1;\n\t\t\n\tif (x) {\n", pane.file.?.content); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_col); + // After an opening brace, a level more: a tab, as the indent is tabs. + pane.body.cur_row = 2; + pane.body.cur_col = 9; + p.update(.{ .key = .{ .cp = Key.enter } }); + try std.testing.expectEqualStrings("\t\tvalue = 1;\n\t\t\n\tif (x) {\n\t\t\n", pane.file.?.content); + // o opens a line with the indent as it is. + p.update(.{ .key = .{ .cp = Key.escape } }); + pane.body.cur_row = 0; + p.update(.{ .key = .{ .cp = 'o' } }); + try std.testing.expect(std.mem.startsWith(u8, pane.file.?.content, "\t\tvalue = 1;\n\t\t\n")); +} + test "insert and normal modes edit complete Unicode graphemes" { const gpa = std.testing.allocator; const p = try Pardes.init(gpa, .{ .cols = 60, .rows = 12 }); diff --git a/src/modal.zig b/src/modal.zig index 11eda7b9..316a66fa 100644 --- a/src/modal.zig +++ b/src/modal.zig @@ -1773,12 +1773,10 @@ pub fn indentWidth(line: []const u8) usize { return w; } -/// full indent LEVELS of a line as spaces (helix indent_level_for_line: -/// partial levels round down) — what o/O/insert-newline copy. +/// A line's indent as it is, tabs and spaces both, as acme's autoindent +/// copies it: what o/O/insert-newline give the new line. pub fn indentText(line: []const u8) []const u8 { - const level = indentWidth(line) / INDENT_W; - const max = " "; // 8 levels is plenty (ponytail) - return max[0..@min(level * INDENT_W, max.len)]; + return line[0 .. std.mem.indexOfNone(u8, line, " \t") orelse line.len]; } /// Copy full indent levels and add one after (, [, {, or ), without parsing. diff --git a/test/snapshots/layout-open.golden b/test/snapshots/layout-open.golden index a3d9dfdd..d2cbb9a9 100644 --- a/test/snapshots/layout-open.golden +++ b/test/snapshots/layout-open.golden @@ -1,4 +1,4 @@ -== snap before grid=100x31 cursor=2,9 +== snap before grid=100x31 cursor=4,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/layout-open/cwd Tty+bash Save /tmp/pardes-snap/layout-open/cwd Tty+bash Save diff --git a/test/snapshots/shrinktag.golden b/test/snapshots/shrinktag.golden index d73c0072..6e64d89b 100644 --- a/test/snapshots/shrinktag.golden +++ b/test/snapshots/shrinktag.golden @@ -1,4 +1,4 @@ -== snap before grid=100x31 cursor=2,9 +== snap before grid=100x31 cursor=4,9 |Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo @@ -20,7 +20,7 @@ | de Filter Collapse Del | ls | w.txt -== snap shrunk grid=100x31 cursor=2,9 +== snap shrunk grid=100x31 cursor=4,9 |4: w.txt ls |5: w.txt |6: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo @@ -31,9 +31,9 @@ |18: |19: |20: -== snap move-preview grid=100x31 cursor=2,9 +== snap move-preview grid=100x31 cursor=4,9 |8: ls ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ -== snap restored grid=100x31 cursor=2,9 +== snap restored grid=100x31 cursor=4,9 |4: ls ls |5: w.txt w.txt |6: @@ -44,7 +44,7 @@ |18: de Filter Collapse Del |19: ls |20: w.txt -== snap bottom-shrunk grid=100x31 cursor=2,9 +== snap bottom-shrunk grid=100x31 cursor=4,9 |17: |18: |19: -- cgit v1.3