summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 09:49:03 -0300
committerGabriel Schneider <[email protected]>2026-10-01 09:49:03 -0300
commiteda60572013d046d08e253e5847a0ba588c10339 (patch)
treeaf17805628e1dc735bfd59d7184cfa7b1ad1d120
parent802de4f12f125abc261c58bdb5385f6be3ad469a (diff)
downloadpardes-eda60572013d046d08e253e5847a0ba588c10339.tar.gz
pardes-eda60572013d046d08e253e5847a0ba588c10339.zip
Grep's miss says `Grep: text not found`: it is a literal search, and "pattern" suggested a regular expression
Still ENOENT through a mount ("not found" is in 9ns's table, and the unit tests' errno-words check holds it so). The guide's quote follows. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/diff-pager-proposals.md164
-rw-r--r--docs/typ/guide.typ2
-rw-r--r--src/builtins.zig2
-rw-r--r--src/ninep/ctl.zig2
-rw-r--r--test/fs.py2
5 files changed, 168 insertions, 4 deletions
diff --git a/docs/diff-pager-proposals.md b/docs/diff-pager-proposals.md
new file mode 100644
index 00000000..f75361df
--- /dev/null
+++ b/docs/diff-pager-proposals.md
@@ -0,0 +1,164 @@
+# Diffs in a pager: what delta does, what pardes could
+
+Proposals, 2026-10-01, none decided. They come from reading delta,
+diff-so-fancy and difftastic (all in `~/05-genizah/`) against pardes as of
+`3815a0bf`, the commit that made `pardes -` a terminal's `PAGER` and
+`GIT_PAGER`. The rule kept throughout: the pane's text stays the plain
+text git printed, and whatever is added is something a look or an exec
+can act on.
+
+## How the others read a diff
+
+**delta** reads stdin a line at a time and keeps each line twice: the raw
+bytes and a copy with the ANSI stripped (`delta.rs:235`). Every match runs
+on the stripped copy, so git's colour does not change the parse. The raw
+copy is kept for two cases where colour is the information: a hunk line
+whose colour is not git's plain red or green (`--color-moved`), and every
+line under `--word-diff` or `--color-words`, which pass through as git
+coloured them (`hunk.rs:136-171`). A state machine (`delta.rs:20-38`) moves
+on `commit`, `diff`, `---`/`+++`, `@@`, and the `-`/`+`/space prefixes, with
+further states for merge conflicts, submodules, `git blame`, `git grep` and
+`git show REV:path`. Which git command it is reading it learns from its
+parent process's command line (`utils/process.rs`), down to scanning the
+pids next to its own. Within a line, it pairs each removed line with the
+first added line within 0.6 edit distance, tokenises both on `\w+` and
+aligns them (`edits.rs`, `align.rs`). Its OSC 8 links name only the new
+side's `file:line`. `--navigate` writes `Δ` and `•` before file and hunk
+headers and puts a search for them into a copy of less's history file, so
+that `n` finds them.
+
+**diff-so-fancy** matches git's colour codes where they appear, colouring
+for itself where there are none. It rewrites the diff for show: file
+headers become a box, `@@ -a,b +c,d @@` becomes `@ file:N @`, and the
+`+`/`-` markers go, leaving old against new to colour. Within a line it
+uses git's diff-highlight, which pairs lines only in a block with as many
+added as removed lines, and marks what lies between a common prefix and a
+common suffix. Behind `OSC1717=V1` it emits a per-line escape carrying the
+line's kind, its old and new numbers and its path. It is the only one of
+the three whose output gives every line its place in both files.
+
+**difftastic** reads no diff. Git runs it as the diff program
+(`GIT_EXTERNAL_DIFF`) and hands it both whole files. It parses each with
+tree-sitter and finds the cheapest set of changes with Dijkstra, falling
+back to a line diff past 1 MB, 3M graph vertices or any parse error. What
+it prints is not a patch: change is shown only by colour, and its JSON is
+unstable and counts lines from 0.
+
+## What pardes does now
+
+`pardes -` reads stdin, strips terminal escapes (`stripEscapes`,
+`main.zig`), and puts the text in a `<cwd>/+Pager` pane. Git starts its
+pager at the top of the work tree even when run from a subdirectory
+(checked), so `<cwd>` is already the base git's repository-relative paths
+need.
+
+A pane that is a diff is read by `src/diff.zig`. It is already at delta's
+level, or past it:
+
+- A hunk's end comes from its `@@` counts, as patch reads it, so a removed
+ `--- x` line is never taken for a header. delta only counts this way for
+ a plain `diff -u`.
+- `syntax.highlightDiff` colours each hunk's old side and new side each as
+ one piece in the file's own language, so a string or a comment that
+ spans lines colours as it does in the file. delta runs one highlighter
+ through the removed lines and then the added ones.
+- git's other prefixes (`c/`, `i/`, `w/`, `--no-prefix`) are understood, and
+ so are rows a terminal wrapped.
+- A look on any line goes to the place it names (`look.zig:1573`): a file
+ header to the file, an `@@` or a hunk line to `path:N`. delta's links
+ are the nearest thing it has.
+
+## Gaps
+
+1. **A +Pager pane is never a diff.** Whether a pane is coloured and looked
+ at as a diff is decided by its name ending `.diff` or `.patch`
+ (`panes.zig:451`); only a finished command pane is checked by content
+ (`looksLikeDiff`, `File.zig:1545`). So `git log -p` in a terminal pages
+ to plain text, with no colour and no look on its lines. (From reading
+ the code, not yet run.)
+2. **Some of what git says is only colour, and stripping loses it.** With
+ `git diff --color-words`, `foo(1)` changed to `bar(1)` arrives as
+ `let x = foo(1);bar(1);` (checked). `--color-moved` loses its moved
+ blocks the same way.
+3. **No changed words within a line**, which is most of what people use
+ delta for.
+4. **Nothing for commits or blame.** A `git grep` line, `path:N:text`, is a
+ look already.
+
+## Proposals
+
+### A. Read +Pager as a diff when it is one
+
+Either `colorAlgo` checks a +Pager pane's content with `looksLikeDiff`, or
+`pardes -` names the pane `+Pager.diff` when what it read is a diff. The
+name has the merit that a 9P client, or a reader of the tag, sees what the
+pane is. Either is a few lines, and everything in `diff.zig`, the
+painter and the look comes along. Leaning to the content check, so that a
+name always means where the text came from.
+
+### B. Turn colour that carries meaning into git's own text
+
+While stripping, a red or green run inside a hunk line under word diff
+becomes `[-foo(1)-]` or `{+bar(1)+}`: exactly what `git diff
+--word-diff=plain` prints. The pane stays plain text, searchable and
+editable, and says what the colour said. `--color-moved` has no text form
+in git; it can be kept as a style layer beside the text, dropped at the
+first edit, or let go.
+
+### C. Changed words within a line, drawn only
+
+Pair removed lines with added ones and align their tokens as delta does
+(edit distance, then an alignment table), and set an emph bit in the
+style bytes beside `diff_added`/`diff_removed`. The text is untouched. A
+change made only of whitespace, an indent or a gap, is not marked (the
+patch carried in our own delta build, `local-patches` in the genizah).
+Later, the tokens could be tree-sitter's leaves in place of `\w+`, which
+gets much of difftastic's quality without its graph search.
+
+### D. Moving through a diff by address, not by marks
+
+delta's marks exist because less cannot search by structure. In pardes
+`:/^@@/` and `:/^diff /` are text, and a look of them moves. They can sit
+in a diff pane's tag. Or an `Index` exec writes a +Index pane with one
+line per file or hunk, each a look:
+
+ @p12:345 src/look.zig +12 -3
+
+### E. A diff you can act on
+
+This is what a pager that only prints cannot do.
+
+- **Stage, Unstage, Revert** in the tag: the hunk under the cursor, with
+ its file header and its `@@` counts recounted, written to
+ `git apply --cached`, `git apply --cached -R`, or `git apply -R`.
+- **Apply** for the whole pane: edit the diff, then apply it, which is
+ `git add -p`'s `e` for a whole buffer.
+- **The old side.** A look on a removed line goes today to the new line now
+ standing where it was. The `index e1f909f..0136052` line names the old
+ blob, so a look on a removed line can open `git cat-file -p e1f909f` at
+ its old line instead.
+- **Commits.** A look on `commit abc1234`, or on a blame line's hash, runs
+ `git show abc1234` into another +Pager, as acme's plumber would.
+
+These can be helpers over 9P (a script reads the pane's body and dot,
+runs git, writes back), in keeping with the roadmap's helper step, rather
+than code in the core.
+
+### F. Further off
+
+- **pardes as git's diff program**, where difftastic sits: it is handed
+ both whole files and still prints an ordinary unified diff, aligned by
+ syntax tree. Not worth it until C with tree-sitter tokens falls short.
+- **Reading OSC 1717**, so that a diff from delta or diff-so-fancy, whose
+ text is too rewritten to parse again, can still be looked at line by
+ line.
+
+## Open questions
+
+1. A: check the content, or name the pane `.diff`?
+2. B: word colour to `[-…-]{+…+}` text, a style layer beside the text, or
+ both?
+3. E: in the core, or as helpers over 9P?
+
+Suggested order: A, then the text half of B, then E's commit and old-side
+looks, then C.
diff --git a/docs/typ/guide.typ b/docs/typ/guide.typ
index 6d140da7..077484dc 100644
--- a/docs/typ/guide.typ
+++ b/docs/typ/guide.typ
@@ -146,7 +146,7 @@ stops at 512 hits, Find at 512 names; the walk stops at 20000 files or
hit, or a directory it could not open, is said at the end (`cut at 512
hits`, `N files read only in part (first 256 KiB)`, `walk cut at N
entries`, `N directories skipped: permission denied`). A search that finds
-nothing and skipped nothing fails, `Grep: pattern not found`, and leaves
+nothing and skipped nothing fails, `Grep: text not found`, and leaves
the `+Search` as it was.
A relative path is looked for where the click was, then where you have
diff --git a/src/builtins.zig b/src/builtins.zig
index 40cd83c9..ca80be26 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -1560,7 +1560,7 @@ pub const Mini = struct {
/// A Find or Grep that finds nothing fails as one, in words 9ns reads as
/// ENOENT (`not found`), the same for both.
const e_search_miss_find = "Find: name not found";
-const e_search_miss_grep = "Grep: pattern not found";
+const e_search_miss_grep = "Grep: text not found";
/// List the files below this pane's directory whose names hold its argument, in a +Search.
pub const Find = struct {
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 3ec1f4d6..40ffebc8 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -2565,7 +2565,7 @@ test "Grep with no hit says grep: no match and opens nothing, a missing pane dir
for (p.panes) |slot| count += @intFromBool(slot != null);
const missed = wr(p, Node.of(serialOf(p), .exec), "Grep zzqqnomatchzz\n");
try testing.expectEqual(E.NOENT, missed.errno());
- try testing.expectEqualStrings("Grep: pattern not found", missed.reply.ename);
+ try testing.expectEqualStrings("Grep: text not found", missed.reply.ename);
const unfound = wr(p, Node.of(serialOf(p), .exec), "Find zzqqnomatchzz\n");
try testing.expectEqual(E.NOENT, unfound.errno());
try testing.expectEqualStrings("Find: name not found", unfound.reply.ename);
diff --git a/test/fs.py b/test/fs.py
index 11233e80..52ac61c8 100644
--- a/test/fs.py
+++ b/test/fs.py
@@ -702,7 +702,7 @@ def workflow_canary(binary):
rows = client.read(f'/pane/{grep}/body').decode()
assert 'a.txt:2' in rows and 'b.txt:2' in rows, rows
client.remove(f'/pane/{grep}')
- execute_refused(client, a, 'Grep zzqqnomatch', 'Grep: pattern not found')
+ execute_refused(client, a, 'Grep zzqqnomatch', 'Grep: text not found')
found = made_by(a, b'Find b.txt\n')
assert 'b.txt' in client.read(f'/pane/{found}/body').decode()
client.remove(f'/pane/{found}')