Diagnostic Quick Fixes
Note
Status: implemented. A diagnostic may carry a fix — a title and literal text edits — and the Source editor offers it in the diagnostic tooltip and the Problems panel. The first producer is the dangling jump arrow, whose fix inserts the escape its message already recommends.
Table of contents
- Goal and scope
- Ubiquitous language
- Writer-facing behavior
- Architecture
- Key design decisions
- Error and boundary cases
- Integration
- Testability
- Alternatives not chosen
- Out of scope
Goal and scope
The compiler diagnoses a problem and, for some diagnostics, knows the exact
repair. The dangling jump arrow is the first: DLG1113 tells the writer that an
arrow with no link is read literally and to escape it (\=>); a fix turns that
advice into one click, at the moment the warning appears.
This note adds a fix channel to the diagnostic model, projects it to the editor, and offers each fix in the diagnostic tooltip and the Problems panel. Applying a fix is an ordinary buffer edit, so undo, dirty state, autosave, and recompilation all inherit.
In scope: the fix model, the dangling-arrow producer, the projection into the report payload, and the editor action.
The CLI applies the same fixes with ddown compile --fix; see
Compile CLI — Fix Mode.
Ubiquitous language
| Term | Meaning |
|---|---|
| Fix | A diagnostic's suggested repair: a title plus text edits. |
| Edit | A source span and the text that replaces it; an insertion is an empty span. |
| Action | The editor's affordance for one fix — a CodeMirror lint action. |
| Quick fix | The writer-facing name for the fix-and-action pair. |
Writer-facing behavior
The dangling-arrow warning gains an action in its tooltip:
warning DLG1113: `=>` makes a jump only when a link follows it. …
Escape as literal text
Activating it inserts the backslash before the arrow — the buffer reads
\=> — and the next compile is quiet, because an escaped arrow is prose and no
longer a dangling indicator.
The same fix leads the diagnostic's row in the Problems panel: a lightbulb whose hover help names the repair, in a slot every row reserves so rows with and without one align. Both surfaces appear only while editing.
Architecture
A fix is born with its diagnostic, travels with it through the store and the projection, and is applied by the editor as a plain text edit.
flowchart LR
P["JumpAssembler\nreports DLG1113 + fix"] --> D["Diagnostic\n+ Fixes"]
D --> PR["DiagnosticProjection\nspan → LSP range"]
PR --> L["LspDiagnostic\n+ fixes (relative edits)"]
L --> J["report JSON\n(omitted when empty)"]
J --> M["TS model"]
M --> O["diagnostics-overlay\ntoEditorDiagnostic"]
M --> N["Problems panel\nlightbulb per fix"]
O --> A["lint action"]
N --> H["SourceViewHandle\napplyDiagnosticFix"]
A --> H
H --> E["one edit transaction"]
| Type | Responsibility | Change |
|---|---|---|
Diagnostic |
One located problem | Carries an ordered list of fixes; empty by default. |
DiagnosticFix, DiagnosticEdit |
New core value types | A title plus the edits that apply it; a span plus its replacement text. |
JumpAssembler |
Reports the dangling arrow as it degrades it | Attaches the escape fix. |
DiagnosticProjection, LspDiagnostic, LspFix, LspEdit |
Locate a diagnostic and its fixes in LSP terms | Projects fixes with ranges relative to the diagnostic span. |
DisplayGraphJson |
The report payload | Serializes fixes; an empty list is omitted like the other absent fields. |
model.ts (LspDiagnostic) |
The client's view of a diagnostic | Gains the optional fixes. |
diagnostics-overlay.ts, SourceViewHandle.applyDiagnosticFix |
The editor's fix entry points | Maps each fix to a lint action, and applies one for the panel. |
problems-panel.ts, app.ts |
The Problems list and its wiring | Leads a fixable row with a lightbulb, in a slot every row reserves. |
Key design decisions
D1 — The producer attaches fixes to its diagnostic
The stage that knows the repair attaches it where the diagnostic is made
(JumpAssembler, at the moment it degrades the arrow). Consumers then only
forward or ignore it: the CLI renders the message and applies the fix on
request, the published reference keeps rendering the message alone, the
projection carries the fix, and a language server could serve it without
moving knowledge around.
Deriving fixes in the visualization from a diagnostic's code and span was rejected: it would re-derive what the producer already knew, and the fix would grow a second home the day a second consumer wanted it.
D2 — A fix is data, not a callback
A fix is a title and literal edits. It is serializable, comparable, and testable without an editor; the client applies text and knows nothing about tags, arrows, or escaping. A callback would tie the model to a process and defeat the projection.
D3 — Edit ranges are relative to the diagnostic span
Every edit is expressed against the diagnostic's own span — the arrow's fix is
"insert \ at offset 0". The compiler's spans are absolute, but the payload is
pushed once and then goes stale while the writer types; the diagnostic's
range is the one position the editor keeps remapping, so anchoring edits to it
keeps them correct without the client tracking change deltas or re-reading the
source.
An LSP server computes edits fresh per request, so the relative shape costs nothing later. Absolute offsets were rejected: between the last compile and the next save they can point at the wrong text, and a misplaced insertion corrupts the buffer.
D4 — Applying a fix is one ordinary edit transaction
The action dispatches all of its edits in a single transaction tagged
userEvent: "input", exactly as typing would. Undo restores the previous text in
one step, the live-edit state machine sees a dirty buffer, autosave arms, and the
recompile clears the warning through the normal path. No special-case save or
notification exists.
D5 — A fix repeats the remedy its message names
DLG1113's message already says "escape the arrow". The fix is the same remedy,
one click closer; a fix never appears that the diagnostic's own prose does not
explain. Fixes that would need a different explanation belong in the message
first.
D6 — Fixes are an edit-mode affordance
The exported report and the View mode are read-only, so they render the diagnostic and no action anywhere: the tooltip drops its actions and the Problems panel re-renders without lightbulbs, leaving its fix slot empty. The quick fix is part of authoring, alongside typing, and needs the live loop to save and recompile.
Error and boundary cases
| Case | Behavior |
|---|---|
| A diagnostic with no fixes | Unchanged: no fixes field, no tooltip action, and an empty fix slot in the panel. |
| Several diagnostics, one with a fix | Only that diagnostic offers an action or a lightbulb; there is no fix-all. |
| A collapsed diagnostic range (the text it named is gone) | The action and the panel's lightbulb are no-ops; the warning refreshes next compile. |
| The report lags the buffer (mid-debounce typing) | Edits stay anchored to the remapped diagnostic range (D3). |
| A read-only report or View mode | Diagnostics render; the tooltip offers no actions and the panel no lightbulbs (D6). |
| The mode flips while diagnostics are listed | Both surfaces recompute from the same list, so an Edit-mode action cannot linger. |
| A fixable row beside a fix-less row | Both reserve the leading fix slot, so the messages align. |
| An arrow in a choice body or control branch | The diagnostic is reported there too; its fix rides along unchanged. |
| A fix with several edits | Applied in order within one transaction (D4). |
Integration
- Payload:
diagnostics[].fixesin the report JSON; empty lists are omitted like the other absent fields, so nothing changes for diagnostics without a fix. - Live loop: applying a fix dirties the buffer and inherits autosave and the generation-safe save (see the Autosave note).
- Editor:
toEditorDiagnosticmaps a fix to a lint action;SourceViewHandle.applyDiagnosticFixresolves a diagnostic's range for the Problems panel. Semantic tokens and completions are untouched. - Problems panel:
app.tswires the panel'sapplyFixto the handle, and the panel re-renders when the editor's editability flips. - Help: the editor's help text gains one line for the action.
Testability
- Core: the assembler test asserts the dangling arrow's fix — its title and the insertion at the arrow's start — and that no other diagnostic carries one.
- Projection: a diagnostic's fixes survive projection with ranges relative to the diagnostic span, including a multi-edit fix.
- Serialization: the payload carries
fixes, and omits the field when empty. - Web unit: an action is created per fix; activating it dispatches one transaction with the expected edit; a read-only editor drops the actions when the mode flips; the panel leads a fixable row with a lightbulb whose hover help names the repair, offers it only while editable, and routes the click through the handle without navigating.
- Web live e2e: the served session offers the action on a dangling arrow;
activating it inserts
\, saves, and the recompile clears the warning. - Coverage: the new core, projection, and overlay paths at 100% line and branch coverage.
Alternatives not chosen
| Alternative | Why not |
|---|---|
| Derive fixes in the visualization from code and span | Re-derives producer knowledge and gives the fix a second home (D1). |
| Absolute edit offsets | Stale within the live-edit window; a misplaced insert corrupts text (D3). |
| Callbacks instead of data | Not serializable, not testable without an editor, not portable to a server (D2). |
| A client-side "Make literal" command for valid sigils | A valid tag or jump carries no diagnostic, and the escape is one typed character. |
| Attaching fixes to the descriptor | A descriptor is shared by every instance; a fix needs a span. |
Out of scope
- General refactors, "fix all", and fixes in the published reference.
- A literalize command or suggestions for valid sigils: a valid sigil carries no diagnostic, the escape is one typed character, and suggestions for tag-like prose would be noise.
- Further producers: each is one attach call, and its message must already name the remedy (D5).