diff --git a/Cargo.lock b/Cargo.lock index f59e5119a..daf20fca1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,6 +543,7 @@ dependencies = [ "samlang-printer", "samlang-profiling", "samlang-services", + "serde_json", "tokio", "tower-lsp", ] diff --git a/crates/samlang-cli/Cargo.toml b/crates/samlang-cli/Cargo.toml index 628362571..b74c8f8f3 100644 --- a/crates/samlang-cli/Cargo.toml +++ b/crates/samlang-cli/Cargo.toml @@ -14,5 +14,6 @@ samlang-parser = { path = "../samlang-parser" } samlang-printer = { path = "../samlang-printer" } samlang-profiling = { path = "../samlang-profiling" } samlang-services = { path = "../samlang-services" } +serde_json = "1.0" tokio = { version = "1.28.2", features = ["io-util", "io-std", "macros", "rt-multi-thread"] } tower-lsp = "0.18.0" diff --git a/crates/samlang-cli/src/main.rs b/crates/samlang-cli/src/main.rs index bba7372cd..26b35d7af 100644 --- a/crates/samlang-cli/src/main.rs +++ b/crates/samlang-cli/src/main.rs @@ -149,6 +149,7 @@ mod lsp { fn get_diagnostics(&self, absolute_source_path: &Path) -> Vec<(Url, Vec)> { let heap = &self.0.heap; + let sources = &self.0.string_sources; let mut collected = vec![]; for module_reference in self.0.all_modules() { if let Some(url) = @@ -159,14 +160,21 @@ mod lsp { .get_errors(module_reference) .iter() .map(|e| { - let (loc, message, related_locs) = e.to_ide_format(heap); + let samlang_errors::ErrorInIDEFormat { + location: loc, + ide_error, + full_error, + reference_locs, + } = e.to_ide_format(heap, sources); + let mut extra_data_map = serde_json::Map::new(); + extra_data_map.insert("rendered".to_string(), serde_json::Value::String(full_error)); Diagnostic { range: samlang_loc_to_lsp_range(&loc), severity: Some(DiagnosticSeverity::ERROR), - message, + message: ide_error, source: Some("samlang".to_string()), related_information: Some( - related_locs + reference_locs .iter() .enumerate() .filter_map(|(i, l)| { @@ -182,6 +190,7 @@ mod lsp { }) .collect(), ), + data: Some(serde_json::Value::Object(extra_data_map)), ..Default::default() } }) diff --git a/crates/samlang-errors/src/lib.rs b/crates/samlang-errors/src/lib.rs index baaabb27c..7794c1cea 100644 --- a/crates/samlang-errors/src/lib.rs +++ b/crates/samlang-errors/src/lib.rs @@ -796,6 +796,14 @@ impl ErrorDetail { } } +#[derive(Debug, PartialEq, Eq)] +pub struct ErrorInIDEFormat { + pub location: Location, + pub ide_error: String, + pub full_error: String, + pub reference_locs: Vec, +} + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct CompileTimeError { pub location: Location, @@ -826,14 +834,22 @@ impl CompileTimeError { printer.push('\n'); } - pub fn to_ide_format(&self, heap: &Heap) -> (Location, String, Vec) { - let empty_sources = HashMap::new(); - let mut printer = printer::ErrorPrinterState::new(ErrorPrinterStyle::IDE, &empty_sources); - let printable_error_segments = printer.print_error_detail(heap, &self.detail); - let printed_error = printer.consume(); + pub fn to_ide_format( + &self, + heap: &Heap, + sources: &HashMap, + ) -> ErrorInIDEFormat { + let mut ide_printer = printer::ErrorPrinterState::new(ErrorPrinterStyle::IDE, sources); + let printable_error_segments = ide_printer.print_error_detail(heap, &self.detail); + let ide_error = ide_printer.consume(); let reference_locs = printable_error_segments.into_iter().filter_map(|s| s.get_loc_reference_opt()).collect_vec(); - (self.location, printed_error, reference_locs) + + let mut full_error_printer = + printer::ErrorPrinterState::new(ErrorPrinterStyle::Terminal, sources); + ErrorSet::print_one_error_message(heap, &mut full_error_printer, self); + let full_error = full_error_printer.consume(); + ErrorInIDEFormat { location: self.location, ide_error, full_error, reference_locs } } } @@ -1073,6 +1089,15 @@ mod tests { #[test] fn boilterplate() { format!("{:?}", ErrorPrinterStyle::IDE); + format!( + "{:?}", + ErrorInIDEFormat { + location: Location::dummy(), + ide_error: "ide".to_string(), + full_error: "full".to_string(), + reference_locs: vec![] + } + ); assert!(ErrorPrinterStyle::Terminal.clone() != ErrorPrinterStyle::Text); format!( "{:?}", @@ -1127,8 +1152,19 @@ Found 1 error. error_set.pretty_print_error_messages_no_frame_for_test(&heap) ); assert_eq!( - (Location::dummy(), "Cannot resolve module `DUMMY`.".to_string(), vec![]), - error_set.errors()[0].to_ide_format(&heap) + ErrorInIDEFormat { + location: Location::dummy(), + ide_error: "Cannot resolve module `DUMMY`.".to_string(), + full_error: r#"Error ------------------------------------ DUMMY.sam:0:0-0:0 + +Cannot resolve module `DUMMY`. + + +"# + .to_string(), + reference_locs: vec![] + }, + error_set.errors()[0].to_ide_format(&heap, &HashMap::new()) ); error_set.report_cannot_resolve_name_error( diff --git a/crates/samlang-services/src/server_state.rs b/crates/samlang-services/src/server_state.rs index 5fcb93cf1..6dfbb7642 100644 --- a/crates/samlang-services/src/server_state.rs +++ b/crates/samlang-services/src/server_state.rs @@ -15,7 +15,7 @@ use std::{ pub struct ServerState { pub heap: Heap, enable_profiling: bool, - pub(super) string_sources: HashMap, + pub string_sources: HashMap, pub(super) parsed_modules: HashMap>, dep_graph: DependencyGraph, pub(super) checked_modules: HashMap>>, diff --git a/crates/samlang-wasm/src/lib.rs b/crates/samlang-wasm/src/lib.rs index 3cffaf57a..7c0005b0c 100644 --- a/crates/samlang-wasm/src/lib.rs +++ b/crates/samlang-wasm/src/lib.rs @@ -116,13 +116,14 @@ pub fn type_check(source: String) -> JsValue { .get_errors(&mod_ref) .iter() .map(|e| { - let (loc, message, _related_locs) = e.to_ide_format(&state.heap); + let ide_error = e.to_ide_format(&state.heap, &HashMap::new()); + let loc = ide_error.location; Diagnostic { start_line: loc.start.0 + 1, start_col: loc.start.1 + 1, end_line: loc.end.0 + 1, end_col: loc.end.1 + 1, - message, + message: ide_error.ide_error, severity: 8, } }) diff --git a/packages/samlang-vscode/src/diagnostics.ts b/packages/samlang-vscode/src/diagnostics.ts new file mode 100644 index 000000000..e79536dcb --- /dev/null +++ b/packages/samlang-vscode/src/diagnostics.ts @@ -0,0 +1,105 @@ +// Forked from https://github.com/rust-lang/rust-analyzer/blob/1a5bb27c018c947dab01ab70ffe1d267b0481a17/editors/code/src/diagnostics.ts + +import * as path from 'path'; +import * as vscode from 'vscode'; +import type { LanguageClient } from 'vscode-languageclient/node'; + +export const DIAGNOSTICS_URI_SCHEME = 'samlang-diagnostics-view'; + +function getRendered(client: LanguageClient, uri: vscode.Uri): string { + const diags = client?.diagnostics?.get(vscode.Uri.parse(uri.fragment, true)); + if (!diags) { + return 'Unable to find original samlang diagnostic due to missing diagnostic in client'; + } + + const diag = diags[parseInt(uri.query)]; + if (!diag) { + return 'Unable to find original samlang diagnostic due to bad index'; + } + const rendered = (diag as unknown as { data?: { rendered?: string } }).data?.rendered; + + if (!rendered) { + return 'Unable to find original samlang diagnostic due to missing render'; + } + + return rendered; +} + +export class TextDocumentProvider implements vscode.TextDocumentContentProvider { + private _onDidChange = new vscode.EventEmitter(); + + public constructor(public client: LanguageClient) {} + + get onDidChange(): vscode.Event { + return this._onDidChange.event; + } + + triggerUpdate(uri: vscode.Uri) { + if (uri.scheme === DIAGNOSTICS_URI_SCHEME) { + this._onDidChange.fire(uri); + } + } + + dispose() { + this._onDidChange.dispose(); + } + + provideTextDocumentContent(uri: vscode.Uri): string { + try { + return getRendered(this.client, uri); + } catch (e) { + return e as string; + } + } +} + +export class ErrorLinkProvider implements vscode.DocumentLinkProvider { + public constructor(public client: LanguageClient, private readonly resolvedRoot: string) {} + + provideDocumentLinks( + document: vscode.TextDocument, + _token: vscode.CancellationToken + ): vscode.ProviderResult { + if (document.uri.scheme !== DIAGNOSTICS_URI_SCHEME) { + return null; + } + + let stringContents: string; + try { + stringContents = getRendered(this.client, document.uri); + } catch { + return null; + } + const lines = stringContents.split('\n'); + + const result: vscode.DocumentLink[] = []; + + for (const [lineNumber, line] of lines.entries()) { + for (const pathLineMatched of line.matchAll(/(?<= )[A-Za-z0-9\./]+\.sam:[0-9]+:[0-9]+/g)) { + const [filename, firstLine, firstCol] = pathLineMatched[0].split(':'); + if (filename == null || firstLine == null || firstCol == null) continue; + const range = new vscode.Range( + lineNumber, + pathLineMatched.index, + lineNumber, + pathLineMatched.index + pathLineMatched[0].length + ); + + try { + result.push( + new vscode.DocumentLink( + range, + vscode.Uri.from({ + scheme: 'file', + path: path.resolve(this.resolvedRoot, filename), + fragment: `L${firstLine},${firstCol}`, + }) + ) + ); + } catch {} + } + } + + return result; + } +} diff --git a/packages/samlang-vscode/src/extension.ts b/packages/samlang-vscode/src/extension.ts index 16c072291..08a7039ab 100755 --- a/packages/samlang-vscode/src/extension.ts +++ b/packages/samlang-vscode/src/extension.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; import { LanguageClient, TransportKind } from 'vscode-languageclient/node'; +import { DIAGNOSTICS_URI_SCHEME, TextDocumentProvider, ErrorLinkProvider } from './diagnostics'; let languageClient: LanguageClient | undefined; @@ -10,7 +11,29 @@ function createLanguageClient(absoluteServerModule: string) { 'samlang', 'samlang Language Client', { command: absoluteServerModule, args: ['lsp'], transport: TransportKind.stdio }, - { documentSelector: [{ scheme: 'file', language: 'samlang' }] } + { + documentSelector: [{ scheme: 'file', language: 'samlang' }], + middleware: { + // Forked from https://github.com/rust-lang/rust-analyzer/blob/1a5bb27c018c947dab01ab70ffe1d267b0481a17/editors/code/src/client.ts#L203-L225 + async handleDiagnostics(uri, diagnosticList, next) { + diagnosticList.forEach((diag, idx) => { + const rendered = (diag as unknown as { data?: { rendered?: string } }).data?.rendered; + if (rendered) { + diag.code = { + target: vscode.Uri.from({ + scheme: DIAGNOSTICS_URI_SCHEME, + path: `/diagnostic message [${idx.toString()}]`, + fragment: uri.toString(), + query: idx.toString(), + }), + value: 'Click for full compiler diagnostic', + }; + } + }); + return next(uri, diagnosticList); + }, + }, + } ); } @@ -19,24 +42,36 @@ export function activate(context: vscode.ExtensionContext): void { if (typeof serverModule !== 'string') { throw new Error(`Invalid LSP program path: ${serverModule}.`); } - const resolvedServerModules = (vscode.workspace.workspaceFolders ?? []) - .map((folder) => path.join(folder.uri.fsPath, serverModule)) - .filter((it) => fs.existsSync(it)); - if (resolvedServerModules.length > 1) throw new Error('Too many samlang LSP programs found.'); - const absoluteServerModule = resolvedServerModules[0]; - if (absoluteServerModule == null) throw new Error('No valid samlang LSP program found.'); + const resolvedRootAndServerModules = (vscode.workspace.workspaceFolders ?? []) + .map((folder) => [folder.uri.fsPath, path.join(folder.uri.fsPath, serverModule)] as const) + .filter(([_, it]) => fs.existsSync(it)); + if (resolvedRootAndServerModules.length > 1) + throw new Error('Too many samlang LSP programs found.'); + if (resolvedRootAndServerModules[0] == null) + throw new Error('No valid samlang LSP program found.'); + const [resolvedRoot, absoluteServerModule] = resolvedRootAndServerModules[0]; languageClient = createLanguageClient(absoluteServerModule); + const diagnosticProvider = new TextDocumentProvider(languageClient); + const errorLinkProvider = new ErrorLinkProvider(languageClient, resolvedRoot); + languageClient.start(); context.subscriptions.push( vscode.commands.registerCommand('samlang.restartClient', async () => { console.info('Restarting client...'); await languageClient?.stop(); languageClient = createLanguageClient(absoluteServerModule); + diagnosticProvider.client = languageClient; + errorLinkProvider.client = languageClient; await languageClient.start(); console.info('Client restarted'); - }) + }), + vscode.languages.registerDocumentLinkProvider( + { scheme: DIAGNOSTICS_URI_SCHEME }, + errorLinkProvider + ), + vscode.workspace.registerTextDocumentContentProvider(DIAGNOSTICS_URI_SCHEME, diagnosticProvider) ); // eslint-disable-next-line no-console console.info('Congratulations, your extension "vscode-samlang" is now active!'); diff --git a/packages/samlang-vscode/tsconfig.json b/packages/samlang-vscode/tsconfig.json index ddf2c354f..035ada780 100755 --- a/packages/samlang-vscode/tsconfig.json +++ b/packages/samlang-vscode/tsconfig.json @@ -10,7 +10,7 @@ "noUncheckedIndexedAccess": true, "resolveJsonModule": true, "skipLibCheck": true, - "types": [], + "types": ["vscode"], "module": "es2015", "target": "es2019", "outDir": "out",