Skip to content

Commit

Permalink
[ide] Pretty rendering of errors (#1197)
Browse files Browse the repository at this point in the history
  • Loading branch information
SamChou19815 committed May 12, 2024
1 parent cbae791 commit ad124c0
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 23 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/samlang-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
15 changes: 12 additions & 3 deletions crates/samlang-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ mod lsp {

fn get_diagnostics(&self, absolute_source_path: &Path) -> Vec<(Url, Vec<Diagnostic>)> {
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) =
Expand All @@ -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)| {
Expand All @@ -182,6 +190,7 @@ mod lsp {
})
.collect(),
),
data: Some(serde_json::Value::Object(extra_data_map)),
..Default::default()
}
})
Expand Down
52 changes: 44 additions & 8 deletions crates/samlang-errors/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Location>,
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct CompileTimeError {
pub location: Location,
Expand Down Expand Up @@ -826,14 +834,22 @@ impl CompileTimeError {
printer.push('\n');
}

pub fn to_ide_format(&self, heap: &Heap) -> (Location, String, Vec<Location>) {
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<ModuleReference, String>,
) -> 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 }
}
}

Expand Down Expand Up @@ -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!(
"{:?}",
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion crates/samlang-services/src/server_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::{
pub struct ServerState {
pub heap: Heap,
enable_profiling: bool,
pub(super) string_sources: HashMap<ModuleReference, String>,
pub string_sources: HashMap<ModuleReference, String>,
pub(super) parsed_modules: HashMap<ModuleReference, Module<()>>,
dep_graph: DependencyGraph,
pub(super) checked_modules: HashMap<ModuleReference, Module<Rc<Type>>>,
Expand Down
5 changes: 3 additions & 2 deletions crates/samlang-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
})
Expand Down
105 changes: 105 additions & 0 deletions packages/samlang-vscode/src/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -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<vscode.Uri>();

public constructor(public client: LanguageClient) {}

get onDidChange(): vscode.Event<vscode.Uri> {
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<vscode.DocumentLink[]> {
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;
}
}
51 changes: 43 additions & 8 deletions packages/samlang-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
},
},
}
);
}

Expand All @@ -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!');
Expand Down
2 changes: 1 addition & 1 deletion packages/samlang-vscode/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"types": [],
"types": ["vscode"],
"module": "es2015",
"target": "es2019",
"outDir": "out",
Expand Down

0 comments on commit ad124c0

Please sign in to comment.