Skip to content
This repository was archived by the owner on Jul 31, 2023. It is now read-only.

Add document and workspace symbol provider #107

Merged
merged 8 commits into from
Jan 23, 2017
Merged
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
133 changes: 91 additions & 42 deletions locate/locate.js
Original file line number Diff line number Diff line change
@@ -3,24 +3,57 @@ const locator = require('ruby-method-locate'),
minimatch = require('minimatch');
const fs = require('fs'),
path = require('path');
const _ = require('lodash');

function flatten(tree, result) {
let t;
for (let a in tree) {
t = tree[a];
if (typeof t === 'string' || typeof t === 'number') continue;
if (t.posn) {
if (a in result) result[a].push({
line: t.posn.line,
char: t.posn.char
});
else result[a] = [{
line: t.posn.line,
char: t.posn.char
}];
const DECLARATION_TYPES = ['class', 'module', 'method', 'classMethod'];

function flatten(locateInfo, file, containerName = '') {
return _.flatMap(locateInfo, (symbols, type) => {
if (!_.includes(DECLARATION_TYPES, type)) {
// Skip top-level include or posn property etc.
return [];
}
flatten(t, result);
}
return _.flatMap(symbols, (inner, name) => {
const posn = inner.posn || { line: 0, char: 0 };
const symbolInfo = {
name: name,
type: type,
file: file,
line: posn.line,
char: posn.char,
containerName: containerName || ''
};
_.extend(symbolInfo, _.omit(inner, DECLARATION_TYPES));
const sep = { method: '#', classMethod: '.' }[type] || '::';
const fullName = containerName ? `${containerName}${sep}${name}` : name;
return [symbolInfo].concat(flatten(inner, file, fullName));
});
});
}
function camelCaseRegExp(query) {
const escaped = _.escapeRegExp(query)
const prefix = escaped.charAt(0);
return new RegExp(
`[${prefix.toLowerCase()}${prefix.toUpperCase()}]` +
escaped.slice(1).replace(/[A-Z]|([a-z])/g, (char, lower) => {
if (lower) return `[${char.toUpperCase()}${char}]`;
const lowered = char.toLowerCase()
return `.*(?:${char}|_${lowered})`;
})
);
}
function filter(symbols, query, matcher) {
// TODO: Ask MS to expose or separate matchesFuzzy method.
// https://github.com/Microsoft/vscode/blob/a1d3c8a3006d0a3d68495122ea09a2a37bca69db/src/vs/base/common/filters.ts
const isLowerCase = (query.toLowerCase() === query)
const exact = new RegExp('^' + _.escapeRegExp(query) + '$', 'i');
const prefix = new RegExp('^' + _.escapeRegExp(query), 'i');
const substring = new RegExp(_.escapeRegExp(query), isLowerCase ? 'i' : '');
const camelCase = camelCaseRegExp(query);
return _([exact, prefix, substring, camelCase])
.flatMap(regexp => symbols.filter(symbolInfo => matcher(symbolInfo, regexp)))
.uniq()
.value();
}
module.exports = class Locate {
constructor(root, settings) {
@@ -33,27 +66,45 @@ module.exports = class Locate {
// always: do this file now (if it's in the tree)
// add lookup hooks
}
listInFile(absPath) {
const waitForParse = (absPath in this.tree) ? Promise.resolve() : this.parse(absPath);
return waitForParse.then(() => _.clone(this.tree[absPath] || []));
}
find(name) {
let result = [];
let tree;
for (let file in this.tree) {
tree = this.tree[file];
//jshint -W083
const extract = obj => ({
file: file,
line: obj.line,
char: obj.char
});
//jshint +W083
for (let n in tree) {
// because our word pattern is designed to match symbols
// things like Gem::RequestSet may request a search for ':RequestSet'
if (n === name || n === name + '=' || ':' + n === name) {
result = result.concat(tree[n].map(extract));
}
}
}
return result;
// because our word pattern is designed to match symbols
// things like Gem::RequestSet may request a search for ':RequestSet'
const escapedName = _.escapeRegExp(_.trimStart(name, ':'));
const regexp = new RegExp(`^${escapedName}=?\$`);
return _(this.tree)
.values()
.flatten()
.filter(symbol => regexp.test(symbol.name))
.map(_.clone)
.value();
}
query(query) {
const segmentMatch = query.match(/^(?:([^.#:]+)(.|#|::))([^.#:]+)$/) || [];
const containerQuery = segmentMatch[1];
const separator = segmentMatch[2];
const nameQuery = segmentMatch[3];
const separatorToTypesTable = {
'.': ['classMethod', 'method'],
'#': ['method'],
'::': ['class', 'module'],
};
const segmentTypes = separatorToTypesTable[separator];
const symbols = _(this.tree).values().flatten().value();

// query whole name (matches `class Foo::Bar` or `def File.read`)
const plainMatches = filter(symbols, query, (symbolInfo, regexp) => regexp.test(symbolInfo.name));
if (!containerQuery) return plainMatches;

// query name and containerName separatedly (matches `def foo` in `class Bar`)
const nameMatches = filter(symbols, nameQuery, (symbolInfo, regexp) => {
return _.includes(segmentTypes, symbolInfo.type) && regexp.test(symbolInfo.name);
});
const containerMatches = filter(nameMatches, containerQuery, (symbolInfo, regexp) => regexp.test(symbolInfo.containerName))
return _.uniq(plainMatches.concat(containerMatches));
}
rm(absPath) {
if (absPath in this.tree) delete this.tree[absPath];
@@ -62,12 +113,9 @@ module.exports = class Locate {
const relPath = path.relative(this.root, absPath);
if (this.settings.exclude && minimatch(relPath, this.settings.exclude)) return;
if (this.settings.include && !minimatch(relPath, this.settings.include)) return;
this.rm(absPath);
locator(absPath)
return locator(absPath)
.then(result => {
if (!result) return;
this.tree[absPath] = {};
flatten(result, this.tree[absPath]);
this.tree[absPath] = result ? flatten(result, absPath) : [];
}, err => {
if (err.code === 'EMFILE') {
// if there are too many open files
@@ -76,6 +124,7 @@ module.exports = class Locate {
} else {
// otherwise, report it
console.log(err);
this.rm(absPath);
}
});
}
@@ -97,4 +146,4 @@ module.exports = class Locate {
});
});
}
};
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@
"vscode-debugprotocol": "~1.6.0-pre4",
"vscode-debugadapter": "~1.6.0-pre8",
"xmldom": "^0.1.19",
"lodash": "^4.17.3",
"minimatch": "^3.0.3"
},
"devDependencies": {
34 changes: 33 additions & 1 deletion ruby.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
let vscode = require('vscode');
const vscode = require('vscode');
const { Location, Position, SymbolKind, SymbolInformation } = vscode;

let Locate = require('./locate/locate');
let cp = require('child_process');
@@ -177,6 +178,37 @@ function activate(context) {
}
};
subs.push(vscode.languages.registerDefinitionProvider(['ruby', 'erb'], defProvider));
const symbolKindTable = {
class: () => SymbolKind.Class,
module: () => SymbolKind.Module,
method: symbolInfo => symbolInfo.name === 'initialize' ? SymbolKind.Constructor : SymbolKind.Method,
classMethod: () => SymbolKind.Method,
};
const defaultSymbolKind = symbolInfo => {
console.warn(`Unknown symbol type: ${symbolInfo.type}`);
return SymbolKind.Variable;
};
// NOTE: Workaround for high CPU usage on IPC (channel.onread) when too many symbols returned.
// For channel.onread see issue like this: https://github.com/Microsoft/vscode/issues/6026
const numOfSymbolLimit = 3000;
const symbolConverter = matches => matches.slice(0, numOfSymbolLimit).map(match => {
const symbolKind = (symbolKindTable[match.type] || defaultSymbolKind)(match);
const uri = vscode.Uri.file(match.file);
const location = new Location(uri, new Position(match.line, match.char));
return new SymbolInformation(match.name, symbolKind, match.containerName, location);
});
const docSymbolProvider = {
provideDocumentSymbols: (document, token) => {
return locate.listInFile(document.fileName).then(symbolConverter);
}
};
subs.push(vscode.languages.registerDocumentSymbolProvider(['ruby', 'erb'], docSymbolProvider));
const workspaceSymbolProvider = {
provideWorkspaceSymbols: (query, token) => {
return symbolConverter(locate.query(query));
}
};
subs.push(vscode.languages.registerWorkspaceSymbolProvider(workspaceSymbolProvider));
}

subs.push(vscode.window.onDidChangeActiveTextEditor(balanceEvent));