Skip to content

Commit addd135

Browse files
Show the code of messages in the editor font and link the lines they name
The server sends each diagnostic's message notes and the source references of its notes and hints. The panel shows the notes under Found, and renders every message line (headline, notes, hints, declared-here gloss) with the code the verifier marks in backticks in the editor font and each reference, such as `line 134`, as a link that opens that line. Needs a liquidjava-verifier release with liquid-java/liquidjava#528, #530 and #533. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e333873 commit addd135

11 files changed

Lines changed: 156 additions & 20 deletions

File tree

‎client/src/types/diagnostics.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,25 @@ export type LJHint = {
3232
message: string;
3333
};
3434

35+
// a place in the source that a line of a diagnostic, a message note or a hint, names, such as `line 134`
36+
export type LJSourceReference = {
37+
message: string;
38+
text: string;
39+
// where text starts in message
40+
start: number;
41+
position: SourcePosition;
42+
};
43+
3544
type BaseDiagnostic = {
3645
title: string;
3746
message: string;
3847
hints: LJHint[];
3948
file: string;
4049
position: SourcePosition | null;
50+
// lines under the message that explain it, such as where a value comes from
51+
messageNotes: string[];
52+
// the places the message notes and the hints name
53+
references: LJSourceReference[];
4154
}
4255

4356
export type CustomError = BaseDiagnostic & {

‎client/src/webview/message-text.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { LJSourceReference } from "../types/diagnostics";
2+
import { escapeHtml } from "./utils";
3+
4+
// a run of backticks, the code, and a run of the same length, as the verifier marks code in its messages
5+
const CODE_SPAN = /(`+)([\s\S]+?)(?<!`)\1(?!`)/y;
6+
7+
/**
8+
* Renders a line of a diagnostic, such as a note or a hint: the code it marks with backticks in the editor font,
9+
* without the backticks, and each place in the source it names as a link that opens that place
10+
* @param references the places the diagnostic names, of which only those of this line are used
11+
*/
12+
export function renderMessageText(message: string, references: LJSourceReference[] = []): string {
13+
const links = references.filter(reference =>
14+
reference.message === message && message.startsWith(reference.text, reference.start));
15+
let html = "";
16+
let index = 0;
17+
while (index < message.length) {
18+
const link = links.find(reference => reference.start === index);
19+
if (link) {
20+
html += renderReferenceLink(link);
21+
index += link.text.length;
22+
continue;
23+
}
24+
CODE_SPAN.lastIndex = index;
25+
const span = message[index] === "`" ? CODE_SPAN.exec(message) : null;
26+
if (span) {
27+
html += `<code class="lj-code">${escapeHtml(unpad(span[2]))}</code>`;
28+
index += span[0].length;
29+
continue;
30+
}
31+
html += escapeHtml(message[index]);
32+
index++;
33+
}
34+
return html;
35+
}
36+
37+
// the code without the space on each side that the verifier adds to code that holds a backtick or a space at an end
38+
function unpad(code: string): string {
39+
return code.length >= 2 && code.startsWith(" ") && code.endsWith(" ") ? code.slice(1, -1) : code;
40+
}
41+
42+
function renderReferenceLink(reference: LJSourceReference): string {
43+
const { position } = reference;
44+
return /*html*/`<a href="#" class="link location-link message-reference" data-file="${escapeHtml(position.file ?? "")}" data-line="${position.lineStart}" data-column="${position.colStart}">${escapeHtml(reference.text)}</a>`;
45+
}

