about summary refs log tree commit diff
path: root/editors/code/src
diff options
context:
space:
mode:
Diffstat (limited to 'editors/code/src')
-rw-r--r--editors/code/src/client.ts48
-rw-r--r--editors/code/src/commands.ts21
-rw-r--r--editors/code/src/config.ts4
-rw-r--r--editors/code/src/diagnostics.ts212
-rw-r--r--editors/code/src/lsp_ext.ts6
-rw-r--r--editors/code/src/main.ts65
6 files changed, 324 insertions, 32 deletions
diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts
index 23e039722ee..82cdf0390ac 100644
--- a/editors/code/src/client.ts
+++ b/editors/code/src/client.ts
@@ -1,8 +1,10 @@
+import * as anser from "anser";
 import * as lc from "vscode-languageclient/node";
 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";
@@ -100,13 +102,32 @@ export async function createClient(
                     }
                 },
             },
+            async provideInlayHints(document, viewPort, token, next) {
+                const inlays = await next(document, viewPort, token);
+                if (!inlays) {
+                    return inlays;
+                }
+                // U+200C is a zero-width non-joiner to prevent the editor from forming a ligature
+                // between code and hints
+                for (const inlay of inlays) {
+                    if (typeof inlay.label === "string") {
+                        inlay.label = `\u{200c}${inlay.label}\u{200c}`;
+                    } else if (Array.isArray(inlay.label)) {
+                        for (const it of inlay.label) {
+                            it.value = `\u{200c}${it.value}\u{200c}`;
+                        }
+                    }
+                }
+                return inlays;
+            },
             async handleDiagnostics(
                 uri: vscode.Uri,
-                diagnostics: vscode.Diagnostic[],
+                diagnosticList: vscode.Diagnostic[],
                 next: lc.HandleDiagnosticsSignature
             ) {
                 const preview = config.previewRustcOutput;
-                diagnostics.forEach((diag, idx) => {
+                const errorCode = config.useRustcErrorCode;
+                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:)
@@ -119,23 +140,33 @@ export async function createClient(
                         ?.rendered;
                     if (rendered) {
                         if (preview) {
-                            const index = rendered.match(/^(note|help):/m)?.index || 0;
-                            diag.message = rendered
+                            const decolorized = anser.ansiToText(rendered);
+                            const index =
+                                decolorized.match(/^(note|help):/m)?.index || rendered.length;
+                            diag.message = decolorized
                                 .substring(0, index)
                                 .replace(/^ -->[^\n]+\n/m, "");
                         }
+                        let value;
+                        if (errorCode) {
+                            if (typeof diag.code === "string" || typeof diag.code === "number") {
+                                value = diag.code;
+                            } else {
+                                value = diag.code?.value;
+                            }
+                        }
                         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(),
                             }),
-                            value: "Click for full compiler diagnostic",
+                            value: value ?? "Click for full compiler diagnostic",
                         };
                     }
                 });
-                return next(uri, diagnostics);
+                return next(uri, diagnosticList);
             },
             async provideHover(
                 document: vscode.TextDocument,
@@ -302,6 +333,7 @@ class ExperimentalFeatures implements lc.StaticFeature {
         caps.codeActionGroup = true;
         caps.hoverActions = true;
         caps.serverStatusNotification = true;
+        caps.colorDiagnosticOutput = true;
         caps.commands = {
             commands: [
                 "rust-analyzer.runSingle",
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 312087e4cff..cb4e13e2c60 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -788,8 +788,23 @@ export function openDocs(ctx: CtxInit): Cmd {
 
 export function cancelFlycheck(ctx: CtxInit): Cmd {
     return async () => {
+        await ctx.client.sendNotification(ra.cancelFlycheck);
+    };
+}
+
+export function clearFlycheck(ctx: CtxInit): Cmd {
+    return async () => {
+        await ctx.client.sendNotification(ra.clearFlycheck);
+    };
+}
+
+export function runFlycheck(ctx: CtxInit): Cmd {
+    return async () => {
+        const editor = ctx.activeRustEditor;
         const client = ctx.client;
-        await client.sendRequest(ra.cancelFlycheck);
+        const params = editor ? { uri: editor.document.uri.toString() } : null;
+
+        await client.sendNotification(ra.runFlycheck, { textDocument: params });
     };
 }
 
@@ -797,12 +812,12 @@ export function resolveCodeAction(ctx: CtxInit): Cmd {
     return async (params: lc.CodeAction) => {
         const client = ctx.client;
         params.command = undefined;
-        const item = await client?.sendRequest(lc.CodeActionResolveRequest.type, params);
+        const item = await client.sendRequest(lc.CodeActionResolveRequest.type, params);
         if (!item?.edit) {
             return;
         }
         const itemEdit = item.edit;
-        const edit = await client?.protocol2CodeConverter.asWorkspaceEdit(itemEdit);
+        const edit = await client.protocol2CodeConverter.asWorkspaceEdit(itemEdit);
         // filter out all text edits and recreate the WorkspaceEdit without them so we can apply
         // snippet edits on our own
         const lcFileSystemEdit = {
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index d8dbd1df16d..eb4f965291f 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -241,6 +241,10 @@ export class Config {
     get previewRustcOutput() {
         return this.get<boolean>("diagnostics.previewRustcOutput");
     }
+
+    get useRustcErrorCode() {
+        return this.get<boolean>("diagnostics.useRustcErrorCode");
+    }
 }
 
 const VarRegex = new RegExp(/\$\{(.+?)\}/g);
diff --git a/editors/code/src/diagnostics.ts b/editors/code/src/diagnostics.ts
new file mode 100644
index 00000000000..9695d8bf26d
--- /dev/null
+++ b/editors/code/src/diagnostics.ts
@@ -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;
+    }
+}
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index 875261c48a6..29349cc20f5 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -79,7 +79,11 @@ export const relatedTests = new lc.RequestType<lc.TextDocumentPositionParams, Te
     "rust-analyzer/relatedTests"
 );
 
-export const cancelFlycheck = new lc.RequestType0<void, void>("rust-analyzer/cancelFlycheck");
+export const cancelFlycheck = new lc.NotificationType0("rust-analyzer/cancelFlycheck");
+export const clearFlycheck = new lc.NotificationType0("rust-analyzer/clearFlycheck");
+export const runFlycheck = new lc.NotificationType<{
+    textDocument: lc.TextDocumentIdentifier | null;
+}>("rust-analyzer/runFlycheck");
 
 // Experimental extensions
 
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 25f1e83d109..dd439317c70 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -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,
@@ -79,7 +102,7 @@ async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
     );
     vscode.workspace.onDidChangeConfiguration(
         async (_) => {
-            await ctx.client?.sendNotification("workspace/didChangeConfiguration", {
+            await ctx.client?.sendNotification(lc.DidChangeConfigurationNotification.type, {
                 settings: "",
             });
         },
@@ -150,6 +173,8 @@ function createCommands(): Record<string, CommandFactory> {
         moveItemUp: { enabled: commands.moveItemUp },
         moveItemDown: { enabled: commands.moveItemDown },
         cancelFlycheck: { enabled: commands.cancelFlycheck },
+        clearFlycheck: { enabled: commands.clearFlycheck },
+        runFlycheck: { enabled: commands.runFlycheck },
         ssr: { enabled: commands.ssr },
         serverVersion: { enabled: commands.serverVersion },
         // Internal commands which are invoked by the server.