diff --git a/src/server/imports.ts b/src/server/imports.ts index d246bd5..a3ce881 100644 --- a/src/server/imports.ts +++ b/src/server/imports.ts @@ -1,21 +1,12 @@ import { pascalCase } from 'change-case'; import { MultiMap } from 'mnemonist'; -import { AST, Node } from 'motoko/lib/ast'; import { Context, getContext } from './context'; -import { Import, Program, matchNode } from './syntax'; +import { Import } from './syntax'; import { formatMotoko, getRelativeUri } from './utils'; -interface ResolvedField { - name: string; - visibility: string; - ast: AST; -} - export default class ImportResolver { // module name -> uri private readonly _moduleNameUriMap = new MultiMap(Set); - // uri -> resolved field - private readonly _fieldMap = new MultiMap(Set); // import path -> file system uri private readonly _fileSystemMap = new Map(); @@ -25,7 +16,7 @@ export default class ImportResolver { this._moduleNameUriMap.clear(); } - update(uri: string, program: Program | undefined): boolean { + update(uri: string): boolean { const info = getImportInfo(uri, this.context); if (!info) { return false; @@ -33,49 +24,6 @@ export default class ImportResolver { const [name, importUri] = info; this._moduleNameUriMap.set(name, importUri); this._fileSystemMap.set(importUri, uri); - if (program?.export) { - // Resolve field names - const { ast } = program.export; - const node = - matchNode(ast, 'LetD', (_pat: Node, exp: Node) => exp) || // Named - matchNode(ast, 'ExpD', (exp: Node) => exp); // Unnamed - if (node) { - matchNode( - node, - 'ObjBlockE', - (_type: string, ...fields: Node[]) => { - this._fieldMap.delete(uri); - fields.forEach((field) => { - if (field.name !== 'DecField') { - console.error( - 'Error: expected `DecField`, received', - field.name, - ); - return; - } - const [dec, visibility] = field.args!; - if (visibility !== 'Public') { - return; - } - matchNode(dec, 'LetD', (pat: Node, exp: Node) => { - const name = matchNode( - pat, - 'VarP', - (field: string) => field, - ); - if (name) { - this._fieldMap.set(uri, { - name, - visibility, - ast: exp, - }); - } - }); - }); - }, - ); - } - } return true; } @@ -92,9 +40,6 @@ export default class ImportResolver { changed = true; } } - if (this._fieldMap.delete(uri)) { - changed = true; - } return changed; } @@ -110,31 +55,8 @@ export default class ImportResolver { * Finds all available module-level imports. * @returns Array of `[name, path]` entries */ - getNameEntries(uri: string): [string, string][] { - return [...this._moduleNameUriMap.entries()].map(([name, path]) => [ - name, - getRelativeUri(uri, path), - ]); - } - - // /** - // * Finds all importable fields. - // * @returns Array of `[name, field, path]` entries - // */ - // getFieldEntries(uri: string): [ResolvedField, string][] { - // return [...this._fieldMap.entries()].map(([path, field]) => [ - // field, - // getRelativeUri(uri, path), - // ]); - // } - - /** - * Finds all importable fields for a given document. - * @returns Array of `[name, field, path]` entries - */ - getFields(uri: string): ResolvedField[] { - const fields = this._fieldMap.get(uri); - return fields ? [...fields] : []; + getNameEntries(): [string, string][] { + return [...this._moduleNameUriMap.entries()]; } /** diff --git a/src/server/navigation.ts b/src/server/navigation.ts index 6d28927..88785dc 100644 --- a/src/server/navigation.ts +++ b/src/server/navigation.ts @@ -25,7 +25,7 @@ export function findMostSpecificNodeForPosition( ast: AST, position: Position, scoreFn: (node: Node) => number | boolean, - isMouseCursor = false, + deep = false, ): (Node & { start: Span; end: Span }) | undefined { const nodes = findNodes( ast, @@ -38,7 +38,7 @@ export function findMostSpecificNodeForPosition( (position.line !== node.start[0] - 1 || position.character >= node.start[1]) && (position.line !== node.end[0] - 1 || - position.character < node.end[1] + (isMouseCursor ? 0 : 1)), + position.character < node.end[1] + (deep ? 0 : 1)), ); // Find the most specific AST node for the cursor position @@ -124,7 +124,7 @@ const nodePriorities: Record = { export function findDefinition( uri: string, position: Position, - isMouseCursor = false, + deep = false, ): Definition | undefined { // Get relevant AST node const context = getContext(uri); @@ -141,7 +141,7 @@ export function findDefinition( status.ast, position, (node) => nodePriorities[node.name] || 0, - isMouseCursor, + deep, ); if (!node) { return; @@ -266,7 +266,7 @@ function followImport( console.log('Missing export for', uri); return; } - if (status?.outdated) { + if (status.outdated) { console.log('Outdated AST for', uri); return; } diff --git a/src/server/server.ts b/src/server/server.ts index fd4dbbd..a2c6864 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -65,7 +65,6 @@ import { Class, Field, ObjBlock, - Program, SyntaxWithFields, Type, asNode, @@ -74,6 +73,7 @@ import { import { formatMotoko, getFileText, + getRelativeUri, rangeContainsPosition, resolveFilePath, resolveVirtualPath, @@ -829,14 +829,12 @@ function notifyWriteUri(uri: string, content: string) { contexts.forEach((context) => { const { astResolver, importResolver } = context; - let program: Program | undefined; try { astResolver.notify(uri, content); - // program = astResolver.request(uri)?.program; // TODO: re-enable for field imports } catch (err) { console.error(`Error while parsing (${uri}): ${err}`); } - importResolver.update(uri, program); + importResolver.update(uri); }); } } @@ -923,6 +921,16 @@ connection.onSignatureHelp((): SignatureHelp | null => { return null; }); +function getCompletionItemKind(field: Field): CompletionItemKind { + if (field.exp instanceof Class) { + return CompletionItemKind.Class; + } + if (field.exp instanceof ObjBlock) { + return CompletionItemKind.Module; + } + return CompletionItemKind.Variable; +} + connection.onCompletion((event) => { const { position } = event; const { uri } = event.textDocument; @@ -939,37 +947,36 @@ connection.onCompletion((event) => { ?.slice(1) ?? ['', '']; if (!dot) { - context.importResolver - .getNameEntries(uri) - .forEach(([name, path]) => { - if (name.startsWith(identStart)) { - const status = context.astResolver.request(uri); - const existingImport = status?.program?.imports.find( - (i) => - i.name === name || - i.fields.some(([, alias]) => alias === name), - ); - if (existingImport || !status?.program) { - // Skip alternatives with already imported name - return; - } - const edits: TextEdit[] = [ - TextEdit.insert( - findNewImportPosition(uri, context), - `import ${name} "${path}";\n`, - ), - ]; - list.items.push({ - label: name, - detail: path, - insertText: name, - kind: path.startsWith('mo:') - ? CompletionItemKind.Module - : CompletionItemKind.Class, // TODO: resolve actors, classes, etc. - additionalTextEdits: edits, - }); + context.importResolver.getNameEntries().forEach(([name, path]) => { + path = getRelativeUri(uri, path); + if (name.startsWith(identStart)) { + const status = context.astResolver.request(uri); + const existingImport = status?.program?.imports.find( + (i) => + i.name === name || + i.fields.some(([, alias]) => alias === name), + ); + if (existingImport || !status?.program) { + // Skip alternatives with already imported name + return; } - }); + const edits: TextEdit[] = [ + TextEdit.insert( + findNewImportPosition(uri, context), + `import ${name} "${path}";\n`, + ), + ]; + list.items.push({ + label: name, + detail: path, + insertText: name, + kind: path.startsWith('mo:') + ? CompletionItemKind.Module + : CompletionItemKind.Class, // TODO: resolve actors, classes, etc. + additionalTextEdits: edits, + }); + } + }); if (identStart) { keywords.forEach((keyword) => { @@ -1003,39 +1010,85 @@ connection.onCompletion((event) => { }); }); } + } else { + // Check for an identifier before the dot (e.g. `Module.abc`) + const end = position.character - dot.length - identStart.length; + const preMatch = /(\s*\.\s*)?([a-zA-Z_][a-zA-Z0-9_]*)$/.exec( + lines[position.line].substring(0, end), + ); + if (preMatch) { + const [, preDot, preIdent] = preMatch; + const targetFields: Field[] = []; + const definition = findDefinition( + uri, + { + line: position.line, + character: position.character - identStart.length - 1, + }, + true, + ); + if (definition) { + // TODO: find a workaround for outdated ASTs + console.log('>>>>>', definition.body); //// + } else if (!preDot) { + context.importResolver + .getNameEntries() + .forEach(([name, path]) => { + const importUri = + context.importResolver.getFileSystemURI(path); + if (!importUri) { + console.warn( + 'File system URI not found for path:', + path, + ); + return; + } + // TODO: check if imported with a different name + if (name !== preIdent) { + return; + } + const status = + context.astResolver.request(importUri); + if (!status) { + return; + } + const exportFields = status.program?.exportFields; + if (!exportFields?.length) { + return; + } + exportFields.forEach((exportField) => { + if (exportField.exp instanceof ObjBlock) { + targetFields.push( + ...exportField.exp.fields, + ); + } + }); + }); + } + + // Display resolved fields + targetFields.forEach((field) => { + const { name, visibility, ast } = field; + if (visibility !== 'public') { + return; + } + if (name?.startsWith(identStart)) { + const docComment = findDocComment(asNode(ast)); + list.items.push({ + label: name, + detail: docComment, + insertText: name, + kind: getCompletionItemKind(field), // TODO: resolve actors, classes, etc. + documentation: docComment && { + kind: 'markdown', + value: docComment, + }, + // additionalTextEdits: import + }); + } + }); + } } - // else { - // // Check for an identifier before the dot (e.g. `Module.abc`) - // const end = position.character - dot.length - identStart.length; - // const preMatch = /(\s*\.\s*)?([a-zA-Z_][a-zA-Z0-9_]*)$/.exec( - // lines[position.line].substring(0, end), - // ); - // if (preMatch) { - // const [, preDot, preIdent] = preMatch; - // if (!preDot) { - // importResolver - // .getNameEntries(preIdent) - // .forEach(([name, uri]) => { - // const importUri = program?.imports.find()?.path; - // importResolver - // .getFields(uri) - // .forEach(([{ name }, path]) => { - // if (name.startsWith(identStart)) { - // list.items.push({ - // label: name, - // detail: path, - // insertText: name, - // kind: path.startsWith('mo:') - // ? CompletionItemKind.Module - // : CompletionItemKind.Class, // TODO: resolve actors, classes, etc. - // // additionalTextEdits: import - // }); - // } - // }); - // }); - // } - // } - // } } catch (err) { console.error('Error during autocompletion:'); console.error(err); @@ -1043,34 +1096,29 @@ connection.onCompletion((event) => { return list; }); -connection.onHover((event) => { - function findDocComment(node: Node): string | undefined { - const definition = findDefinition(uri, event.position, true); - let docNode: Node | undefined = definition?.cursor || node; - let depth = 0; // Max AST depth to display doc comment - while ( - !docNode.doc && - docNode.parent && - // Unresolved import - !( - docNode.name === 'LetD' && - asNode(docNode.args?.[1])?.name === 'ImportE' - ) && - depth < 2 - ) { - docNode = docNode.parent; - depth++; - } - if (docNode.name === 'Prog' && !docNode.doc) { - // Get doc comment at top of file - const doc = asNode(docNode.args?.[0])?.doc; - if (doc) { - return doc; - } - } - return docNode.doc; +function findDocComment(node: Node | undefined): string | undefined { + if (!node) { + return; + } + let depth = 0; // Max AST depth to display doc comment + while ( + !node.doc && + node.parent && + // Unresolved import + !(node.name === 'LetD' && asNode(node.args?.[1])?.name === 'ImportE') && + depth < 2 + ) { + node = node.parent; + depth++; } + if (node.name === 'Prog' && !node.doc) { + // Get doc comment at top of file + return asNode(node.args?.[0])?.doc; + } + return node.doc; +} +connection.onHover((event) => { const { position } = event; const { uri } = event.textDocument; const { astResolver } = getContext(uri); @@ -1119,7 +1167,8 @@ connection.onHover((event) => { ).trim(); // Doc comments - const doc = findDocComment(node); + const definition = findDefinition(uri, position, true); + const doc = findDocComment(definition?.cursor || node); if (doc) { const typeInfo = node.type ? formatMotoko(node.type).trim() diff --git a/src/server/syntax.ts b/src/server/syntax.ts index ec645f1..fea6fc7 100644 --- a/src/server/syntax.ts +++ b/src/server/syntax.ts @@ -87,7 +87,9 @@ export function fromAST(ast: AST): Syntax { const export_ = ast.args[ast.args.length - 1]; if (export_) { prog.export = fromAST(export_); - prog.exportFields.push(...getFieldsFromAST(export_)); + prog.exportFields.push( + ...getFieldsFromAST(export_, 'public'), + ); } } } @@ -105,26 +107,34 @@ export function fromAST(ast: AST): Syntax { ); return; } - const [dec, _visibility] = field.args!; - // if (visibility !== 'Public') { - // return; - // } - obj.fields.push(...getFieldsFromAST(dec)); + const [dec, visibilityAst] = field.args!; + const visibility: Visibility | undefined = matchNode( + visibilityAst, + 'Public', + () => 'public', + typeof visibilityAst === 'string' + ? ((visibilityAst as string).toLowerCase() as Visibility) + : undefined, + ); + if (!visibility) { + console.warn('Unexpected visibility AST node:', visibilityAst); + } + obj.fields.push(...getFieldsFromAST(dec, visibility || 'private')); }); return obj; } return new Syntax(ast); } -function getFieldsFromAST(ast: AST): Field[] { +function getFieldsFromAST(ast: AST, visibility: Visibility): Field[] { const simplyNamedFields = matchNode(ast, 'TypD', (name: string, type: Node) => { - const field = new Field(ast, new Type(type)); + const field = new Field(ast, new Type(type), visibility); field.name = name; return [field]; }) || matchNode(ast, 'VarD', (name: string, exp: Node) => { - const field = new Field(ast, new Type(exp)); + const field = new Field(ast, new Type(exp), visibility); field.name = name; return [field]; }) || @@ -149,10 +159,10 @@ function getFieldsFromAST(ast: AST): Field[] { const cls = new Class(ast, name, sort); decs.forEach((ast) => { matchNode(ast, 'DecField', (dec: Node) => { - cls.fields.push(...getFieldsFromAST(dec)); + cls.fields.push(...getFieldsFromAST(dec, visibility)); }); }); - const field = new Field(ast, cls); + const field = new Field(ast, cls, visibility); field.name = name; return [field]; }, @@ -173,13 +183,13 @@ function getFieldsFromAST(ast: AST): Field[] { fields.push([name, pat, exp]); }); return fields.map(([name, pat, exp]) => { - const field = new Field(ast, fromAST(exp)); + const field = new Field(ast, fromAST(exp), visibility); field.name = name; field.pat = fromAST(pat); return field; }); } else { - const field = new Field(ast, fromAST(exp)); + const field = new Field(ast, fromAST(exp), visibility); return [field]; } } @@ -253,6 +263,8 @@ export function matchNode( return defaultValue; } +export type Visibility = 'public' | 'private' | 'system'; + export class Syntax { ast: AST; @@ -289,7 +301,7 @@ export class Field extends Syntax { name: string | undefined; pat: Syntax | undefined; - constructor(ast: AST, public exp: Syntax) { + constructor(ast: AST, public exp: Syntax, public visibility: Visibility) { super(ast); } }