summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig2
-rw-r--r--src/config.zig6
-rw-r--r--src/dump.zig9
-rw-r--r--src/lsp/lsp.zig26
-rw-r--r--src/lsp/lsp_zls.zig2
-rw-r--r--src/output_pane.zig195
-rw-r--r--src/pardes.zig103
7 files changed, 259 insertions, 84 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index d32e4ed3..500ce264 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -198,7 +198,7 @@ pub const Ascii = struct {
pub const Save = struct {
pub fn run(c: Ctx) void {
// an output buffer has no file behind it — nothing to write
- if (c.pane.file) |f| if (!f.output) c.p.emit(.{ .save_file = .{ .pane = @intCast(c.id) } });
+ if (c.pane.file) |f| if (output_pane.fileTraits(f.output).saves) c.p.emit(.{ .save_file = .{ .pane = @intCast(c.id) } });
}
};
diff --git a/src/config.zig b/src/config.zig
index 141d8376..65ac397b 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -308,8 +308,10 @@ pub const grep_marker = " Grep /";
pub const rename_marker = " Rename /";
pub const symbol_marker = " WsSymbols /";
-/// Output-buffer names (acme's +Errors). Load-bearing beyond cosmetics: n/N
-/// finds its results buffer by name, so the writer and the stepper must agree.
+/// Output-buffer names (acme's +Errors). Cosmetic now, and deliberately so: a
+/// buffer is DERIVED from the command that opened it (output_pane.traits), and
+/// nothing identifies one by matching this text any more — renaming any of
+/// these changes only what you read in a tag.
pub const search_buffer = "+Search";
pub const help_buffer = "+Help";
pub const hover_buffer = "+Hover";
diff --git a/src/dump.zig b/src/dump.zig
index 51221197..0f8cb810 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -68,8 +68,13 @@ pub const File = struct {
path: []const u8 = "",
content: []const u8 = "",
content_b64: []const u8 = "",
- /// an output buffer (no file behind the name) — see pardes.File.output
- output: bool = false,
+ /// non-empty = an output buffer (no file behind the name): the WORD of the
+ /// command that opened it, plus that command's argument — see
+ /// output_pane.Origin. A word rather than an integer for the reason every
+ /// other command in here is a word: reordering builtins.zig stays free,
+ /// and a dump stays something a person can read.
+ origin: []const u8 = "",
+ origin_arg: []const u8 = "",
};
pub const Image = struct {
diff --git a/src/lsp/lsp.zig b/src/lsp/lsp.zig
index 610c5604..abb91874 100644
--- a/src/lsp/lsp.zig
+++ b/src/lsp/lsp.zig
@@ -17,7 +17,6 @@
//! implementations are measured against each other: same core, same harness,
//! same rows, different `query`.
const std = @import("std");
-const config = @import("../config.zig");
/// What the caller wants to know. The helix command each one backs is named
/// alongside, because the keymap is helix's and these are its verbs.
@@ -62,27 +61,10 @@ pub const Kind = enum {
/// away from what `gd` actually did.
explain,
- /// Whether an answer of exactly one row should JUMP rather than open a
- /// results buffer. Helix: the five gotos jump on a single location and
- /// show a picker on several; a symbol list is always a picker.
- pub fn jumpsWhenSingle(k: Kind) bool {
- return switch (k) {
- .definition, .declaration, .type_definition, .implementation, .references => true,
- else => false,
- };
- }
-
- /// The buffer an answer opens. Kept distinct from `+Search` only where the
- /// content is not a list of locations — n/N over prose is nonsense.
- pub fn bufferName(k: Kind) []const u8 {
- return switch (k) {
- .hover => config.hover_buffer,
- .code_action, .format, .rename => config.lsp_buffer,
- // prose about the backend, never a list of locations
- .status, .explain => config.lsp_buffer,
- else => config.search_buffer,
- };
- }
+ // What an ANSWER becomes — which buffer it opens, whether a single row
+ // jumps instead, whether n/N walk it — is not here: it is one row per Kind
+ // in output_pane.traits, beside the same questions asked of `/`, Find,
+ // Grep and Help. A Kind added above will not compile until it has one.
};
/// One question. `source` is a snapshot of the buffer taken by the shell
diff --git a/src/lsp/lsp_zls.zig b/src/lsp/lsp_zls.zig
index 6f5bbcdd..8a36a151 100644
--- a/src/lsp/lsp_zls.zig
+++ b/src/lsp/lsp_zls.zig
@@ -681,7 +681,7 @@ fn documentSymbols(
}
/// Depth-first so the rows read in source order, with the parent's name as a
-/// qualifier — `Kind.jumpsWhenSingle` rather than a bare `jumpsWhenSingle`,
+/// qualifier — `Pardes.runBuiltin` rather than a bare `runBuiltin`,
/// because a flat list of method names is not navigable.
fn emitSymbols(
gpa: std.mem.Allocator,
diff --git a/src/output_pane.zig b/src/output_pane.zig
index 64bfc630..45eb0931 100644
--- a/src/output_pane.zig
+++ b/src/output_pane.zig
@@ -1,28 +1,189 @@
//! Output panes: acme's +Errors, a file pane with no file behind it, holding
//! text the core produced itself (+Search results, +Help, the LSP answer
-//! buffers). The ONLY thing that makes one different from a file pane is that
-//! it cannot be saved — every mode, motion, chord and look works because it IS
-//! a file pane, and that reuse is the point.
+//! buffers). It IS a file pane — every mode, motion, chord and look works for
+//! free — and that reuse is the point.
//!
-//! Thin on purpose, for now. The `f.output` tests still scattered through
-//! pardes.zig each ask a one-line question inside kind-agnostic code (does
-//! this pane claim a column? can it be saved? is it a doc?) and pulling them
-//! here would trade one line for a call. What belongs here is what an output
-//! pane KNOWS about itself, and today that is just how one is opened.
+//! What it is NOT any more is a file pane with a bool on it. An output buffer
+//! remembers THE COMMAND THAT OPENED IT (`Origin`), and every special case it
+//! gets is one `traits` lookup on that field. Before this, a dozen places
+//! re-derived what a pane was from the outside, each asking a different wrong
+//! question: `endsWith(path, "+Search")` (the NAME decided what a pane WAS,
+//! which is backwards — a name is a consequence), a `search_kind` field on the
+//! pane that ran the search rather than on the buffer that answered it, and
+//! `f.output` booleans sprinkled through kind-agnostic layout code. Now the
+//! buffer knows, and the table below is the whole answer: one screen you read
+//! top to bottom to see every way an output pane differs from a file, and one
+//! row to add to introduce another kind.
const std = @import("std");
const pardes = @import("pardes.zig");
const Pardes = pardes.Pardes;
const Pane = pardes.Pane;
+const File = pardes.File;
const file_pane = @import("file_pane.zig");
+const builtins = @import("builtins.zig");
+const Builtin = builtins.Builtin();
const config = @import("config.zig");
+const lsp = @import("lsp/lsp.zig");
-/// `content` is gpa-owned and adopted. The name carries `dir` so looks inside
-/// the buffer resolve like anywhere else.
-pub fn open(p: *Pardes, id: usize, dir: []const u8, name: []const u8, content: []u8) !*Pane {
- const path = try std.fmt.allocPrint(p.gpa, "{s}/{s}", .{ std.mem.trimEnd(u8, dir, "/"), name });
+/// What opened this buffer — THE field, and the only input to `traits`.
+///
+/// Two vocabularies because the core has exactly two: a `Builtin` is a word
+/// you can execute anywhere, and that covers Find, Grep, Help and every
+/// language query that has a name (Hover, Diagnostics...). The rest are KEYS —
+/// helix binds the five gotos and `=` as motions and `/` as a search input,
+/// and a motion has no word to click. Recording the key's `lsp.Kind` (or
+/// `.search` for the bare `/`) is not a parallel tag enum: both are the values
+/// the caller already holds when it opens the buffer.
+pub const Origin = union(enum) {
+ cmd: Builtin,
+ query: lsp.Kind,
+ /// the bare `/` — the pane's own text, searched in core
+ search,
+};
+
+/// The cap on a recorded argument, matching the one lspRequest already puts on
+/// an effect payload. ponytail: a longer pattern is TRUNCATED here, because
+/// `arg` is a record of what was asked and never the text anything re-runs; if
+/// something ever re-runs it, this becomes gpa-owned like `content`.
+pub const max_arg = 128;
+
+/// An output buffer's own state, hung off `File.output`.
+pub const Output = struct {
+ from: Origin,
+ /// the command's ARGUMENT: the pattern a Grep matched, the new name a
+ /// Rename took, the SPC prefix a Help lists. Inline rather than allocated
+ /// — it is a fixed-size record, and an owned slice would buy a free in
+ /// deinitPane and an errdefer at every open site for nothing.
+ arg_buf: [max_arg]u8 = undefined,
+ arg_len: u8 = 0,
+
+ pub fn arg(o: *const Output) []const u8 {
+ return o.arg_buf[0..o.arg_len];
+ }
+};
+
+pub fn setArg(o: *Output, text: []const u8) void {
+ o.arg_len = @intCast(@min(text.len, max_arg));
+ @memcpy(o.arg_buf[0..o.arg_len], text[0..o.arg_len]);
+}
+
+/// Every way an output pane differs from a file pane. One struct so the
+/// QUESTIONS are visible even where today every buffer answers alike: a column
+/// that never varies still says "this is decided here", which is what makes
+/// the next kind of buffer a row rather than a hunt.
+pub const Traits = struct {
+ /// the buffer's name (`+Search`...). DERIVED from the command, never the
+ /// thing that identifies it — that inversion is what this file undoes.
+ name: []const u8,
+ /// n/N walk the rows: each is a `path:LINE:COL text` location the ordinary
+ /// look path resolves, so the buffer IS helix's picker. Prose (a hover
+ /// blurb, a rename diff) has nowhere to step to.
+ steps: bool = false,
+ /// an answer of exactly ONE row jumps straight there instead of opening
+ /// this buffer at all — helix: the gotos jump on a single location and
+ /// show a picker on several, a symbol list is always a picker.
+ jumps: bool = false,
+ /// a DOCUMENT for layout purposes: claims a column of its own, is a split
+ /// parent, pays for a split. A result list is not — it belongs to the pane
+ /// that asked for it, lands directly below it and takes its rows from
+ /// there, so opening or closing one never resizes a bystander.
+ doc: bool = false,
+ /// Esc dismisses it: there is no file behind the pane and nothing worth
+ /// keeping, so "never mind" means close it — Esc IS the Del its tag offers.
+ dismiss: bool = true,
+ /// there is a file behind it to write. Also what its tag says: no Save to
+ /// offer means the plain pane tail rather than the file one.
+ saves: bool = false,
+};
+
+/// A REAL file pane, as a row of the same table — so kind-agnostic code asks
+/// one question and gets one answer whichever it is holding. `name` is unused:
+/// a file already has a path.
+const file_row: Traits = .{ .name = "", .doc = true, .dismiss = false, .saves = true };
+
+/// THE TABLE. Everything above, answered from the command that opened the
+/// buffer. Exhaustive on purpose: a new `lsp.Kind` or a new output-opening
+/// builtin should not compile until someone has said what its buffer does.
+pub fn traits(o: Origin) Traits {
+ return switch (o) {
+ // rows are `location text`, so n/N walk them
+ .search => .{ .name = config.search_buffer, .steps = true },
+ .cmd => |b| switch (b) {
+ .Help => .{ .name = config.help_buffer },
+ // Find (file names) and Grep (file contents) both list locations;
+ // no other builtin opens a buffer, and the day one does it lands
+ // here rather than in a call site.
+ else => .{ .name = config.search_buffer, .steps = true },
+ },
+ .query => |k| switch (k) {
+ .hover => .{ .name = config.hover_buffer },
+ // prose: an action list, a diff, a report about the backend
+ .code_action, .format, .rename, .status, .explain => .{ .name = config.lsp_buffer },
+ .definition, .declaration, .type_definition, .implementation, .references => .{
+ .name = config.search_buffer,
+ .steps = true,
+ .jumps = true,
+ },
+ .document_symbols, .workspace_symbols, .diagnostics, .workspace_diagnostics, .select_refs => .{
+ .name = config.search_buffer,
+ .steps = true,
+ },
+ },
+ };
+}
+
+/// The same table asked of a file pane's `output` field, null (a real file)
+/// included. This is what the kind-agnostic code in pardes.zig calls.
+pub fn fileTraits(out: ?Output) Traits {
+ return traits((out orelse return file_row).from);
+}
+
+/// How the dump spells an origin. A WORD, never an integer, for the reason the
+/// dump already stores tag words: reordering builtins.zig stays free. Nothing
+/// collides — a builtin is CamelCase, an lsp.Kind is snake_case, and `/` is
+/// neither.
+pub fn word(o: Origin) []const u8 {
+ return switch (o) {
+ .cmd => |b| @tagName(b),
+ .query => |k| @tagName(k),
+ .search => "/",
+ };
+}
+
+/// the inverse; null for "" (a real file) and for a word this build no longer
+/// has, which is a dump from another version and not a reason to fail a load
+pub fn fromWord(w: []const u8) ?Origin {
+ if (w.len == 0) return null;
+ if (std.mem.eql(u8, w, "/")) return .search;
+ if (std.meta.stringToEnum(Builtin, w)) |b| return .{ .cmd = b };
+ if (std.meta.stringToEnum(lsp.Kind, w)) |k| return .{ .query = k };
+ return null;
+}
+
+/// Is the results buffer `pane`'s n/N is armed on the one `from` filled?
+/// `]d`/`[d` are the only keys that care WHICH search is showing — they step
+/// the diagnostics list when it is up and ask for one when it is not — and
+/// this is how they ask now that the buffer remembers: `search_pane` is a
+/// SLOT, so this doubles as the check that the slot is still ours.
+pub fn resultsFrom(p: *Pardes, pane: *Pane, from: Origin) bool {
+ const rp = p.panes[pane.search_pane orelse return false] orelse return false;
+ const f = rp.file orelse return false;
+ const o = f.output orelse return false;
+ return std.meta.eql(o.from, from);
+}
+
+/// Open one. `content` is gpa-owned and adopted; the NAME comes from the table
+/// (the caller says what ran, not what to call it) and carries `dir` so looks
+/// inside the buffer resolve like anywhere else.
+pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u8, content: []u8) !*Pane {
+ const path = try std.fmt.allocPrint(p.gpa, "{s}/{s}", .{
+ std.mem.trimEnd(u8, dir, "/"), traits(from).name,
+ });
errdefer p.gpa.free(path);
const pane = try p.newDocPane(id);
- pane.file = .{ .path = path, .content = content, .output = true };
+ var out: Output = .{ .from = from };
+ setArg(&out, arg);
+ pane.file = .{ .path = path, .content = content, .output = out };
pane.cur_pinned = true;
return pane;
}
@@ -49,11 +210,15 @@ pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) void {
out.append(arena, '\n') catch return;
}
const content = p.gpa.dupe(u8, out.items) catch return;
+ // the buffer says what made it, so finding the open one is asking that and
+ // not matching its name
for (p.panes, 0..) |slot, i| {
const hp = slot orelse continue;
const hf = if (hp.file) |*f| f else continue;
- if (!hf.output or !std.mem.endsWith(u8, hf.path, "/" ++ config.help_buffer)) continue;
+ const ho = if (hf.output) |*o| o else continue;
+ if (!std.meta.eql(ho.from, Origin{ .cmd = .Help })) continue;
file_pane.setContent(p, hf, content);
+ setArg(ho, prefix);
hf.scroll = 0;
hp.cur_row = 0;
hp.msel.active = false;
@@ -65,7 +230,7 @@ pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) void {
p.gpa.free(content);
return;
};
- const np = open(p, free, dir, config.help_buffer, content) catch {
+ const np = open(p, free, dir, .{ .cmd = .Help }, prefix, content) catch {
p.gpa.free(content);
return;
};
diff --git a/src/pardes.zig b/src/pardes.zig
index 5412569b..01026347 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -600,12 +600,13 @@ pub const FileSnap = struct {
pub const File = struct {
path: []u8,
content: []u8,
- /// an OUTPUT buffer (acme's +Errors): a file pane with no file behind it,
- /// where the core shows text it produced itself (search results). The ONLY
- /// difference is that it cannot be saved — every mode, motion, chord and
- /// look works because it IS a file pane. `path` still names it (and gives
- /// its directory) so looks inside it resolve like anywhere else.
- output: bool = false,
+ /// set = this is an OUTPUT buffer (acme's +Errors): a file pane with no
+ /// file behind it, showing text the core produced itself. It records the
+ /// COMMAND that opened it, and output_pane.zig's one table turns that into
+ /// every way it differs from a file — ask it, never this field's shape.
+ /// `path` still names it (and gives its directory) so looks inside it
+ /// resolve like anywhere else.
+ output: ?output_pane.Output = null,
scroll: usize = 0,
/// one syntax.Syn byte per content byte in the highlighted window
highlights: []u8 = &.{},
@@ -678,15 +679,13 @@ pub const Pane = struct {
/// (submit/cancel restore it, and the marker itself says WHICH search this
/// is). Enter fills the output buffer in pane search_pane with one result
/// per row; n/N walk it, search_row being the row they last landed on
- /// (null = fresh results, nothing stepped to yet).
+ /// (null = fresh results, nothing stepped to yet). WHICH command filled
+ /// that buffer is the BUFFER's own record, never a second field here —
+ /// output_pane.resultsFrom is how `]d` asks.
search_edit: bool = false,
search_at: u16 = 0,
search_pane: ?usize = null,
search_row: ?usize = null,
- /// which language query filled search_pane, or null when a plain `/`,
- /// Find or Grep did. Read only by `]d`/`[d`, which step the diagnostics
- /// list when it is already up and ask for one when it is not.
- search_kind: ?lsp.Kind = null,
/// the editable tag tail: seeded with the default builtins on first touch;
/// edited in place by a modal one-line editor sharing the pane's mode
tag_tail: std.ArrayList(u8) = .empty,
@@ -928,8 +927,11 @@ pub const Pardes = struct {
/// this one" — the id bump makes the older answer stale and lspResponse
/// drops it. A queue would only buy the right to render an answer nobody
/// is waiting for any more.
+ /// `arg` rides along only so the buffer the answer opens can record what
+ /// was asked (a rename's new name, a symbol query) — the query itself has
+ /// it in the effect already.
lsp_seq: u32 = 0,
- lsp_wait: ?struct { id: u32, kind: lsp.Kind, pane: usize } = null,
+ lsp_wait: ?struct { id: u32, kind: lsp.Kind, pane: usize, arg: Effect.Buf(128) } = null,
/// pending effects, drained by the shell after each update. Growable: one
/// update can emit unbounded writes (a big selection executed is chunked
@@ -1217,10 +1219,10 @@ pub const Pardes = struct {
}
/// the editable tail: the user's edited buffer once touched, else defaults
- /// (an output buffer has no file to Save, so it gets the plain tail)
+ /// (a buffer with nothing to Save gets the plain tail — the table decides)
fn curTail(pane: *Pane) []const u8 {
if (pane.tag_init) return pane.tag_tail.items;
- if (pane.file) |f| if (!f.output) return file_pane_tail;
+ if (pane.file) |f| if (output_pane.fileTraits(f.output).saves) return file_pane_tail;
return pane_tail;
}
@@ -2252,11 +2254,11 @@ pub const Pardes = struct {
// Esc: exit select mode / drop pending prefixes + count. The
// selection itself SURVIVES (helix; `;` collapses it) — motions
- // replace it anyway. On an OUTPUT BUFFER (+Search, +Help) there is no
- // file behind the pane and nothing worth keeping, so "never mind"
- // means dismiss it: Esc IS Del, the same builtin its tag offers.
+ // replace it anyway. A buffer the table marks `dismiss` (every output
+ // buffer today) instead takes Esc as "never mind" and closes: Esc IS
+ // Del, the same builtin its tag offers.
if (hit(key, config.escape)) {
- if (pane.file) |f| if (f.output) return p.runBuiltin(.Del, p.active, "", null);
+ if (pane.file) |f| if (output_pane.fileTraits(f.output).dismiss) return p.runBuiltin(.Del, p.active, "", null);
pane.select = false;
pane.pending = 0;
pane.pending2 = 0;
@@ -2419,11 +2421,11 @@ pub const Pardes = struct {
// n/N stepper — `d` differs from `n` only in what it guarantees is
// showing. ]D / [D jump to the last/first, helix's pair.
if (hit(key, config.goto_diagnostic)) {
- if (pane.search_kind == .diagnostics and p.searchStep(p.active, if (fwd) 1 else -1)) return;
+ if (output_pane.resultsFrom(p, pane, .{ .query = .diagnostics }) and p.searchStep(p.active, if (fwd) 1 else -1)) return;
return p.lspRequest(p.active, .diagnostics, "");
}
if (hit(key, config.goto_diagnostic_end)) {
- if (pane.search_kind != .diagnostics) return p.lspRequest(p.active, .diagnostics, "");
+ if (!output_pane.resultsFrom(p, pane, .{ .query = .diagnostics })) return p.lspRequest(p.active, .diagnostics, "");
if (!fwd) { // [D = first
pane.search_row = null;
_ = p.searchStep(p.active, 1);
@@ -2706,7 +2708,7 @@ pub const Pardes = struct {
} else if (kind == .find) look.find(arena, dir, pat, &out) else {
const pl = p.paneCursorLines(pane) catch return;
// a real file names itself; a terminal or an output buffer has no path
- const has_path = if (pane.file) |f| !f.output else false;
+ const has_path = if (pane.file) |f| f.output == null else false;
var idbuf: [16]u8 = undefined;
// relative to `dir`, like Find's rows — and `dir` IS this file's
// own directory, so that is exactly its basename
@@ -2732,7 +2734,14 @@ pub const Pardes = struct {
p.gpa.free(content);
return;
};
- const np = output_pane.open(p, free, dir, config.search_buffer, content) catch {
+ // the buffer records WHICH search filled it, pattern and all: Find and
+ // Grep are builtins (words you can execute), the bare `/` is a key
+ const from: output_pane.Origin = switch (kind) {
+ .text => .search,
+ .find => .{ .cmd = .Find },
+ .grep => .{ .cmd = .Grep },
+ };
+ const np = output_pane.open(p, free, dir, from, pat, content) catch {
p.gpa.free(content);
return;
};
@@ -2740,7 +2749,6 @@ pub const Pardes = struct {
p.active = id;
pane.search_pane = free;
pane.search_row = null;
- pane.search_kind = null;
}
/// n/N: step to the next/previous row of this pane's results buffer and
@@ -2753,8 +2761,9 @@ pub const Pardes = struct {
const rid = pane.search_pane orelse return false;
const rp = p.panes[rid] orelse return false;
const rf = if (rp.file) |*f| f else return false;
- // a freed slot can be reused by an unrelated pane: the name says ours
- if (!rf.output or !std.mem.endsWith(u8, rf.path, "/" ++ config.search_buffer)) return false;
+ // one question covers both hazards: a freed slot can be reused by an
+ // unrelated pane, and a buffer of PROSE has nowhere to step to
+ if (!output_pane.fileTraits(rf.output).steps) return false;
// fresh results: n starts at the first row, N has nothing behind it
const nrows: i64 = @intCast(std.mem.count(u8, rf.content, "\n"));
const step: i64 = if (pane.search_row) |c| @as(i64, @intCast(c)) + delta else if (delta > 0) 0 else -1;
@@ -2795,7 +2804,7 @@ pub const Pardes = struct {
// program's output and an output buffer is our own text.
if (kind != .status) {
const f = pane.file orelse return;
- if (f.output) return;
+ if (f.output != null) return;
}
if (arg.len > 128) return; // the effect's arg is a Buf(128)
const off = if (pane.file) |f| modal.hxOff(f.content, .{
@@ -2803,7 +2812,7 @@ pub const Pardes = struct {
.col = @intCast(@max(0, pane.cur_col)),
}) else 0;
p.lsp_seq +%= 1;
- p.lsp_wait = .{ .id = p.lsp_seq, .kind = kind, .pane = id };
+ p.lsp_wait = .{ .id = p.lsp_seq, .kind = kind, .pane = id, .arg = .from(arg) };
p.emit(.{ .lsp = .{
.id = p.lsp_seq,
.kind = kind,
@@ -2826,8 +2835,9 @@ pub const Pardes = struct {
const pane = p.panes[w.pane] orelse return;
if (rows.len == 0) return;
+ const from: output_pane.Origin = .{ .query = w.kind };
const nrows = std.mem.count(u8, rows, "\n");
- if (w.kind.jumpsWhenSingle() and nrows == 1) {
+ if (output_pane.traits(from).jumps and nrows == 1) {
const ln = std.mem.trimEnd(u8, rows, "\n");
var hi: usize = 0;
while (hi < ln.len and config.isFileChar(ln[hi])) hi += 1;
@@ -2845,18 +2855,18 @@ pub const Pardes = struct {
p.gpa.free(content);
return;
};
- const np = output_pane.open(p, free, dir, w.kind.bufferName(), content) catch {
+ const np = output_pane.open(p, free, dir, from, w.arg.slice(), content) catch {
p.gpa.free(content);
return;
};
p.placeDoc(w.pane, free, np);
p.active = w.pane;
// prose is not a list of locations: n/N over a hover blurb would step
- // to nowhere, so only location buffers arm the stepper.
- if (std.mem.eql(u8, w.kind.bufferName(), config.search_buffer)) {
+ // to nowhere, so only stepping buffers arm the stepper — and WHICH
+ // query filled it is now the buffer's own record, not a field here.
+ if (output_pane.traits(from).steps) {
pane.search_pane = free;
pane.search_row = null;
- pane.search_kind = w.kind;
}
}
@@ -4677,7 +4687,7 @@ pub const Pardes = struct {
var k = f.idx;
while (k > 0) : (k -= 1) {
sib = p.col_terms[f.col][k - 1];
- if (p.panes[sib]) |pp| if (if (pp.file) |ff| !ff.output else true) break;
+ if (p.panes[sib]) |pp| if (if (pp.file) |ff| output_pane.fileTraits(ff.output).doc else true) break;
}
if (p.panes[sib]) |s| s.vweight += @as(f32, @floatFromInt(@max(1, p.rects[id].h)));
}
@@ -4722,10 +4732,10 @@ pub const Pardes = struct {
};
src.vweight = @floatFromInt(BOX_H + keep);
nw.vweight = @floatFromInt(@max(1, src_h -| (BOX_H + keep)));
- // an output buffer is worth exactly its own text: a three-hit +Search
+ // a non-doc buffer is worth exactly its own text: a three-hit +Search
// is four rows, not half the source. Nothing else can want the rows,
// so they go straight back to the pane they were taken from.
- if (nw.file) |f| if (f.output) {
+ if (nw.file) |f| if (!output_pane.fileTraits(f.output).doc) {
// trimmed: every row ends in a newline, and the empty line after
// the last one is not a result
const want: f32 = @floatFromInt(BOX_H + file_pane.lineCount(std.mem.trimEnd(u8, f.content, "\n")));
@@ -5069,13 +5079,13 @@ pub const Pardes = struct {
/// right) — nothing is displaced, the other columns just narrow. A full
/// column bar, or a result list whose source died, stacks into the leftmost.
pub fn placeDoc(p: *Pardes, from_id: usize, free: usize, nt: *Pane) void {
- const doc = if (nt.file) |f| !f.output else true; // an image is a doc
+ const doc = if (nt.file) |f| output_pane.fileTraits(f.output).doc else true; // an image is a doc
var src_id: ?usize = null;
- if (p.panes[from_id]) |src| if (if (src.file) |f| !f.output else src.image != null) {
+ if (p.panes[from_id]) |src| if (if (src.file) |f| output_pane.fileTraits(f.output).doc else src.image != null) {
src_id = from_id;
};
if (doc and src_id == null) for (p.panes, 0..) |sl, i| {
- if (sl) |pp| if (i != free and (if (pp.file) |f| !f.output else pp.image != null)) {
+ if (sl) |pp| if (i != free and (if (pp.file) |f| output_pane.fileTraits(f.output).doc else pp.image != null)) {
src_id = i;
break;
};
@@ -5118,7 +5128,10 @@ pub const Pardes = struct {
const tag = try p.tagText(arena, pane);
const body = try p.bodyText(arena, pane);
const scroll: usize = @intCast(@max(0, pane.scroll()));
- const dp: dump.Pane = if (pane.file) |f| .{
+ // BY POINTER: `origin_arg` is a slice into the File's own inline
+ // buffer, and a by-value capture would leave it pointing at a
+ // stack copy that dies before the ZON is written.
+ const dp: dump.Pane = if (pane.file) |*f| .{
.kind = .file,
.tag = tag,
.body = body,
@@ -5130,7 +5143,8 @@ pub const Pardes = struct {
.path = f.path,
.content = f.content,
.content_b64 = try dump.encodeBytes(arena, f.content),
- .output = f.output,
+ .origin = if (f.output) |o| output_pane.word(o.from) else "",
+ .origin_arg = if (f.output) |*o| o.arg() else "",
},
} else if (pane.image) |iv| blk: {
const bytes = look.readFile(arena, iv.path) catch "";
@@ -5276,7 +5290,14 @@ pub const Pardes = struct {
const path = try gpa.dupe(u8, f.path);
errdefer gpa.free(path);
const pane = try p.newDocPane(i);
- pane.file = .{ .path = path, .content = content, .output = f.output, .scroll = src.scroll };
+ // an unknown origin word is a dump from another build, not
+ // a reason to refuse the load: it comes back as a file
+ const out: ?output_pane.Output = if (output_pane.fromWord(f.origin)) |o| blk: {
+ var ov: output_pane.Output = .{ .from = o };
+ output_pane.setArg(&ov, f.origin_arg);
+ break :blk ov;
+ } else null;
+ pane.file = .{ .path = path, .content = content, .output = out, .scroll = src.scroll };
pane.cur_pinned = true;
pane.cur_row = @intCast(src.scroll);
pane.cols = @max(1, src.cols);