Skip to content

Commit

Permalink
Implement "outline view" and "go to symbol" functionality (#232)
Browse files Browse the repository at this point in the history
* Implement basic document and workspace symbol providers

* Refactor

* Add high-level syntax wrapper for object block expressions

* Support nested document symbols

* Improve type system constraints

* Remove debug console message

* Resolve symbol kind based on expression type

* Refactor

* Refactor 'exportFields' -> 'namedExports'

* Fix circular imports

* Filter by symbol name instead of filename

* Simplify

* 0.13.9

* Add 'rust-cache' to CI
  • Loading branch information
rvanasa authored Aug 14, 2023
1 parent 571c5c7 commit 1ec8213
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 25 deletions.
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- uses: Swatinem/rust-cache@v2
- run: npm ci
- run: npm run compile
- run: npm test
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion 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.13.8",
"version": "0.13.9",
"publisher": "dfinity-foundation",
"repository": "https://github.com/dfinity/vscode-motoko",
"engines": {
Expand Down
10 changes: 6 additions & 4 deletions src/server/ast.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AST } from 'motoko/lib/ast';
import { getContext } from './context';
import { Context } from './context';
import { Program, fromAST } from './syntax';
import { resolveVirtualPath, tryGetFileText } from './utils';

Expand All @@ -16,12 +16,14 @@ export interface AstImport {
field?: string;
}

const globalCache = new Map<string, AstStatus>(); // Share non-typed ASTs across all contexts
export const globalASTCache = new Map<string, AstStatus>(); // Share non-typed ASTs across all contexts

export default class AstResolver {
private readonly _cache = globalCache;
private readonly _cache = globalASTCache;
private readonly _typedCache = new Map<string, AstStatus>();

constructor(private readonly context: Context) {}

clear() {
this._cache.clear();
this._typedCache.clear();
Expand Down Expand Up @@ -55,7 +57,7 @@ export default class AstResolver {
status.text = text;
}
try {
const { motoko } = getContext(uri);
const { motoko } = this.context;
const virtualPath = resolveVirtualPath(uri);
let ast: AST;
try {
Expand Down
2 changes: 1 addition & 1 deletion src/server/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class Context {
constructor(uri: string, motoko: Motoko) {
this.uri = uri;
this.motoko = motoko;
this.astResolver = new AstResolver();
this.astResolver = new AstResolver(this);
this.importResolver = new ImportResolver(this);
}
}
Expand Down
9 changes: 2 additions & 7 deletions src/server/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,14 @@ interface ResolvedField {
}

export default class ImportResolver {
public readonly context: Context;

// module name -> uri
private readonly _moduleNameUriMap = new MultiMap<string, string>(Set);
// uri -> resolved field
private readonly _fieldMap = new MultiMap<string, ResolvedField>(Set);
// import path -> file system uri
private readonly _fileSystemMap = new Map<string, string>();

constructor(context: Context) {
this.context = context;
}
constructor(private readonly context: Context) {}

clear() {
this._moduleNameUriMap.clear();
Expand All @@ -39,7 +35,7 @@ export default class ImportResolver {
this._fileSystemMap.set(importUri, uri);
if (program?.export) {
// Resolve field names
const { ast } = program.export;
const ast = program.export;
const node =
matchNode(ast, 'LetD', (_pat: Node, exp: Node) => exp) || // Named
matchNode(ast, 'ExpD', (exp: Node) => exp); // Unnamed
Expand All @@ -58,7 +54,6 @@ export default class ImportResolver {
return;
}
const [dec, visibility] = field.args!;
// TODO: `system` visibility
if (visibility !== 'Public') {
return;
}
Expand Down
5 changes: 5 additions & 0 deletions src/server/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export function findMostSpecificNodeForPosition(
return node as (Node & { start: Span; end: Span }) | undefined;
}

export function defaultRange(): Range {
const pos = Position.create(0, 0);
return Range.create(pos, pos);
}

export function rangeFromNode(
node: Node | undefined,
multiLineFromBeginning = false,
Expand Down
67 changes: 62 additions & 5 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
CompletionList,
Diagnostic,
DiagnosticSeverity,
DocumentSymbol,
FileChangeType,
InitializeResult,
Location,
Expand All @@ -23,11 +24,13 @@ import {
Range,
ReferenceParams,
SignatureHelp,
SymbolKind,
TextDocumentPositionParams,
TextDocumentSyncKind,
TextDocuments,
TextEdit,
WorkspaceFolder,
WorkspaceSymbol,
createConnection,
} from 'vscode-languageserver/node';
import { URI } from 'vscode-uri';
Expand All @@ -38,6 +41,7 @@ import {
TestResult,
} from '../common/connectionTypes';
import { watchGlob as virtualFilePattern } from '../common/watchConfig';
import { globalASTCache } from './ast';
import {
Context,
addContext,
Expand All @@ -49,13 +53,14 @@ import DfxResolver from './dfx';
import { organizeImports } from './imports';
import { getAstInformation } from './information';
import {
defaultRange,
findDefinition,
findMostSpecificNodeForPosition,
locationFromDefinition,
rangeFromNode,
} from './navigation';
import { deployPlayground } from './playground';
import { Program, asNode, findNodes } from './syntax';
import { Field, ObjBlock, Program, asNode, findNodes } from './syntax';
import {
formatMotoko,
getFileText,
Expand Down Expand Up @@ -434,7 +439,8 @@ connection.onInitialize((event): InitializeResult => {
},
hoverProvider: true,
// executeCommandProvider: { commands: [] },
// workspaceSymbolProvider: true,
workspaceSymbolProvider: true,
documentSymbolProvider: true,
// diagnosticProvider: {
// documentSelector: ['motoko'],
// interFileDependencies: true,
Expand Down Expand Up @@ -1189,9 +1195,60 @@ connection.onDefinition(
// },
// );

// connection.onWorkspaceSymbol((_event) => {
// return [];
// });
connection.onWorkspaceSymbol((event) => {
if (!event.query.length) {
return [];
}
const results: WorkspaceSymbol[] = [];
const visitDocumentSymbol = (
uri: string,
symbol: DocumentSymbol,
parent?: DocumentSymbol,
) => {
results.push({
name: symbol.name,
kind: symbol.kind,
location: Location.create(uri, symbol.range),
containerName: parent?.name,
});
symbol.children?.forEach((s) => visitDocumentSymbol(uri, s, symbol));
};
globalASTCache.forEach((status) => {
status.program?.namedExports.forEach((field) => {
visitDocumentSymbol(status.uri, getDocumentSymbol(field));
});
});
return results;
});

connection.onDocumentSymbol((event) => {
const { uri } = event.textDocument;
const results: DocumentSymbol[] = [];
const status = getContext(uri).astResolver.request(uri);
status?.program?.namedExports.forEach((field) => {
results.push(getDocumentSymbol(field));
});
return results;
});

function getDocumentSymbol(field: Field): DocumentSymbol {
const range = rangeFromNode(asNode(field.ast)) || defaultRange();
const kind =
field.exp instanceof ObjBlock ? SymbolKind.Module : SymbolKind.Field;
const children: DocumentSymbol[] = [];
if (field.exp instanceof ObjBlock) {
field.exp.fields.forEach((field) => {
children.push(getDocumentSymbol(field));
});
}
return {
name: field.name,
kind,
range,
selectionRange: rangeFromNode(asNode(field.pat?.ast)) || range,
children,
};
}

connection.onReferences(
async (_event: ReferenceParams): Promise<Location[]> => {
Expand Down
65 changes: 60 additions & 5 deletions src/server/syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,52 @@ export function fromAST(ast: AST): Syntax {
});
if (ast.args.length) {
const export_ = ast.args[ast.args.length - 1];
prog.export = fromAST(export_);
if (export_) {
prog.export = export_;
prog.namedExports.push(...getFieldsFromAST(export_));
}
}
}
return prog;
} else if (ast.name === 'ObjBlockE' && ast.args) {
const sort = ast.args[0] as ObjSort;
const fields = ast.args.slice(1) as Node[];

const obj = new ObjBlock(ast, sort);
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;
// }
obj.fields.push(...getFieldsFromAST(dec));
});
return obj;
} else {
return new Syntax(ast);
}
}

function getFieldsFromAST(ast: AST): Field[] {
const fields: [string, Node, Node][] =
matchNode(ast, 'LetD', (pat: Node, exp: Node) => {
const name = matchNode(pat, 'VarP', (field: string) => field);
return name ? [[name, pat, exp]] : undefined;
}) || [];
return fields.map(([name, pat, exp]) => {
const field = new Field(ast, name);
field.pat = fromAST(pat);
field.exp = fromAST(exp);
return field;
});
}

export function asNode(ast: AST | undefined): Node | undefined {
return ast && typeof ast === 'object' && !Array.isArray(ast)
? ast
Expand Down Expand Up @@ -121,17 +158,35 @@ export class Syntax {

export class Program extends Syntax {
imports: Import[] = [];
export: Syntax | undefined;
namedExports: Field[] = [];
export: AST | undefined;
}

export type ObjSort = 'Object' | 'Actor' | 'Module' | 'Memory';

export class ObjBlock extends Syntax {
fields: Field[] = [];

constructor(ast: AST, public sort: ObjSort) {
super(ast);
}
}

export class Field extends Syntax {
pat: Syntax | undefined;
exp: Syntax | undefined;

constructor(ast: AST, public name: string) {
super(ast);
}
}

export class Import extends Syntax {
name: string | undefined;
fields: [string, string][] = []; // [name, alias]
path: string;

constructor(ast: AST, path: string) {
constructor(ast: AST, public path: string) {
super(ast);
this.path = path;
}
}

Expand Down

0 comments on commit 1ec8213

Please sign in to comment.