Fuzzy line-based matching toolkit used by patch exact-replace.
This namespace USED to also carry the Codex apply_patch envelope
parser, but envelope mode was retired — we consolidated to a single
per-intent mutation surface (patch exact-replace + write /
move / delete). The fuzzy matcher stays because it is what
lets multi-line :search blocks tolerate whitespace and typographic
drift before they fall through to :no-match.
Public surface (all pure): seek-sequence lines pattern start eof? -> start-index or nil seek-sequence-with-pass lines pattern start eof? -> {:start :pass :indent-delta?} or nil split-content-lines string -> vec of lines (no trailing \n element) char-offset-at-line content line-idx -> char offset apply-indent-delta delta lines -> re-indented lines
It also owns the reusable HASHLINE layer (content-addressed editing):
line-hash / lines->anchors text -> 6-hex anchor / {ln hash}
render-hashline-block / -range-block tuples -> <hash>| text gutter
indices-matching-hash / resolve-anchor-edit self-locating range replace
Fuzzy line-based matching toolkit used by `patch` exact-replace.
This namespace USED to also carry the Codex `apply_patch` envelope
parser, but envelope mode was retired — we consolidated to a single
per-intent mutation surface (`patch` exact-replace + `write` /
`move` / `delete`). The fuzzy matcher stays because it is what
lets multi-line `:search` blocks tolerate whitespace and typographic
drift before they fall through to `:no-match`.
Public surface (all pure):
seek-sequence lines pattern start eof? -> start-index or nil
seek-sequence-with-pass lines pattern start eof?
-> {:start :pass :indent-delta?} or nil
split-content-lines string -> vec of lines (no trailing \n element)
char-offset-at-line content line-idx -> char offset
apply-indent-delta delta lines -> re-indented lines
It also owns the reusable HASHLINE layer (content-addressed editing):
line-hash / lines->anchors text -> 6-hex anchor / {ln hash}
render-hashline-block / -range-block tuples -> `<hash>| text` gutter
indices-matching-hash / resolve-anchor-edit self-locating range replace(anchor->line anchor)Parse the line number out of a <lineno>:<hash> anchor.
Parse the line number out of a `<lineno>:<hash>` anchor.
(anchor-map->tuples m)Inverse of lines->anchor-map: {anchor text} → [[ln text]…] tuples,
line number parsed from each <lineno>:<hash> key, sorted by line. For the
channel/human gutter and any internal consumer that still wants tuples.
Inverse of `lines->anchor-map`: `{anchor text}` → `[[ln text]…]` tuples,
line number parsed from each `<lineno>:<hash>` key, sorted by line. For the
channel/human gutter and any internal consumer that still wants tuples.(apply-indent-delta delta lines)Re-indent lines by delta leading spaces (positive adds, negative
strips). Blank lines untouched. Used by exact-replace when a fuzzy
:relative-indent hit fires and the replace payload must follow the
file's actual indentation rather than the SEARCH block's.
Re-indent `lines` by `delta` leading spaces (positive adds, negative strips). Blank lines untouched. Used by exact-replace when a fuzzy :relative-indent hit fires and the `replace` payload must follow the file's actual indentation rather than the SEARCH block's.
(char-offset-at-line content line-idx)Char offset in content where 0-based line line-idx starts.
Returns (count content) if line-idx reaches past the last line.
Public so the exact-replace path can map line indices back to char
positions for substring splicing.
Char offset in `content` where 0-based line `line-idx` starts. Returns `(count content)` if `line-idx` reaches past the last line. Public so the exact-replace path can map line indices back to char positions for substring splicing.
How far (in lines) a content hash may sit from its stated line number
before resolve-one-anchor calls the anchor MISPLACED and refuses. The
common path never needs it — within one patch batch every hunk resolves
against the file's ORIGINAL snapshot, so the stated line number is exact and
the fast path fires. This window only forgives small drift when the model
reuses anchors across patch calls without re-reading; anything larger (the
~200-line gap of a genuinely wrong/stale anchor — the corruption this whole
scheme exists to stop) is refused so the model re-reads. Deliberately tight:
a false refuse costs one re-read, a false accept corrupts the file.
How far (in lines) a content hash may sit from its stated line number before `resolve-one-anchor` calls the anchor MISPLACED and refuses. The common path never needs it — within one patch batch every hunk resolves against the file's ORIGINAL snapshot, so the stated line number is exact and the fast path fires. This window only forgives small drift when the model reuses anchors across patch calls without re-reading; anything larger (the ~200-line gap of a genuinely wrong/stale anchor — the corruption this whole scheme exists to stop) is refused so the model re-reads. Deliberately tight: a false refuse costs one re-read, a false accept corrupts the file.
Hex chars in a line's content hash. Anchors now carry the LINE NUMBER too
(<lineno>:<hash> — see line-anchor / lines->anchors), so the hash no
longer has to be globally unique: the line number LOCATES the line and the
hash only VERIFIES the content there (drift + misattribution). That
collapses the hash's job from whole-file disambiguation — which forced
width 4 / 65536 buckets back when the hash was the SOLE anchor (see git
history for the old collision-vs-token table) — to a local check inside a
hash-line-drift-tolerance-sized window. 3 hex (4096 buckets) is ample
there: the residual in-window collision chance is ~0.1%, and the line
number disambiguates even that. Bump this and the mask/pad follow
automatically. (This is exactly Can Bölük's original lineno:hash hashline
shape — the bare-hash variant was the vis-specific detour that lost the
line coordinate and, with it, the wrong-line guard.)
Hex chars in a line's content hash. Anchors now carry the LINE NUMBER too (`<lineno>:<hash>` — see `line-anchor` / `lines->anchors`), so the hash no longer has to be globally unique: the line number LOCATES the line and the hash only VERIFIES the content there (drift + misattribution). That collapses the hash's job from whole-file disambiguation — which forced width 4 / 65536 buckets back when the hash was the SOLE anchor (see git history for the old collision-vs-token table) — to a local check inside a `hash-line-drift-tolerance`-sized window. 3 hex (4096 buckets) is ample there: the residual in-window collision chance is ~0.1%, and the line number disambiguates even that. Bump this and the mask/pad follow automatically. (This is exactly Can Bölük's original `lineno:hash` hashline shape — the bare-hash variant was the vis-specific detour that lost the line coordinate and, with it, the wrong-line guard.)
Separator between the line number and the content hash inside an anchor
(<lineno>:<hash>). A single char so the gutter stays narrow.
Separator between the line number and the content hash inside an anchor (`<lineno>:<hash>`). A single char so the gutter stays narrow.
Separator between the anchor and the line text in rendered output.
Separator between the anchor and the line text in rendered output.
(indices-matching-hash lines h)0-based indices of lines whose content line-hash equals the bare hash
h. Pure content match — the line-number coordinate is applied separately
by resolve-one-anchor, so this just answers "which lines carry this
content".
0-based indices of `lines` whose content `line-hash` equals the bare hash `h`. Pure content match — the line-number coordinate is applied separately by `resolve-one-anchor`, so this just answers "which lines carry this content".
(line-anchor ln text)The editable anchor for a line: <line-number>:<content-hash> (e.g.
325:0e3). The line number LOCATES the line; the hash VERIFIES its
content. patch :from_anchor parses this back via resolve-anchor-range,
matching the line number against live content and refusing if the hash no
longer agrees (the line changed) or that content now lives far from the
stated line (a misattributed / stale anchor). Two coordinates, so a single
reused hash can no longer silently land an edit on the wrong line.
The editable anchor for a line: `<line-number>:<content-hash>` (e.g. `325:0e3`). The line number LOCATES the line; the hash VERIFIES its content. `patch :from_anchor` parses this back via `resolve-anchor-range`, matching the line number against live content and refusing if the hash no longer agrees (the line changed) or that content now lives far from the stated line (a misattributed / stale anchor). Two coordinates, so a single reused hash can no longer silently land an edit on the wrong line.
(line-hash line)Stable hash-width-hex-char content hash of line (trimmed). Folds
the spec'd String/hashCode algorithm over the whitespace-trimmed
line, so it is deterministic across JVM runs. Identical trimmed lines
share a hash — a dup-line collision makes a :from_anchor anchor
ambiguous and resolve-anchor-edit refuses it (caller falls back to
:search).
Hot path: runs once per line on every cat render AND every patch
resolve. Formats with Integer/toHexString + a left-pad rather than
java.util.Formatter, which benches ~1.5x slower; the trimmed
String/hashCode is a JIT intrinsic so we lean on it instead of a
hand loop.
Stable `hash-width`-hex-char content hash of `line` (trimmed). Folds the spec'd `String/hashCode` algorithm over the whitespace-trimmed line, so it is deterministic across JVM runs. Identical trimmed lines share a hash — a dup-line collision makes a `:from_anchor` anchor ambiguous and `resolve-anchor-edit` refuses it (caller falls back to `:search`). Hot path: runs once per line on every `cat` render AND every patch resolve. Formats with `Integer/toHexString` + a left-pad rather than java.util.Formatter, which benches ~1.5x slower; the trimmed `String/hashCode` is a JIT intrinsic so we lean on it instead of a hand loop.
(line-index-at content off)0-based line index of char offset off in content (count of
before it).
0-based line index of char offset `off` in `content` (count of ` ` before it).
(lines->anchor-map tuples)Ordered {anchor text} map for [[ln text]…] tuples: a REAL
java.util.LinkedHashMap (natively insertion-ordered), built in line order
— each KEY is the line's <lineno>:<hash> line-anchor, each VALUE the
verbatim text. Being an ordered hashmap (not a Clojure map), it stays in
file order across the Clojure → JSON/charred → GraalPy dict boundary at ANY
size, with NO comparator. EVERY line is keyed (blanks included, so the read
stays gap-free); duplicate text differs by line number, so keys are unique.
THE single model-facing line payload cat returns; the key IS the
patch :from_anchor.
Ordered `{anchor text}` map for `[[ln text]…]` tuples: a REAL
`java.util.LinkedHashMap` (natively insertion-ordered), built in line order
— each KEY is the line's `<lineno>:<hash>` `line-anchor`, each VALUE the
verbatim text. Being an ordered hashmap (not a Clojure map), it stays in
file order across the Clojure → JSON/charred → GraalPy dict boundary at ANY
size, with NO comparator. EVERY line is keyed (blanks included, so the read
stays gap-free); duplicate text differs by line number, so keys are unique.
THE single model-facing line payload `cat` returns; the key IS the
`patch :from_anchor`.(lines->anchors tuples){line-number anchor} map of every non-blank line in tuples, where each
anchor is <line-number>:<content-hash> (line-anchor). The canonical
:anchors payload cat returns — the SINGLE place it is built (read-file /
read-file-ranges / tail-file / rg all route here). Blank lines are omitted:
the model only ever sees anchors it can actually edit by. Line numbers come
straight from the [ln text] tuples, so a windowed read (range / tail /
by-hash) carries the file's real line numbers with NO second full-file pass
— the #N file-wide-ordinal scheme (and its whole-file rescan) is gone now
that the line number, not the hash, disambiguates duplicate lines.
`{line-number anchor}` map of every non-blank line in `tuples`, where each
anchor is `<line-number>:<content-hash>` (`line-anchor`). The canonical
`:anchors` payload `cat` returns — the SINGLE place it is built (read-file /
read-file-ranges / tail-file / rg all route here). Blank lines are omitted:
the model only ever sees anchors it can actually edit by. Line numbers come
straight from the `[ln text]` tuples, so a windowed read (range / tail /
by-hash) carries the file's real line numbers with NO second full-file pass
— the `#N` file-wide-ordinal scheme (and its whole-file rescan) is gone now
that the line number, not the hash, disambiguates duplicate lines.(render-lineno-block tuples)Render [line-number text] tuples as a HUMAN line-number gutter
<ln>│ <text>, line numbers right-aligned to the widest number in
the block. Unlike render-hashline-block (the MODEL surface, whose
gutter is the editable :from_anchor anchor), this is the channel/TUI
display surface: humans navigate by line number, not by content hash.
Render `[line-number text]` tuples as a HUMAN line-number gutter `<ln>│ <text>`, line numbers right-aligned to the widest number in the block. Unlike `render-hashline-block` (the MODEL surface, whose gutter is the editable `:from_anchor` anchor), this is the channel/TUI display surface: humans navigate by line number, not by content hash.
(render-lineno-range-block ranges)render-lineno-block analogue for :ranges windows — -- range S-E --
headers followed by the human line-number gutter for each window.
`render-lineno-block` analogue for `:ranges` windows — `-- range S-E --` headers followed by the human line-number gutter for each window.
(resolve-anchor-edit current from_anchor to_anchor replace)Content-addressed line-range replace returning full {:new-content S :applied-line N} (or {:error …}). Thin wrapper over
resolve-anchor-edit-span; prefer the span variant inside a batch.
Content-addressed line-range replace returning full `{:new-content S
:applied-line N}` (or `{:error …}`). Thin wrapper over
`resolve-anchor-edit-span`; prefer the span variant inside a batch.(resolve-anchor-edit-span current from_anchor to_anchor replace)Resolve a content-addressed line-range edit to a CHAR SPAN against current,
WITHOUT building new content: {:start S :end E :replacement R :applied-line N}
or {:error {:reason KW …}}. Lets a multi-edit batch resolve every anchor
against the ORIGINAL snapshot and splice all spans together atomically, so an
earlier edit can't drift a later edit's hash/ordinal. to_anchor defaults to
from_anchor (single line). The stated line is tried first and only then
small drift is considered; duplicate hashes at other lines do not make an
exact lineno:hash anchor ambiguous.
Resolve a content-addressed line-range edit to a CHAR SPAN against `current`,
WITHOUT building new content: `{:start S :end E :replacement R :applied-line N}`
or `{:error {:reason KW …}}`. Lets a multi-edit batch resolve every anchor
against the ORIGINAL snapshot and splice all spans together atomically, so an
earlier edit can't drift a later edit's hash/ordinal. `to_anchor` defaults to
`from_anchor` (single line). The stated line is tried first and only then
small drift is considered; duplicate hashes at other lines do not make an
exact `lineno:hash` anchor ambiguous.(resolve-anchor-range current from_anchor to_anchor)Resolve from_anchor (and to_anchor, defaulting to from_anchor for a single
line) against LIVE current. Each is a <line-number>:<hash> anchor: the
line number LOCATES it, the hash VERIFIES the content still matches AND sits
near the stated line (else :hashline-misplaced — the wrong-line guard). Both
coordinates are REQUIRED; a bare hash with no line number is refused
(:hashline-malformed). Returns {:from-line N :to-line N} (1-based,
INCLUSIVE) or {:error {:reason KW …}}.
Shared by resolve-anchor-edit (WRITE — patch :from_anchor) and the cat
:anchor READ path so both address lines identically.
Resolve `from_anchor` (and `to_anchor`, defaulting to `from_anchor` for a single
line) against LIVE `current`. Each is a `<line-number>:<hash>` anchor: the
line number LOCATES it, the hash VERIFIES the content still matches AND sits
near the stated line (else `:hashline-misplaced` — the wrong-line guard). Both
coordinates are REQUIRED; a bare hash with no line number is refused
(`:hashline-malformed`). Returns `{:from-line N :to-line N}` (1-based,
INCLUSIVE) or `{:error {:reason KW …}}`.
Shared by `resolve-anchor-edit` (WRITE — patch :from_anchor) and the cat
`:anchor` READ path so both address lines identically.(seek-sequence lines pattern start eof?)Find pattern (a vec of strings) inside lines (vec of strings)
starting at start. When eof? is true, first try the EOF position
then fall back to start.
Match strictness, in order: exact -> rstrip -> unicode -> relative-indent
-> trim. Returns the start index or nil. For pass attribution use
seek-sequence-with-pass.
Find `pattern` (a vec of strings) inside `lines` (vec of strings) starting at `start`. When `eof?` is true, first try the EOF position then fall back to `start`. Match strictness, in order: exact -> rstrip -> unicode -> relative-indent -> trim. Returns the start index or nil. For pass attribution use `seek-sequence-with-pass`.
(seek-sequence-with-pass lines pattern start eof?)Like seek-sequence but returns {:start <i> :pass <kw> :indent-delta <n>?}
so callers can surface which fuzzy pass produced the hit. pass is one of
:exact :rstrip :unicode :relative-indent :trim. nil when no pass matches.
Pass priority is deliberately:
:exact → :rstrip → :unicode → :relative-indent → :trim
:relative-indent runs BEFORE :trim because :trim is destructive
(it drops both leading and trailing whitespace from each line and so
collapses different indentation structures to the same key). The more
structure-preserving :relative-indent should win whenever it can.
Like `seek-sequence` but returns `{:start <i> :pass <kw> :indent-delta <n>?}`
so callers can surface which fuzzy pass produced the hit. `pass` is one of
`:exact :rstrip :unicode :relative-indent :trim`. nil when no pass matches.
Pass priority is deliberately:
:exact → :rstrip → :unicode → :relative-indent → :trim
`:relative-indent` runs BEFORE `:trim` because `:trim` is destructive
(it drops both leading and trailing whitespace from each line and so
collapses different indentation structures to the same key). The more
structure-preserving `:relative-indent` should win whenever it can.(split-content-lines s)Split a file blob into a vec of lines. Trailing empty element (from a final newline) is dropped, matching the convention used by the exact-replace path.
Split a file blob into a vec of lines. Trailing empty element (from a final newline) is dropped, matching the convention used by the exact-replace path.
(tokenize-with-offsets s)Vec of [token ^long start ^long end] for every maximal non-whitespace
run in s. start/end are raw char offsets (end exclusive).
Vec of `[token ^long start ^long end]` for every maximal non-whitespace run in `s`. `start`/`end` are raw char offsets (end exclusive).
(tuples->ranges tuples)Split flat [[ln text]…] tuples into contiguous :ranges windows
[{:range [start end] :lines [[ln text]…]}…], breaking the run whenever
the line number jumps by more than 1. Produces exactly the shape
render-lineno-range-block / render-hashline-range-block consume, so a
flat tuple list (e.g. grouped grep hits) renders with the same
-- range S-E -- gap headers as a native multi-range read.
Split flat `[[ln text]…]` tuples into contiguous `:ranges` windows
`[{:range [start end] :lines [[ln text]…]}…]`, breaking the run whenever
the line number jumps by more than 1. Produces exactly the shape
`render-lineno-range-block` / `render-hashline-range-block` consume, so a
flat tuple list (e.g. grouped grep hits) renders with the same
`-- range S-E --` gap headers as a native multi-range read.(ws-agnostic-line-span content search)Whitespace-agnostic token-subsequence match. Folds search and
content to whitespace-free token streams and looks for search's
tokens as a contiguous subsequence of content's. Because all
whitespace (incl. newline structure) is discarded, a SEARCH block whose
line breaks drifted from the file still locates.
Returns {:line-start <i> :line-end <j> :occurrences <n>} (0-based,
:line-end exclusive) snapped to whole lines around the FIRST hit, or
nil when zero tokens match. :occurrences is the total contiguous-hit
count so callers can refuse ambiguous (>1) applies.
Whitespace-agnostic token-subsequence match. Folds `search` and
`content` to whitespace-free token streams and looks for `search`'s
tokens as a contiguous subsequence of `content`'s. Because all
whitespace (incl. newline structure) is discarded, a SEARCH block whose
line breaks drifted from the file still locates.
Returns `{:line-start <i> :line-end <j> :occurrences <n>}` (0-based,
`:line-end` exclusive) snapped to whole lines around the FIRST hit, or
nil when zero tokens match. `:occurrences` is the total contiguous-hit
count so callers can refuse ambiguous (>1) applies.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |