Skip to content

Commit

Permalink
Add error code explanation tooltips (#214)
Browse files Browse the repository at this point in the history
* Bump 'motoko' npm package

* Progress

* Cache Motoko diagnostics on server side

* Update 'error-codes.json' import

* Refactor tooltip logic for error code explanations

* Refactor

* Refactor

* Remove Markdown heading from explanation

* 0.13.0

* Deduplicate explanations
  • Loading branch information
rvanasa authored May 19, 2023
1 parent a66f613 commit 6a43645
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 81 deletions.
18 changes: 9 additions & 9 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "vscode-motoko",
"displayName": "Motoko",
"description": "Motoko language support",
"version": "0.12.2",
"version": "0.13.0",
"publisher": "dfinity-foundation",
"repository": "https://github.com/dfinity/vscode-motoko",
"engines": {
Expand Down Expand Up @@ -167,7 +167,7 @@
"fast-glob": "3.2.12",
"ic0": "0.2.7",
"mnemonist": "0.39.5",
"motoko": "3.5.7",
"motoko": "3.6.0",
"prettier": "2.8.0",
"prettier-plugin-motoko": "0.5.2",
"url-relative": "1.0.0",
Expand Down
184 changes: 114 additions & 70 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,16 @@ import { Program, asNode, findNodes } from './syntax';
import {
formatMotoko,
getFileText,
rangeContainsPosition,
resolveFilePath,
resolveVirtualPath,
} from './utils';

const errorCodes: Record<
string,
string
> = require('motoko/contrib/generated/errorCodes.json');

interface Settings {
motoko: MotokoSettings;
}
Expand Down Expand Up @@ -475,7 +481,7 @@ connection.onDidChangeWatchedFiles((event) => {
const path = resolveVirtualPath(change.uri);
deleteVirtual(path);
notifyDeleteUri(change.uri);
connection.sendDiagnostics({
sendDiagnostics({
uri: change.uri,
diagnostics: [],
});
Expand Down Expand Up @@ -644,7 +650,7 @@ function checkWorkspace() {
}
previousCheckedFiles.forEach((uri) => {
if (!checkedFiles.includes(uri)) {
connection.sendDiagnostics({ uri, diagnostics: [] });
sendDiagnostics({ uri, diagnostics: [] });
}
});
checkedFiles.forEach((uri) => notify(uri));
Expand Down Expand Up @@ -694,7 +700,7 @@ function checkImmediate(uri: string | TextDocument): boolean {
const skipExtension = '.mo_'; // Skip type checking `*.mo_` files
const resolvedUri = typeof uri === 'string' ? uri : uri?.uri;
if (resolvedUri?.endsWith(skipExtension)) {
connection.sendDiagnostics({
sendDiagnostics({
uri: resolvedUri,
diagnostics: [],
});
Expand All @@ -713,7 +719,7 @@ function checkImmediate(uri: string | TextDocument): boolean {

const { uri: contextUri, motoko, error } = getContext(resolvedUri);
console.log('~', virtualPath, `(${contextUri || 'default'})`);
let diagnostics = motoko.check(virtualPath) as any as Diagnostic[];
let diagnostics = motoko.check(virtualPath) as Diagnostic[];
if (error) {
// Context initialization error
// diagnostics.length = 0;
Expand All @@ -739,8 +745,7 @@ function checkImmediate(uri: string | TextDocument): boolean {
diagnostics = diagnostics.filter(
({ message, severity }) =>
severity === DiagnosticSeverity.Error ||
// @ts-ignore
!new RegExp(settings.hideWarningRegex).test(message),
!new RegExp(settings!.hideWarningRegex).test(message),
);
}
}
Expand Down Expand Up @@ -770,15 +775,15 @@ function checkImmediate(uri: string | TextDocument): boolean {
});

Object.entries(diagnosticMap).forEach(([path, diagnostics]) => {
connection.sendDiagnostics({
sendDiagnostics({
uri: URI.file(path).toString(),
diagnostics,
});
});
return true;
} catch (err) {
console.error(`Error while compiling Motoko file: ${err}`);
connection.sendDiagnostics({
sendDiagnostics({
uri: typeof uri === 'string' ? uri : uri.uri,
diagnostics: [
{
Expand Down Expand Up @@ -1045,81 +1050,110 @@ connection.onHover((event) => {
const { position } = event;
const { uri } = event.textDocument;
const { astResolver } = getContext(uri);
const status = astResolver.requestTyped(uri);
if (!status || status.outdated || !status.ast) {
return;
}
// Find AST nodes which include the cursor position
const node = findMostSpecificNodeForPosition(
status.ast,
position,
(node) => !!node.type,
true, // Mouse cursor
);
if (!node) {
return;
}

const text = getFileText(uri);
const lines = text.split(/\r?\n/g);

const startLine = lines[node.start[0] - 1];
const isSameLine = node.start[0] === node.end[0];

const codeSnippet = (source: string) => `\`\`\`motoko\n${source}\n\`\`\``;
const docs: string[] = [];
const source = (
isSameLine ? startLine.substring(node.start[1], node.end[1]) : startLine
).trim();
const doc = findDocComment(node);
if (doc) {
const typeInfo = node.type ? formatMotoko(node.type).trim() : '';
const lineIndex = typeInfo.indexOf('\n');
if (typeInfo) {
if (lineIndex === -1) {
docs.push(codeSnippet(typeInfo));
let range: Range | undefined;

// Error code explanations
const codes: string[] = [];
diagnosticMap.get(uri)?.forEach((diagnostic) => {
if (rangeContainsPosition(diagnostic.range, position)) {
const code = diagnostic.code;
if (typeof code === 'string' && !codes.includes(code)) {
codes.push(code);
if (errorCodes.hasOwnProperty(code)) {
// Show explanation without Markdown heading
docs.push(errorCodes[code].replace(/^# M[0-9]+\s+/, ''));
}
}
} else if (!isSameLine) {
docs.push(codeSnippet(source));
}
docs.push(doc);
if (lineIndex !== -1) {
docs.push(`*Type definition:*\n${codeSnippet(typeInfo)}`);
});

const status = astResolver.requestTyped(uri);
if (status && !status.outdated && status.ast) {
// Find AST nodes which include the cursor position
const node = findMostSpecificNodeForPosition(
status.ast,
position,
(node) => !!node.type,
true, // Mouse cursor
);
if (node) {
range = rangeFromNode(node, true);

const startLine = lines[node.start[0] - 1];
const isSameLine = node.start[0] === node.end[0];

const codeSnippet = (source: string) =>
`\`\`\`motoko\n${source}\n\`\`\``;
const source = (
isSameLine
? startLine.substring(node.start[1], node.end[1])
: startLine
).trim();

// Doc comments
const doc = findDocComment(node);
if (doc) {
const typeInfo = node.type
? formatMotoko(node.type).trim()
: '';
const lineIndex = typeInfo.indexOf('\n');
if (typeInfo) {
if (lineIndex === -1) {
docs.push(codeSnippet(typeInfo));
}
} else if (!isSameLine) {
docs.push(codeSnippet(source));
}
docs.push(doc);
if (lineIndex !== -1) {
docs.push(`*Type definition:*\n${codeSnippet(typeInfo)}`);
}
} else if (node.type) {
docs.push(codeSnippet(formatMotoko(node.type)));
} else if (!isSameLine) {
docs.push(codeSnippet(source));
}

// Syntax explanations
const info = getAstInformation(node /* , source */);
if (info) {
docs.push(info);
}
if (settings?.debugHover) {
let debugText = `\n${node.name}`;
if (node.args?.length) {
// Show AST debug information
debugText += ` [${node.args
.map(
(arg) =>
`\n ${
typeof arg === 'object'
? Array.isArray(arg)
? '[...]'
: arg?.name
: JSON.stringify(arg)
}`,
)
.join('')}\n]`;
}
docs.push(codeSnippet(debugText));
}
}
} else if (node.type) {
docs.push(codeSnippet(formatMotoko(node.type)));
} else if (!isSameLine) {
docs.push(codeSnippet(source));
}
const info = getAstInformation(node /* , source */);
if (info) {
docs.push(info);
}
if (settings?.debugHover) {
let debugText = `\n${node.name}`;
if (node.args?.length) {
// Show AST debug information
debugText += ` [${node.args
.map(
(arg) =>
`\n ${
typeof arg === 'object'
? Array.isArray(arg)
? '[...]'
: arg?.name
: JSON.stringify(arg)
}`,
)
.join('')}\n]`;
}
docs.push(codeSnippet(debugText));

if (!docs.length) {
return;
}
return {
contents: {
kind: MarkupKind.Markdown,
value: docs.join('\n\n---\n\n'),
},
range: rangeFromNode(node, true),
range,
};
});

Expand Down Expand Up @@ -1275,6 +1309,16 @@ connection.onRequest(DEPLOY_PLAYGROUND, (params) =>
),
);

const diagnosticMap = new Map<string, Diagnostic[]>();
async function sendDiagnostics(params: {
uri: string;
diagnostics: Diagnostic[];
}) {
const { uri, diagnostics } = params;
diagnosticMap.set(uri, diagnostics);
return connection.sendDiagnostics(params);
}

let validatingTimeout: ReturnType<typeof setTimeout>;
let validatingUri: string | undefined;
documents.onDidChangeContent((event) => {
Expand All @@ -1296,7 +1340,7 @@ documents.onDidChangeContent((event) => {

documents.onDidOpen((event) => scheduleCheck(event.document.uri));
documents.onDidClose(async (event) => {
await connection.sendDiagnostics({
await sendDiagnostics({
uri: event.document.uri,
diagnostics: [],
});
Expand Down
Loading

0 comments on commit 6a43645

Please sign in to comment.