summaryrefslogtreecommitdiff
path: root/src/tagline.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tagline.zig')
-rw-r--r--src/tagline.zig90
1 files changed, 63 insertions, 27 deletions
diff --git a/src/tagline.zig b/src/tagline.zig
index 8bd8000a..92c18510 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -102,20 +102,29 @@ pub fn revealHeader(p: *Pardes) void {
}
}
+/// The computed start of a pane's tag -- its path and dirty marker, a
+/// PDF's page, a terminal's directory -- without the space that parts it
+/// from the tag's own text. It is never stored, and never edited.
+pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 {
+ if (comptime pdf_enabled) if (pane.pdf) |pv|
+ return std.fmt.allocPrint(arena, "{s} [{d}/{d}]", .{ pv.path, pv.page + 1, pv.page_count });
+ if (pane.image) |*state| return panes.Image.tagPrefix(arena, state);
+ if (pane.file) |f| {
+ 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 name;
+ }
+ return pane.cwdSlice();
+}
+
pub fn tagPrefix(p: *Pardes, pane: *Pane) ![]u8 {
- const arena = p.scratch.allocator();
- const prefix = prefix: {
- if (comptime pdf_enabled) if (pane.pdf) |pv|
- break :prefix try std.fmt.allocPrint(arena, "{s} [{d}/{d}]", .{ pv.path, pv.page + 1, pv.page_count });
- if (pane.image) |*state| break :prefix try panes.Image.tagPrefix(arena, state);
- if (pane.file) |f| {
- 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)
- break :prefix try std.fmt.allocPrint(arena, "{s}{s}", .{ name, dirty_marker });
- break :prefix name;
- }
- break :prefix pane.cwdSlice();
- };
+ return prefixIn(p.scratch.allocator(), pane);
+}
+
+/// The prefix as the tag shows it, built in `arena`.
+pub fn prefixIn(arena: std.mem.Allocator, pane: *Pane) ![]u8 {
+ const prefix = try pathPrefix(arena, pane);
// The space between the prefix and the tag's own text is the text's
// (every default starts with one), but the prefix is computed, so it
// cannot trust the text to keep it: with it deleted, Look and Exec
@@ -159,21 +168,23 @@ pub fn tagText(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) ![]u8 {
/// 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);
+ const whole = tagText(p, p.scratch.allocator(), pane) catch return;
if (!t.cur_pinned) {
+ // Save is looked for in the tag's own text, after the prefix.
+ const lead = whole.len - curTail(pane).len;
+ const tail = whole[lead..];
var words = std.mem.tokenizeAny(u8, tail, " \t\n");
- const at = while (words.next()) |word| {
+ const at = lead + 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));
+ const cursor = modal.positionAt(whole, @min(at, whole.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 = tagText(p, p.scratch.allocator(), pane) catch return;
- const line = tag[0 .. std.mem.indexOfScalar(u8, tag, '\n') orelse tag.len];
+ const line = whole[0 .. std.mem.indexOfScalar(u8, whole, '\n') orelse whole.len];
const width = p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET);
pane.tag_scroll = @intCast(panes.File.displayWidth(line) + 1 -| width);
}
@@ -181,9 +192,9 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void {
// 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);
+ const rows = modal.cursorLineCount(whole);
t.cur_row = @intCast(@min(@as(usize, @intCast(@max(0, t.cur_row))), rows - 1));
- const line = modal.lineSlice(tail, @intCast(t.cur_row));
+ const line = modal.lineSlice(whole, @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;
@@ -196,15 +207,40 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void {
pane.focus = .tag;
}
-/// Where display column `col` of tag line `row` (tag scroll included)
-/// lands in the tag's own text, or null when it is on the prefix, which
-/// only the first line has.
+/// Where display column `col` of the tag's shown row `row` (tag scroll
+/// included) puts its cursor: the tag as shown, prefix and all.
pub fn tagOffsetAt(p: *Pardes, pane: *Pane, row: usize, col: usize) ?modal.Cursor {
const at = row + pane.tag_top;
- const width = if (at == 0) panes.File.displayWidth(tagPrefix(p, pane) catch return null) else 0;
- if (col < width) return null;
- const line = modal.lineSlice(curTail(pane), at);
- return .{ .row = at, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col - width)) };
+ const line = modal.lineSlice(tagText(p, p.scratch.allocator(), pane) catch return null, at);
+ return .{ .row = at, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col)) };
+}
+
+/// Whether display column `col` of the tag's shown row `row` is on a name
+/// a click drafts: a saving file's path.
+pub fn onName(pane: *Pane, row: usize, col: usize) bool {
+ if (row + pane.tag_top != 0) return false;
+ const f = pane.file orelse return false;
+ if (!panes.Output.fileTraits(f.output).saves) return false;
+ const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path;
+ return col < panes.File.displayWidth(name);
+}
+
+/// A key typed into a saving file's path, with the tag's cursor there,
+/// drafts a new name as a click on it does: the path is computed, so it is
+/// never the tag's own text to edit, but a buffer's name can be drafted.
+/// True when it did.
+pub fn typedIntoName(p: *Pardes, pane: *Pane, key: pardes.Key) bool {
+ const f = pane.file orelse return false;
+ if (!panes.Output.fileTraits(f.output).saves or pane.tag.cur_row != 0) return false;
+ const at: usize = @intCast(@max(0, pane.tag.cur_col));
+ const back = key.cp == pardes.Key.backspace or pardes.hit(key, config.insert_backspace_alias);
+ const forward = key.cp == pardes.Key.delete or pardes.hit(key, config.insert_delete_alias);
+ const typed = key.text.len > 0 and !key.ctrl and !key.alt;
+ const inside = if (back) at > 0 and at <= f.path.len else if (forward or typed) at < f.path.len else false;
+ if (!inside) return false;
+ draftName(p, pane, panes.File.displayWidth(f.path[0..at]));
+ edit.handleInsert(p, &pane.input, key);
+ return true;
}
/// Start a name draft for a file that can take one, its caret at display