Skip to content

Commit 1b8141b

Browse files
Parse + decorate rendered ANSI cargo output
Use ANSI control characters to display text decorations matching the VScode terminal theme, and strip them out when providing text content for rustc diagnostics. This adds the small `anser` library to parse the control codes, and it also supports HTML output so it should be fairly easy to switch to a rendered HTML/webview implementation if desired.
1 parent f32e20e commit 1b8141b

File tree

5 files changed

+272
-24
lines changed

5 files changed

+272
-24
lines changed

editors/code/package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editors/code/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"test": "cross-env TEST_VARIABLE=test node ./out/tests/runTests.js"
3636
},
3737
"dependencies": {
38+
"anser": "^2.1.1",
3839
"d3": "^7.6.1",
3940
"d3-graphviz": "^5.0.2",
4041
"vscode-languageclient": "^8.0.2"

editors/code/src/client.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as vscode from "vscode";
33
import * as ra from "../src/lsp_ext";
44
import * as Is from "vscode-languageclient/lib/common/utils/is";
55
import { assert } from "./util";
6+
import * as diagnostics from "./diagnostics";
67
import { WorkspaceEdit } from "vscode";
78
import { Config, substituteVSCodeVariables } from "./config";
89
import { randomUUID } from "crypto";
@@ -120,12 +121,12 @@ export async function createClient(
120121
},
121122
async handleDiagnostics(
122123
uri: vscode.Uri,
123-
diagnostics: vscode.Diagnostic[],
124+
diagnosticList: vscode.Diagnostic[],
124125
next: lc.HandleDiagnosticsSignature
125126
) {
126127
const preview = config.previewRustcOutput;
127128
const errorCode = config.useRustcErrorCode;
128-
diagnostics.forEach((diag, idx) => {
129+
diagnosticList.forEach((diag, idx) => {
129130
// Abuse the fact that VSCode leaks the LSP diagnostics data field through the
130131
// Diagnostic class, if they ever break this we are out of luck and have to go
131132
// back to the worst diagnostics experience ever:)
@@ -154,16 +155,16 @@ export async function createClient(
154155
}
155156
diag.code = {
156157
target: vscode.Uri.from({
157-
scheme: "rust-analyzer-diagnostics-view",
158-
path: "/diagnostic message",
158+
scheme: diagnostics.URI_SCHEME,
159+
path: `/diagnostic message [${idx.toString()}]`,
159160
fragment: uri.toString(),
160161
query: idx.toString(),
161162
}),
162163
value: value ?? "Click for full compiler diagnostic",
163164
};
164165
}
165166
});
166-
return next(uri, diagnostics);
167+
return next(uri, diagnosticList);
167168
},
168169
async provideHover(
169170
document: vscode.TextDocument,

editors/code/src/diagnostics.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import * as anser from "anser";
2+
import * as vscode from "vscode";
3+
import { ProviderResult, Range, TextEditorDecorationType, ThemeColor, window } from "vscode";
4+
import { Ctx } from "./ctx";
5+
6+
export const URI_SCHEME = "rust-analyzer-diagnostics-view";
7+
8+
export class TextDocumentProvider implements vscode.TextDocumentContentProvider {
9+
private _onDidChange = new vscode.EventEmitter<vscode.Uri>();
10+
11+
public constructor(private readonly ctx: Ctx) {}
12+
13+
get onDidChange(): vscode.Event<vscode.Uri> {
14+
return this._onDidChange.event;
15+
}
16+
17+
triggerUpdate(uri: vscode.Uri) {
18+
if (uri.scheme === URI_SCHEME) {
19+
this._onDidChange.fire(uri);
20+
}
21+
}
22+
23+
dispose() {
24+
this._onDidChange.dispose();
25+
}
26+
27+
async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
28+
const contents = getRenderedDiagnostic(this.ctx, uri);
29+
return anser.ansiToText(contents);
30+
}
31+
}
32+
33+
function getRenderedDiagnostic(ctx: Ctx, uri: vscode.Uri): string {
34+
const diags = ctx.client?.diagnostics?.get(vscode.Uri.parse(uri.fragment, true));
35+
if (!diags) {
36+
return "Unable to find original rustc diagnostic";
37+
}
38+
39+
const diag = diags[parseInt(uri.query)];
40+
if (!diag) {
41+
return "Unable to find original rustc diagnostic";
42+
}
43+
const rendered = (diag as unknown as { data?: { rendered?: string } }).data?.rendered;
44+
45+
if (!rendered) {
46+
return "Unable to find original rustc diagnostic";
47+
}
48+
49+
return rendered;
50+
}
51+
52+
interface AnserStyle {
53+
fg: string;
54+
bg: string;
55+
fg_truecolor: string;
56+
bg_truecolor: string;
57+
decorations: Array<anser.DecorationName>;
58+
}
59+
60+
export class AnsiDecorationProvider implements vscode.Disposable {
61+
private _decorationTypes = new Map<AnserStyle, TextEditorDecorationType>();
62+
63+
public constructor(private readonly ctx: Ctx) {}
64+
65+
dispose(): void {
66+
for (const decorationType of this._decorationTypes.values()) {
67+
decorationType.dispose();
68+
}
69+
70+
this._decorationTypes.clear();
71+
}
72+
73+
async provideDecorations(editor: vscode.TextEditor) {
74+
if (editor.document.uri.scheme !== URI_SCHEME) {
75+
return;
76+
}
77+
78+
const decorations = (await this._getDecorations(editor.document.uri)) || [];
79+
for (const [decorationType, ranges] of decorations) {
80+
editor.setDecorations(decorationType, ranges);
81+
}
82+
}
83+
84+
private _getDecorations(
85+
uri: vscode.Uri
86+
): ProviderResult<[TextEditorDecorationType, Range[]][]> {
87+
const stringContents = getRenderedDiagnostic(this.ctx, uri);
88+
const lines = stringContents.split("\n");
89+
90+
const result = new Map<TextEditorDecorationType, Range[]>();
91+
// Populate all known decoration types in the result. This forces any
92+
// lingering decorations to be cleared if the text content changes to
93+
// something without ANSI codes for a given decoration type.
94+
for (const decorationType of this._decorationTypes.values()) {
95+
result.set(decorationType, []);
96+
}
97+
98+
for (const [lineNumber, line] of lines.entries()) {
99+
const totalEscapeLength = 0;
100+
101+
// eslint-disable-next-line camelcase
102+
const parsed = anser.ansiToJson(line, { use_classes: true });
103+
104+
let offset = 0;
105+
106+
for (const span of parsed) {
107+
const { content, ...style } = span;
108+
109+
const range = new Range(
110+
lineNumber,
111+
offset - totalEscapeLength,
112+
lineNumber,
113+
offset + content.length - totalEscapeLength
114+
);
115+
116+
offset += content.length;
117+
118+
const decorationType = this._getDecorationType(style);
119+
120+
if (!result.has(decorationType)) {
121+
result.set(decorationType, []);
122+
}
123+
124+
result.get(decorationType)!.push(range);
125+
}
126+
}
127+
128+
return [...result];
129+
}
130+
131+
private _getDecorationType(style: AnserStyle): TextEditorDecorationType {
132+
let decorationType = this._decorationTypes.get(style);
133+
134+
if (decorationType) {
135+
return decorationType;
136+
}
137+
138+
const fontWeight = style.decorations.find((s) => s === "bold");
139+
const fontStyle = style.decorations.find((s) => s === "italic");
140+
const textDecoration = style.decorations.find((s) => s === "underline");
141+
142+
decorationType = window.createTextEditorDecorationType({
143+
backgroundColor: AnsiDecorationProvider._convertColor(style.bg, style.bg_truecolor),
144+
color: AnsiDecorationProvider._convertColor(style.fg, style.fg_truecolor),
145+
fontWeight,
146+
fontStyle,
147+
textDecoration,
148+
});
149+
150+
this._decorationTypes.set(style, decorationType);
151+
152+
return decorationType;
153+
}
154+
155+
// NOTE: This could just be a kebab-case to camelCase conversion, but I think it's
156+
// a short enough list to just write these by hand
157+
static readonly _anserToThemeColor: Record<string, ThemeColor> = {
158+
"ansi-black": "ansiBlack",
159+
"ansi-white": "ansiWhite",
160+
"ansi-red": "ansiRed",
161+
"ansi-green": "ansiGreen",
162+
"ansi-yellow": "ansiYellow",
163+
"ansi-blue": "ansiBlue",
164+
"ansi-magenta": "ansiMagenta",
165+
"ansi-cyan": "ansiCyan",
166+
167+
"ansi-bright-black": "ansiBrightBlack",
168+
"ansi-bright-white": "ansiBrightWhite",
169+
"ansi-bright-red": "ansiBrightRed",
170+
"ansi-bright-green": "ansiBrightGreen",
171+
"ansi-bright-yellow": "ansiBrightYellow",
172+
"ansi-bright-blue": "ansiBrightBlue",
173+
"ansi-bright-magenta": "ansiBrightMagenta",
174+
"ansi-bright-cyan": "ansiBrightCyan",
175+
};
176+
177+
private static _convertColor(
178+
color?: string,
179+
truecolor?: string
180+
): ThemeColor | string | undefined {
181+
if (!color) {
182+
return undefined;
183+
}
184+
185+
if (color === "ansi-truecolor") {
186+
if (!truecolor) {
187+
return undefined;
188+
}
189+
return `rgb(${truecolor})`;
190+
}
191+
192+
const paletteMatch = color.match(/ansi-palette-(.+)/);
193+
if (paletteMatch) {
194+
const paletteColor = paletteMatch[1];
195+
// anser won't return both the RGB and the color name at the same time,
196+
// so just fake a single foreground control char with the palette number:
197+
const spans = anser.ansiToJson(`\x1b[38;5;${paletteColor}m`);
198+
const rgb = spans[1].fg;
199+
200+
if (rgb) {
201+
return `rgb(${rgb})`;
202+
}
203+
}
204+
205+
const themeColor = AnsiDecorationProvider._anserToThemeColor[color];
206+
if (themeColor) {
207+
return new ThemeColor("terminal." + themeColor);
208+
}
209+
210+
return undefined;
211+
}
212+
}

editors/code/src/main.ts

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as lc from "vscode-languageclient/node";
33

44
import * as commands from "./commands";
55
import { CommandFactory, Ctx, fetchWorkspace } from "./ctx";
6+
import * as diagnostics from "./diagnostics";
67
import { activateTaskProvider } from "./tasks";
78
import { setContextValue } from "./util";
89

@@ -48,30 +49,52 @@ async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
4849
ctx.pushExtCleanup(activateTaskProvider(ctx.config));
4950
}
5051

52+
const diagnosticProvider = new diagnostics.TextDocumentProvider(ctx);
5153
ctx.pushExtCleanup(
5254
vscode.workspace.registerTextDocumentContentProvider(
53-
"rust-analyzer-diagnostics-view",
54-
new (class implements vscode.TextDocumentContentProvider {
55-
async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
56-
const diags = ctx.client?.diagnostics?.get(
57-
vscode.Uri.parse(uri.fragment, true)
58-
);
59-
if (!diags) {
60-
return "Unable to find original rustc diagnostic";
61-
}
62-
63-
const diag = diags[parseInt(uri.query)];
64-
if (!diag) {
65-
return "Unable to find original rustc diagnostic";
66-
}
67-
const rendered = (diag as unknown as { data?: { rendered?: string } }).data
68-
?.rendered;
69-
return rendered ?? "Unable to find original rustc diagnostic";
70-
}
71-
})()
55+
diagnostics.URI_SCHEME,
56+
diagnosticProvider
7257
)
7358
);
7459

