diff options
Diffstat (limited to 'src/look.zig')
| -rw-r--r-- | src/look.zig | 120 |
1 files changed, 99 insertions, 21 deletions
diff --git a/src/look.zig b/src/look.zig index a413bf50..d90d27b8 100644 --- a/src/look.zig +++ b/src/look.zig @@ -10,6 +10,7 @@ const platform_has_fs = fs.platform_has_fs; const layout = @import("layout.zig"); const animation = @import("animation.zig"); const modal = @import("modal.zig"); +const tagline = @import("tagline.zig"); const filesystem = @import("fs.zig"); const panes = @import("panes.zig"); const mouse = @import("mouse.zig"); @@ -466,6 +467,7 @@ pub fn expandedWord(line: []const u8, col: usize) ?ExpandedWord { pub fn expandedSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel { var sl = at; if (sl.c0 != sl.c1 or sl.r0 != sl.r1) return sl; + if (sl.r0 >= 0 and sl.r0 < pane.tag_rows) return tagWordSel(p, pane, sl); const text = edit.paneText(p, pane) catch return null; var it = std.mem.splitAny(u8, text, "\n"); var v: i32 = 0; @@ -481,6 +483,34 @@ pub fn expandedSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel { return null; } +/// A tag word is expanded in the tag's own text, so a word the wrapping +/// broke across rows is still one word, as acme's frame wraps a tag +/// without breaking its text. +fn tagWordSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel { + const arena = p.scratch.allocator(); + const shown = tagline.shownTag(p, arena, pane) catch return null; + const whole = tagline.tagText(p, arena, pane) catch return null; + const row: usize = @as(usize, @intCast(at.r0)) + pane.tag_top; + const shown_line = modal.lineSlice(shown.text, row); + const real = shown.toReal(modal.lineStartOffset(shown.text, row) + panes.File.rawAtDisplay(shown_line, @intCast(@max(0, at.c0)))); + const start = if (std.mem.lastIndexOfScalar(u8, whole[0..@min(real, whole.len)], '\n')) |nl| nl + 1 else 0; + const end = std.mem.indexOfScalarPos(u8, whole, start, '\n') orelse whole.len; + const b = expandedWord(whole[start..end], real - start) orelse return null; + const lo = shownCell(shown.text, shown.fromReal(start + b.lo), pane.tag_top); + const hi = shownCell(shown.text, shown.fromReal(start + b.hi - 1), pane.tag_top); + return .{ .state = at.state, .r0 = lo.row, .c0 = lo.col, .r1 = hi.row, .c1 = hi.col }; +} + +fn shownCell(text: []const u8, off: usize, top: usize) struct { row: i32, col: i32 } { + const before = text[0..@min(off, text.len)]; + const row = std.mem.count(u8, before, "\n"); + const line_start = if (std.mem.lastIndexOfScalar(u8, before, '\n')) |nl| nl + 1 else 0; + return .{ + .row = @as(i32, @intCast(row)) - @as(i32, @intCast(top)), + .col = @intCast(panes.File.rawDisplayCol(text[line_start..], off - line_start)), + }; +} + /// the word under the modal cursor as a pane-local selection (paneText /// coords: row 0 is the tag; file panes carry the line-number prefix) pub fn cursorWordSel(p: *Pardes, pane: *Pane) Pane.Sel { @@ -1143,14 +1173,49 @@ pub fn followPdfLink(p: *Pardes, id: usize, probe: *const panes.Pdf.WordProbe) v } } +/// A look that found nothing: said on the message row, logged as an `err` +/// record, and marked so that /look reads back nothing (ninep/ctl.zig run). +fn missed(p: *Pardes, id: usize, comptime fmt: []const u8, args: anytype) void { + p.look_missed = true; + var said: [160]u8 = undefined; + const why = std.fmt.bufPrint(&said, fmt, args) catch "no match"; + var msg: [176]u8 = undefined; + p.setMessage(id, std.fmt.bufPrint(&msg, "Look: {s}", .{why}) catch "Look: no match"); + const serial = if (p.panes[id]) |pane| pane.serial else 0; + var rec: [200]u8 = undefined; + pardes.ctlfs.events.notePath(p, "err", std.fmt.bufPrint(&rec, "{d} look: {s}", .{ serial, why }) catch return); +} + +/// After a look at `path:N` focused its pane: a line past the end focuses +/// nothing, leaves no pane it opened, and is a miss; a bare line number +/// selects the line, its newline included, as acme's does. +fn settleLine(p: *Pardes, id: usize, path: []const u8, at: Spot, before: usize, opened: ?usize) void { + const shown = p.panes[p.active] orelse return; + const f = if (shown.file) |*file| file else return; + if (at.line > panes.File.nlines(p.gpa, f)) { + if (opened) |free| p.removePane(free, null) catch {}; + if (p.panes[before] != null) p.active = before; + return missed(p, id, "{s} has no line {d}", .{ path, at.line }); + } + if (at.line > 0 and at.col == 0 and at.end_line == 0) { + const start = modal.lineStartOffset(f.content, at.line - 1); + const next = std.mem.indexOfScalarPos(u8, f.content, start, '\n'); + const end = if (next) |i| i + 1 else f.content.len; + @import("ninep/pane.zig").setDot(shown, .{ .q0 = @intCast(start), .q1 = @intCast(end) }); + } +} + /// `file:addr` or `:addr`, acme's look.c:450-543: a colon followed by an /// address character (addr.c:28, isaddrc) that is not a line spot this /// parser already reads (`file:12`, `file:12:5`). Not a URL. fn addressed(tok: []const u8) ?struct { path: []const u8, expr: []const u8 } { if (std.mem.indexOf(u8, tok, "://") != null) return null; - if (std.mem.indexOfAny(u8, tok, " \t") != null) return null; const colon = std.mem.indexOfScalar(u8, tok, ':') orelse return null; const expr = tok[colon + 1 ..]; + // A written look is a whole line: the path has no blanks, but a + // pattern in the address may (`calc.py:/return a/`). + if (std.mem.indexOfAny(u8, tok[0..colon], " \t") != null) return null; + if (std.mem.indexOfAny(u8, expr, " \t") != null and std.mem.indexOfAny(u8, expr, "/?") == null) return null; if (expr.len == 0 or std.mem.indexOfScalar(u8, "0123456789+-/$.#,;", expr[0]) == null) return null; if (std.ascii.isDigit(expr[0])) { const pl = parsePathLine(tok); @@ -1178,12 +1243,10 @@ fn lookAddress(p: *Pardes, id: usize, path: []const u8, expr: []const u8) void { if (path.len > 0 and (f.output != null or !std.mem.endsWith(u8, f.path, std.fs.path.basename(path)))) return; var a: Addr = .{ .text = f.content, .lim = null, .expr = expr }; const r = a.address(ninep_pane.dotOf(pane)) orelse { - var said: [128]u8 = undefined; - return p.setMessage(id, std.fmt.bufPrint(&said, "Look: {s}", .{a.err}) catch "Look: bad address"); + return missed(p, id, "{s}", .{a.err}); }; if (a.i < expr.len or r.q0 > r.q1) { - var said: [128]u8 = undefined; - return p.setMessage(id, std.fmt.bufPrint(&said, "Look: {s}", .{if (r.q0 > r.q1) "addresses out of order" else "bad address syntax"}) catch "Look: bad address"); + return missed(p, id, "{s}", .{if (r.q0 > r.q1) "addresses out of order" else "bad address syntax"}); } clearNavigationSelection(p, pane); ninep_pane.setDot(pane, r); @@ -1210,6 +1273,10 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { if (comptime pdf_enabled) if (panes.Pdf.lookSection(p, id, pl.path, pl.at)) return; var realbuf: [4096]u8 = undefined; const found = resolveLookTarget(p, id, txt, &realbuf); + // A line past the end of an open pane is a miss (the target's line is + // already clamped, so the one written is asked). + if (found == .pane and found.pane.id < MAX_PANES) if (p.panes[found.pane.id]) |shown| if (shown.file) |*f| if (pl.at.line > panes.File.nlines(p.gpa, f)) + return missed(p, id, "{s} has no line {d}", .{ trimmed, pl.at.line }); // A miss changes nothing, the selection included; anything else starts // from a clean one. if (found != .none) clearNavigationSelection(p, pane); @@ -1220,10 +1287,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { p.reportError(id, "search", err); return; }; - if (rows == 0) { - var said: [96]u8 = undefined; - return p.setMessage(id, std.fmt.bufPrint(&said, "Look: no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)}) catch "Look: no match"); - } + if (rows == 0) return missed(p, id, "no match for \"{s}\"", .{@import("Messages.zig").clip(trimmed, 48)}); clearNavigationSelection(p, pane); const at = pane.search_row; _ = searchStep(p, id, 1); @@ -1236,8 +1300,10 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { .pane => |t| { if (t.id >= MAX_PANES) return; const target = p.panes[t.id] orelse return; + const before = p.active; clearNavigationSelection(p, target); focusPaneLine(p, t.id, t.at, .center); + settleLine(p, id, trimmed, t.at, before, null); }, .url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }), .dir => |dir| { @@ -1263,20 +1329,30 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { exec.placeNew(p, id, id, free, .doc); return; }; + // An open pane on this path too short for the line: a miss that + // touches nothing. + if (openPaneTarget(p, target.path, target.at)) |open| if (p.panes[open.pane.id].?.file) |*f| + if (target.at.line > panes.File.nlines(p.gpa, f)) + return missed(p, id, "{s} has no line {d}", .{ target.path, target.at.line }); // focus an existing pane on this path (rescrolled), else open - if (focusPaneByPath(p, target.path, target.at)) return; - const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots); - const nt = panes.File.open(p, free, target.path, target.at.line) catch |err| - return p.reportError(id, "look", err); - if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1); - exec.placeNew(p, id, id, free, .doc); - layout.compute(p); - nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2; - if (target.at.end_line != 0) { - const centered = nt.file.?.scroll; - selectSpan(p, nt, target.at); - nt.file.?.scroll = centered; + const before = p.active; + var opened: ?usize = null; + if (!focusPaneByPath(p, target.path, target.at)) { + const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots); + const nt = panes.File.open(p, free, target.path, target.at.line) catch |err| + return p.reportError(id, "look", err); + if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1); + exec.placeNew(p, id, id, free, .doc); + layout.compute(p); + nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2; + if (target.at.end_line != 0) { + const centered = nt.file.?.scroll; + selectSpan(p, nt, target.at); + nt.file.?.scroll = centered; + } + opened = free; } + settleLine(p, id, target.path, target.at, before, opened); }, .image => |target| { if (focusPaneByPath(p, target.path, .{})) return; @@ -1297,6 +1373,8 @@ test "Look hover waits without mutating the pane and input cancels it" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = try p.setTestFile("alpha beta gamma\n"); + pane.tag_expanded = false; // a one-row tag keeps this small screen's rows + p.sync(); while (p.nextEffect()) |_| {} const other_id = p.freeSlot() orelse return error.NoSparePaneForHoverTest; _ = try p.newShell(other_id, ""); |
