diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Output.zig | 19 | ||||
| -rw-r--r-- | src/look.zig | 44 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 40 | ||||
| -rw-r--r-- | src/pardes.zig | 20 | ||||
| -rw-r--r-- | src/recent.zig | 40 |
5 files changed, 139 insertions, 24 deletions
diff --git a/src/Output.zig b/src/Output.zig index 3b8e6a7a..e6d6f46c 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -555,13 +555,20 @@ pub fn recentText(p: *Pardes, w: *std.Io.Writer, look_rows: bool) !void { for (p.panes) |slot| if (slot) |pane| if (pane.file) |f| if (f.output == null and std.mem.eql(u8, f.path, e.path)) { is_open = true; }; - const state: []const u8 = if (is_open) "open" else "closed"; - if (!look_rows) - try w.print("{s} {s}\n", .{ state, e.path }) - else if (e.line == 0) - try w.print("{s} {s}\n", .{ e.path, state }) - else + const state: []const u8 = if (is_open) "open" else "(closed)"; + if (!look_rows) { + try w.print("{s} {s}\n", .{ if (is_open) "open" else "closed", e.path }); + } else if (e.line == 0) { + try w.print("{s} {s}\n", .{ e.path, state }); + } else if (e.aline != 0 and (e.aline != e.line or e.acol != e.col)) { + // A selection: both its ends, in order, `path:L:C-L2:C2`. + const a_first = e.aline < e.line or (e.aline == e.line and e.acol < e.col); + const s = if (a_first) [2]u32{ e.aline, e.acol } else [2]u32{ e.line, e.col }; + const t = if (a_first) [2]u32{ e.line, e.col } else [2]u32{ e.aline, e.acol }; + try w.print("{s}:{d}:{d}-{d}:{d} {s}\n", .{ e.path, s[0], s[1], t[0], t[1], state }); + } else { try w.print("{s}:{d}:{d} {s}\n", .{ e.path, e.line, e.col, state }); + } } } diff --git a/src/look.zig b/src/look.zig index ca9aa80a..0baa9afc 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1381,8 +1381,52 @@ fn spotAt(text: []const u8, q: u32) struct { line: usize, col: usize } { return .{ .line = line, .col = upto.len - start + 1 }; } +/// A remembered place in `path` (Recent, the jumplist): the file opened or +/// focused, then `at` clamped to it as it is now, a line past the end its +/// last line, never a miss; a selection's two ends restored where they +/// still fit. +pub fn reopenAt(p: *Pardes, from: usize, path: []const u8, at: Spot) void { + lookAt(p, from, path); + const id = p.active; + const pane = p.panes[id] orelse return; + const f = if (pane.file) |*file| file else return; + if (!std.mem.eql(u8, f.path, path)) return; + const n = @max(1, panes.File.nlines(p.gpa, f)); + const clamp = struct { + fn spot(text: []const u8, lines: usize, line: usize, col: usize) [2]usize { + const l = std.math.clamp(line, 1, lines); + const width = modal.lineSlice(text, l - 1).len + 1; + return .{ l, std.math.clamp(col, 1, width) }; + } + }; + const head = clamp.spot(f.content, n, at.line, at.col); + var to: Spot = .{ .line = head[0], .col = head[1] }; + if (at.end_line != 0) { + const end = clamp.spot(f.content, n, at.end_line, at.end_col); + to.end_line = end[0]; + to.end_col = end[1]; + } + clearNavigationSelection(p, pane); + focusPaneLine(p, id, to, .center); +} + +/// Whether `pane` is the +Recent list, whose rows are remembered places. +fn isRecentList(pane: *const Pane) bool { + const f = pane.file orelse return false; + const o = f.output orelse return false; + return std.meta.eql(o.from, panes.Output.Origin{ .cmd = .Recent }); +} + pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { const pane = p.panes[id] orelse return; + // A +Recent row is a remembered place: reopened and clamped to the file + // as it is now, never a miss for a line the file no longer has. + if (isRecentList(pane)) { + const row = std.mem.trim(u8, operand, " \t\r\n"); + const loc = row[0 .. std.mem.indexOfScalar(u8, row, ' ') orelse row.len]; + const pl = parsePathLine(loc); + if (pl.path.len > 0 and pl.path[0] == '/' and pl.at.line != 0) return reopenAt(p, id, pl.path, pl.at); + } p.noteHaptic(.look); noteLookSource(p, id); const command = std.mem.trim(u8, operand, " \t\r\n"); diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 477a91fe..889ee7be 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -2141,8 +2141,8 @@ test "a closed file is found again: Recent and /recent list it closed, a look at const rows = p.panes[p.paneBySerial(try std.fmt.parseInt(u32, std.mem.trimEnd(u8, rd(p, root_exec, 0, 64).bytes, "\n"), 10)).?].?; const at = std.mem.indexOf(u8, rows.file.?.content, try std.fmt.bufPrint(&want, "{s}:3:", .{a})).?; const row = rows.file.?.content[at .. std.mem.indexOfScalarPos(u8, rows.file.?.content, at, '\n').?]; - try testing.expect(std.mem.endsWith(u8, row, " closed")); - try testing.expectEqual(Status.ok, wr(p, Node.of(rows.serial, .look), try std.fmt.bufPrint(&line, "{s}\n", .{row[0 .. row.len - " closed".len]})).reply.status); + try testing.expect(std.mem.endsWith(u8, row, " (closed)")); + try testing.expectEqual(Status.ok, wr(p, Node.of(rows.serial, .look), try std.fmt.bufPrint(&line, "{s}\n", .{row[0 .. row.len - " (closed)".len]})).reply.status); p.sync(); const back = p.panes[p.active].?; try testing.expectEqualStrings(a, back.file.?.path); @@ -2163,6 +2163,42 @@ test "a closed file is found again: Recent and /recent list it closed, a look at try testing.expectEqualStrings(a, p.panes[p.active].?.file.?.path); } +test "a remembered place is clamped to the file as it is now, its selection's two ends kept where they fit" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "c.txt", .data = "one\ntwo\nthree\nfour\nfive\n" }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + var line: [4200]u8 = undefined; + _ = wr(p, root_look, try std.fmt.bufPrint(&line, "{s}/c.txt:2\n", .{dir})); + p.sync(); + const c = p.panes[p.active].?; + // A selection over lines 2 to 4, then closed. + @import("pane.zig").setDot(c, .{ .q0 = 4, .q1 = 19 }); + try testing.expect(c.body.vsel.active); + _ = wr(p, Node.of(c.serial, .ctl), "Del\n"); + p.sync(); + // The file shrinks to two lines while it is closed. + try tmp.dir.writeFile(testing.io, .{ .sub_path = "c.txt", .data = "one\ntwo\n" }); + _ = wr(p, root_exec, "Recent\n"); + const rows = p.panes[p.paneBySerial(try std.fmt.parseInt(u32, std.mem.trimEnd(u8, rd(p, root_exec, 0, 64).bytes, "\n"), 10)).?].?; + const at = std.mem.indexOf(u8, rows.file.?.content, "c.txt:").?; + const row = rows.file.?.content[at .. std.mem.indexOfScalarPos(u8, rows.file.?.content, at, '\n').?]; + // Both ends are in the row, a range. + try testing.expect(std.mem.indexOfScalar(u8, row, '-') != null); + var full: [4400]u8 = undefined; + const loc = try std.fmt.bufPrint(&full, "{s}/{s}\n", .{ dir, row[0 .. std.mem.indexOfScalar(u8, row, ' ').?] }); + // A look at it opens the file, never a miss, clamped to its last line. + try testing.expectEqual(Status.ok, wr(p, Node.of(rows.serial, .look), loc).reply.status); + p.sync(); + const back = p.panes[p.active].?; + try testing.expect(std.mem.endsWith(u8, back.file.?.path, "/c.txt")); + try testing.expect(back.body.cur_row <= 1); + try testing.expect(back.body.vsel.active); +} + test "size is monotonic: growing is never refused, and a size once taken is taken again" { const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 }); defer p.deinit(); diff --git a/src/pardes.zig b/src/pardes.zig index 0950d13a..5fdcfe3c 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -4700,10 +4700,18 @@ pub const Pardes = struct { } /// A file pane's path and dot, for the recent list (0 when none). - fn recentDot(pane: *const Pane) ?struct { path: []const u8, line: u32, col: u32 } { + fn recentDot(pane: *const Pane) ?struct { path: []const u8, line: u32, col: u32, aline: u32 = 0, acol: u32 = 0 } { const f = pane.file orelse return null; if (f.output != null) return null; - return .{ .path = f.path, .line = @intCast(@max(0, pane.body.cur_row) + 1), .col = @intCast(@max(0, pane.body.cur_col) + 1) }; + // The selection's anchor too, so a reopen restores both its ends. + const sel = pane.body.vsel.active; + return .{ + .path = f.path, + .line = @intCast(@max(0, pane.body.cur_row) + 1), + .col = @intCast(@max(0, pane.body.cur_col) + 1), + .aline = if (sel) @intCast(@max(0, pane.body.vsel.row) + 1) else 0, + .acol = if (sel) @intCast(@max(0, pane.body.vsel.col) + 1) else 0, + }; } /// The recent entry a closed jump names, by its path's hash. @@ -4714,7 +4722,7 @@ pub const Pardes = struct { pub fn deinit(p: *Pardes) void { // Every file open now is closed with its dot, and the list kept. - for (p.panes) |slot| if (slot) |pane| if (recentDot(pane)) |d| p.recent.closed(p.recentGpa(), d.path, d.line, d.col); + for (p.panes) |slot| if (slot) |pane| if (recentDot(pane)) |d| p.recent.closedSel(p.recentGpa(), d.path, d.line, d.col, d.aline, d.acol); p.saveRecent(); p.recent.deinit(p.recentGpa()); look.cancelLookHover(p); @@ -4827,7 +4835,7 @@ pub const Pardes = struct { fn retirePane(p: *Pardes, pane: *Pane) void { // Closed, a file is remembered with its dot: Recent and a jump back // to it reopen it there. - if (recentDot(pane)) |d| p.recent.closed(p.recentGpa(), d.path, d.line, d.col); + if (recentDot(pane)) |d| p.recent.closedSel(p.recentGpa(), d.path, d.line, d.col, d.aline, d.acol); const watched = (if (pane.file) |f| f.output == null else false) or pane.hasPdf(); for (p.panes, 0..) |slot, id| if (slot == pane) { if (watched) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = false } }); @@ -6694,10 +6702,8 @@ pub const Pardes = struct { // places it, and the entry is that pane from here on. if (j.serial == 0) { const e = p.recentByHash(j.path_hash) orelse return; - var where: [recent_path_cap + 32]u8 = undefined; - const loc = std.fmt.bufPrint(&where, "{s}:{d}:{d}", .{ e.path, @max(1, j.line), @max(1, j.col) }) catch return; const from = p.active; - look.lookAt(p, from, loc); + look.reopenAt(p, from, e.path, .{ .line = @max(1, j.line), .col = @max(1, j.col) }); const opened = p.panes[p.active] orelse return; if (p.active == from) return; p.jumps[@intCast(next)].pane = @intCast(p.active); diff --git a/src/recent.zig b/src/recent.zig index b6a1a66a..9f18d737 100644 --- a/src/recent.zig +++ b/src/recent.zig @@ -15,6 +15,9 @@ pub const Entry = struct { /// Where its dot was when it last closed (1-based; 0 none). line: u32 = 0, col: u32 = 0, + /// The selection's other end then (its anchor), 0 when there was none. + aline: u32 = 0, + acol: u32 = 0, }; pub const List = struct { @@ -53,6 +56,11 @@ pub const List = struct { /// `path` closed with its dot at `line`:`col`: where a reopen lands. /// Its place in the list is its opening's. pub fn closed(l: *List, gpa: std.mem.Allocator, path: []const u8, line: u32, col: u32) void { + l.closedSel(gpa, path, line, col, 0, 0); + } + + /// `closed`, with the selection's anchor too (0 0 for none). + pub fn closedSel(l: *List, gpa: std.mem.Allocator, path: []const u8, line: u32, col: u32, aline: u32, acol: u32) void { const i = l.find(path) orelse found: { l.opened(gpa, path); // Out of memory for it: nothing to note the dot on. @@ -60,6 +68,8 @@ pub const List = struct { }; l.entries.items[i].line = line; l.entries.items[i].col = col; + l.entries.items[i].aline = aline; + l.entries.items[i].acol = acol; l.dirty = true; } @@ -67,9 +77,10 @@ pub const List = struct { return l.entries.items[l.find(path) orelse return null]; } - /// One line a file, `<line> <col> <path>`, most recent first. + /// One line a file, `<line> <col> <aline> <acol> <path>`, most recent + /// first. pub fn write(l: *const List, w: *std.Io.Writer) !void { - for (l.entries.items) |e| try w.print("{d} {d} {s}\n", .{ e.line, e.col, e.path }); + for (l.entries.items) |e| try w.print("{d} {d} {d} {d} {s}\n", .{ e.line, e.col, e.aline, e.acol, e.path }); } /// The lines `write` wrote; a line that is not one is passed over. @@ -77,13 +88,21 @@ pub const List = struct { var lines = std.mem.splitScalar(u8, bytes, '\n'); while (lines.next()) |line| { if (l.entries.items.len >= cap) break; - var it = std.mem.splitScalar(u8, line, ' '); - const ln = std.fmt.parseInt(u32, it.next() orelse continue, 10) catch continue; - const col = std.fmt.parseInt(u32, it.next() orelse continue, 10) catch continue; - const path = it.rest(); + // Numbers, then the path (which starts with `/`): two of them in + // a list written before the anchor was kept, four since. + var nums: [4]u32 = @splat(0); + var n: usize = 0; + var rest = line; + while (rest.len > 0 and rest[0] != '/' and n < nums.len) : (n += 1) { + const sp = std.mem.indexOfScalar(u8, rest, ' ') orelse break; + nums[n] = std.fmt.parseInt(u32, rest[0..sp], 10) catch break; + rest = rest[sp + 1 ..]; + } + if (n != 2 and n != 4) continue; + const path = rest; if (path.len == 0 or path[0] != '/' or l.find(path) != null) continue; const owned = gpa.dupe(u8, path) catch return; - l.entries.append(gpa, .{ .path = owned, .line = ln, .col = col }) catch { + l.entries.append(gpa, .{ .path = owned, .line = nums[0], .col = nums[1], .aline = nums[2], .acol = nums[3] }) catch { gpa.free(owned); return; }; @@ -105,12 +124,15 @@ test "the list keeps a path once, most recent first, its dot from closing, cappe var buf: [256]u8 = undefined; var w = std.Io.Writer.fixed(&buf); try l.write(&w); - try std.testing.expectEqualStrings("7 3 /a\n0 0 /b\n", w.buffered()); + try std.testing.expectEqualStrings("7 3 0 0 /a\n0 0 0 0 /b\n", w.buffered()); var again: List = .{}; defer again.deinit(gpa); again.read(gpa, w.buffered()); again.read(gpa, "junk\n"); - try std.testing.expectEqual(@as(usize, 2), again.entries.items.len); + // A list written before the anchor was kept still reads. + again.read(gpa, "4 2 /old\n"); + try std.testing.expectEqual(@as(u32, 4), again.get("/old").?.line); + try std.testing.expectEqual(@as(usize, 3), again.entries.items.len); for (0..cap + 5) |i| { var name: [16]u8 = undefined; l.opened(gpa, std.fmt.bufPrint(&name, "/f{d}", .{i}) catch unreachable); |
