Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/elixirDocumentSymbolProvider.ts
Original file line number Diff line number Diff line change
@@ -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<vscode.SymbolInformation[]> {
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);
});
}
}
8 changes: 6 additions & 2 deletions src/elixirMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ 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';
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
Expand Down Expand Up @@ -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) {
Expand Down
107 changes: 107 additions & 0 deletions src/elixirReferenceProvider.ts
Original file line number Diff line number Diff line change
@@ -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<vscode.Location[]> {
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([]);
}
}
})
});
}
}
}
187 changes: 187 additions & 0 deletions src/elixirSymbolExtractor.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading