diff options
| author | arcnmx <git@git.arcn.mx> | 2023-01-09 10:36:22 -0800 |
|---|---|---|
| committer | arcnmx <git@git.arcn.mx> | 2023-01-09 10:36:22 -0800 |
| commit | 25242fe93f6ae330abbaba96d5a91fd55ed1395a (patch) | |
| tree | ec123e0f7eecf4fc171c02b74cd105dc238f18d9 /editors/code/src | |
| parent | b3ef934ccbfa8506dd562eb26848a35dcdc912ff (diff) | |
| download | rust-25242fe93f6ae330abbaba96d5a91fd55ed1395a.tar.gz rust-25242fe93f6ae330abbaba96d5a91fd55ed1395a.zip | |
:arrow_up: rust-analyzer
Merge commit '368e0bb32f1178cf162c2ce5f7e10b7ae211eb26'
Diffstat (limited to 'editors/code/src')
| -rw-r--r-- | editors/code/src/client.ts | 48 | ||||
| -rw-r--r-- | editors/code/src/commands.ts | 21 | ||||
| -rw-r--r-- | editors/code/src/config.ts | 4 | ||||
| -rw-r--r-- | editors/code/src/diagnostics.ts | 212 | ||||
| -rw-r--r-- | editors/code/src/lsp_ext.ts | 6 | ||||
| -rw-r--r-- | editors/code/src/main.ts | 65 |
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. |