‎client/src/webview/script.spec.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function diagnostic(file: string, title: string, category: 'error' | 'warning' =
1818
const details = {
1919
title,
2020
message: `${title} message`,
21-
hints: [],
21+
hints: [], messageNotes: [], references: [],
2222
file,
2323
position: { ...position, file },
2424
};
@@ -61,7 +61,7 @@ const stateMachine: LJStateMachine = {
6161
function stateError(title: string, sm: LJStateMachine, line = position.lineStart, file = currentFile): LJDiagnostic {
6262
const errorPosition = { ...position, file, lineStart: line, lineEnd: line };
6363
return {
64-
title, message: `${title} message`, hints: [], file, position: errorPosition,
64+
title, message: `${title} message`, hints: [], messageNotes: [], references: [], file, position: errorPosition,
6565
category: 'error', type: 'state-refinement-error', translationTable: {}, expected: 'Open(counter)',
6666
found: { implication: null as never, origin: null, simplification: 'Closed(counter)' },
6767
customMessage: '', declarationPosition: null, declarationPositions: [], declarationNotes: [], stateMachines: [sm],

‎client/src/webview/state-errors.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function stateError(line: number, overrides: Partial<StateRefinementError> = {})
2020
const file = overrides.file ?? callerFile;
2121
return {
2222
category: 'error', type: 'state-refinement-error', title: 'State Refinement Error',
23-
message: 'found childrenAllowed(root¹⁸) but expected maybeHasChildren(root¹⁸)', hints: [],
23+
message: 'found childrenAllowed(root¹⁸) but expected maybeHasChildren(root¹⁸)', hints: [], messageNotes: [], references: [],
2424
file, position: { file, lineStart: line, colStart: 15, lineEnd: line, colEnd: 35 },
2525
translationTable: {}, expected: 'maybeHasChildren(root¹⁸)',
2626
found: { implication: null as never, origin: null, simplification: null },

‎client/src/webview/styles.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,19 @@ export function getStyles(): string {
232232
padding: 0.5rem;
233233
background-color: var(--vscode-editor-background);
234234
border-radius: 4px;
235+
}
236+
/* code that a message marks with backticks, set apart from its prose */
237+
.lj-code {
238+
padding: 0 0.2em;
239+
border-radius: 3px;
235240
font-family: var(--vscode-editor-font-family, monospace);
236241
font-size: var(--vscode-editor-font-size, inherit);
242+
color: var(--vscode-textPreformat-foreground);
243+
background-color: var(--vscode-textPreformat-background);
244+
overflow-wrap: break-word;
245+
}
246+
.message-note {
247+
margin: 0 0 0.25rem;
237248
}
238249
.hint-message {
239250
white-space: pre-wrap;

‎client/src/webview/views/diagnostics/diagnostics.spec.ts‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ const otherFile = 'file:///workspace/Other.java';
99
function error(file = currentFile, title = 'Invalid refinement'): CustomError {
1010
return {
1111
category: 'error', type: 'custom-error', title,
12-
message: 'The refinement does not hold.', hints: [], file, position: null,
12+
message: 'The refinement does not hold.', hints: [], messageNotes: [], references: [], file, position: null,
1313
};
1414
}
1515

1616
function warning(file = currentFile): CustomWarning {
1717
return {
1818
category: 'warning', type: 'custom-warning', title: 'Verification warning',
19-
message: 'Some code could not be verified.', hints: [], file, position: null,
19+
message: 'Some code could not be verified.', hints: [], messageNotes: [], references: [], file, position: null,
2020
};
2121
}
2222

@@ -87,6 +87,22 @@ describe('diagnostics view', () => {
8787
expect(document.querySelector('#show-all-button')).toBeNull();
8888
});
8989

90+
it('shows the code of notes and hints in the editor font, and links the lines they name', () => {
91+
const note = 'Origin: line 134, `new BufferedInputStream(stream)` makes `markState(b) == unmarked`';
92+
const position = { file: '/workspace/Reader.java', lineStart: 133, colStart: 8, lineEnd: 133, colEnd: 8 };
93+
document.body.innerHTML = renderDiagnosticsView([{
94+
...error(), messageNotes: [note], references: [{ message: note, text: 'line 134', start: 8, position }],
95+
hints: [{ kind: 'general', message: "Call `` b.f('`') `` first" }],
96+
}], false, currentFile);
97+
98+
const link = document.querySelector('.message-note a.location-link');
99+
expect(link?.textContent).toBe('line 134');
100+
expect([link?.getAttribute('data-file'), link?.getAttribute('data-line')]).toEqual(['/workspace/Reader.java', '133']);
101+
expect(text('.message-note')).toBe('Origin: line 134, new BufferedInputStream(stream) makes markState(b) == unmarked');
102+
expect(Array.from(document.querySelectorAll('.lj-code')).map(code => code.textContent))
103+
.toEqual(['new BufferedInputStream(stream)', 'markState(b) == unmarked', "b.f('`')"]);
104+
});
105+
90106
it('shows every declaration of a refinement with their notes under them', () => {
91107
const declaredAt = (lineStart: number) =>
92108
({ file: '/workspace/DoorRefinements.java', lineStart, colStart: 29, lineEnd: lineStart, colEnd: 58 });

‎client/src/webview/views/diagnostics/errors.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { renderDiagnosticDataAttributes, renderExpressionSection, renderDiagnosticHeader, renderCustomSection, renderLocation, renderDiagnosticContextButton, renderDiagnosticStateMachineButton, renderHints } from "../sections";
1+
import { renderDiagnosticDataAttributes, renderExpressionSection, renderDiagnosticHeader, renderCustomSection, renderLocation, renderDiagnosticContextButton, renderDiagnosticStateMachineButton, renderHints, renderMessageNotes } from "../sections";
22
import { renderCounterexample } from "./counterexample";
33
import { renderHighlightedLines } from "../../highlighting";
44
import { renderVCImplication } from "./vc-implications";
@@ -77,9 +77,10 @@ export function renderError(error: LJError, actions = ''): string {
7777
const message = error.type === 'refinement-error' || error.type === 'state-refinement-error' ? error.customMessage : error.message;
7878
const header = renderDiagnosticHeader(error.title, message || '', actions);
7979
const content = (errorContentRenderers[error.type] as (error: LJError) => string)?.(error) || '';
80-
const hint = renderHints(error.hints, getDiagnosticKey(error));
80+
const notes = renderMessageNotes(error);
81+
const hint = renderHints(error.hints, getDiagnosticKey(error), error.references);
8182
const location = renderLocation(error);
82-
return /*html*/`${header}${content}${hint}${location}`;
83+
return /*html*/`${header}${content}${notes}${hint}${location}`;
8384
}
8485

8586
function renderDiagnosticContextAction(error: LJError): string {

‎client/src/webview/views/diagnostics/warnings.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { CustomWarning, ExternalClassNotFoundWarning, ExternalMethodNotFoundWarning, LJWarning, UnsatisfiableRefinementWarning } from "../../../types/diagnostics";
2-
import { renderDiagnosticDataAttributes, renderExpressionSection, renderDiagnosticHeader, renderHints, renderLocation } from "../sections";
2+
import { renderDiagnosticDataAttributes, renderExpressionSection, renderDiagnosticHeader, renderHints, renderLocation, renderMessageNotes } from "../sections";
33
import { renderCopyDiagnosticButton } from "./diagnostics";
44
import { getDiagnosticKey } from "../../diagnostic-reveal";
55

@@ -34,7 +34,8 @@ const warningContentRenderers: WarningContentRenderers = {
3434
export function renderWarning(warning: LJWarning, actions = ''): string {
3535
const header = renderDiagnosticHeader(warning.title, warning.message, actions);
3636
const content = (warningContentRenderers[warning.type] as (warning: LJWarning) => string)?.(warning) || '';
37-
const hint = renderHints(warning.hints, getDiagnosticKey(warning));
37+
const notes = renderMessageNotes(warning);
38+
const hint = renderHints(warning.hints, getDiagnosticKey(warning), warning.references);
3839
const location = renderLocation(warning);
39-
return /*html*/`${header}${content}${hint}${location}`;
40+
return /*html*/`${header}${content}${notes}${hint}${location}`;
4041
}

‎client/src/webview/views/sections.ts‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type { LJDiagnostic, LJHint, SourcePosition } from "../../types/diagnostics";
1+
import type { LJDiagnostic, LJHint, LJSourceReference, SourcePosition } from "../../types/diagnostics";
22
import { escapeHtml, getFileName } from "../utils";
33
import { renderHighlightedExpression, renderHighlightedInlineExpression } from "../highlighting";
44
import { getDiagnosticRevealTarget, getDiagnosticRevealTargetKey } from "../diagnostic-reveal";
55
import { renderCodicon, renderCodiconButton } from "../icons";
6+
import { renderMessageText } from "../message-text";
67
import { LJVariable } from "../../types/context";
78

89
export const renderMainHeader = (title: string, selectedTab: NavTab, badges = NO_NAV_BADGES): string => /*html*/`
@@ -34,8 +35,9 @@ export function setHintOpen(key: string, open: boolean) {
3435
* Several call sequences are alternatives, so they are listed as options apart from the hints that explain the error
3536
* @param hints The hints of the diagnostic
3637
* @param key Identifies the diagnostic, so opened hints stay open after a new verification
38+
* @param references The places in the source the hints name
3739
*/
38-
export function renderHints(hints: LJHint[] | undefined, key?: string): string {
40+
export function renderHints(hints: LJHint[] | undefined, key?: string, references: LJSourceReference[] = []): string {
3941
const shown = (hints ?? []).filter(hint => hint.message.trim());
4042
if (shown.length === 0) return '';
4143
const options = shown.filter(hint => hint.kind === 'call-sequence');
@@ -49,17 +51,26 @@ export function renderHints(hints: LJHint[] | undefined, key?: string): string {
4951
${listsOptions ? /*html*/`
5052
<div class="hint-options">
5153
<p class="hint-options-title">Any one of these:</p>
52-
<ul>${options.map(hint => `<li>${renderHintMessage(hint)}</li>`).join('')}</ul>
54+
<ul>${options.map(hint => `<li>${renderHintMessage(hint, references)}</li>`).join('')}</ul>
5355
</div>
5456
` : ''}
55-
${notes.map(hint => `<div class="hint-note">${renderHintMessage(hint)}</div>`).join('')}
57+
${notes.map(hint => `<div class="hint-note">${renderHintMessage(hint, references)}</div>`).join('')}
5658
</div>
5759
</details>
5860
`;
5961
}
6062

61-
function renderHintMessage(hint: LJHint): string {
62-
return `<span class="hint-message">${escapeHtml(hint.message.trim())}</span>`;
63+
function renderHintMessage(hint: LJHint, references: LJSourceReference[]): string {
64+
return `<span class="hint-message">${renderMessageText(hint.message, references)}</span>`;
65+
}
66+
67+
/**
68+
* Renders the notes under the message of a diagnostic, such as where a value comes from, with their links
69+
*/
70+
export function renderMessageNotes(diagnostic: LJDiagnostic): string {
71+
if (diagnostic.messageNotes.length === 0) return '';
72+
return /*html*/`<div class="section message-notes">${diagnostic.messageNotes
73+
.map(note => `<p class="message-note">${renderMessageText(note, diagnostic.references)}</p>`).join('')}</div>`;
6374
}
6475

6576
export const renderToggleSection = (title: string, targetId: string, isExpanded: boolean = true): string => /*html*/`
@@ -74,7 +85,7 @@ export const renderDiagnosticHeader = (title: string, message: string, actions =
7485
<h3>${title}</h3>
7586
${actions.trim() ? `<div class="diagnostic-actions">${actions}</div>` : ''}
7687
</div>
77-
<div class="diagnostic-header"><p>${message}</p></div>
88+
<div class="diagnostic-header"><p>${renderMessageText(message)}</p></div>
7889
`;
7990

8091
export function renderDiagnosticDataAttributes(diagnostic: LJDiagnostic): string {
@@ -97,7 +108,7 @@ export const renderLocation = (diagnostic: LJDiagnostic): string => {
97108
const links = positions.map(renderLocationLink).join("\n");
98109
// the notes explain the declarations, so they are shown only under their links
99110
const notes = declarationPositions.length > 0 ? declaration?.declarationNotes ?? [] : [];
100-
const noteLines = notes.map(note => `<span class="location-note">${escapeHtml(note)}</span>`).join("");
111+
const noteLines = notes.map(note => `<span class="location-note">${renderMessageText(note)}</span>`).join("");
101112
return renderCustomSection(title, /*html*/`<pre>${links}${noteLines}</pre>`);
102113
};
103114

‎server/src/main/java/dtos/diagnostics/LJDiagnosticDTO.java‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,33 @@ public class LJDiagnosticDTO {
1616
public final List<HintDTO> hints;
1717
public final String file;
1818
public final SourcePositionDTO position;
19+
/** Lines under the message that explain it, such as where a value comes from */
20+
public final List<String> messageNotes;
21+
/** The places in the source that the message notes and hints name */
22+
public final List<SourceReferenceDTO> references;
1923

2024
public LJDiagnosticDTO(String category, String type, LJDiagnostic diagnostic) {
2125
this(category, type, diagnostic.getTitle(), diagnostic.getMessage(), HintDTO.from(diagnostic.getHints()),
22-
diagnostic.getFile(), SourcePositionDTO.from(diagnostic.getPosition()));
26+
diagnostic.getFile(), SourcePositionDTO.from(diagnostic.getPosition()), diagnostic.getMessageNotes(),
27+
SourceReferenceDTO.from(diagnostic.getReferences()));
2328
}
2429

2530
public LJDiagnosticDTO(String category, String type, String title, String message, List<HintDTO> hints, String file,
2631
SourcePositionDTO position) {
32+
this(category, type, title, message, hints, file, position, List.of(), List.of());
33+
}
34+
35+
private LJDiagnosticDTO(String category, String type, String title, String message, List<HintDTO> hints,
36+
String file, SourcePositionDTO position, List<String> messageNotes, List<SourceReferenceDTO> references) {
2737
this.category = category;
2838
this.type = type;
2939
this.title = title;
3040
this.message = message;
3141
this.hints = hints;
3242
this.file = file;
3343
this.position = position;
44+
this.messageNotes = messageNotes;
45+
this.references = references;
3446
}
3547

3648
public static LJDiagnosticDTO from(LJDiagnostic diagnostic) {

0 commit comments

Comments
 (0)