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