diff --git a/package-lock.json b/package-lock.json index 25fa236..dd5ac91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "vscode-motoko", - "version": "0.12.2", + "version": "0.13.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-motoko", - "version": "0.12.2", + "version": "0.13.0", "dependencies": { "@wasmer/wasi": "1.2.2", "change-case": "4.1.2", "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", @@ -7022,9 +7022,9 @@ } }, "node_modules/motoko": { - "version": "3.5.7", - "resolved": "https://registry.npmjs.org/motoko/-/motoko-3.5.7.tgz", - "integrity": "sha512-26O5DOUYMYNACR6Eaiz0GY/8CtYh3ji/ah7Hp+5uCAmq+fXyOUoMeU9EWuLOEGQE0/lVpNSD6lyhqIT6ruwAzw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/motoko/-/motoko-3.6.0.tgz", + "integrity": "sha512-+wLxdIkcM+ohUA9x7PzZveNjSRVa4vbWOwsskOhT7FX7Kihjmo2RldMRWTnywqE/hqoKQt33t3E8BTz7APWjew==", "dependencies": { "cross-fetch": "3.1.5", "debug": "4.3.4", @@ -14534,9 +14534,9 @@ } }, "motoko": { - "version": "3.5.7", - "resolved": "https://registry.npmjs.org/motoko/-/motoko-3.5.7.tgz", - "integrity": "sha512-26O5DOUYMYNACR6Eaiz0GY/8CtYh3ji/ah7Hp+5uCAmq+fXyOUoMeU9EWuLOEGQE0/lVpNSD6lyhqIT6ruwAzw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/motoko/-/motoko-3.6.0.tgz", + "integrity": "sha512-+wLxdIkcM+ohUA9x7PzZveNjSRVa4vbWOwsskOhT7FX7Kihjmo2RldMRWTnywqE/hqoKQt33t3E8BTz7APWjew==", "requires": { "cross-fetch": "3.1.5", "debug": "4.3.4", diff --git a/package.json b/package.json index db94ad8..35c7e0c 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", diff --git a/src/server/server.ts b/src/server/server.ts index 2d88f09..cde0c3e 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -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; } @@ -475,7 +481,7 @@ connection.onDidChangeWatchedFiles((event) => { const path = resolveVirtualPath(change.uri); deleteVirtual(path); notifyDeleteUri(change.uri); - connection.sendDiagnostics({ + sendDiagnostics({ uri: change.uri, diagnostics: [], }); @@ -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)); @@ -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: [], }); @@ -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; @@ -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), ); } } @@ -770,7 +775,7 @@ function checkImmediate(uri: string | TextDocument): boolean { }); Object.entries(diagnosticMap).forEach(([path, diagnostics]) => { - connection.sendDiagnostics({ + sendDiagnostics({ uri: URI.file(path).toString(), diagnostics, }); @@ -778,7 +783,7 @@ function checkImmediate(uri: string | TextDocument): boolean { return true; } catch (err) { console.error(`Error while compiling Motoko file: ${err}`); - connection.sendDiagnostics({ + sendDiagnostics({ uri: typeof uri === 'string' ? uri : uri.uri, diagnostics: [ { @@ -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, }; }); @@ -1275,6 +1309,16 @@ connection.onRequest(DEPLOY_PLAYGROUND, (params) => ), ); +const diagnosticMap = new Map(); +async function sendDiagnostics(params: { + uri: string; + diagnostics: Diagnostic[]; +}) { + const { uri, diagnostics } = params; + diagnosticMap.set(uri, diagnostics); + return connection.sendDiagnostics(params); +} + let validatingTimeout: ReturnType; let validatingUri: string | undefined; documents.onDidChangeContent((event) => { @@ -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: [], }); diff --git a/src/server/utils.ts b/src/server/utils.ts index 9d9f3fa..f57be33 100644 --- a/src/server/utils.ts +++ b/src/server/utils.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'; import { join, sep } from 'path'; import * as motokoPlugin from 'prettier-plugin-motoko'; import * as prettier from 'prettier/standalone'; +import { Position, Range } from 'vscode-languageserver/node'; import { URI, Utils } from 'vscode-uri'; const fileSeparatorPattern = new RegExp(sep.replace(/[/\\]/g, '\\$&'), 'g'); @@ -47,6 +48,9 @@ export function tryGetFileText(uri: string): string | null { } } +/** + * Formats a Motoko code snippet. + */ export function formatMotoko(source: string): string { try { return prettier.format(source, { @@ -59,6 +63,9 @@ export function formatMotoko(source: string): string { } } +/** + * Gets the relative path from one URI to another. + */ export function getRelativeUri(from: string, to: string): string { if (from === to) { // Fix vulnerability with `url-relative` package (https://security.snyk.io/vuln/SNYK-JS-URLRELATIVE-173691) @@ -67,9 +74,37 @@ export function getRelativeUri(from: string, to: string): string { return require('url-relative')(from, to); } +/** + * Gets the absolute URI from the given input paths (similar to `path.resolve()`). + */ export function getAbsoluteUri(base: string, ...paths: string[]): string { // if (/^[a-z]+:/i.test(path)) { // return path; // } return Utils.joinPath(URI.parse(base), ...paths).toString(); } + +/** + * Checks whether a VS Code `Range` contains the given `Position`. + */ +export function rangeContainsPosition( + range: Range, + position: Position, +): boolean { + if (position.line < range.start.line || position.line > range.end.line) { + return false; + } + if ( + position.line === range.start.line && + position.character < range.start.character + ) { + return false; + } + if ( + position.line === range.end.line && + position.character >= range.end.character + ) { + return false; + } + return true; +}