Delimiter rebalancing for an edit whose spliced result would NOT parse.
An editor refusal is INFORMATION: the caller learns the replacement did not fit
where it aimed it, and re-reads. A repair that runs on the REPLACEMENT ALONE
cannot know that — it balances a fragment against nothing, so a partial line
([{:keys [a b]}, deliberately open because its enclosing form closes it) comes
back "repaired" into a complete form, the splice accepts it, and the file parses
while meaning something else. That trade is never worth it: it turns one honest
refusal into a silent semantic rewrite the caller has to discover later.
So the repair runs on the WHOLE spliced file, where the enclosing forms and the caller's own indentation decide where a delimiter belongs, and it is accepted only when it is provably confined to the lines the caller wrote:
Fail any one and the edit is REFUSED with its parse error intact. A repair that
has to reach outside the edit is guessing about code nobody in this call wrote,
and guessing is what produced the corruption above. DELETING a delimiter is the
same guess from the other side: (-> s str/trim) whose opening paren was lost
reads as -> s str/trim), and dropping that surplus ) — character for character
the same mistake as an honest ) too many — writes a body of three loose symbols
that parses. The caller is told which of the two to look for instead.
Add-only still leaves WHERE to guess: a balancer has only the caller's indentation
to go on, so a closer omitted in the MIDDLE of a line comes back at that line's END
and regroups the arguments between. The text the edit REPLACED settles it. Where a
line survived the edit as the same code, every delimiter it dropped goes back
exactly where that text had it: (map? x) (str …) retyped as (map? x (str …) is
restored, not closed at the end, and a LOST OPENER stops being indistinguishable
from a surplus closer, because the replaced text says which of the two it was. Only
where the code around it survived — code the caller DELETED takes its own
delimiters with it, and they are never resurrected inside what they wrote.
That witness binds both ways. A repair may not ADD a delimiter to a line whose code
survived either: the text it replaced already says which delimiters that code had, so
a balancer closing an untouched line — because the line UNDER it dropped a closer of
its own — is guessing against evidence, and (if (seq names) closed a second time
parses while the branches it was meant to guard move into a form nobody wrote. That is
refused too, and the caller is told which line still carries the omission.
Both witnesses answer WHERE. When neither can — the balancer's answer reaches past the edit, or retypes something of the caller's — one candidate is left: the closers the text never wrote, appended at the END of the last line this call wrote. It is tried last, and only for a call that wrote ONE region, because appending closes everything between the omission and that seat into the form those closers close, and with a single edited region that is code THIS call wrote, handed back to it in the note. Two edits in one call have untouched lines between them, and those are never regrouped. How MANY of them is read off the lines AFTER that seat: they close what they close, and only what they cannot supply is added — save for a call with no replaced text to bound its own lines, where nothing short of closing everything they left open is a claim this namespace will make.
A dropped " is the same mistake one character over, and no balancer can see it: a repair
only puts back ()[]{}, while every string after the missing quote is inside out. Where
the text a region REPLACED ended with a quote and the line standing there now does not,
that one quote goes back at the same seat, under its own rule — never a general balancing
of strings, whose misplacement parses just as happily as the omission.
The repair itself is not here. rebalance is HANDED a balancer, String -> String | nil (nil = unrepairable), and owns only the decision to TRUST its answer, so the two
halves stay separable: com.blockether.parinferish/repair in :indent mode is that
function for Clojure — parinfer reads the caller's indentation, which is the only
evidence a balancer has — and a host that resolves its balancer per language passes
whichever one the file's language registered.
Delimiter rebalancing for an edit whose spliced result would NOT parse.
An editor refusal is INFORMATION: the caller learns the replacement did not fit
where it aimed it, and re-reads. A repair that runs on the REPLACEMENT ALONE
cannot know that — it balances a fragment against nothing, so a partial line
(`[{:keys [a b]}`, deliberately open because its enclosing form closes it) comes
back "repaired" into a complete form, the splice accepts it, and the file parses
while meaning something else. That trade is never worth it: it turns one honest
refusal into a silent semantic rewrite the caller has to discover later.
So the repair runs on the WHOLE spliced file, where the enclosing forms and the
caller's own indentation decide where a delimiter belongs, and it is accepted
only when it is provably confined to the lines the caller wrote:
1. the repaired file parses clean;
2. it keeps the same number of lines and the same final newline;
3. every line it changes lies inside an edited span;
4. it only ADDED delimiters the caller omitted — one they WROTE is never
deleted, moved or retyped — and every other character, whitespace and line
endings included, is theirs, in order.
Fail any one and the edit is REFUSED with its parse error intact. A repair that
has to reach outside the edit is guessing about code nobody in this call wrote,
and guessing is what produced the corruption above. DELETING a delimiter is the
same guess from the other side: `(-> s str/trim)` whose opening paren was lost
reads as `-> s str/trim)`, and dropping that surplus `)` — character for character
the same mistake as an honest `)` too many — writes a body of three loose symbols
that parses. The caller is told which of the two to look for instead.
Add-only still leaves WHERE to guess: a balancer has only the caller's indentation
to go on, so a closer omitted in the MIDDLE of a line comes back at that line's END
and regroups the arguments between. The text the edit REPLACED settles it. Where a
line survived the edit as the same code, every delimiter it dropped goes back
exactly where that text had it: `(map? x) (str …)` retyped as `(map? x (str …)` is
restored, not closed at the end, and a LOST OPENER stops being indistinguishable
from a surplus closer, because the replaced text says which of the two it was. Only
where the code around it survived — code the caller DELETED takes its own
delimiters with it, and they are never resurrected inside what they wrote.
That witness binds both ways. A repair may not ADD a delimiter to a line whose code
survived either: the text it replaced already says which delimiters that code had, so
a balancer closing an untouched line — because the line UNDER it dropped a closer of
its own — is guessing against evidence, and `(if (seq names)` closed a second time
parses while the branches it was meant to guard move into a form nobody wrote. That is
refused too, and the caller is told which line still carries the omission.
Both witnesses answer WHERE. When neither can — the balancer's answer reaches past the
edit, or retypes something of the caller's — one candidate is left: the closers the text
never wrote, appended at the END of the last line this call wrote. It is tried last, and
only for a call that wrote ONE region, because appending closes everything between the
omission and that seat into the form those closers close, and with a single edited region
that is code THIS call wrote, handed back to it in the note. Two edits in one call have
untouched lines between them, and those are never regrouped. How MANY of them is read off
the lines AFTER that seat: they close what they close, and only what they cannot supply is
added — save for a call with no replaced text to bound its own lines, where nothing short of
closing everything they left open is a claim this namespace will make.
A dropped " is the same mistake one character over, and no balancer can see it: a repair
only puts back `()[]{}`, while every string after the missing quote is inside out. Where
the text a region REPLACED ended with a quote and the line standing there now does not,
that one quote goes back at the same seat, under its own rule — never a general balancing
of strings, whose misplacement parses just as happily as the omission.
The repair itself is not here. `rebalance` is HANDED a balancer, `String -> String |
nil` (nil = unrepairable), and owns only the decision to TRUST its answer, so the two
halves stay separable: `com.blockether.parinferish/repair` in `:indent` mode is that
function for Clojure — parinfer reads the caller's indentation, which is the only
evidence a balancer has — and a host that resolves its balancer per language passes
whichever one the file's language registered.(changed-span before after)The 1-based, inclusive [from to] line range of after that differs from
before — shared leading and trailing LINES are dropped, so it is exactly the
region one splice touched, whatever op produced it. nil when the two are equal.
The comparison is per LINE, not per character: a character-level diff ends inside
the first surviving line and would hand a repair one line of licence the edit
never wrote. A caller that already knows which lines it wrote passes those
instead; this is for the structural editors, which splice a byte range and count
lines afterwards.
The 1-based, inclusive `[from to]` line range of `after` that differs from `before` — shared leading and trailing LINES are dropped, so it is exactly the region one splice touched, whatever op produced it. nil when the two are equal. The comparison is per LINE, not per character: a character-level diff ends inside the first surviving line and would hand a repair one line of licence the edit never wrote. A caller that already knows which lines it wrote passes those instead; this is for the structural editors, which splice a byte range and count lines afterwards.
(rebalance {:keys [balancer source original spans] :as request})Try to make source — the content an edit WOULD have written, which does not
parse — parse, by repairing its delimiters WITHOUT letting the repair reach past
the caller's own lines. spans are [from-line to-line] pairs, 1-based and
inclusive, in source's own coordinates; parses-clean? re-parses a candidate;
balancer is the language pack's :balance-fn; subject names whose delimiters a
refusal is about and defaults to the edit that produced source — a formatter
handed a WHOLE file passes its own.
original is the content this edit REPLACED, when there is one. It is the better
evidence and is tried FIRST: a delimiter dropped from a line whose code survived
goes back where that line had it, which is the only way to put a closer back in the
MIDDLE of a line, or to tell a lost opener from one closer too many. The balancer's
own answer — indentation, and nothing else — is the fallback.
Two candidates follow it, and both put what the text is MISSING at the end of the last
line this call wrote: closed-at-tail appends the closers nothing else could place, for
a call that wrote one region, and requoted puts back a " the replaced text proves
ended that region, which no balancer can supply. Neither ever speaks in a refusal — that
still describes the BALANCER's answer, because it is the repair the caller can reason
about from what they wrote.
Answers nil when there is no balancer to ask, {:ok? true :content S :notes [..]}
for a repair that may be written, and {:ok? false :why msg} for one that was
found and REJECTED — the caller puts why in its refusal, because "a repair
exists but it reaches outside your edit" is exactly what tells the caller to
re-read the region instead of retrying the same replacement.
Try to make `source` — the content an edit WOULD have written, which does not
parse — parse, by repairing its delimiters WITHOUT letting the repair reach past
the caller's own lines. `spans` are `[from-line to-line]` pairs, 1-based and
inclusive, in `source`'s own coordinates; `parses-clean?` re-parses a candidate;
`balancer` is the language pack's `:balance-fn`; `subject` names whose delimiters a
refusal is about and defaults to the edit that produced `source` — a formatter
handed a WHOLE file passes its own.
`original` is the content this edit REPLACED, when there is one. It is the better
evidence and is tried FIRST: a delimiter dropped from a line whose code survived
goes back where that line had it, which is the only way to put a closer back in the
MIDDLE of a line, or to tell a lost opener from one closer too many. The balancer's
own answer — indentation, and nothing else — is the fallback.
Two candidates follow it, and both put what the text is MISSING at the end of the last
line this call wrote: `closed-at-tail` appends the closers nothing else could place, for
a call that wrote one region, and `requoted` puts back a `"` the replaced text proves
ended that region, which no balancer can supply. Neither ever speaks in a refusal — that
still describes the BALANCER's answer, because it is the repair the caller can reason
about from what they wrote.
Answers nil when there is no balancer to ask, `{:ok? true :content S :notes [..]}`
for a repair that may be written, and `{:ok? false :why msg}` for one that was
found and REJECTED — the caller puts `why` in its refusal, because "a repair
exists but it reaches outside your edit" is exactly what tells the caller to
re-read the region instead of retrying the same replacement.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 |