summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig72
1 files changed, 64 insertions, 8 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index f0d22d57..b16d39b7 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1112,7 +1112,33 @@ test "first document fallback splits like Tty from the originating pane" {
};
}
-test "Lspwhy after a Hover asks to explain that hover, at its place" {
+test "a frame no one watches is not drawn, and is drawn once someone does" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const Watch = struct {
+ var presents: usize = 0;
+ var seen = false;
+ const vtable: Host.VTable = .{ .present = present, .watched = watched };
+ fn present(_: ?*anyopaque, _: *const Surface) void {
+ presents += 1;
+ }
+ fn watched(_: ?*anyopaque) bool {
+ return seen;
+ }
+ };
+ Watch.presents = 0;
+ Watch.seen = false;
+ p.needs_frame = true;
+ try p.pump(.{ .vtable = &Watch.vtable });
+ try std.testing.expectEqual(@as(usize, 0), Watch.presents);
+ try std.testing.expect(p.needs_frame); // still owed
+ Watch.seen = true;
+ try p.pump(.{ .vtable = &Watch.vtable });
+ try std.testing.expectEqual(@as(usize, 1), Watch.presents);
+}
+
+test "Lspwhy after a Hover asks to explain that hover, in its file at its place, run from any pane" {
if (!lsp.supports.contains(.hover)) return error.SkipZigTest;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
@@ -1131,12 +1157,16 @@ test "Lspwhy after a Hover asks to explain that hover, at its place" {
pane.body.cur_row = 0;
pane.body.cur_col = 0;
p.lsp_wait = null;
- p.lspRequest(0, .explain, "");
+ // Run from another file's pane: still the hover's file and place.
+ const other = p.freeSlot().?;
+ _ = try p.openTutorView(other);
+ p.lspRequest(other, .explain, "");
var seen = false;
while (p.nextEffect()) |e| if (e == .lsp) {
try std.testing.expectEqual(lsp.Kind.explain, e.lsp.kind);
try std.testing.expectEqualStrings("hover ", e.lsp.arg.slice());
try std.testing.expectEqual(hover_at, e.lsp.offset);
+ try std.testing.expectEqual(@as(u8, 0), e.lsp.pane);
seen = true;
};
try std.testing.expect(seen);
@@ -4971,8 +5001,13 @@ pub const Pardes = struct {
// An empty window (Delcol took the last column): its first column.
if (layout.firstColumn(p)) |c| return tagline.enterHeader(p, c);
const col = p.keyboardColumn(from_id) orelse return;
+ // A trial: laying it out gives it a serial (layout.compute), which a
+ // refusal takes back. Every other column has its own already, so
+ // the counter as it is now is the trial's to return to.
+ for (0..p.ncol) |k| _ = layout.columnSerial(p, k);
+ const unspent = p.next_column_serial;
// Said, not silently nothing: "no space" is 9ns's word for ENOSPC.
- const c = layout.insertColumn(p, col, false) orelse {
+ const c = layout.insertColumnUnannounced(p, col, false) orelse {
var said: [96]u8 = undefined;
return p.reportFailure(from_id, if (p.ncol >= MAX_COLS)
std.fmt.bufPrint(&said, "Newcol: no space for a column: {d} max", .{MAX_COLS}) catch "Newcol: no space for a column"
@@ -4984,10 +5019,13 @@ pub const Pardes = struct {
// out again, and where they cannot hold every minimum there is no
// new column, as a size too small is refused.
for (0..p.ncol) |k| if (!layout.columnAtMinimums(p, k) and !layout.shareColumn(p, k)) {
- layout.dropColumn(p, c);
+ layout.dropColumnUnannounced(p, c);
+ p.next_column_serial = unspent;
layout.compute(p);
return p.reportFailure(from_id, "Newcol: no space for a column: the panes' tags would not fit");
};
+ // Kept: now it has a serial, and the log hears of it.
+ layout.noteColumn(p, "newcol", layout.columnSerial(p, c));
tagline.enterHeader(p, c);
}
@@ -5389,6 +5427,15 @@ pub const Pardes = struct {
p.noteLateFailure(id);
}
+ /// A Save makes the directories its file goes in, and any above them,
+ /// first: one rule, for the pane's own name and `Save path` alike. A
+ /// directory it cannot make leaves the write to fail and say why.
+ fn makeParents(path: []const u8) void {
+ if (comptime !hosted) return;
+ const local = filesystem.localPath(path) orelse return;
+ filesystem.makeDirs(std.fs.path.dirname(local) orelse "/");
+ }
+
pub fn saveFailed(p: *Pardes, id: u8, path: []const u8, err: anyerror) void {
if (p.panes[id]) |pane| if (pane.file) |*f| {
// The `-%` spelling fs.zig already uses for "make this dirty".
@@ -5478,8 +5525,7 @@ pub const Pardes = struct {
const was = f.saved_revision;
const failures = p.fs.failures;
f.saved_revision = f.revision;
- if (f.make_dirs) if (filesystem.localPath(f.path)) |local|
- filesystem.makeDirs(std.fs.path.dirname(local) orelse "/");
+ makeParents(f.path);
p.hostWriteFile(sf.pane, f.path, f.content);
const saved_pane = p.panes[sf.pane] orelse return;
if (saved_pane.serial != serial) return;
@@ -5508,6 +5554,7 @@ pub const Pardes = struct {
if (pane.file) |*f| {
const was = f.saved_revision;
const failures = p.fs.failures;
+ makeParents(st.path.slice());
p.hostWriteFile(st.pane, st.path.slice(), f.content);
const after = p.panes[st.pane] orelse return;
if (after.serial != st.serial) return;
@@ -5523,6 +5570,7 @@ pub const Pardes = struct {
if (!pane.isTerminal()) return;
const text = panes.terminal.screenTextAlloc(pane, p.gpa) catch return;
defer p.gpa.free(text);
+ makeParents(st.path.slice());
p.hostWriteFile(st.pane, st.path.slice(), text);
},
.write_dump => {
@@ -5603,6 +5651,10 @@ pub const Pardes = struct {
// A shell with a clock draws what `advance` stepped; one without
// steps its own `.tick`s and draws while anything animates.
if (!p.needs_frame and !p.continuous() and (v.now != null or !p.animationActive())) return;
+ // No one to see it: the frame stays owed (needs_frame), not drawn.
+ // A pty streaming into an unwatched session spent most of its time
+ // drawing what nobody looked at.
+ if (v.watched) |f| if (!f(h.ctx)) return;
_ = p.frame_arena.reset(.retain_capacity);
const surface = try p.render(p.frame_arena.allocator());
if (v.present) |f| f(h.ctx, surface);
@@ -6307,8 +6359,12 @@ pub const Pardes = struct {
pub const SavePath = Effect.Buf(effect_path_cap);
- pub fn lspRequest(p: *Pardes, id: usize, kind: lsp.Kind, arg: []const u8) void {
+ pub fn lspRequest(p: *Pardes, asked: usize, kind: lsp.Kind, arg: []const u8) void {
if (!normal.multiOnce(p)) return; // one question per keystroke, from the primary
+ // Lspwhy narrates the last query where it was asked -- that pane's
+ // file, place and argument -- from wherever it is run.
+ const last_pane: ?usize = if (kind == .explain) (if (p.lsp_last) |last| p.paneBySerial(last.serial) else null) else null;
+ const id = last_pane orelse asked;
var said: [128]u8 = undefined;
if (!lsp.supports.contains(kind))
return p.reportFailure(id, std.fmt.bufPrint(&said, "{s}: no language server here answers it", .{lspWord(kind)}) catch "no language server answers it");
@@ -6338,7 +6394,7 @@ pub const Pardes = struct {
if (kind == .explain) {
if (p.lsp_last) |last| {
sent = std.fmt.bufPrint(&explained, "{s} {s}", .{ @tagName(last.kind), last.arg.slice() }) catch "";
- if (last.serial == pane.serial) at = last.offset;
+ if (last_pane != null) at = last.offset;
}
} else if (kind != .status) p.lsp_last = .{ .kind = kind, .arg = .from(arg), .serial = pane.serial, .offset = @intCast(off) };
p.lsp_seq +%= 1;