diff --git a/src/elixirDocumentSymbolProvider.ts b/src/elixirDocumentSymbolProvider.ts new file mode 100644 index 0000000..ca71c51 --- /dev/null +++ b/src/elixirDocumentSymbolProvider.ts @@ -0,0 +1,58 @@ +import * as vscode from 'vscode'; +import { ElixirSymbol, ElixirSymbolExtractor } from './elixirSymbolExtractor'; + +export class ElixirDocumentSymbolProvider implements vscode.DocumentSymbolProvider { + provideDocumentSymbols( + document: vscode.TextDocument, + token: vscode.CancellationToken): Thenable { + return new Promise( + (resolve, reject) => { + let src = document.getText(); + let symbolExtractor = new ElixirSymbolExtractor(); + const symbols = []; + let elixirSymbols = symbolExtractor.extractSymbols(src); + for (let symbol of elixirSymbols) { + switch (symbol[0]) { + case ElixirSymbol.FUNCTION: { + let [, name, arity, line] = symbol; + symbols.push({ + name: name + '/' + arity, + kind: vscode.SymbolKind.Function, + location: new vscode.Location(document.uri, new vscode.Position(line - 1, 1)) + }); + break; + } + case ElixirSymbol.MACRO: { + let [, name, arity, line] = symbol; + symbols.push({ + name: name + '/' + arity, + kind: vscode.SymbolKind.Function, + location: new vscode.Location(document.uri, new vscode.Position(line - 1, 1)) + }); + break; + } + case ElixirSymbol.MODULE: { + let [, name, line] = symbol; + symbols.push({ + name: name, + kind: vscode.SymbolKind.Class, + location: new vscode.Location(document.uri, new vscode.Position(line - 1, 1)) + }); + break; + } + case ElixirSymbol.VALUE: { + let [, name, line] = symbol; + symbols.push({ + name: name, + kind: vscode.SymbolKind.Field, + location: new vscode.Location(document.uri, new vscode.Position(line - 1, 1)) + }); + break; + } + default: + } + } + resolve(symbols); + }); + } +} diff --git a/src/elixirMain.ts b/src/elixirMain.ts index 3c51740..fa1dbeb 100644 --- a/src/elixirMain.ts +++ b/src/elixirMain.ts @@ -4,8 +4,10 @@ import * as vscode from 'vscode'; import { configuration } from './configuration'; import { ElixirAutocomplete } from './elixirAutocomplete'; import { ElixirDefinitionProvider } from './elixirDefinitionProvider'; +import { ElixirDocumentSymbolProvider } from './elixirDocumentSymbolProvider'; import { ElixirFormatterProvider } from './elixirFormatter'; import { ElixirHoverProvider } from './elixirHoverProvider'; +import { ElixirReferenceProvider } from './elixirReferenceProvider'; import { ElixirSenseAutocompleteProvider } from './elixirSenseAutocompleteProvider'; import { ElixirSenseClient } from './elixirSenseClient'; import { ElixirSenseDefinitionProvider } from './elixirSenseDefinitionProvider'; @@ -13,7 +15,7 @@ import { ElixirSenseHoverProvider } from './elixirSenseHoverProvider'; import { ElixirSenseServerProcess } from './elixirSenseServerProcess'; import { ElixirSenseSignatureHelpProvider } from './elixirSenseSignatureHelpProvider'; import { ElixirServer } from './elixirServer'; -import { ElixirDocumentSymbolProvider } from './elixirSymbolProvider'; +import { ElixirWorkspaceSymbolProvider } from './elixirWorkspaceSymbolProvider'; const ELIXIR_MODE: vscode.DocumentFilter = { language: 'elixir', scheme: 'file' }; // tslint:disable-next-line:prefer-const @@ -57,7 +59,9 @@ export function activate(ctx: vscode.ExtensionContext) { ctx.subscriptions.push(vscode.languages.setLanguageConfiguration('elixir', configuration)); } - ctx.subscriptions.push(vscode.languages.registerDocumentSymbolProvider({ language: 'elixir' }, new ElixirDocumentSymbolProvider())); + ctx.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(ELIXIR_MODE, new ElixirDocumentSymbolProvider())); + ctx.subscriptions.push(vscode.languages.registerWorkspaceSymbolProvider(new ElixirWorkspaceSymbolProvider())); + ctx.subscriptions.push(vscode.languages.registerReferenceProvider(ELIXIR_MODE, new ElixirReferenceProvider())); const disposables = []; if (useElixirSense) { diff --git a/src/elixirReferenceProvider.ts b/src/elixirReferenceProvider.ts new file mode 100644 index 0000000..c973ef6 --- /dev/null +++ b/src/elixirReferenceProvider.ts @@ -0,0 +1,107 @@ +import cp = require('child_process'); +import * as vscode from 'vscode'; + +enum Subject { + MODULE, + FUNCTION, + FQ_FUNCTION +} + +function extractSubject(src, line, row) { + let subject = ''; + let j = row; + if (/^[a-z0-9_\?\.\!]$/i.test(line[row])) { + while (j >= 0) { + if (line[j] == ' ') { break; } + subject = line[j] + subject; + j--; + } + j = row + 1; + while (j < line.length) { + if (line[j] == ' ') { break; } + if (line[j] == '(') { break; } + subject = subject + line[j]; + j++; + } + if (subject.indexOf('.') == - 1) { + if (subject[0] == subject[0].toUpperCase()) return [Subject.MODULE, subject]; + else return [Subject.FUNCTION, subject]; + } + else { + let parts = subject.split('.'); + let last = parts[parts.length - 1] + if (last[0] == last[0].toUpperCase()) + return [Subject.MODULE, subject]; + else return [Subject.FQ_FUNCTION, subject] + } + } + return undefined; +} + +function extractModule(src, lineNo) { + const lines = src.split('\n'); + const subDoc = lines.slice(0, lineNo).join('\n'); + const x = subDoc.lastIndexOf('defmodule'); + if (x > -1) { + let j = x + 10; + let name = ''; + while (/^[a-z0-9_\?\.\!]$/i.test(subDoc[j]) && j < subDoc.length) { + name += subDoc[j++]; + } + return name; + } + return undefined; +} + +function prepareArgs(type, subject, src, line) { + if (type == Subject.FUNCTION) { + // find owning module + const module = extractModule(src, line); + return module + '.' + subject; + } + return subject; +} + +export class ElixirReferenceProvider implements vscode.ReferenceProvider { + + public provideReferences(document: vscode.TextDocument, position: vscode.Position, options: { includeDeclaration: boolean }, token: vscode.CancellationToken): Thenable { + const dir = vscode.workspace.getWorkspaceFolder(document.uri); + if (dir == undefined) + vscode.window.showWarningMessage('No workspace is opened. Finding references needs a workspace with a mix project.'); + if (dir.uri.path) { + //TODO: check if a mix project + const lineNo = position.line; + const line = document.lineAt(lineNo).text; + const src = document.getText(); + const [type, subject] = extractSubject(src, line, position.character); + const args = prepareArgs(type, subject, src, lineNo); + + return new Promise((resolve, reject) => { + const cmd = `mix xref callers ${args}`; + console.log(cmd) + const cwd = vscode.workspace.rootPath ? vscode.workspace.rootPath : ''; + cp.exec(cmd, { cwd }, (error, stdout, stderr) => { + if (error !== null) { + const message = 'Error while execuing `mix xref`'; + vscode.window.showErrorMessage(message); + reject(message); + } + else { + const lines = stdout.split('\n'); + if (lines.length > 1) { + let references = []; + for (let aline of lines) { + let [file, lineNumber, name] = aline.split(':'); + references.push(new vscode.Location(vscode.Uri.file(cwd + '/' + file), + new vscode.Range(Number(lineNumber) - 1, 0, Number(lineNumber) - 1, 0))); + } + resolve(references); + } else { + resolve([]); + } + } + }) + }); + } + } +} diff --git a/src/elixirSymbolExtractor.ts b/src/elixirSymbolExtractor.ts new file mode 100644 index 0000000..dc2f083 --- /dev/null +++ b/src/elixirSymbolExtractor.ts @@ -0,0 +1,187 @@ +import * as fs from 'fs'; + +export enum ElixirSymbol { + FUNCTION, + MACRO, + VALUE, + MODULE +} + +export class ElixirSymbolExtractor { + keywords = ['defmodule', 'def', 'defp', 'defmacro', 'require', 'alias', 'do', 'end', 'case', 'try', 'rescue', 'do:', 'import', '=']; + tracked = ['defmodule', 'def', 'defp', 'defmacro', '=']; + line; + symbols; + i; + + public extractSymbols(src) { + this.symbols = []; + this.i = 0; + this.line = 1; + let tokens = this.tokenize(this.eraseComments(src)); + let groups = this.processTokens(tokens); + this.processGroups(groups); + return this.symbols; + } + + public extractSymbolsFromFile(file) { + const src = fs.readFileSync(file, 'utf8'); + return this.extractSymbols(src); + } + + eraseComments(src) { + let lines = src.split("\n"); + let erase = false; + for (let i = 0; i < lines.length; i++) { + let line = lines[i].trim(); + if (line.startsWith("#")) { + lines[i] = ""; + } + if (line.indexOf("\"\"\"") > -1 && !erase) { + erase = true; + } + if (line.startsWith("\"\"\"") && erase) { + lines[i] = ""; + erase = false; + } + if (erase) { + lines[i] = ""; + } + } + return lines.join("\n"); + } + + isValidChar(char: string) { + return /^[a-z0-9_\?\(\)\,\.\{\}!\=\:"]+$/i.test(char); + } + + tokenize(src: string) { + let tokens = []; + let source = src.replace(/\"\"\"w+\"\"\"/, ""); + while (this.i < source.length) { + const token = this.nextToken(source); + tokens.push([token, this.line]); + } + return tokens; + } + + nextToken(source: string) { + let token = ''; + while (!this.isValidChar(source.charAt(this.i))) { + this.i++; + if (source.charAt(this.i - 1) == '\n') this.line++; + if (this.i > source.length) break; + } + while (this.isValidChar(source.charAt(this.i))) { + token += source.charAt(this.i); + this.i++; + if (this.i > source.length) break; + } + return token; + } + + processTokens(tokens) { + let groups = []; + let current = []; + for (let t of tokens) { + const [token, line] = t; + if (this.keywords.indexOf(token) > -1) { + if (current.length > 0) { + groups.push(current); + current = []; + } + if (this.tracked.indexOf(token) > -1) + current.push([token, line]); + } else { + current.push([token, line]); + } + } + return groups; + } + + processGroups(groups) { + for (let i = 0; i < groups.length; i++) { + const group = groups[i]; + const first = group[0]; + const ff = first[0]; + switch (ff) { + case 'defmodule': + this.handleDefmodule(group); + break; + case 'def': + this.handleDef(group, groups[i - 1], ElixirSymbol.FUNCTION); + break; + case 'defp': + this.handleDef(group, groups[i - 1], ElixirSymbol.FUNCTION); + break; + case 'defmacro': + this.handleDef(group, groups[i - 1], ElixirSymbol.MACRO); + break; + case '=': + this.handleAssignment(groups[i - 1], group); + break; + default: + } + } + } + + isValidValue(value) { + return /^[a-z0-9_\?]+$/i.test(value) && value != '_'; + } + + handleAssignment(g1, g2) { + const val = g1[g1.length - 1][0]; + const line = g1[g1.length - 1][1]; + if (this.isValidValue(val)) + this.symbols.push([ElixirSymbol.VALUE, val, line]); + } + + handleDefmodule(g) { + const line = g[0][1]; + const moduleName = g[1][0]; + this.symbols.push([ElixirSymbol.MODULE, moduleName, line]); + } + + parseSignature(sig) { + if (sig.includes("(")) { + const bracketStart = sig.indexOf("("); + const bracketEnd = sig.indexOf(")"); + const args = sig.substring(bracketStart, bracketEnd > -1 ? bracketEnd + 1 : sig.length); + let arity; + if (args.includes(',')) + arity = args.split(',').length; + else if (args == '()') + arity = 0; + else arity = 1; + const name = sig.substring(0, bracketStart); + return [name, arity]; + } else + return [sig.trim(), 0]; + } + + handleDef(g, g2, type) { + const line = g[0][1]; + let args = ''; + for (let j = 1; j < g.length; j++) { + args += g[j][0]; + } + const sig = args; + const [name, arity] = this.parseSignature(sig); + this.symbols.push([type, name, arity, line]); + } + + parseModules(modules) { + const x = modules.indexOf('{'); + const y = modules.indexOf('}'); + if (x >= 0 && y >= 0) { + const foo = modules.substring(0, modules.indexOf('.')); + const z = modules.substring(x + 1, y); + const names = z.split(','); + for (var j = 0; j < names.length; j++) { + names[j] = foo + '.' + names[j]; + } + return names; + } + return [].push(modules); + } +} diff --git a/src/elixirSymbolProvider.ts b/src/elixirSymbolProvider.ts deleted file mode 100644 index bf6ae70..0000000 --- a/src/elixirSymbolProvider.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as vscode from 'vscode'; -import { DocumentSymbolProvider, WorkspaceSymbolProvider } from 'vscode'; - -function function_name(line) { - line = line.trim(); - const j = line.startsWith('defp') ? 5 : 4; - const x = line.indexOf('(', j); - if (x > 0) return line.substring(j, x); - else return null; -} - -function variable_name(line) { - line = line.trim().split('='); - const left = line[0].trim(); - if (/^[a-z0-9_]+$/i.test(left)) return left; - else return null; -} - -export class ElixirDocumentSymbolProvider implements DocumentSymbolProvider { - provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Thenable { - return new Promise((resolve, reject) => { - const symbols = []; - for (let i = 0; i < document.lineCount; i++) { - let line = document.lineAt(i); - if (line.text.trim().startsWith('def ') || line.text.trim().startsWith('defp ')) { - symbols.push({ - name: function_name(line.text), - kind: vscode.SymbolKind.Function, - location: new vscode.Location(document.uri, line.range) - }); - } else if (line.text.trim().indexOf('=') > 0) { - const name = variable_name(line.text); - if (name !== undefined) { - symbols.push({ - name: variable_name(line.text), - kind: vscode.SymbolKind.Variable, - location: new vscode.Location(document.uri, line.range) - }); - } - } - } - resolve(symbols); - }); - } -} diff --git a/src/elixirWorkspaceSymbolProvider.ts b/src/elixirWorkspaceSymbolProvider.ts new file mode 100644 index 0000000..8647880 --- /dev/null +++ b/src/elixirWorkspaceSymbolProvider.ts @@ -0,0 +1,67 @@ +import * as vscode from 'vscode'; +import { ElixirSymbol, ElixirSymbolExtractor } from './elixirSymbolExtractor'; + +export class ElixirWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { + + public provideWorkspaceSymbols(query: string, token: vscode.CancellationToken): Thenable { + return findWorkspaceSymbols(query); + } +} + +function partsMatch(query, name) { + let hits = 0; + const parts = name.split('_'); + for (let i = 0; i < parts.length; i++) { + if (parts[i][0] == query[i]) { + hits++; + } else { + hits = 0; + } + if (hits >= 2) { + return true; + } + } + return false; +} + +function isMatch(query, name) { + const lname = name.toLowerCase(); + const lquery = query.toLowerCase(); + return lname.indexOf(lquery) > -1 || partsMatch(lquery, lname); +} + +function findWorkspaceSymbols(query: string): Promise { + if (query.length < 2) return; + let symbols = []; + let files = vscode.workspace.findFiles('{lib,web}/**/*.ex', ''); + + return new Promise((resolve, reject) => { + files.then((value) => { + let symbolExtractor = new ElixirSymbolExtractor(); + for (let file of value) { + const path = file.path + const syms = symbolExtractor.extractSymbolsFromFile(path); + const module = syms[0][1]; + for (let symbol of syms) { + const name = symbol[1]; + if ((symbol[0] == ElixirSymbol.FUNCTION || symbol[0] == ElixirSymbol.MACRO) && isMatch(query, name)) { + const arity = symbol[2]; + const line = symbol[3]; + symbols.push( + new vscode.SymbolInformation(name + '/' + arity, + vscode.SymbolKind.Function, + new vscode.Range(line - 1, 1, line - 1, 1), + vscode.Uri.file(path), + module + )); + } + } + } + resolve(symbols); + }, + (reason) => { + console.log(reason); + reject(reason); + }); + }); +}