diff --git a/locate/locate.js b/locate/locate.js index a3792d5a2..cd0d9a9e8 100644 --- a/locate/locate.js +++ b/locate/locate.js @@ -7,30 +7,55 @@ const _ = require('lodash'); const DECLARATION_TYPES = ['class', 'module', 'method', 'classMethod']; -function flatten(locateInfo, file, parent) { +function flatten(locateInfo, file, containerName = '') { return _.flatMap(locateInfo, (symbols, type) => { if (!_.includes(DECLARATION_TYPES, type)) { // Skip top-level include or posn property etc. return []; } return _.flatMap(symbols, (inner, name) => { - const sep = { method: '#', classMethod: '.' }[type] || '::'; const posn = inner.posn || { line: 0, char: 0 }; - const fullName = parent ? `${parent.fullName}${sep}${name}` : name; + // TODO: parse name with multiple segments, e.g. File.read or ActiveRecord::Base, if necessary. const symbolInfo = { name: name, type: type, file: file, line: posn.line, char: posn.char, - parent: parent, - fullName: fullName + containerName: containerName || '' }; _.extend(symbolInfo, _.omit(inner, DECLARATION_TYPES)); - return [symbolInfo].concat(flatten(inner, file, symbolInfo)); + 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, stringProvider) { + // 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 => regexp.test(stringProvider(symbolInfo)))) + .uniq() + .value(); +} module.exports = class Locate { constructor(root, settings) { this.settings = settings; @@ -58,6 +83,18 @@ module.exports = class Locate { .map(_.clone) .value(); } + query(query) { + const match = query.match(/^(?:(.*)[.#])?([^.#]*)$/); + const containerQuery = match[1]; + const nameQuery = match[2]; + if (!nameQuery) return []; + + const symbols = _(this.tree).values().flatten().value(); + const matchedSymbols = filter(symbols, nameQuery, symbolInfo => symbolInfo.name); + if (!containerQuery) return matchedSymbols; + + return filter(matchedSymbols, containerQuery, symbolInfo => symbolInfo.containerName); + } rm(absPath) { if (absPath in this.tree) delete this.tree[absPath]; } diff --git a/ruby.js b/ruby.js index 07b102915..264922f3b 100644 --- a/ruby.js +++ b/ruby.js @@ -188,19 +188,24 @@ function activate(context) { console.warn(`Unknown symbol type: ${symbolInfo.type}`); return SymbolKind.Variable; }; + const symbolConverter = matches => matches.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(matches => matches.map(match => { - const symbolKind = (symbolKindTable[match.type] || defaultSymbolKind)(match); - const parentName = match.parent ? match.parent.fullName : ''; - const uri = vscode.Uri.file(match.file); - const location = new Location(uri, new Position(match.line, match.char)); - return new SymbolInformation(match.name, symbolKind, parentName, location); - })); + 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));