60+
const decorationProvider = new diagnostics.AnsiDecorationProvider(ctx);
61+
ctx.pushExtCleanup(decorationProvider);
62+
63+
async function decorateVisibleEditors(document: vscode.TextDocument) {
64+
for (const editor of vscode.window.visibleTextEditors) {
65+
if (document === editor.document) {
66+
await decorationProvider.provideDecorations(editor);
67+
}
68+
}
69+
}
70+
71+
vscode.workspace.onDidChangeTextDocument(
72+
async (event) => await decorateVisibleEditors(event.document),
73+
null,
74+
ctx.subscriptions
75+
);
76+
vscode.workspace.onDidOpenTextDocument(decorateVisibleEditors, null, ctx.subscriptions);
77+
vscode.window.onDidChangeActiveTextEditor(
78+
async (editor) => {
79+
if (editor) {
80+
diagnosticProvider.triggerUpdate(editor.document.uri);
81+
await decorateVisibleEditors(editor.document);
82+
}
83+
},
84+
null,
85+
ctx.subscriptions
86+
);
87+
vscode.window.onDidChangeVisibleTextEditors(
88+
async (visibleEditors) => {
89+
for (const editor of visibleEditors) {
90+
diagnosticProvider.triggerUpdate(editor.document.uri);
91+
await decorationProvider.provideDecorations(editor);
92+
}
93+
},
94+
null,
95+
ctx.subscriptions
96+
);
97+
7598
vscode.workspace.onDidChangeWorkspaceFolders(
7699
async (_) => ctx.onWorkspaceFolderChanges(),
77100
null,

0 commit comments

Comments
 (0)