• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as anser from "anser";
2import * as vscode from "vscode";
3import { ProviderResult, Range, TextEditorDecorationType, ThemeColor, window } from "vscode";
4import { Ctx } from "./ctx";
5
6export const URI_SCHEME = "rust-analyzer-diagnostics-view";
7
8export 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
33function 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
52interface AnserStyle {
53    fg: string;
54    bg: string;
55    fg_truecolor: string;
56    bg_truecolor: string;
57    decorations: Array<anser.DecorationName>;
58}
59
60export 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}
213