rust/editors/code/src/diagnostics.ts
2023-07-13 22:44:29 +09:00

221 lines
6.8 KiB
TypeScript

import * as anser from "anser";
import * as vscode from "vscode";
import {
type ProviderResult,
Range,
type TextEditorDecorationType,
ThemeColor,
window,
} from "vscode";
import type { Ctx } from "./ctx";
import { unwrapUndefinable } from "./undefinable";
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 span = unwrapUndefinable(spans[1]);
const rgb = span.fg;
if (rgb) {
return `rgb(${rgb})`;
}
}
const themeColor = AnsiDecorationProvider._anserToThemeColor[color];
if (themeColor) {
return new ThemeColor("terminal." + themeColor);
}
return undefined;
}
}