summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig633
1 files changed, 254 insertions, 379 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index a29973e7..ce7c7065 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -687,9 +687,8 @@ test "pane-tag Exec prefers Tty and argument builtins before shell fallback" {
// Argument-taking builtins use the same gesture path: a tag sweep is the
// complete command line, and Theme consumes its tail without a PTY write.
- pane.tag_tail_len = 0;
- try std.testing.expect(pane.appendTag(" Theme dark"));
- pane.tag_init = true;
+ if (pane.tag.own) |own| p.gpa.free(own);
+ pane.tag.own = try p.gpa.dupe(u8, " Theme dark");
p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .press, .col = tag_x + 1, .row = tag_y } });
p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .drag, .col = tag_x + 10, .row = tag_y } });
p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .release, .col = tag_x + 10, .row = tag_y } });
@@ -707,8 +706,8 @@ test "pane-tag Exec prefers Tty and argument builtins before shell fallback" {
// A word absent from the builtin vocabulary takes the fallback, including
// the terminating carriage return expected by the shell prompt.
- pane.tag_tail_len = 0;
- try std.testing.expect(pane.appendTag(" DefinitelyNotABuiltin"));
+ if (pane.tag.own) |own| p.gpa.free(own);
+ pane.tag.own = try p.gpa.dupe(u8, " DefinitelyNotABuiltin");
p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .press, .col = tag_x + 1, .row = tag_y } });
p.update(.{ .mouse = .{ .button = config.exec_button, .kind = .release, .col = tag_x + 1, .row = tag_y } });
var sent: [256]u8 = undefined;
@@ -743,20 +742,20 @@ test "Collapse builtin keeps content and restores the pane from its default tag"
try std.testing.expectEqual(revision, pane.file.?.revision);
try std.testing.expectEqualStrings("keep this text\n", pane.file.?.content);
try std.testing.expect((try p.render(p.scratch.allocator())).cursor == null);
- p.enterTagEdit(pane, -1);
+ p.enterTag(pane);
const tag_cursor = (try p.render(p.scratch.allocator())).cursor orelse return error.MissingTagCursor;
try std.testing.expectEqual(p.rects[0].y, tag_cursor.y);
- Pardes.exitTagEdit(pane);
+ pane.focus = .body;
try std.testing.expect(p.executeBuiltinLine(0, "Collapse"));
p.sync();
try std.testing.expect(!pane.collapsed);
try std.testing.expectEqual(height, p.rects[0].h);
// Unedited defaults from the preceding version gain the new command.
- pane.tag_init = false;
- pane.tag_tail_len = 0;
+ if (pane.tag.own) |own| p.gpa.free(own);
+ pane.tag.own = null;
p.restoreTailAt(pane, pre_collapse_file_pane_tail, "", .file);
- try std.testing.expect(!pane.tag_init);
+ try std.testing.expect(pane.tag.own == null);
try std.testing.expectEqualStrings(file_pane_tail, Pardes.curTail(pane));
}
@@ -859,8 +858,8 @@ test "TreeContext command and custom tag survive a workspace round trip" {
try std.testing.expect(pane.file.?.tree_context);
try std.testing.expect(p.executeBuiltinLine(0, "TreeContext invalid"));
try std.testing.expect(pane.file.?.tree_context);
- try std.testing.expect(pane.appendTag(" Save Keep Del"));
- pane.tag_init = true;
+ if (pane.tag.own) |own| p.gpa.free(own);
+ pane.tag.own = try p.gpa.dupe(u8, " Save Keep Del");
try dump.dumpState(p);
const restored = try dump.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
defer restored.deinit();
@@ -874,22 +873,23 @@ test "Togglettymode builtin and keyboard chord share mode transitions including
defer p.deinit();
const pane = p.panes[0].?;
try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Mode ") != null);
- p.seedTail(pane);
- try std.testing.expect(pane.appendTag(" Togglettymode"));
+ try pane.appendTag(" Togglettymode");
try std.testing.expectEqual(Text.Mode.tty, pane.body.mode);
try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode"));
try std.testing.expectEqual(Text.Mode.normal, pane.body.mode);
p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } });
try std.testing.expectEqual(Text.Mode.tty, pane.body.mode);
- p.enterTagEdit(pane, -1);
- try std.testing.expectEqual(Text.Mode.insert, pane.body.mode);
+ // The tag has its own mode: the body keeps tty while the tag is typed into.
+ p.enterTag(pane);
+ try std.testing.expect(pane.focus == .tag);
+ try std.testing.expectEqual(Text.Mode.tty, pane.body.mode);
try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode"));
try std.testing.expectEqual(Text.Mode.normal, pane.body.mode);
- try std.testing.expect(!pane.tag_edit);
- p.enterTagEdit(pane, -1);
+ try std.testing.expect(pane.focus == .body);
+ p.enterTag(pane);
p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } });
try std.testing.expectEqual(Text.Mode.tty, pane.body.mode);
- try std.testing.expect(!pane.tag_edit);
+ try std.testing.expect(pane.focus == .body);
p.acknowledgePanelPresentation(&.{});
const tag = try p.tagText(p.scratch.allocator(), pane);
@@ -974,10 +974,8 @@ test "compact tag commands stay beside their own prefix across layout changes" {
try std.testing.expectEqualStrings(above_tag, try p.tagText(p.scratch.allocator(), above));
p.update(.{ .resize = .{ .cols = 100, .rows = 30 } });
while (p.nextEffect()) |_| {}
-
- p.seedTail(below);
- try std.testing.expectEqualStrings(tail, below.tagSlice());
- try std.testing.expect(below.appendTag(" lots and lots of typing out here"));
+ try std.testing.expectEqualStrings(tail, Pardes.curTail(below));
+ try below.appendTag(" lots and lots of typing out here");
try std.testing.expectEqual(above_tag.len, (try p.tagText(p.scratch.allocator(), above)).len);
p.update(.{ .resize = .{ .cols = 100, .rows = 2 } });
@@ -997,14 +995,14 @@ test "legacy default tag tails upgrade while custom tails remain owned" {
legacy_pane_tail,
});
p.restoreTail(terminal, terminal_old);
- try std.testing.expect(!terminal.tag_init);
+ try std.testing.expect(terminal.tag.own == null);
try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal));
const terminal_previous = try std.fmt.allocPrint(p.scratch.allocator(), "{s} New Del", .{
try p.tagPrefix(terminal),
});
p.restoreTail(terminal, terminal_previous);
- try std.testing.expect(!terminal.tag_init);
+ try std.testing.expect(terminal.tag.own == null);
try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal));
const terminal_prefilter = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{
@@ -1012,60 +1010,60 @@ test "legacy default tag tails upgrade while custom tails remain owned" {
prev_terminal_pane_tail,
});
p.restoreTail(terminal, terminal_prefilter);
- try std.testing.expect(!terminal.tag_init);
+ try std.testing.expect(terminal.tag.own == null);
try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal));
for ([_][]const u8{ pre_mode_terminal_pane_tail, pre_tty_first_terminal_pane_tail, pre_collapse_terminal_pane_tail }) |old_tail| {
const old = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{ try p.tagPrefix(terminal), old_tail });
p.restoreTail(terminal, old);
- try std.testing.expect(!terminal.tag_init);
+ try std.testing.expect(terminal.tag.own == null);
try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal));
}
const custom_mode = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s} Keep", .{ try p.tagPrefix(terminal), pre_mode_terminal_pane_tail });
p.restoreTail(terminal, custom_mode);
- try std.testing.expect(terminal.tag_init);
+ try std.testing.expect(terminal.tag.own != null);
try std.testing.expectEqualStrings(pre_mode_terminal_pane_tail ++ " Keep", Pardes.curTail(terminal));
- terminal.tag_tail_len = 0;
- terminal.tag_init = false;
+ if (terminal.tag.own) |own| p.gpa.free(own);
+ terminal.tag.own = null;
const terminal_custom = try std.fmt.allocPrint(p.scratch.allocator(), "{s} Keep Del", .{
try p.tagPrefix(terminal),
});
p.restoreTail(terminal, terminal_custom);
- try std.testing.expect(terminal.tag_init);
+ try std.testing.expect(terminal.tag.own != null);
try std.testing.expectEqualStrings(" Keep Del", Pardes.curTail(terminal));
- terminal.tag_tail_len = 0;
- terminal.tag_init = false;
+ if (terminal.tag.own) |own| p.gpa.free(own);
+ terminal.tag.own = null;
p.restoreDumpTail(terminal, .{
.kind = .terminal,
.tag = "TTY /historical/cwd Keep Del",
.body = "",
.terminal = .{ .cwd = "/historical/cwd" },
});
- try std.testing.expect(terminal.tag_init);
+ try std.testing.expect(terminal.tag.own != null);
try std.testing.expectEqualStrings(" Keep Del", Pardes.curTail(terminal));
- terminal.tag_tail_len = 0;
- terminal.tag_init = false;
+ if (terminal.tag.own) |own| p.gpa.free(own);
+ terminal.tag.own = null;
p.restoreDumpTail(terminal, .{
.kind = .terminal,
.tag = "TTY /historical/cwd New Del",
.body = "",
.terminal = .{ .cwd = "/historical/cwd" },
});
- try std.testing.expect(!terminal.tag_init);
+ try std.testing.expect(terminal.tag.own == null);
try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal));
// This happens to be the historical FILE default, but on a terminal it is
// user-owned text and must not be swallowed by migration.
- terminal.tag_tail_len = 0;
- terminal.tag_init = false;
+ if (terminal.tag.own) |own| p.gpa.free(own);
+ terminal.tag.own = null;
const terminal_fileish = try std.fmt.allocPrint(p.scratch.allocator(), "{s} Save Del", .{
try p.tagPrefix(terminal),
});
p.restoreTail(terminal, terminal_fileish);
- try std.testing.expect(terminal.tag_init);
+ try std.testing.expect(terminal.tag.own != null);
try std.testing.expectEqualStrings(" Save Del", Pardes.curTail(terminal));
// A savable file has a distinct old default. Save remains first after the
@@ -1076,25 +1074,25 @@ test "legacy default tag tails upgrade while custom tails remain owned" {
legacy_file_pane_tail,
});
p.restoreTail(file, file_old);
- try std.testing.expect(!file.tag_init);
+ try std.testing.expect(file.tag.own == null);
try std.testing.expectEqualStrings(" Save Tty Collapse Del", Pardes.curTail(file));
const file_custom = try std.fmt.allocPrint(p.scratch.allocator(), "{s} Save Mine Del", .{
try p.tagPrefix(file),
});
p.restoreTail(file, file_custom);
- try std.testing.expect(file.tag_init);
+ try std.testing.expect(file.tag.own != null);
try std.testing.expectEqualStrings(" Save Mine Del", Pardes.curTail(file));
// Conversely the generic pane's current default is a custom edit on a
// savable file. Legacy recognition is scoped to the source pane class.
- file.tag_tail_len = 0;
- file.tag_init = false;
+ if (file.tag.own) |own| p.gpa.free(own);
+ file.tag.own = null;
const file_generic = try std.fmt.allocPrint(p.scratch.allocator(), "{s} New Del", .{
try p.tagPrefix(file),
});
p.restoreTail(file, file_generic);
- try std.testing.expect(file.tag_init);
+ try std.testing.expect(file.tag.own != null);
try std.testing.expectEqualStrings(" New Del", Pardes.curTail(file));
const image_doc = try panes.Image.create(p, 1, "/tmp/legacy image.ppm", &.{});
@@ -1105,8 +1103,8 @@ test "legacy default tag tails upgrade while custom tails remain owned" {
.body = "",
.image = .{ .path = "/tmp/legacy image.ppm" },
});
- try std.testing.expect(image_doc.tag_init);
- try std.testing.expectEqualStrings(" Keep Del", image_doc.tagSlice());
+ try std.testing.expect(image_doc.tag.own != null);
+ try std.testing.expectEqualStrings(" Keep Del", Pardes.curTail(image_doc));
}
test "Joincol folds the active column into its right neighbor, keeping its panes" {
@@ -1247,11 +1245,11 @@ test "pane defaults keep creation of scratch panes in column tags" {
try std.testing.expectEqualStrings("New Tty Find Grep Joincol Delcol", p.columnTag(0));
const terminal = p.panes[0].?;
p.restoreTailAt(terminal, pre_column_terminal_pane_tail, "", .terminal);
- try std.testing.expect(!terminal.tag_init);
+ try std.testing.expect(terminal.tag.own == null);
try std.testing.expectEqualStrings(terminal_pane_tail, Pardes.curTail(terminal));
const file = try p.setTestFile("unchanged\n");
p.restoreTailAt(file, pre_column_file_pane_tail, "", .file);
- try std.testing.expect(!file.tag_init);
+ try std.testing.expect(file.tag.own == null);
try std.testing.expectEqualStrings(file_pane_tail, Pardes.curTail(file));
}
@@ -1398,8 +1396,8 @@ test "image dump restores source bytes renderer choices and exact custom tail" {
pane.image.?.glyph_art = true;
pane.image.?.pmode = .terminal;
pane.image.?.ascii = false;
- try std.testing.expect(pane.appendTag(" Keep Del"));
- pane.tag_init = true;
+ if (pane.tag.own) |own| p.gpa.free(own);
+ pane.tag.own = try p.gpa.dupe(u8, " Keep Del");
try dump.dumpState(p);
var first_dump = try dump.readZon(gpa, p.dump_out.?, "image-first-dump");
@@ -1416,8 +1414,8 @@ test "image dump restores source bytes renderer choices and exact custom tail" {
try std.testing.expect(restored_pane.image.?.glyph_art);
try std.testing.expectEqual(image.PaletteMode.terminal, restored_pane.image.?.pmode);
try std.testing.expect(!restored_pane.image.?.ascii);
- try std.testing.expect(restored_pane.tag_init);
- try std.testing.expectEqualStrings(" Keep Del", restored_pane.tagSlice());
+ try std.testing.expect(restored_pane.tag.own != null);
+ try std.testing.expectEqualStrings(" Keep Del", Pardes.curTail(restored_pane));
try std.testing.expectEqualSlices(u8, source, restored_pane.image.?.raw);
try dump.dumpState(restored);
@@ -2095,20 +2093,20 @@ test "Save reaches every tagline with text behind it and no other" {
pane_tail,
});
p.restoreTail(out, dumped);
- try std.testing.expect(!out.tag_init);
+ try std.testing.expect(out.tag.own == null);
try std.testing.expectEqualStrings(locations_pane_tail, Pardes.curTail(out));
// A scratch is an output buffer that has worn the FILE tails all along,
// so its own older default upgrades from that family too.
out.file.?.output = .{ .from = .{ .cmd = std.meta.stringToEnum(Builtin, "New").? } };
- out.tag_tail_len = 0;
- out.tag_init = false;
+ if (out.tag.own) |own| p.gpa.free(own);
+ out.tag.own = null;
const scratch_dumped = try std.fmt.allocPrint(p.scratch.allocator(), "{s}{s}", .{
try p.tagPrefix(out),
prev_file_pane_tail,
});
p.restoreTail(out, scratch_dumped);
- try std.testing.expect(!out.tag_init);
+ try std.testing.expect(out.tag.own == null);
try std.testing.expectEqualStrings(" Save Tty Collapse Del", Pardes.curTail(out));
// An image's bytes on disk already are what they are: nothing of the
@@ -2629,24 +2627,25 @@ test "Unicode display cells map back to body and tag byte cursors" {
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = text_x + 2, .row = body_y } });
try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col);
- // A click in the wide path glyph likewise becomes a byte cursor at the
+ // A click in the wide path glyph drafts a name with its caret at the
// grapheme start; arrow motion then advances by the full UTF-8 cluster.
- p.enterTagEdit(pane, 1);
- try std.testing.expectEqual(@as(u16, 0), pane.tag_col);
- p.tagInsertKey(pane, .{ .cp = Key.right });
- try std.testing.expectEqual(@as(u16, 3), pane.tag_col);
- p.tagInsertKey(pane, .{ .cp = Key.right });
- try std.testing.expectEqual(@as(u16, 6), pane.tag_col);
+ p.draftName(pane, 1);
+ try std.testing.expectEqual(@as(i32, 0), pane.input.cur_col);
+ edit.insertKey(p, &pane.input, .{ .cp = Key.right });
+ try std.testing.expectEqual(@as(i32, 3), pane.input.cur_col);
+ edit.insertKey(p, &pane.input, .{ .cp = Key.right });
+ try std.testing.expectEqual(@as(i32, 6), pane.input.cur_col);
+ pane.closePrompt();
- const before = try gpa.dupe(u8, pane.tagSlice());
+ const before = try gpa.dupe(u8, Pardes.curTail(pane));
defer gpa.free(before);
- p.enterTagEdit(pane, -1);
- const insertion = pane.tag_col;
- p.tagInsertKey(pane, .{ .cp = 'λ', .text = "λ" });
- try std.testing.expectEqual(insertion + 2, pane.tag_col);
- p.tagInsertKey(pane, .{ .cp = Key.backspace });
- try std.testing.expectEqual(insertion, pane.tag_col);
- try std.testing.expectEqualStrings(before, pane.tagSlice());
+ p.enterTag(pane);
+ const insertion = pane.tag.cur_col;
+ edit.insertKey(p, &pane.tag, .{ .cp = 'λ', .text = "λ" });
+ try std.testing.expectEqual(insertion + 2, pane.tag.cur_col);
+ edit.insertKey(p, &pane.tag, .{ .cp = Key.backspace });
+ try std.testing.expectEqual(insertion, pane.tag.cur_col);
+ try std.testing.expectEqualStrings(before, Pardes.curTail(pane));
}
test "tabbed file aligns syntax cursor and mouse at the nearest insertion gap" {
@@ -4110,6 +4109,9 @@ pub const Pardes = struct {
if (pane.file) |*file| panes.File.deinit(p, pane, file);
if (pane.ovl) |o| p.gpa.free(o.text);
if (pane.input.own) |text| p.gpa.free(text);
+ if (pane.tag.own) |text| p.gpa.free(text);
+ for (pane.tag.ed_undo[0..pane.tag.ed_undo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text);
+ for (pane.tag.ed_redo[0..pane.tag.ed_redo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text);
for (pane.body.ed_undo[0..pane.body.ed_undo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text);
for (pane.body.ed_redo[0..pane.body.ed_redo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text);
panes.Terminal.deinitEmulator(pane, p.gpa);
@@ -4762,7 +4764,7 @@ pub const Pardes = struct {
);
if (pane.image) |*state| return panes.Image.tagPrefix(arena, state);
if (pane.file) |f| {
- const name = if (pane.tag_name_len) |len| pane.tag_name[0..len] else f.path;
+ const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path;
if (panes.Output.fileTraits(f.output).saves and f.revision != f.saved_revision)
return std.fmt.allocPrint(arena, "{s}{s}", .{ name, dirty_marker });
return arena.dupe(u8, name);
@@ -4770,10 +4772,10 @@ pub const Pardes = struct {
return arena.dupe(u8, pane.cwdSlice());
}
- /// the editable tail: the user's edited buffer once touched, else defaults
- fn curTail(pane: *Pane) []const u8 {
- if (pane.tag_init) return pane.tagSlice();
- return defaultTail(pane);
+ /// The tag's own text: what the user made of it, or until then the
+ /// default a pane of its kind starts with.
+ pub fn curTail(pane: *Pane) []const u8 {
+ return pane.tag.own orelse defaultTail(pane);
}
pub fn defaultTail(pane: *const Pane) []const u8 {
@@ -4793,6 +4795,8 @@ pub const Pardes = struct {
return null;
}
+ /// The tag as it is shown, read and pointed at: its computed prefix and
+ /// its own text after it.
pub fn tagText(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) ![]u8 {
const prefix = try p.tagPrefix(pane);
const tail = curTail(pane);
@@ -4814,7 +4818,7 @@ pub const Pardes = struct {
}
var start: usize = 0;
const name = if (pane.file) |file|
- if (pane.tag_name_len) |len| pane.tag_name[0..len] else file.path
+ if (pane.prompt == .name) pane.input.own orelse file.path else file.path
else if (pane.image) |state| blk: {
// Image tags put renderer controls before the path.
start = prefix_len -| state.path.len;
@@ -4827,88 +4831,76 @@ pub const Pardes = struct {
return .{ .lo = start + basename, .hi = start + name.len };
}
- pub fn seedTail(_: *Pardes, pane: *Pane) void {
- if (pane.tag_init) return;
+ /// Give the keyboard to the tag in normal mode (`:`), its cursor where
+ /// the tag last had it. The first time there is nothing to remember, and
+ /// it starts on Save, so that `:` and the execute key write the file.
+ pub fn enterTag(p: *Pardes, pane: *Pane) void {
+ const t = &pane.tag;
const tail = curTail(pane);
- if (tail.len > pane.tag_tail.len) return;
- @memcpy(pane.tag_tail[0..tail.len], tail);
- pane.tag_tail_len = tail.len;
- pane.tag_init = true;
- }
-
- pub fn enterTagEdit(p: *Pardes, pane: *Pane, col: i32) void {
- if (pane.tag_edit) pane.tag_saved_col = pane.tag_col;
- const edit0: i32 = @intCast((p.tagPrefix(pane) catch return).len);
- p.seedTail(pane);
- if (!pane.tag_init) return;
- if (!pane.tag_edit) pane.tag_mode = pane.body.mode;
- pane.tag_edit = true;
- pane.tag_sel = false;
- // a one-line tag has no use for normal mode: always insert
- pane.body.mode = .insert;
- pane.body.normal.clear();
- const end = edit0 + @as(i32, @intCast(pane.tag_tail_len));
- const tail = pane.tagSlice();
- const lead: i32 = @intCast(tail.len - std.mem.trimStart(u8, tail, " ").len);
- if (col < 0) {
- const text = p.tagText(p.scratch.allocator(), pane) catch return;
- if (pane.tag_saved_col) |saved| {
- pane.tag_col = @intCast(modal.graphemeStart(text, @min(saved, text.len)));
- } else {
- pane.tag_col = @intCast(@min(edit0 + lead, end));
- }
- if (pane.tag_saved_col == null) if (p.paneIdOf(pane)) |id| {
+ if (!t.cur_pinned) {
+ var words = std.mem.tokenizeAny(u8, tail, " \t\n");
+ const at = while (words.next()) |word| {
+ if (std.mem.eql(u8, word, "Save")) break @intFromPtr(word.ptr) - @intFromPtr(tail.ptr);
+ } else tail.len - std.mem.trimStart(u8, tail, " ").len;
+ const cursor = modal.positionAt(tail, @min(at, tail.len));
+ t.cur_row = @intCast(cursor.row);
+ t.cur_col = @intCast(cursor.col);
+ t.cur_pinned = true;
+ // Reveal the suffix together with its commands, not merely the
+ // first command letter at the viewport's far right edge.
+ if (p.paneIdOf(pane)) |id| {
+ const tag = p.tagText(p.scratch.allocator(), pane) catch return;
+ const line = tag[0 .. std.mem.indexOfScalar(u8, tag, '\n') orelse tag.len];
const width = p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET);
- // Reveal the suffix together with its commands, not merely the
- // first command letter at the viewport's far right edge.
- pane.tag_scroll = @intCast(panes.File.displayWidth(text) + 1 -| width);
- };
- } else {
- const text = p.tagText(p.scratch.allocator(), pane) catch return;
- pane.tag_col = @intCast(@min(text.len, panes.File.rawAtDisplay(text, @as(usize, @intCast(col)) + pane.tag_scroll)));
+ pane.tag_scroll = @intCast(panes.File.displayWidth(line) + 1 -| width);
+ }
}
+ // The text may have changed since (a 9P write, a rename), so the
+ // remembered cursor is pulled back inside it. It is the cursor that
+ // is remembered, not a selection.
+ const rows = modal.cursorLineCount(tail);
+ t.cur_row = @intCast(@min(@as(usize, @intCast(@max(0, t.cur_row))), rows - 1));
+ const line = modal.lineSlice(tail, @intCast(t.cur_row));
+ t.cur_col = @intCast(modal.graphemeStart(line, @min(line.len, @as(usize, @intCast(@max(0, t.cur_col))))));
+ t.vsel.active = false;
+ t.msel.active = false;
+ t.nsel = 0;
+ t.select = false;
+ t.append_at = null;
+ t.sticky_col = -1;
+ t.mode = .normal;
+ t.normal.clear();
+ pane.focus = .tag;
}
- pub fn exitTagEdit(pane: *Pane) void {
- if (pane.tag_edit) pane.tag_saved_col = pane.tag_col;
- if (pane.isTerminal()) {
- if (pane.tag_edit) pane.body.mode = pane.tag_mode;
- } else pane.body.mode = .normal;
- pane.tag_edit = false;
- pane.tag_sel = false;
- pane.tag_name_len = null;
- pane.tag_scroll = 0;
- }
-
- pub fn tagSelBounds(pane: *Pane) struct { lo: usize, hi: usize } {
- const a: usize = pane.tag_anchor;
- const c: usize = pane.tag_col;
- return .{ .lo = @min(a, c), .hi = @max(a, c) };
- }
-
- fn tagChordText(p: *Pardes, pane: *Pane) ?[]const u8 {
- const text = p.tagText(p.scratch.allocator(), pane) catch return null;
- if (pane.tag_sel) {
- const b = tagSelBounds(pane);
- const hi = modal.nextGrapheme(text, b.hi);
- return if (hi > b.lo) text[b.lo..hi] else null;
- }
- const b = config.wordBounds(text, @min(@as(usize, pane.tag_col), text.len));
- return if (b.hi > b.lo) text[b.lo..b.hi] else null;
+ /// Where display column `col` of the tagline (tag scroll included) lands
+ /// in the tag's own text, or null when it is on the prefix.
+ pub fn tagOffsetAt(p: *Pardes, pane: *Pane, col: usize) ?usize {
+ const prefix = p.tagPrefix(pane) catch return null;
+ const width = panes.File.displayWidth(prefix);
+ if (col < width) return null;
+ const tail = curTail(pane);
+ const line = tail[0 .. std.mem.indexOfScalar(u8, tail, '\n') orelse tail.len];
+ return modal.graphemeStart(line, panes.File.rawAtDisplay(line, col - width));
}
- fn tagNameEnd(pane: *const Pane) ?usize {
- const f = pane.file orelse return null;
- if (!panes.Output.fileTraits(f.output).saves or f.path.len > pane.tag_name.len) return null;
- return if (pane.tag_name_len) |len| len else f.path.len;
+ /// Start a name draft for a file that can take one, its caret at display
+ /// column `col` of the path. Nothing changes until it is typed into.
+ pub fn draftName(p: *Pardes, pane: *Pane, col: usize) void {
+ const f = pane.file orelse return;
+ if (!panes.Output.fileTraits(f.output).saves) return;
+ const name = p.gpa.dupe(u8, f.path) catch return;
+ pane.closePrompt();
+ pane.prompt = .name;
+ pane.input = .{ .what = .input, .own = name, .mode = .insert, .cur_pinned = true, .cur_col = @intCast(@min(name.len, panes.File.rawAtDisplay(name, col))) };
}
- /// Commit a staged buffer name, never a disk rename or write. Keep the
+ /// Commit a drafted buffer name, never a disk rename or write. Keep the
/// draft visible on failure so the user can correct it or cancel with Esc.
pub fn commitTagName(p: *Pardes, pane: *Pane) bool {
- const len = pane.tag_name_len orelse return true;
+ if (pane.prompt != .name) return true;
const id = p.paneIdOf(pane) orelse return false;
- const name = pane.tag_name[0..len];
+ const name = pane.input.own orelse "";
if (name.len == 0 or name[name.len - 1] == '/' or !std.unicode.utf8ValidateSlice(name)) {
p.reportError(id, "name", error.InvalidName);
return false;
@@ -4917,8 +4909,6 @@ pub const Pardes = struct {
p.reportError(id, "name", error.InvalidName);
return false;
};
- const old_prefix = (p.tagPrefix(pane) catch return false).len;
- const tail_col = @as(usize, pane.tag_col) -| old_prefix;
const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ paneDir(pane), name }) catch |err| {
p.reportError(id, "name", err);
return false;
@@ -4927,181 +4917,10 @@ pub const Pardes = struct {
p.reportError(id, "name", err);
return false;
};
- pane.tag_name_len = null;
- const prefix = (p.tagPrefix(pane) catch return false).len;
- pane.tag_col = @intCast(prefix + @min(tail_col, pane.tag_tail_len));
- pane.tag_sel = false;
- return true;
- }
-
- /// Replace one range in the rendered tag. The file name and command tail
- /// retain separate identities even when a selection crosses their boundary.
- pub fn replaceTagText(p: *Pardes, pane: *Pane, start: usize, finish: usize, input: []const u8) bool {
- const prefix_len = (p.tagPrefix(pane) catch return false).len;
- const end = prefix_len + pane.tag_tail_len;
- const lo = @min(start, end);
- const hi = @max(lo, @min(finish, end));
- const name_end = tagNameEnd(pane);
- const editing_name = if (name_end) |n| lo <= n else false;
- if (!editing_name and lo < prefix_len) return false;
- var bytes: [limits.max_tag_tail]u8 = undefined;
- var count: usize = 0;
- for (input) |byte| {
- if (byte == '\n' or byte == '\r') continue;
- if (count == bytes.len) return false;
- bytes[count] = byte;
- count += 1;
- }
- const tail_lo = lo -| prefix_len;
- const tail_hi = hi -| prefix_len;
- const tail_insert = if (editing_name) 0 else count;
- const tail_len = pane.tag_tail_len - (tail_hi - tail_lo) + tail_insert;
- if (tail_len > pane.tag_tail.len) return false;
- if (editing_name) {
- const n = name_end.?;
- const name_hi = @min(hi, n);
- const name_len = n - (name_hi - lo) + count;
- if (name_len > pane.tag_name.len) return false;
- if (pane.tag_name_len == null) @memcpy(pane.tag_name[0..n], pane.file.?.path);
- if (count > name_hi - lo)
- std.mem.copyBackwards(u8, pane.tag_name[lo + count .. name_len], pane.tag_name[name_hi..n])
- else
- std.mem.copyForwards(u8, pane.tag_name[lo + count .. name_len], pane.tag_name[name_hi..n]);
- @memcpy(pane.tag_name[lo..][0..count], bytes[0..count]);
- pane.tag_name_len = @intCast(name_len);
- }
- if (tail_insert > tail_hi - tail_lo)
- std.mem.copyBackwards(u8, pane.tag_tail[tail_lo + tail_insert .. tail_len], pane.tag_tail[tail_hi..pane.tag_tail_len])
- else
- std.mem.copyForwards(u8, pane.tag_tail[tail_lo + tail_insert .. tail_len], pane.tag_tail[tail_hi..pane.tag_tail_len]);
- if (!editing_name) @memcpy(pane.tag_tail[tail_lo..][0..count], bytes[0..count]);
- pane.tag_tail_len = tail_len;
- pane.tag_col = @intCast(lo + count);
- pane.tag_sel = false;
+ pane.closePrompt();
return true;
}
- fn tagInsertKey(p: *Pardes, pane: *Pane, key: Key) void {
- if (hit(key, config.escape)) {
- exitTagEdit(pane); // the tag is ALWAYS insert; Esc leaves it
- return;
- }
- // the acme chords on the selection or the word at the cursor
- if (hit(key, config.look_key) or hit(key, config.exec_key)) {
- if (pane.tag_name_len != null) {
- const in_name = pane.tag_col < (p.tagPrefix(pane) catch return).len;
- if (!p.commitTagName(pane)) return;
- if (hit(key, config.look_key) or in_name) {
- exitTagEdit(pane);
- return;
- }
- }
- if (p.tagChordText(pane)) |txt| {
- pane.tag_sel = false;
- exec.runBuiltin(p, if (hit(key, config.look_key)) config.look_cmd else config.exec_cmd, p.active, "", txt);
- }
- return;
- }
- const text = p.tagText(p.scratch.allocator(), pane) catch return;
- const end = text.len;
- if (key.text.len > 0) {
- const range: @TypeOf(tagSelBounds(pane)) = if (pane.tag_sel) tagSelBounds(pane) else .{ .lo = pane.tag_col, .hi = pane.tag_col };
- const hi = if (pane.tag_sel) modal.nextGrapheme(text, range.hi) else range.hi;
- _ = p.replaceTagText(pane, range.lo, hi, key.text);
- return;
- }
- switch (key.cp) {
- Key.backspace, Key.delete => {
- if (pane.tag_sel) {
- const range = tagSelBounds(pane);
- _ = p.replaceTagText(pane, range.lo, modal.nextGrapheme(text, range.hi), "");
- } else if (key.cp == Key.backspace and pane.tag_col > 0) {
- _ = p.replaceTagText(pane, modal.prevGrapheme(text, pane.tag_col), pane.tag_col, "");
- } else if (key.cp == Key.delete and pane.tag_col < end) {
- _ = p.replaceTagText(pane, pane.tag_col, modal.nextGrapheme(text, pane.tag_col), "");
- }
- },
- Key.left => if (pane.tag_col > 0) {
- pane.tag_col = @intCast(modal.prevGrapheme(text, pane.tag_col));
- },
- Key.right => if (pane.tag_col < end) {
- pane.tag_col = @intCast(modal.nextGrapheme(text, pane.tag_col));
- },
- Key.home => pane.tag_col = 0,
- Key.end => pane.tag_col = @intCast(end),
- else => {},
- }
- }
-
- /// write a helix range back onto the tag cursor + selection: the rendered
- /// tag's one-line mirror of setPaneRange.
- fn setTagRange(pane: *Pane, text: []const u8, r: modal.Selection) void {
- const lo = @min(r.anchor, r.head);
- const hi = @max(r.anchor, r.head);
- pane.tag_col = @intCast(modal.selectionCursor(text, r));
- pane.tag_sel = modal.nextGrapheme(text, lo) < hi; // one grapheme IS the block cursor
- if (pane.tag_sel) pane.tag_anchor = @intCast(if (r.head > r.anchor) lo else modal.prevGrapheme(text, hi));
- }
-
- fn tagNormalKey(p: *Pardes, pane: *Pane, key: Key) void {
- const text = p.tagText(p.scratch.allocator(), pane) catch return;
- const cur: usize = @min(@as(usize, pane.tag_col), text.len);
- // Esc abandons the command line: back to the body, tail kept as text
- if (hit(key, config.escape) or hit(key, &.{.{ .cp = 'j' }})) return exitTagEdit(pane);
- if (hit(key, config.look_key) or hit(key, config.exec_key)) {
- const cmd = if (hit(key, config.look_key)) config.look_cmd else config.exec_cmd;
- const txt = p.tagChordText(pane) orelse return;
- const id = p.active;
- const body_mode = pane.tag_mode;
- exitTagEdit(pane); // leave the tag FIRST: Del frees the pane
- pane.body.mode = body_mode;
- exec.runBuiltin(p, cmd, id, "", txt);
- return;
- }
- if (hit(key, config.tag_yank)) {
- if (p.tagChordText(pane)) |txt| edit.setClipboard(p, txt);
- return;
- }
- if (hit(key, config.insert) or hit(key, config.append) or
- hit(key, config.insert_line_start) or hit(key, config.insert_line_end))
- {
- const edit0: usize = if (tagNameEnd(pane) != null) 0 else (p.tagPrefix(pane) catch return).len;
- pane.tag_col = @intCast(@max(edit0, if (hit(key, config.append))
- modal.nextGrapheme(text, cur)
- else if (hit(key, config.insert_line_start))
- 0
- else if (hit(key, config.insert_line_end))
- text.len
- else
- cur));
- pane.tag_sel = false;
- pane.body.mode = .insert;
- return;
- }
- const dir: ?Builtin = for (config.window_keys) |wk| {
- var letter = wk.letter;
- letter.cp = std.ascii.toUpper(@intCast(letter.cp));
- if (hit(key, &.{letter})) break wk.cmd;
- } else null;
- if (dir) |d| {
- const from = p.active;
- exec.runBuiltin(p, d, from, "", null);
- if (p.active == from) {
- if (d == .Up) {
- exitTagEdit(pane);
- p.enterHeader(if (p.columnBarHeight() > 0) (layout.findPane(p, from) orelse return).col else null);
- }
- return;
- }
- const dest = p.panes[p.active] orelse return;
- exitTagEdit(pane); // the pane we left is fully out of its tag...
- p.enterTagEdit(dest, -1);
- if (dest.tag_edit) dest.body.mode = .normal;
- return;
- }
- if (lineMotion(text, cur, key)) |r| setTagRange(pane, text, r);
- }
-
fn lineMotion(text: []const u8, cur: usize, key: Key) ?modal.Selection {
const target: ?usize = if (hit(key, config.line_move_left))
modal.prevGrapheme(text, cur)
@@ -5243,8 +5062,7 @@ pub const Pardes = struct {
const pane = p.panes[d].?;
p.exitHeader();
p.active = d;
- p.enterTagEdit(pane, -1);
- if (pane.tag_edit) pane.body.mode = .normal;
+ p.enterTag(pane);
return;
}
if (hit(key, config.topbar_up)) {
@@ -5268,12 +5086,11 @@ pub const Pardes = struct {
pub fn cycleMode(p: *Pardes, id: usize) void {
const pane = p.panes[id] orelse return;
if (pane.file == null and !pane.isTerminal()) return;
- const previous = if (pane.tag_edit) pane.tag_mode else pane.body.mode;
+ const previous = pane.body.mode;
p.active = id;
p.exitHeader();
- if (pane.tag_edit) exitTagEdit(pane);
+ pane.focus = .body;
pane.closePrompt();
- pane.body.mode = previous;
switch (previous) {
.tty => {
pane.body.mode = .normal;
@@ -5293,7 +5110,7 @@ pub const Pardes = struct {
if (!pane.isTerminal()) return;
p.active = id;
p.exitHeader();
- if (pane.tag_edit) exitTagEdit(pane);
+ pane.focus = .body;
pane.closePrompt();
if (pane.body.mode == .tty) {
pane.body.mode = .normal;
@@ -5314,7 +5131,7 @@ pub const Pardes = struct {
}
if (p.topbar_col != null) return p.topbarKey(key);
const pane = p.panes[p.active] orelse return;
- if (pane.isTerminal() and pane.body.mode == .tty and !pane.tag_edit) {
+ if (pane.isTerminal() and pane.body.mode == .tty and pane.focus == .body and pane.prompt == .none) {
p.leader_on = false;
p.ctrl_w_pending = false;
if (key.ctrl and !key.alt and key.cp == p.opts.tty_toggle)
@@ -5352,7 +5169,8 @@ pub const Pardes = struct {
}
// insert mode owns Ctrl-w (delete word backward, helix); the focus
// prefix applies outside raw tty mode
- if (hit(key, config.window_prefix) and pane.body.mode != .insert and pane.prompt == .none) {
+ const t = if (pane.focus == .tag) &pane.tag else &pane.body;
+ if (hit(key, config.window_prefix) and t.mode != .insert and pane.prompt == .none) {
p.ctrl_w_pending = true;
return;
}
@@ -5385,6 +5203,21 @@ pub const Pardes = struct {
if (pane.isTerminal() and tty_toggle) {
return p.toggleTty(p.active);
}
+ if (pane.prompt == .name) {
+ // Enter or the execute key commits the draft and the keyboard
+ // goes back to the body; Esc drops it.
+ if (hit(key, config.look_key) or hit(key, config.exec_key)) {
+ if (p.commitTagName(pane)) pane.focus = .body;
+ return;
+ }
+ if (hit(key, config.escape)) {
+ pane.closePrompt();
+ pane.focus = .body;
+ return;
+ }
+ edit.handleInsert(p, &pane.input, key);
+ return;
+ }
if (pane.prompt != .none) {
if (hit(key, config.search_submit) or hit(key, config.escape)) {
const submit = hit(key, config.search_submit);
@@ -5395,7 +5228,7 @@ pub const Pardes = struct {
look.submitSearch(p, p.active)
else if (normal.selRegexArmed(pane)) |_|
normal.applySelRegex(p, pane, "", false),
- .none, .del_side => unreachable,
+ .none, .del_side, .name => unreachable,
}
pane.closePrompt();
pane.body.mode = .normal;
@@ -5406,12 +5239,27 @@ pub const Pardes = struct {
if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split);
return;
}
- if (pane.tag_edit) {
- if (pane.body.mode == .normal) p.tagNormalKey(pane, key) else p.tagInsertKey(pane, key);
- return;
- }
- if (pane.body.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) {
+ if (t.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) {
const cmd = if (hit(key, config.look_key)) config.look_cmd else config.exec_cmd;
+ if (pane.focus == .tag) {
+ // An explicit selection, else the word under the cursor in the
+ // tag as it is shown: the prefix is part of what a word is.
+ const tag = p.tagText(p.scratch.allocator(), pane) catch return;
+ const tail = curTail(pane);
+ const range = t.primaryRange(tail, 0);
+ const lo = @min(range.anchor, range.head);
+ const hi = @max(range.anchor, range.head);
+ const txt = if ((t.vsel.active and t.vsel.explicit) or t.msel.active)
+ tail[lo..hi]
+ else word: {
+ const b = config.wordBounds(tag, tag.len - tail.len + modal.selectionCursor(tail, range));
+ break :word tag[b.lo..b.hi];
+ };
+ // Out of the tag FIRST: Del frees the pane.
+ pane.focus = .body;
+ if (txt.len > 0) exec.runBuiltin(p, cmd, p.active, "", txt);
+ return;
+ }
edit.pinPaneCursor(p, &pane.body);
const explicit = (p.native_images and pane.hasPdfSelection()) or
(pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active;
@@ -5430,14 +5278,12 @@ pub const Pardes = struct {
exec.runBuiltin(p, cmd, p.active, "", word);
return;
}
- if (pane.body.mode == .normal and pane.hasPdf()) return normal.handlePdfNormal(p, pane, key);
- switch (pane.body.mode) {
- .normal => {
- normal.handleNormal(p, &pane.body, key);
- },
+ if (t.mode == .normal and t.what == .body and pane.hasPdf()) return normal.handlePdfNormal(p, pane, key);
+ switch (t.mode) {
+ .normal => normal.handleNormal(p, t, key),
.insert => {
- if (hit(key, config.escape)) return edit.exitInsert(p, &pane.body);
- edit.handleInsert(p, &pane.body, key);
+ if (hit(key, config.escape)) return edit.exitInsert(p, t);
+ edit.handleInsert(p, t, key);
},
.tty => panes.Terminal.forwardKey(p, p.active, key),
}
@@ -5507,7 +5353,7 @@ pub const Pardes = struct {
const dir = if (request == .save) paneDir(pane) else "";
const slash: []const u8 = if (request == .save and (dir.len == 0 or dir[dir.len - 1] != '/')) "/" else "";
const answer = std.mem.concat(p.gpa, u8, &.{ marker, dir, slash }) catch return;
- if (pane.tag_edit) exitTagEdit(pane);
+ pane.focus = .body;
// Not closePrompt: an s/S snapshot was taken for this very prompt.
if (pane.input.own) |old| p.gpa.free(old);
pane.prompt = switch (request) {
@@ -5806,8 +5652,7 @@ pub const Pardes = struct {
pub fn restoreDumpTail(p: *Pardes, pane: *Pane, src: dump.Pane) void {
if (src.tag_tail) |tail| {
- if (!pane.appendTag(tail)) return;
- pane.tag_init = true;
+ pane.tag.own = p.gpa.dupe(u8, tail) catch return;
return;
}
const tail_class = tailClass(pane);
@@ -5855,7 +5700,7 @@ pub const Pardes = struct {
}
fn restoreTailAt(
- _: *Pardes,
+ p: *Pardes,
pane: *Pane,
saved_tag: []const u8,
pfx: []const u8,
@@ -5886,8 +5731,7 @@ pub const Pardes = struct {
};
for (defaults) |default|
if (std.mem.eql(u8, said, std.mem.trimStart(u8, default, " "))) return;
- if (!pane.appendTag(rest)) return;
- pane.tag_init = true;
+ pane.tag.own = p.gpa.dupe(u8, rest) catch return;
}
/// Close a pane; its rows go to `to`, or where layout.absorbVWeight
@@ -6053,8 +5897,18 @@ pub const Pardes = struct {
p.trackJump();
for (&p.panes, 0..) |*slot, id| {
const pane = slot.* orelse continue;
- if (pane.tag_edit and (id != p.active or p.topbar_col != null))
- exitTagEdit(pane);
+ // The keyboard is on one pane, or on a header: a tag it has left
+ // hands it back to its body, and a name drafted there is dropped.
+ if (id != p.active or p.topbar_col != null) {
+ pane.focus = .body;
+ if (pane.prompt == .name) pane.closePrompt();
+ }
+ // A sweep over the tag is the tag's while it has the keyboard.
+ const swept = pane.sel[sel_slot];
+ if (pane.focus == .body and swept.state == .done and @min(swept.r0, swept.r1) < BOX_H) {
+ pane.clearPointerSelection(sel_slot);
+ pane.sel[sel_slot].state = .none;
+ }
if (comptime terminal_panes) if (pane.terminal) |state| {
var off: u16 = 0;
while (off < state.reply_len) {
@@ -6430,7 +6284,7 @@ pub const Pardes = struct {
s.set(d.cur_x, placement.row, "▌", .{ .fg = .{ .rgb = chrome.lineno } });
}
},
- .none, .select, .tag => if (p.pointer_inside) {
+ .none, .select => if (p.pointer_inside) {
// hover hints on a pane's own trailing edge
var c: usize = 0;
while (c + 1 < p.ncol) : (c += 1) {
@@ -6801,8 +6655,7 @@ pub const Pardes = struct {
.font_role = .tagline,
};
s.fill(r.x, tag_y, config.GUTTER, BOX_H, box_style);
- const box_mode = if (pane.tag_edit) pane.tag_mode else pane.body.mode;
- s.set(r.x, tag_y, switch (box_mode) {
+ s.set(r.x, tag_y, switch (pane.body.mode) {
.normal => config.tag_normal,
.insert => config.tag_insert,
.tty => config.tag_tty,
@@ -6856,9 +6709,24 @@ pub const Pardes = struct {
.bg = .{ .rgb = tag_bg },
.font_role = .tagline,
});
- const tag = try p.tagText(arena, pane);
- if (active and pane.tag_edit) {
- const caret = panes.File.rawDisplayCol(tag, pane.tag_col);
+ const whole = try p.tagText(arena, pane);
+ const tail = curTail(pane);
+ const prefix_len = whole.len - tail.len;
+ // The tagline shows the tag's first line: its prefix and the first
+ // line of its own text.
+ const tag = whole[0 .. std.mem.indexOfScalar(u8, whole, '\n') orelse whole.len];
+ // The keyboard's caret on it, as a byte offset: in its own text when
+ // the tag has the keyboard, in the path while a name is drafted.
+ const caret_at: ?usize = if (!active)
+ null
+ else if (pane.prompt == .name)
+ @min(tag.len, @as(usize, @intCast(@max(0, pane.input.cur_col))))
+ else if (pane.focus == .tag and pane.tag.cur_row == 0)
+ @min(tag.len, prefix_len + @as(usize, @intCast(@max(0, pane.tag.cur_col))))
+ else
+ null;
+ if (caret_at) |at| {
+ const caret = panes.File.rawDisplayCol(tag, at);
var scroll: usize = pane.tag_scroll;
if (caret < scroll) scroll = caret;
if (caret >= scroll + capacity) scroll = caret - capacity + 1;
@@ -6869,10 +6737,10 @@ pub const Pardes = struct {
scroll = panes.File.rawDisplayCol(tag, raw);
}
pane.tag_scroll = @intCast(scroll);
- } else pane.tag_scroll = 0;
+ } else if (pane.focus != .tag) pane.tag_scroll = 0;
const tag_start = panes.File.rawAtDisplay(tag, pane.tag_scroll);
_ = s.print(tx, tag_y, tw, tag[tag_start..], tag_style);
- if (tagTintRange(pane, tag, tag.len - curTail(pane).len)) |name| {
+ if (tagTintRange(pane, tag, prefix_len)) |name| {
const lo = @max(name.lo, tag_start);
const hi = @min(name.hi, tag.len);
if (lo < hi) {
@@ -6889,23 +6757,31 @@ pub const Pardes = struct {
body_layer.paintPointerSelection(p, pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
};
}
- if (pane.tag_edit and pane.tag_sel) {
- const b = tagSelBounds(pane);
- var col = panes.File.rawDisplayCol(tag, b.lo) -| pane.tag_scroll;
- const hi = modal.nextGrapheme(tag, b.hi);
- const end = panes.File.rawDisplayCol(tag, hi);
- while (col + pane.tag_scroll < end and col < tw) : (col += 1) {
- const cell = s.at(tx + @as(u16, @intCast(col)), tag_y);
- cell.default = false;
- cell.style.bg = .{ .rgb = th.sel_bg };
- cell.style.fg = .{ .rgb = th.sel_fg };
+ // The tag's own selections, while it has the keyboard. A range of one
+ // grapheme is the block cursor, not a selection.
+ if (pane.focus == .tag) {
+ var ranges: [Text.max_selections]modal.Selection = undefined;
+ const got = pane.tag.ranges(tail, 0, &ranges);
+ const shown = tag.len - prefix_len;
+ for (ranges[0..got.n]) |range| {
+ const lo = @min(range.anchor, range.head);
+ const hi = @max(range.anchor, range.head);
+ if (modal.nextGrapheme(tail, lo) >= hi or lo >= shown) continue;
+ var col = panes.File.rawDisplayCol(tag, prefix_len + lo) -| pane.tag_scroll;
+ const end = panes.File.rawDisplayCol(tag, prefix_len + @min(hi, shown));
+ while (col + pane.tag_scroll < end and col < tw) : (col += 1) {
+ const cell = s.at(tx + @as(u16, @intCast(col)), tag_y);
+ cell.default = false;
+ cell.style.bg = .{ .rgb = th.sel_bg };
+ cell.style.fg = .{ .rgb = th.sel_fg };
+ }
}
}
- // cursor while editing the tag: byte offset mapped to its display cell
- if (active and pane.tag_edit) {
- // bar while typing, block for `:` normal mode (same rule as a body)
- const col = panes.File.rawDisplayCol(tag, pane.tag_col) -| pane.tag_scroll;
- if (col < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(col)), .y = tag_y, .bar = pane.body.mode == .insert };
+ // the caret: bar while typing, block in normal mode (same rule as a body)
+ if (caret_at) |at| {
+ const col = panes.File.rawDisplayCol(tag, at) -| pane.tag_scroll;
+ const mode = if (pane.prompt == .name) pane.input.mode else pane.tag.mode;
+ if (col < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(col)), .y = tag_y, .bar = mode == .insert };
}
for (pane.sel, 0..) |selection, button| {
@@ -7034,10 +6910,10 @@ test "raw terminal cursor obeys visibility without hiding modal and tag cursors"
const modal_cursor = (try p.render(frame.allocator())).cursor orelse return error.MissingCursor;
try std.testing.expectEqual(rect.x + config.GUTTER + 1, modal_cursor.x);
try std.testing.expectEqual(body_y, modal_cursor.y);
- p.enterTagEdit(pane, 0);
+ p.enterTag(pane);
const tag_cursor = (try p.render(frame.allocator())).cursor orelse return error.MissingCursor;
try std.testing.expectEqual(if (p.settings.tag_bottom) rect.y + rect.h - BOX_H else rect.y, tag_cursor.y);
- Pardes.exitTagEdit(pane);
+ pane.focus = .body;
pane.body.mode = .tty;
try std.testing.expect((try p.render(frame.allocator())).cursor == null);
@@ -7193,7 +7069,7 @@ test "a prompt is typed on its own line in the notice band and leaves the tag ro
p.update(.{ .key = .{ .cp = '/' } });
p.update(.{ .key = .{ .cp = 't', .text = "tw" } });
p.update(.{ .key = .{ .cp = Key.left } });
- try std.testing.expect(pane.prompt == .search and !pane.tag_edit);
+ try std.testing.expect(pane.prompt == .search and pane.focus == .body);
try std.testing.expectEqualStrings(config.search_marker ++ "tw", pane.promptText().?);
_ = frame.reset(.retain_capacity);
_ = try p.render(frame.allocator());
@@ -7243,8 +7119,7 @@ test "a stillborn pane gives its column to the first pane that joins it" {
try std.testing.expect(p.executeBuiltinLine(shell, "Newcol"));
const tagged = p.active;
p.sync();
- p.seedTail(p.panes[tagged].?);
- try std.testing.expect(p.panes[tagged].?.appendTag(" make"));
+ try p.panes[tagged].?.appendTag(" make");
try std.testing.expect(p.executeBuiltinLine(tagged, "Tty"));
p.update(.tick);
try std.testing.expect(p.panes[tagged] != null);