diff --git a/README.md b/README.md index 09cd24f..059670b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Erlang/OTP Support for Visual Studio Code -This extension provides Erlang/OTP support for [Visual Studio Code](https://code.visualstudio.com/)and is available at the [Marketplace](https://marketplace.visualstudio.com/items?itemName=yuce.erlang-otp). +This experimantal extension provides Erlang/OTP support for [Visual Studio Code](https://code.visualstudio.com/). ## News @@ -19,7 +19,6 @@ setting `erlang.enableExperimentalAutoComplete` to `true` in your user settings ## Planned Features * Build support -* Erlang shell ## Work In Progress diff --git a/package.json b/package.json index b75be4c..a02a13e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "erlang-otp", "displayName": "Erlang/OTP", "description": "Erlang/OTP support with syntax highlighting, auto-indent and snippets", - "version": "0.2.1", + "version": "0.2.2", "author": { "name": "Yuce Tekol" }, @@ -59,6 +59,9 @@ "typescript": "^1.7.5", "vscode": "^0.11.0" }, + "dependencies": { + "whatels": "^0.2.2" + }, "repository": { "type": "git", "url": "https://github.com/yuce/erlang-vscode" diff --git a/src/completion_provider.ts b/src/completion_provider.ts index a20c72e..781a12c 100644 --- a/src/completion_provider.ts +++ b/src/completion_provider.ts @@ -3,22 +3,39 @@ import {CompletionItemProvider, TextDocument, Position, CancellationToken, CompletionItem, CompletionItemKind} from 'vscode'; - -let fs = require('fs'); +import {Symbols, FunctionInfo, CallbackAction} from 'whatels'; +import {WhatelsClient} from './whatels_client'; +import fs = require('fs'); +import path = require('path'); const RE_MODULE = /(\w+):$/; -interface FunctionCompletionData { - name: string; - // detail: string; -} - export class ErlangCompletionProvider implements CompletionItemProvider { - private modules:any = null; + private stdModules: any = null; private moduleNames: string[] = null; + private docPath: string; private genericCompletionItems: CompletionItem[] = null; + private moduleCompletionItems: CompletionItem[] = null; + private stdLibCompletionItems: CompletionItem[] = null; + private workspaceCompletionItems: CompletionItem[] = null; - constructor(private completionPath: string) {} + constructor(private whatelsClient: WhatelsClient, + private completionPath: string) + { + whatelsClient.subscribe((action, msg) => { + if (action == CallbackAction.getSymbols) { + this.genericCompletionItems = null; + if (msg.path == this.docPath) { + // invalidate completion items of the current doc + this.docPath = ''; + this.moduleCompletionItems = null; + } + } + else if (action == CallbackAction.discardPath) { + this.genericCompletionItems = null; + } + }); + } public provideCompletionItems(doc: TextDocument, pos: Position, @@ -27,67 +44,144 @@ export class ErlangCompletionProvider implements CompletionItemProvider { return new Promise((resolve, reject) => { const line = doc.lineAt(pos.line); const m = RE_MODULE.exec(line.text.substring(0, pos.character)); - if (this.modules === null) { - this.readCompletionJson(this.completionPath, modules => { - this.modules = modules; - (m === null)? - this.resolveModuleNames(resolve) - : this.resolveFunNames(m[1], resolve); - }); + if (m === null) { + this.resolveGenericItems(resolve, reject, doc.fileName); } else { - (m === null)? - this.resolveModuleNames(resolve) - : this.resolveFunNames(m[1], resolve); + resolve([]); + // const moduleName = m[1]; + // this.whatelsClient.getAllPathSymbols().then( + // pathSymbols => this.resolveModuleItems(resolve, moduleName, pathSymbols), + // err => reject(err) + // ); + // if (this.stdModules === null) { + // this.readCompletionJson(this.completionPath, modules => { + // this.stdModules = modules; + // this.resolveFunNames(m[1], resolve); + // }); + // } + // else { + // this.resolveFunNames(m[1], resolve); + // } } }); } - private resolveFunNames(module, resolve) { - resolve(this.makeModuleFunsCompletion(module)); + private resolveGenericItems(resolve, reject, path: string) { + this.getGenericCompletionItems(path).then( + items => resolve(items), + err => reject(err) + ) } - private resolveModuleNames(resolve) { - if (!this.genericCompletionItems) { - this.genericCompletionItems = this.makeGenericCompletion(); - } - resolve(this.genericCompletionItems); + private getGenericCompletionItems(path: string): Thenable { + return new Promise((resolve, reject) => { + if (this.genericCompletionItems) { + resolve(this.genericCompletionItems); + } + else { + let cis: CompletionItem[] = []; + Promise.all([this.getModuleCompletionItems(path), + this.getWorkspaceCompletionItems(), + this.getStdLibCompletionItems()]).then( + allCompletionItems => { + allCompletionItems.forEach(items => { + items.forEach(ci => cis.push(ci)); + }); + resolve(cis); + }, + err => reject(err) + ); + this.genericCompletionItems = cis; + } + }); } - private makeFunctionCompletionItem(name: string): CompletionItem { - const item = new CompletionItem(name); - // item.documentation = cd.detail; - item.kind = CompletionItemKind.Function; - return item; + private getModuleCompletionItems(path: string): Thenable { + return new Promise((resolve, reject) => { + if (this.moduleCompletionItems && path == this.docPath) { + resolve(this.moduleCompletionItems); + } + else { + this.whatelsClient.getPathSymbols(path).then( + symbols => { + this.docPath = path; + resolve(this.createModuleCompletionItems(path, symbols)); + }, + err => reject(err) + ); + } + }); } - private makeModuleNameCompletionItem(name: string): CompletionItem { - const item = new CompletionItem(name); - item.kind = CompletionItemKind.Module; - return item; + private getStdLibCompletionItems(): Thenable { + return new Promise((resolve, reject) => { + if (this.stdLibCompletionItems) { + resolve(this.stdLibCompletionItems); + } + else { + this.readCompletionJson(this.completionPath, modules => { + this.stdModules = modules; + resolve(this.createStdLibCompletionItems(modules)); + }); + } + }); } - private makeModuleFunsCompletion(module: string): CompletionItem[] { - const moduleFuns = this.modules[module] || []; - return moduleFuns.map(name => { - return this.makeFunctionCompletionItem(name); + private getWorkspaceCompletionItems(): Thenable { + return new Promise((resolve, reject) => { + if (this.workspaceCompletionItems) { + resolve(this.workspaceCompletionItems); + } + else { + this.whatelsClient.getAllPathSymbols().then( + pathSymbols => resolve(this.createWorkspaceCompletionItems(pathSymbols)), + err => reject(err) + ); + } }); } - private makeGenericCompletion(): CompletionItem[] { - const modules = this.modules || {}; - const names = []; - for (let k in modules) { - names.push(k); + private createModuleCompletionItems(path: string, symbols: Symbols) { + let cis: CompletionItem[] = []; + if (symbols && symbols.functions) { + let funNames = new Set(symbols.functions.map(f => { + return f.name; + })); + funNames.forEach(name => { + var item = new CompletionItem(name); + item.kind = CompletionItemKind.Function; + cis.push(item); + }); } - names.sort(); - return names.map(name => { - return this.makeModuleNameCompletionItem(name); - }); + return this.moduleCompletionItems = cis; + } + + private createStdLibCompletionItems(modules) { + let cis: CompletionItem[] = []; + for (var k in modules) { + var item = new CompletionItem(k); + item.kind = CompletionItemKind.Module; + cis.push(item); + } + return this.stdLibCompletionItems = cis; + } + + private createWorkspaceCompletionItems(pathSymbols) { + let cis: CompletionItem[] = []; + for (var p in pathSymbols) { + var item = new CompletionItem(path.basename(p, '.erl')); + item.kind = CompletionItemKind.Module; + cis.push(item); + } + console.log('createWorkspaceCompletionItems'); + console.log(cis); + console.log(this); + return this.workspaceCompletionItems = cis; } private readCompletionJson(filename: string, done: Function): any { - fs.readFile(filename, (err, data) => { + fs.readFile(filename, 'utf8', (err, data) => { if (err) { console.log(`Cannot read: ${filename}`); done({}); diff --git a/src/extension.ts b/src/extension.ts index 4f9c215..8974c49 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -32,7 +32,9 @@ import {ExtensionContext, Disposable, workspace, window, languages, Hover} from 'vscode'; import {ErlangCompletionProvider} from './completion_provider'; -// import {range, debounce} from 'lodash'; +import {ErlangDocumentSymbolProvider, ErlangWorkspaceDocumentSymbolProvider} from './symbol_provider'; +import {WhatelsClient} from './whatels_client'; + export function activate(ctx: ExtensionContext) { languages.setLanguageConfiguration('erlang', { @@ -64,13 +66,27 @@ export function activate(ctx: ExtensionContext) { // enable auto completion let config = workspace.getConfiguration('erlang'); if (config['enableExperimentalAutoComplete']) { - let completionJsonPath = ctx.asAbsolutePath("./priv/erlang-libs.json"); + const whatelsClient = createWhatelsClient(workspace.rootPath); + ctx.subscriptions.push(whatelsClient); + const completionJsonPath = ctx.asAbsolutePath("./priv/erlang-libs.json"); ctx.subscriptions.push(languages.registerCompletionItemProvider({ language: 'erlang' - }, new ErlangCompletionProvider(completionJsonPath), ':')); + }, new ErlangCompletionProvider(whatelsClient, completionJsonPath), ':')); + ctx.subscriptions.push(languages.registerDocumentSymbolProvider({ + language: 'erlang' + }, new ErlangDocumentSymbolProvider(whatelsClient))); + ctx.subscriptions.push(languages.registerWorkspaceSymbolProvider( + new ErlangWorkspaceDocumentSymbolProvider(whatelsClient) + )); } } export function deactivate() { } +function createWhatelsClient(rootPath: string) { + const wc = new WhatelsClient(); + wc.watch(`${rootPath}/src/*.erl`); + wc.watch(`${rootPath}/apps/**/src/*.erl`); + return wc; +} diff --git a/src/symbol_provider.ts b/src/symbol_provider.ts new file mode 100644 index 0000000..bf301bb --- /dev/null +++ b/src/symbol_provider.ts @@ -0,0 +1,134 @@ +import {DocumentSymbolProvider, WorkspaceSymbolProvider, TextDocument, + CancellationToken, SymbolInformation, SymbolKind, + Range, Uri} from 'vscode'; +import {Symbols, CallbackAction} from 'whatels'; +import {WhatelsClient} from './whatels_client'; + + +export class ErlangDocumentSymbolProvider implements DocumentSymbolProvider { + private symbols: SymbolInformation[] = null; + private symbolsPath: string; + + constructor(private whatelsClient: WhatelsClient) { + let cb = (action: CallbackAction, msg: any) => { + if (action == CallbackAction.getSymbols && msg.path == this.symbolsPath) { + console.log(`ErlangDocumentSymbolProvider: Invalidating symbols - ${this.symbolsPath}`); + this.symbols = null; + this.symbolsPath = ''; + } + } + whatelsClient.subscribe(cb); + } + + public provideDocumentSymbols(doc: TextDocument, token: CancellationToken) + :Thenable + { + return new Promise((resolve, reject) => { + console.log('ErlangDocumentSymbolProvider: get doc symbol informations'); + if (!this.symbols || this.symbolsPath != doc.fileName) { + this.whatelsClient.getPathSymbols(doc.fileName).then( + symbols => { + this.symbolsPath = doc.fileName; + this.createSymbolInformations(symbols); + this.resolveItems(resolve); + }, + err => reject(err) + ) + } + else { + this.resolveItems(resolve); + } + }); + } + + private resolveItems(resolve) { + resolve(this.symbols || []); + } + + private createSymbolInformations(symbols: Symbols) { + if (!symbols) { + this.symbols = null; + return; + } + // TODO: sort symbols by name + this.symbols = symbols.functions.map(f => { + let range = new Range(f.line - 1, 0, f.line - 1, 0); + let name = `${f.name}/${f.arity}`; + return new SymbolInformation(name, SymbolKind.Function, range); + }); + } +} + +export class ErlangWorkspaceDocumentSymbolProvider implements WorkspaceSymbolProvider { + private symbols: {[index: string]: SymbolInformation[]} = null; + + constructor(private whatelsClient: WhatelsClient) { + let cb = (action: CallbackAction, msg: any) => { + if (action == CallbackAction.getSymbols || action == CallbackAction.discardPath) { + if (this.symbols) { + console.log(`ErlangWorkspaceDocumentSymbolProvider: Invalidating symbols - ${msg.path}`); + this.symbols[msg.path] = null; + } + + } + } + whatelsClient.subscribe(cb); + } + + public provideWorkspaceSymbols(query: string, token: CancellationToken) + :SymbolInformation[] | Thenable + { + return new Promise((resolve, reject) => { + console.log('ErlangWorkspaceDocumentSymbolProvider: get doc symbol informations'); + if (!this.symbols) { + this.whatelsClient.getAllPathSymbols().then( + symbols => { + this.createSymbolInformations(symbols); + this.resolveItems(resolve, query); + }, + err => reject(err) + ); + } + else { + this.resolveItems(resolve, query); + } + }); + } + + private resolveItems(resolve, query) { + let sis: SymbolInformation[] = []; + if (!this.symbols) { + resolve([]); + } + for (var k in this.symbols) { + var symbols = this.symbols[k] || []; + symbols.forEach(sym => { + if (sym.name.indexOf(query) >= 0) { + sis.push(sym); + } + }); + } + resolve(sis); + } + + private createSymbolInformations(pathSymbols: {[index: string]: Symbols}) { + if (!this.symbols) { + this.symbols = {}; + } + for (var path in pathSymbols) { + this.symbols[path] = []; + var symbols = pathSymbols[path]; + pathSymbols[path].functions.forEach(f => { + const range = new Range(f.line - 1, 0, f.line - 1, 0); + const uri = Uri.file(path); + const name = `${symbols.module}:${f.name}/${f.arity}`; + var si = new SymbolInformation(name, + SymbolKind.Function, + range, + uri); + this.symbols[path].push(si); + }); + } + } + +} \ No newline at end of file diff --git a/src/whatels_client.ts b/src/whatels_client.ts new file mode 100644 index 0000000..d580793 --- /dev/null +++ b/src/whatels_client.ts @@ -0,0 +1,72 @@ + +import {Disposable} from 'vscode'; +import whatels = require('whatels'); + + +export class WhatelsClient implements Disposable { + private wConn: whatels.Connection; + private port: number; + private subscribers: whatels.CallbackFunction[] = []; + + constructor(refreshTime?: number, port?: number) { + this.port = port || 10998; + } + + public subscribe(callback: whatels.CallbackFunction) { + this.subscribers.push(callback); + } + + public getPathSymbols(path: string): Thenable { + return new Promise((resolve, reject) => { + this._connect().then( + conn => resolve(conn.getPathSymbols(path)), + err => reject(err) + ) + }); + } + + public getAllPathSymbols(): Thenable<{[index: string]: whatels.Symbols}> { + return new Promise<{[index: string]: whatels.Symbols}>((resolve, reject) => { + this._connect().then( + conn => resolve(conn.getAllPathSymbols()), + err => reject(err) + ) + }); + } + + public watch(wildcard: string) { + console.log('watch: ', wildcard); + this._connect().then( + conn => { + conn.watch(wildcard); + }, + err => console.error(err) + ); + } + + public dispose() { + this.wConn.close(); + this.wConn = null; + } + + private _connect(): Thenable { + let callback = (action: whatels.CallbackAction, msg: any) => { + this.subscribers.forEach(cb => { + cb(action, msg); + }); + } + + return new Promise((resolve, reject) => { + if (this.wConn) { + resolve(this.wConn); + } + else { + this.wConn = new whatels.Connection(this.port); + this.wConn.connect(callback).then( + () => resolve(this.wConn), + err => reject(err) + ); + } + }); + } +} \ No newline at end of file