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.
This commit is contained in:
parent
f32e20edb9
commit
1b8141b54c
11
editors/code/package-lock.json
generated
11
editors/code/package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "0.5.0-dev",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"anser": "^2.1.1",
|
||||
"d3": "^7.6.1",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"vscode-languageclient": "^8.0.2"
|
||||
@ -394,6 +395,11 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/anser": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/anser/-/anser-2.1.1.tgz",
|
||||
"integrity": "sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ=="
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@ -4096,6 +4102,11 @@
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"anser": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/anser/-/anser-2.1.1.tgz",
|
||||
"integrity": "sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ=="
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
|
@ -35,6 +35,7 @@
|
||||
"test": "cross-env TEST_VARIABLE=test node ./out/tests/runTests.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"anser": "^2.1.1",
|
||||
"d3": "^7.6.1",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"vscode-languageclient": "^8.0.2"
|
||||
|
@ -3,6 +3,7 @@ import * as vscode from "vscode";
|
||||
import * as ra from "../src/lsp_ext";
|
||||
import * as Is from "vscode-languageclient/lib/common/utils/is";
|
||||
import { assert } from "./util";
|
||||
import * as diagnostics from "./diagnostics";
|
||||
import { WorkspaceEdit } from "vscode";
|
||||
import { Config, substituteVSCodeVariables } from "./config";
|
||||
import { randomUUID } from "crypto";
|
||||
@ -120,12 +121,12 @@ export async function createClient(
|
||||
},
|
||||
async handleDiagnostics(
|
||||
uri: vscode.Uri,
|
||||
diagnostics: vscode.Diagnostic[],
|
||||
diagnosticList: vscode.Diagnostic[],
|
||||
next: lc.HandleDiagnosticsSignature
|
||||
) {
|
||||
const preview = config.previewRustcOutput;
|
||||
const errorCode = config.useRustcErrorCode;
|
||||
diagnostics.forEach((diag, idx) => {
|
||||
diagnosticList.forEach((diag, idx) => {
|
||||
// Abuse the fact that VSCode leaks the LSP diagnostics data field through the
|
||||
// Diagnostic class, if they ever break this we are out of luck and have to go
|
||||
// back to the worst diagnostics experience ever:)
|
||||
@ -154,8 +155,8 @@ export async function createClient(
|
||||
}
|
||||
diag.code = {
|
||||
target: vscode.Uri.from({
|
||||
scheme: "rust-analyzer-diagnostics-view",
|
||||
path: "/diagnostic message",
|
||||
scheme: diagnostics.URI_SCHEME,
|
||||
path: `/diagnostic message [${idx.toString()}]`,
|
||||
fragment: uri.toString(),
|
||||
query: idx.toString(),
|
||||
}),
|
||||
@ -163,7 +164,7 @@ export async function createClient(
|
||||
};
|
||||
}
|
||||
});
|
||||
return next(uri, diagnostics);
|
||||
return next(uri, diagnosticList);
|
||||
},
|
||||
async provideHover(
|
||||
document: vscode.TextDocument,
|
||||
|
212
editors/code/src/diagnostics.ts
Normal file
212
editors/code/src/diagnostics.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import * as anser from "anser";
|
||||
import * as vscode from "vscode";
|
||||
import { ProviderResult, Range, TextEditorDecorationType, ThemeColor, window } from "vscode";
|
||||
import { Ctx } from "./ctx";
|
||||
|
||||
export const URI_SCHEME = "rust-analyzer-diagnostics-view";
|
||||
|
||||
export class TextDocumentProvider implements vscode.TextDocumentContentProvider {
|
||||
private _onDidChange = new vscode.EventEmitter<vscode.Uri>();
|
||||
|
||||
public constructor(private readonly ctx: Ctx) {}
|
||||
|
||||
get onDidChange(): vscode.Event<vscode.Uri> {
|
||||
return this._onDidChange.event;
|
||||
}
|
||||
|
||||
triggerUpdate(uri: vscode.Uri) {
|
||||
if (uri.scheme === URI_SCHEME) {
|
||||
this._onDidChange.fire(uri);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onDidChange.dispose();
|
||||
}
|
||||
|
||||
async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
|
||||
const contents = getRenderedDiagnostic(this.ctx, uri);
|
||||
return anser.ansiToText(contents);
|
||||
}
|
||||
}
|
||||
|
||||
function getRenderedDiagnostic(ctx: Ctx, uri: vscode.Uri): string {
|
||||
const diags = ctx.client?.diagnostics?.get(vscode.Uri.parse(uri.fragment, true));
|
||||
if (!diags) {
|
||||
return "Unable to find original rustc diagnostic";
|
||||
}
|
||||
|
||||
const diag = diags[parseInt(uri.query)];
|
||||
if (!diag) {
|
||||
return "Unable to find original rustc diagnostic";
|
||||
}
|
||||
const rendered = (diag as unknown as { data?: { rendered?: string } }).data?.rendered;
|
||||
|
||||
if (!rendered) {
|
||||
return "Unable to find original rustc diagnostic";
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
interface AnserStyle {
|
||||
fg: string;
|
||||
bg: string;
|
||||
fg_truecolor: string;
|
||||
bg_truecolor: string;
|
||||
decorations: Array<anser.DecorationName>;
|
||||
}
|
||||
|
||||
export class AnsiDecorationProvider implements vscode.Disposable {
|
||||
private _decorationTypes = new Map<AnserStyle, TextEditorDecorationType>();
|
||||
|
||||
public constructor(private readonly ctx: Ctx) {}
|
||||
|
||||
dispose(): void {
|
||||
for (const decorationType of this._decorationTypes.values()) {
|
||||
decorationType.dispose();
|
||||
}
|
||||
|
||||
this._decorationTypes.clear();
|
||||
}
|
||||
|
||||
async provideDecorations(editor: vscode.TextEditor) {
|
||||
if (editor.document.uri.scheme !== URI_SCHEME) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decorations = (await this._getDecorations(editor.document.uri)) || [];
|
||||
for (const [decorationType, ranges] of decorations) {
|
||||
editor.setDecorations(decorationType, ranges);
|
||||
}
|
||||
}
|
||||
|
||||
private _getDecorations(
|
||||
uri: vscode.Uri
|
||||
): ProviderResult<[TextEditorDecorationType, Range[]][]> {
|
||||
const stringContents = getRenderedDiagnostic(this.ctx, uri);
|
||||
const lines = stringContents.split("\n");
|
||||
|
||||
const result = new Map<TextEditorDecorationType, Range[]>();
|
||||
// Populate all known decoration types in the result. This forces any
|
||||
// lingering decorations to be cleared if the text content changes to
|
||||
// something without ANSI codes for a given decoration type.
|
||||
for (const decorationType of this._decorationTypes.values()) {
|
||||
result.set(decorationType, []);
|
||||
}
|
||||
|
||||
for (const [lineNumber, line] of lines.entries()) {
|
||||
const totalEscapeLength = 0;
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const parsed = anser.ansiToJson(line, { use_classes: true });
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (const span of parsed) {
|
||||
const { content, ...style } = span;
|
||||
|
||||
const range = new Range(
|
||||
lineNumber,
|
||||
offset - totalEscapeLength,
|
||||
lineNumber,
|
||||
offset + content.length - totalEscapeLength
|
||||
);
|
||||
|
||||
offset += content.length;
|
||||
|
||||
const decorationType = this._getDecorationType(style);
|
||||
|
||||
if (!result.has(decorationType)) {
|
||||
result.set(decorationType, []);
|
||||
}
|
||||
|
||||
result.get(decorationType)!.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
return [...result];
|
||||
}
|
||||
|
||||
private _getDecorationType(style: AnserStyle): TextEditorDecorationType {
|
||||
let decorationType = this._decorationTypes.get(style);
|
||||
|
||||
if (decorationType) {
|
||||
return decorationType;
|
||||
}
|
||||
|
||||
const fontWeight = style.decorations.find((s) => s === "bold");
|
||||
const fontStyle = style.decorations.find((s) => s === "italic");
|
||||
const textDecoration = style.decorations.find((s) => s === "underline");
|
||||
|
||||
decorationType = window.createTextEditorDecorationType({
|
||||
backgroundColor: AnsiDecorationProvider._convertColor(style.bg, style.bg_truecolor),
|
||||
color: AnsiDecorationProvider._convertColor(style.fg, style.fg_truecolor),
|
||||
fontWeight,
|
||||
fontStyle,
|
||||
textDecoration,
|
||||
});
|
||||
|
||||
this._decorationTypes.set(style, decorationType);
|
||||
|
||||
return decorationType;
|
||||
}
|
||||
|
||||
// NOTE: This could just be a kebab-case to camelCase conversion, but I think it's
|
||||
// a short enough list to just write these by hand
|
||||
static readonly _anserToThemeColor: Record<string, ThemeColor> = {
|
||||
"ansi-black": "ansiBlack",
|
||||
"ansi-white": "ansiWhite",
|
||||
"ansi-red": "ansiRed",
|
||||
"ansi-green": "ansiGreen",
|
||||
"ansi-yellow": "ansiYellow",
|
||||
"ansi-blue": "ansiBlue",
|
||||
"ansi-magenta": "ansiMagenta",
|
||||
"ansi-cyan": "ansiCyan",
|
||||
|
||||
"ansi-bright-black": "ansiBrightBlack",
|
||||
"ansi-bright-white": "ansiBrightWhite",
|
||||
"ansi-bright-red": "ansiBrightRed",
|
||||
"ansi-bright-green": "ansiBrightGreen",
|
||||
"ansi-bright-yellow": "ansiBrightYellow",
|
||||
"ansi-bright-blue": "ansiBrightBlue",
|
||||
"ansi-bright-magenta": "ansiBrightMagenta",
|
||||
"ansi-bright-cyan": "ansiBrightCyan",
|
||||
};
|
||||
|
||||
private static _convertColor(
|
||||
color?: string,
|
||||
truecolor?: string
|
||||
): ThemeColor | string | undefined {
|
||||
if (!color) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (color === "ansi-truecolor") {
|
||||
if (!truecolor) {
|
||||
return undefined;
|
||||
}
|
||||
return `rgb(${truecolor})`;
|
||||
}
|
||||
|
||||
const paletteMatch = color.match(/ansi-palette-(.+)/);
|
||||
if (paletteMatch) {
|
||||
const paletteColor = paletteMatch[1];
|
||||
// anser won't return both the RGB and the color name at the same time,
|
||||
// so just fake a single foreground control char with the palette number:
|
||||
const spans = anser.ansiToJson(`\x1b[38;5;${paletteColor}m`);
|
||||
const rgb = spans[1].fg;
|
||||
|
||||
if (rgb) {
|
||||
return `rgb(${rgb})`;
|
||||
}
|
||||
}
|
||||
|
||||
const themeColor = AnsiDecorationProvider._anserToThemeColor[color];
|
||||
if (themeColor) {
|
||||
return new ThemeColor("terminal." + themeColor);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import * as lc from "vscode-languageclient/node";
|
||||
|
||||
import * as commands from "./commands";
|
||||
import { CommandFactory, Ctx, fetchWorkspace } from "./ctx";
|
||||
import * as diagnostics from "./diagnostics";
|
||||
import { activateTaskProvider } from "./tasks";
|
||||
import { setContextValue } from "./util";
|
||||
|
||||
@ -48,30 +49,52 @@ async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
|
||||
ctx.pushExtCleanup(activateTaskProvider(ctx.config));
|
||||
}
|
||||
|
||||
const diagnosticProvider = new diagnostics.TextDocumentProvider(ctx);
|
||||
ctx.pushExtCleanup(
|
||||
vscode.workspace.registerTextDocumentContentProvider(
|
||||
"rust-analyzer-diagnostics-view",
|
||||
new (class implements vscode.TextDocumentContentProvider {
|
||||
async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
|
||||
const diags = ctx.client?.diagnostics?.get(
|
||||
vscode.Uri.parse(uri.fragment, true)
|
||||
);
|
||||
if (!diags) {
|
||||
return "Unable to find original rustc diagnostic";
|
||||
}
|
||||
|
||||
const diag = diags[parseInt(uri.query)];
|
||||
if (!diag) {
|
||||
return "Unable to find original rustc diagnostic";
|
||||
}
|
||||
const rendered = (diag as unknown as { data?: { rendered?: string } }).data
|
||||
?.rendered;
|
||||
return rendered ?? "Unable to find original rustc diagnostic";
|
||||
}
|
||||
})()
|
||||
diagnostics.URI_SCHEME,
|
||||
diagnosticProvider
|
||||
)
|
||||
);
|
||||
|
||||
const decorationProvider = new diagnostics.AnsiDecorationProvider(ctx);
|
||||
ctx.pushExtCleanup(decorationProvider);
|
||||
|
||||
async function decorateVisibleEditors(document: vscode.TextDocument) {
|
||||
for (const editor of vscode.window.visibleTextEditors) {
|
||||
if (document === editor.document) {
|
||||
await decorationProvider.provideDecorations(editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument(
|
||||
async (event) => await decorateVisibleEditors(event.document),
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
vscode.workspace.onDidOpenTextDocument(decorateVisibleEditors, null, ctx.subscriptions);
|
||||
vscode.window.onDidChangeActiveTextEditor(
|
||||
async (editor) => {
|
||||
if (editor) {
|
||||
diagnosticProvider.triggerUpdate(editor.document.uri);
|
||||
await decorateVisibleEditors(editor.document);
|
||||
}
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
vscode.window.onDidChangeVisibleTextEditors(
|
||||
async (visibleEditors) => {
|
||||
for (const editor of visibleEditors) {
|
||||
diagnosticProvider.triggerUpdate(editor.document.uri);
|
||||
await decorationProvider.provideDecorations(editor);
|
||||
}
|
||||
},
|
||||
null,
|
||||
ctx.subscriptions
|
||||
);
|
||||
|
||||
vscode.workspace.onDidChangeWorkspaceFolders(
|
||||
async (_) => ctx.onWorkspaceFolderChanges(),
|
||||
null,
|
||||
|
Loading…
Reference in New Issue
Block a user