提交 5c4e24f0 编写于 作者: B Benjamin Pasero

Merge pull request #1102 from Microsoft/ben/fuzzy-quickopen

Alternative fuzzy searching
// ATTENTION - THIS DIRECTORY CONTAINS THIRD PARTY OPEN SOURCE MATERIALS:
[{
"name": "string_scorer",
"version": "10 March 2015",
"license": "MIT License",
"repositoryURL": "https://github.com/joshaven/string_score",
"description": "The file scorer.ts was inspired by the string_score algorithm from Joshaven Potter."
}]
......@@ -251,12 +251,7 @@ const fuzzyContiguousFilter = or(matchesPrefix, matchesCamelCase, matchesContigu
const fuzzySeparateFilter = or(matchesPrefix, matchesCamelCase, matchesSubString);
const fuzzyRegExpCache: { [key: string]: RegExp; } = {};
let defaultFuzzyMatching = SubstringMatching.Contiguous;
export function setDefaultFuzzyMatching(matcher: SubstringMatching): void {
defaultFuzzyMatching = matcher;
}
export function matchesFuzzy(word: string, wordToMatchAgainst: string, matcher = defaultFuzzyMatching): IMatch[] {
export function matchesFuzzy(word: string, wordToMatchAgainst: string, enableSeparateSubstringMatching = false): IMatch[] {
// Form RegExp for wildcard matches
let regexp = fuzzyRegExpCache[word];
......@@ -272,5 +267,5 @@ export function matchesFuzzy(word: string, wordToMatchAgainst: string, matcher =
}
// Default Filter
return matcher === SubstringMatching.Contiguous ? fuzzyContiguousFilter(word, wordToMatchAgainst) : fuzzySeparateFilter(word, wordToMatchAgainst);
return enableSeparateSubstringMatching ? fuzzySeparateFilter(word, wordToMatchAgainst) : fuzzyContiguousFilter(word, wordToMatchAgainst);
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
// Based on material from:
/*!
* string_score.js: String Scoring Algorithm 0.1.22
*
* http://joshaven.com/string_score
* https://github.com/joshaven/string_score
*
* Copyright (C) 2009-2014 Joshaven Potter <yourtech@gmail.com>
* Special thanks to all of the contributors listed here https://github.com/joshaven/string_score
* MIT License: http://opensource.org/licenses/MIT
*
* Date: Tue Mar 1 2011
* Updated: Tue Mar 10 2015
*/
/**
* Compute a score for the given string and the given query.
*
* Rules:
* Character score: 1
* Same case bonus: 1
* Upper case bonus: 1
* Start of word/path bonus: 7
* Start of string bonus: 8
*/
export function score(target: string, query: string): number {
let score = 0;
if (!target || !query) {
return score; // return early if target or query are undefined
}
const queryLen = query.length;
const targetLower = target.toLowerCase();
const queryLower = query.toLowerCase();
const wordPathBoundary = ['-', '_', ' ', '/', '\\'];
let index = 0;
while (index < queryLen) {
var indexOf = targetLower.indexOf(queryLower[index]);
if (indexOf < 0) {
index++;
continue; // no match
}
// Character Match Bonus
score += 1;
// Same Case Bonous
if (target[indexOf] === query[indexOf]) {
score += 1;
}
// Upper Case Bonus
if (isUpper(target.charCodeAt(indexOf))) {
score += 1;
}
// Prefix Bonus
if (indexOf === 0) {
score += 8;
}
// Start of Word/Path Bonous
if (wordPathBoundary.some(w => w === target[indexOf - 1])) {
score += 7;
}
index++;
}
return score;
}
function isUpper(code: number): boolean {
return 65 <= code && code <= 90;
}
\ No newline at end of file
......@@ -134,7 +134,8 @@ export class QuickOpenEntry {
}
/**
* A good default sort implementation for quick open entries
* A good default sort implementation for quick open entries respecting highlight information
* as well as associated resources.
*/
public static compare(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string): number {
......@@ -153,7 +154,7 @@ export class QuickOpenEntry {
return 1;
}
// Sort by name/path
// Fallback to the full path if labels are identical and we have associated resources
let nameA = elementA.getLabel();
let nameB = elementB.getLabel();
if (nameA === nameB) {
......@@ -169,20 +170,24 @@ export class QuickOpenEntry {
return compareAnything(nameA, nameB, lookFor);
}
public static highlight(entry: QuickOpenEntry, lookFor: string): { labelHighlights: IHighlight[], descriptionHighlights: IHighlight[] } {
/**
* A good default highlight implementation for an entry with label and description.
*/
public static highlight(entry: QuickOpenEntry, lookFor: string, fuzzyHighlight = false): { labelHighlights: IHighlight[], descriptionHighlights: IHighlight[] } {
let labelHighlights: IHighlight[] = [];
let descriptionHighlights: IHighlight[] = [];
// Highlight file aware
if (entry.getResource()) {
// Highlight only inside label
if (lookFor.indexOf(Paths.nativeSep) < 0) {
labelHighlights = Filters.matchesFuzzy(lookFor, entry.getLabel());
// Fuzzy: Highlight in label and description
if (fuzzyHighlight) {
labelHighlights = Filters.matchesFuzzy(lookFor, entry.getLabel(), true);
descriptionHighlights = Filters.matchesFuzzy(lookFor, entry.getDescription(), true);
}
// Highlight in label and description
else {
// Path search: Highlight in label and description
else if (lookFor.indexOf(Paths.nativeSep) >= 0) {
descriptionHighlights = Filters.matchesFuzzy(Strings.trim(lookFor, Paths.nativeSep), entry.getDescription());
// If we have no highlights, assume that the match is split among name and parent folder
......@@ -191,6 +196,11 @@ export class QuickOpenEntry {
descriptionHighlights = Filters.matchesFuzzy(Strings.trim(Paths.dirname(lookFor), Paths.nativeSep), entry.getDescription());
}
}
// Highlight only inside label
else {
labelHighlights = Filters.matchesFuzzy(lookFor, entry.getLabel());
}
}
// Highlight by label otherwise
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import scorer = require('vs/base/common/scorer');
suite('Scorer', () => {
test("score", function() {
const target = 'HelLo-World';
const scores = [];
scores.push(scorer.score(target, 'HelLo-World')); // direct case match
scores.push(scorer.score(target, 'hello-world')); // direct mix-case match
scores.push(scorer.score(target, 'HW')); // direct case prefix (multiple)
scores.push(scorer.score(target, 'H')); // direct case prefix
scores.push(scorer.score(target, 'hw')); // direct mix-case prefix (multiple)
scores.push(scorer.score(target, 'h')); // direct mix-case prefix
scores.push(scorer.score(target, 'W')); // direct case word prefix
scores.push(scorer.score(target, 'w')); // direct mix-case word prefix
scores.push(scorer.score(target, 'Ld')); // in-string case match (multiple)
scores.push(scorer.score(target, 'L')); // in-string case match
scores.push(scorer.score(target, 'ld')); // in-string mix-case match
scores.push(scorer.score(target, 'l')); // in-string mix-case match
scores.push(scorer.score(target, '4')); // no match
// Assert scoring order
let sortedScores = scores.sort();
assert.deepEqual(scores.reverse(), sortedScores);
});
});
\ No newline at end of file
......@@ -28,6 +28,7 @@ export interface IQueryOptions {
includePattern?: glob.IExpression;
maxResults?: number;
fileEncoding?: string;
matchFuzzy?: boolean;
}
export interface ISearchQuery extends IQueryOptions {
......@@ -91,5 +92,6 @@ export class LineMatch implements ILineMatch {
export interface ISearchConfiguration extends IFilesConfiguration {
search: {
exclude: glob.IExpression;
fuzzyFilePicker: boolean;
};
}
\ No newline at end of file
......@@ -38,7 +38,7 @@ import {IEventService} from 'vs/platform/event/common/event';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {IWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService';
import {IKeybindingService, IKeybindingContextKey} from 'vs/platform/keybinding/common/keybindingService';
const ID = 'workbench.component.quickopen';
......
......@@ -32,13 +32,14 @@ import {KeybindingsUtils} from 'vs/platform/keybinding/common/keybindingsUtils';
import {IQuickOpenService} from 'vs/workbench/services/quickopen/browser/quickOpenService';
import {KeyMod, KeyCode} from 'vs/base/common/keyCodes';
const ACTION_ID = 'workbench.action.showCommands';
const ACTION_LABEL = nls.localize('showTriggerActions', "Show All Commands");
const ALL_COMMANDS_PREFIX = '>';
const EDITOR_COMMANDS_PREFIX = '$';
export const ALL_COMMANDS_PREFIX = '>';
export const EDITOR_COMMANDS_PREFIX = '$';
export class ShowAllCommandsAction extends QuickOpenAction {
public static ID = 'workbench.action.showCommands';
public static LABEL = nls.localize('showTriggerActions', "Show All Commands");
constructor(actionId: string, actionLabel: string, @IQuickOpenService quickOpenService: IQuickOpenService) {
super(actionId, actionLabel, ALL_COMMANDS_PREFIX, quickOpenService);
}
......@@ -320,7 +321,6 @@ export class CommandsHandler extends QuickOpenHandler {
public getEmptyLabel(searchString: string): string {
return nls.localize('noCommandsMatching', "No commands matching");
}
}
export class EditorCommandsHandler extends CommandsHandler {
......@@ -359,20 +359,3 @@ export class QuickCommandsEditorAction extends EditorAction {
return super.run();
}
}
\ No newline at end of file
// Register Action
let registry = <IWorkbenchActionRegistry>Registry.as(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(ShowAllCommandsAction, ACTION_ID, ACTION_LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_P,
secondary: [KeyCode.F1]
}));
// Register Quick Open Handler
(<IQuickOpenRegistry>Registry.as(QuickOpenExtensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/commandsHandler',
'CommandsHandler',
ALL_COMMANDS_PREFIX,
nls.localize('commandsHandlerDescriptionDefault', "Show and Run Commands")
)
);
\ No newline at end of file
......@@ -24,11 +24,13 @@ import {Position} from 'vs/platform/editor/common/editor';
import {IQuickOpenService} from 'vs/workbench/services/quickopen/browser/quickOpenService';
import {KeyMod, KeyCode} from 'vs/base/common/keyCodes';
const ACTION_ID = 'workbench.action.gotoLine';
const ACTION_LABEL = nls.localize('gotoLine', "Go to Line...");
const GOTO_LINE_PREFIX = ':';
export const GOTO_LINE_PREFIX = ':';
export class GotoLineAction extends QuickOpenAction {
public static ID = 'workbench.action.gotoLine';
public static LABEL = nls.localize('gotoLine', "Go to Line...");
constructor(actionId: string, actionLabel: string, @IQuickOpenService quickOpenService: IQuickOpenService) {
super(actionId, actionLabel, GOTO_LINE_PREFIX, quickOpenService);
}
......@@ -282,26 +284,3 @@ export class GotoLineHandler extends QuickOpenHandler {
};
}
}
\ No newline at end of file
// Register Action
let registry = <IWorkbenchActionRegistry>Registry.as(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(GotoLineAction, ACTION_ID, ACTION_LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.KEY_G,
mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_G }
}));
// Register Quick Open Handler
(<IQuickOpenRegistry>Registry.as(QuickOpenExtensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/gotoLineHandler',
'GotoLineHandler',
GOTO_LINE_PREFIX,
[
{
prefix: GOTO_LINE_PREFIX,
needsEditor: true,
description: env.isMacintosh ? nls.localize('gotoLineDescriptionMac', "Go to Line") : nls.localize('gotoLineDescriptionWin', "Go to Line")
},
]
)
);
\ No newline at end of file
......@@ -33,13 +33,14 @@ import {Position} from 'vs/platform/editor/common/editor';
import {KeyMod, KeyCode} from 'vs/base/common/keyCodes';
import {OutlineRegistry, getOutlineEntries} from 'vs/editor/contrib/quickOpen/common/quickOpen';
const ACTION_ID = 'workbench.action.gotoSymbol';
const ACTION_LABEL = nls.localize('gotoSymbol', "Go to Symbol...");
const GOTO_SYMBOL_PREFIX = '@';
const SCOPE_PREFIX = ':';
export const GOTO_SYMBOL_PREFIX = '@';
export const SCOPE_PREFIX = ':';
export class GotoSymbolAction extends QuickOpenAction {
public static ID = 'workbench.action.gotoSymbol';
public static LABEL = nls.localize('gotoSymbol', "Go to Symbol...");
constructor(actionId: string, actionLabel: string, @IQuickOpenService quickOpenService: IQuickOpenService) {
super(actionId, actionLabel, GOTO_SYMBOL_PREFIX, quickOpenService);
}
......@@ -432,7 +433,7 @@ export class GotoSymbolHandler extends QuickOpenHandler {
if (model && types.isFunction((<ITokenizedModel>model).getMode)) {
canRun = OutlineRegistry.has(<IModel> model);
canRun = OutlineRegistry.has(<IModel>model);
}
}
......@@ -527,7 +528,7 @@ export class GotoSymbolHandler extends QuickOpenHandler {
return TPromise.as(this.outlineToModelCache[modelId]);
}
return getOutlineEntries(<IModel> model).then(outline => {
return getOutlineEntries(<IModel>model).then(outline => {
let model = new OutlineModel(outline, this.toQuickOpenEntries(outline));
......@@ -628,28 +629,3 @@ export class GotoSymbolHandler extends QuickOpenHandler {
this.activeOutlineRequest = null;
}
}
\ No newline at end of file
// Register Action
let registry = <IWorkbenchActionRegistry>Registry.as(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(GotoSymbolAction, ACTION_ID, ACTION_LABEL, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O }));
// Register Quick Outline Handler
(<IQuickOpenRegistry>Registry.as(QuickOpenExtensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/gotoSymbolHandler',
'GotoSymbolHandler',
GOTO_SYMBOL_PREFIX,
[
{
prefix: GOTO_SYMBOL_PREFIX,
needsEditor: true,
description: env.isMacintosh ? nls.localize('gotoSymbolDescriptionNormalMac', "Go to Symbol") : nls.localize('gotoSymbolDescriptionNormalWin', "Go to Symbol")
},
{
prefix: GOTO_SYMBOL_PREFIX + SCOPE_PREFIX,
needsEditor: true,
description: nls.localize('gotoSymbolDescriptionScoped', "Go to Symbol by Category")
}
]
)
);
\ No newline at end of file
......@@ -15,7 +15,7 @@ import {ITree, IElementCallback} from 'vs/base/parts/tree/common/tree';
import {QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, QuickOpenHandler} from 'vs/workbench/browser/quickopen';
import {IQuickOpenService} from 'vs/workbench/services/quickopen/browser/quickOpenService';
const HELP_PREFIX = '?';
export const HELP_PREFIX = '?';
class HelpEntry extends QuickOpenEntryItem {
private prefix: string;
......@@ -183,13 +183,3 @@ export class HelpHandler extends QuickOpenHandler {
};
}
}
\ No newline at end of file
// Register Quick Open Handler
(<IQuickOpenRegistry>Registry.as(Extensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/helpHandler',
'HelpHandler',
HELP_PREFIX,
nls.localize('helpDescription', "Show Help")
)
);
\ No newline at end of file
......@@ -67,9 +67,8 @@ class MarkerEntry extends QuickOpenEntryItem {
return null;
}
public run(mode:Mode, context:IContext):boolean {
if(mode !== Mode.OPEN) {
public run(mode: Mode, context: IContext): boolean {
if (mode !== Mode.OPEN) {
return false;
}
......@@ -107,7 +106,7 @@ export class MarkersHandler extends QuickOpenHandler {
this._contextService = contextService;
}
public getResults(searchValue:string):TPromise<QuickOpenModel> {
public getResults(searchValue: string): TPromise<QuickOpenModel> {
searchValue = searchValue.trim();
let markers = this._markerService.read({ take: 500 });
......@@ -121,7 +120,6 @@ export class MarkersHandler extends QuickOpenHandler {
}
private static _sort(a: IMarker, b: IMarker): number {
let ret: number;
// 1st: severity matters first
......@@ -144,7 +142,7 @@ export class MarkersHandler extends QuickOpenHandler {
// 4th: start column matters
ret = a.startColumn - b.startColumn;
if(ret !== 0) {
if (ret !== 0) {
return ret;
}
......@@ -153,7 +151,7 @@ export class MarkersHandler extends QuickOpenHandler {
private _filter(marker: IMarker, query: string): boolean {
if(marker.resource.scheme === network.schemas.inMemory) {
if (marker.resource.scheme === network.schemas.inMemory) {
// ignore inmemory-models
return false;
}
......@@ -172,7 +170,7 @@ export class MarkersHandler extends QuickOpenHandler {
return 'marker-handler';
}
public getAutoFocus(searchValue:string):IAutoFocus {
public getAutoFocus(searchValue: string): IAutoFocus {
return {
autoFocusFirstEntry: !!searchValue
};
......@@ -187,7 +185,7 @@ export class MarkersHandler extends QuickOpenHandler {
}
class GotoMarkerAction extends QuickOpenAction {
export class GotoMarkerAction extends QuickOpenAction {
static Prefix = '!';
static Id = 'workbench.action.showErrorsWarnings';
......@@ -197,23 +195,3 @@ class GotoMarkerAction extends QuickOpenAction {
super(actionId, actionLabel, GotoMarkerAction.Prefix, quickOpenService);
}
}
\ No newline at end of file
// Register Action
let registry = <IWorkbenchActionRegistry> Registry.as(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(GotoMarkerAction, GotoMarkerAction.Id, GotoMarkerAction.Label, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M }));
// Register Quick Open Handler
(<IQuickOpenRegistry>Registry.as(QuickOpenExtensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/markersHandler',
'MarkersHandler',
GotoMarkerAction.Prefix,
[
{
prefix: GotoMarkerAction.Prefix,
needsEditor: false,
description: env.isMacintosh ? nls.localize('desc.mac', "Show Errors or Warnings") : nls.localize('desc.win', "Show Errors and Warnings")
},
]
)
);
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import env = require('vs/base/common/platform');
import nls = require('vs/nls');
import {QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandler} from 'vs/workbench/browser/quickopen';
import {Registry} from 'vs/platform/platform';
import {SyncActionDescriptor} from 'vs/platform/actions/common/actions';
import {IWorkbenchActionRegistry, Extensions as ActionExtensions} from 'vs/workbench/browser/actionRegistry';
import {KeyMod, KeyCode} from 'vs/base/common/keyCodes';
import {IConfigurationRegistry, Extensions as ConfigurationExtensions} from 'vs/platform/configuration/common/configurationRegistry';
import {GotoSymbolHandler, GotoSymbolAction, GOTO_SYMBOL_PREFIX, SCOPE_PREFIX} from 'vs/workbench/parts/quickopen/browser/gotoSymbolHandler';
import {CommandsHandler, ShowAllCommandsAction, ALL_COMMANDS_PREFIX} from 'vs/workbench/parts/quickopen/browser/commandsHandler';
import {GotoLineAction, GotoLineHandler, GOTO_LINE_PREFIX} from 'vs/workbench/parts/quickopen/browser/gotoLineHandler';
import {HelpHandler, HELP_PREFIX} from 'vs/workbench/parts/quickopen/browser/helpHandler';
import {MarkersHandler, GotoMarkerAction} from 'vs/workbench/parts/quickopen/browser/markersHandler';
// Register Actions
let registry = <IWorkbenchActionRegistry>Registry.as(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(GotoMarkerAction, GotoMarkerAction.Id, GotoMarkerAction.Label, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M
}));
registry.registerWorkbenchAction(new SyncActionDescriptor(ShowAllCommandsAction, ShowAllCommandsAction.ID, ShowAllCommandsAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_P,
secondary: [KeyCode.F1]
}));
registry.registerWorkbenchAction(new SyncActionDescriptor(GotoLineAction, GotoLineAction.ID, GotoLineAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyCode.KEY_G,
mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_G }
}));
registry.registerWorkbenchAction(new SyncActionDescriptor(GotoSymbolAction, GotoSymbolAction.ID, GotoSymbolAction.LABEL, {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O
}));
// Register Quick Open Handler
(<IQuickOpenRegistry>Registry.as(QuickOpenExtensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/markersHandler',
'MarkersHandler',
GotoMarkerAction.Prefix,
[
{
prefix: GotoMarkerAction.Prefix,
needsEditor: false,
description: env.isMacintosh ? nls.localize('desc.mac', "Show Errors or Warnings") : nls.localize('desc.win', "Show Errors and Warnings")
},
]
)
);
(<IQuickOpenRegistry>Registry.as(QuickOpenExtensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/commandsHandler',
'CommandsHandler',
ALL_COMMANDS_PREFIX,
nls.localize('commandsHandlerDescriptionDefault', "Show and Run Commands")
)
);
(<IQuickOpenRegistry>Registry.as(QuickOpenExtensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/gotoLineHandler',
'GotoLineHandler',
GOTO_LINE_PREFIX,
[
{
prefix: GOTO_LINE_PREFIX,
needsEditor: true,
description: env.isMacintosh ? nls.localize('gotoLineDescriptionMac', "Go to Line") : nls.localize('gotoLineDescriptionWin', "Go to Line")
},
]
)
);
(<IQuickOpenRegistry>Registry.as(QuickOpenExtensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/gotoSymbolHandler',
'GotoSymbolHandler',
GOTO_SYMBOL_PREFIX,
[
{
prefix: GOTO_SYMBOL_PREFIX,
needsEditor: true,
description: env.isMacintosh ? nls.localize('gotoSymbolDescriptionNormalMac', "Go to Symbol") : nls.localize('gotoSymbolDescriptionNormalWin', "Go to Symbol")
},
{
prefix: GOTO_SYMBOL_PREFIX + SCOPE_PREFIX,
needsEditor: true,
description: nls.localize('gotoSymbolDescriptionScoped', "Go to Symbol by Category")
}
]
)
);
(<IQuickOpenRegistry>Registry.as(QuickOpenExtensions.Quickopen)).registerQuickOpenHandler(
new QuickOpenHandlerDescriptor(
'vs/workbench/parts/quickopen/browser/helpHandler',
'HelpHandler',
HELP_PREFIX,
nls.localize('helpDescription', "Show Help")
)
);
\ No newline at end of file
......@@ -10,11 +10,12 @@ import nls = require('vs/nls');
import {ThrottledDelayer} from 'vs/base/common/async';
import types = require('vs/base/common/types');
import strings = require('vs/base/common/strings');
import scorer = require('vs/base/common/scorer');
import paths = require('vs/base/common/paths');
import filters = require('vs/base/common/filters');
import labels = require('vs/base/common/labels');
import {IRange} from 'vs/editor/common/editorCommon';
import {compareAnything} from 'vs/base/common/comparers';
import {ListenerUnbind} from 'vs/base/common/eventEmitter';
import {IAutoFocus} from 'vs/base/parts/quickopen/browser/quickOpen';
import {QuickOpenEntry, QuickOpenModel} from 'vs/base/parts/quickopen/browser/quickOpenModel';
import {QuickOpenHandler} from 'vs/workbench/browser/quickopen';
......@@ -22,7 +23,9 @@ import {FileEntry, OpenFileHandler} from 'vs/workbench/parts/search/browser/open
import {OpenSymbolHandler as _OpenSymbolHandler} from 'vs/workbench/parts/search/browser/openSymbolHandler';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {IWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService';
import {ISearchConfiguration} from 'vs/platform/search/common/search';
import {IConfigurationService, IConfigurationServiceEvent, ConfigurationServiceEventTypes} from 'vs/platform/configuration/common/configuration';
// OpenSymbolHandler is used from an extension and must be in the main bundle file so it can load
export const OpenSymbolHandler = _OpenSymbolHandler
......@@ -42,11 +45,14 @@ export class OpenAnythingHandler extends QuickOpenHandler {
private delayer: ThrottledDelayer<QuickOpenModel>;
private pendingSearch: TPromise<QuickOpenModel>;
private isClosed: boolean;
private fuzzyMatchingEnabled: boolean;
private configurationListenerUnbind: ListenerUnbind;
constructor(
@IMessageService private messageService: IMessageService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IInstantiationService instantiationService: IInstantiationService
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService private configurationService: IConfigurationService
) {
super();
......@@ -59,6 +65,19 @@ export class OpenAnythingHandler extends QuickOpenHandler {
this.resultsToSearchCache = Object.create(null);
this.delayer = new ThrottledDelayer<QuickOpenModel>(OpenAnythingHandler.SEARCH_DELAY);
this.updateFuzzyMatching(contextService.getOptions().globalSettings.settings);
this.registerListeners();
}
private registerListeners(): void {
this.configurationListenerUnbind = this.configurationService.addListener(ConfigurationServiceEventTypes.UPDATED, (e: IConfigurationServiceEvent) => this.updateFuzzyMatching(e.config));
}
private updateFuzzyMatching(configuration: ISearchConfiguration): void {
this.fuzzyMatchingEnabled = configuration.search && configuration.search.fuzzyFilePicker;
this.openFileHandler.setFuzzyMatchingEnabled(this.fuzzyMatchingEnabled);
}
public getResults(searchValue: string): TPromise<QuickOpenModel> {
......@@ -143,7 +162,7 @@ export class OpenAnythingHandler extends QuickOpenHandler {
let result = [...results[0].entries, ...results[1].entries];
// Sort
result.sort((elementA, elementB) => QuickOpenEntry.compare(elementA, elementB, searchValue));
result.sort((elementA, elementB) => this.sort(elementA, elementB, searchValue, this.fuzzyMatchingEnabled));
// Apply Range
result.forEach((element) => {
......@@ -246,19 +265,19 @@ export class OpenAnythingHandler extends QuickOpenHandler {
// Check if this entry is a match for the search value
let targetToMatch = searchInPath ? labels.getPathLabel(entry.getResource(), this.contextService) : entry.getLabel();
if (!filters.matchesFuzzy(searchValue, targetToMatch)) {
if (!filters.matchesFuzzy(searchValue, targetToMatch, this.fuzzyMatchingEnabled)) {
continue;
}
// Apply highlights
const {labelHighlights, descriptionHighlights} = QuickOpenEntry.highlight(entry, searchValue);
const {labelHighlights, descriptionHighlights} = QuickOpenEntry.highlight(entry, searchValue, this.fuzzyMatchingEnabled);
entry.setHighlights(labelHighlights, descriptionHighlights);
results.push(entry);
}
// Sort
results.sort((elementA, elementB) => QuickOpenEntry.compare(elementA, elementB, searchValue));
results.sort((elementA, elementB) => this.sort(elementA, elementB, searchValue, this.fuzzyMatchingEnabled));
// Apply Range
results.forEach((element) => {
......@@ -270,6 +289,28 @@ export class OpenAnythingHandler extends QuickOpenHandler {
return results;
}
private sort(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string, enableFuzzyScoring): number {
// Fuzzy scoring is special
if (enableFuzzyScoring) {
const labelAScore = scorer.score(elementA.getLabel(), lookFor);
const labelBScore = scorer.score(elementB.getLabel(), lookFor);
if (labelAScore !== labelBScore) {
return labelAScore > labelBScore ? -1 : 1;
}
const descriptionAScore = scorer.score(elementA.getDescription(), lookFor);
const descriptionBScore = scorer.score(elementB.getDescription(), lookFor);
if (descriptionAScore !== descriptionBScore) {
return descriptionAScore > descriptionBScore ? -1 : 1;
}
}
return QuickOpenEntry.compare(elementA, elementB, lookFor);
}
public getGroupLabel(): string {
return nls.localize('fileAndTypeResults', "file and symbol results");
}
......
......@@ -89,6 +89,7 @@ export class OpenFileHandler extends QuickOpenHandler {
private queryBuilder: QueryBuilder;
private delayer: ThrottledDelayer<QuickOpenEntry[]>;
private isStandalone: boolean;
private fuzzyMatchingEnabled: boolean;
constructor(
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
......@@ -111,6 +112,10 @@ export class OpenFileHandler extends QuickOpenHandler {
this.isStandalone = standalone;
}
public setFuzzyMatchingEnabled(enabled: boolean): void {
this.fuzzyMatchingEnabled = enabled;
}
public getResults(searchValue: string): TPromise<QuickOpenModel> {
searchValue = searchValue.trim();
let promise: TPromise<QuickOpenEntry[]>;
......@@ -133,7 +138,7 @@ export class OpenFileHandler extends QuickOpenHandler {
rootResources.push(this.contextService.getWorkspace().resource);
}
let query: IQueryOptions = { filePattern: searchValue, rootResources: rootResources };
let query: IQueryOptions = { filePattern: searchValue, matchFuzzy: this.fuzzyMatchingEnabled, rootResources: rootResources };
return this.queryBuilder.file(query).then((query) => this.searchService.search(query)).then((complete) => {
......@@ -148,7 +153,7 @@ export class OpenFileHandler extends QuickOpenHandler {
let entry = this.instantiationService.createInstance(FileEntry, label, description, fileMatch.resource);
// Apply highlights
let {labelHighlights, descriptionHighlights} = QuickOpenEntry.highlight(entry, searchValue);
let {labelHighlights, descriptionHighlights} = QuickOpenEntry.highlight(entry, searchValue, this.fuzzyMatchingEnabled);
entry.setHighlights(labelHighlights, descriptionHighlights);
results.push(entry);
......
......@@ -192,23 +192,19 @@ export class OpenSymbolHandler extends QuickOpenHandler {
}
private sort(searchValue: string, elementA: SymbolEntry, elementB: SymbolEntry): number {
let elementAName = elementA.getName().toLowerCase();
let elementBName = elementB.getName().toLowerCase();
// Compare by name
let r = strings.localeCompare(elementAName, elementBName);
if (r !== 0) {
return r;
}
// Sort by Type if name is identical
let elementAName = elementA.getName().toLowerCase();
let elementBName = elementB.getName().toLowerCase();
if (elementAName === elementBName) {
let elementAType = elementA.getType();
let elementBType = elementB.getType();
if (elementAType !== elementBType) {
return OpenSymbolHandler.SUPPORTED_OPEN_TYPES.indexOf(elementAType) < OpenSymbolHandler.SUPPORTED_OPEN_TYPES.indexOf(elementBType) ? -1 : 1;
}
}
return 0; // Keep default sorting order otherwise
return QuickOpenEntry.compare(elementA, elementB, searchValue);
}
public getGroupLabel(): string {
......
......@@ -197,6 +197,11 @@ configurationRegistry.registerConfiguration({
}
]
}
},
'search.fuzzyFilePicker': {
'type': 'boolean',
'default': false,
'description': nls.localize('enableFuzzy', "Enable or disable fuzzy matching and sorting in the file picker.")
}
}
});
\ No newline at end of file
......@@ -63,7 +63,8 @@ export class QueryBuilder {
includePattern: options.includePattern,
maxResults: options.maxResults,
fileEncoding: options.fileEncoding,
contentPattern: contentPattern
contentPattern: contentPattern,
matchFuzzy: options.matchFuzzy
};
});
}
......
......@@ -32,12 +32,14 @@ export class FileWalker {
private resultCount: number;
private isCanceled: boolean;
private searchInPath: boolean;
private matchFuzzy: boolean;
private walkedPaths: { [path: string]: boolean; };
constructor(config: IRawSearch) {
this.config = config;
this.filePattern = config.filePattern;
this.matchFuzzy = config.matchFuzzy;
this.excludePattern = config.excludePattern;
this.includePattern = config.includePattern;
this.maxResults = config.maxResults || null;
......@@ -194,7 +196,7 @@ export class FileWalker {
// Check for search pattern
if (this.filePattern) {
const res = filters.matchesFuzzy(this.filePattern, this.searchInPath ? path : name);
const res = filters.matchesFuzzy(this.filePattern, this.matchFuzzy || this.searchInPath ? path : name, this.matchFuzzy);
return !!res && res.length > 0;
}
......
......@@ -19,6 +19,7 @@ import {Engine as TextSearchEngine} from 'vs/workbench/services/search/node/text
export interface IRawSearch {
rootPaths: string[];
filePattern?: string;
matchFuzzy?: boolean;
excludePattern?: glob.IExpression;
includePattern?: glob.IExpression;
contentPattern?: IPatternInfo;
......
......@@ -223,7 +223,8 @@ class DiskSearch {
filePattern: query.filePattern,
excludePattern: query.excludePattern,
includePattern: query.includePattern,
maxResults: query.maxResults
maxResults: query.maxResults,
matchFuzzy: query.matchFuzzy
};
if (query.type === QueryType.Text) {
......
......@@ -66,6 +66,25 @@ suite('Search', () => {
});
});
test('Files: examples (fuzzy)', function(done: () => void) {
let engine = new FileSearchEngine({
rootPaths: [require.toUrl('./fixtures')],
filePattern: 'xl',
matchFuzzy: true
});
let count = 0;
engine.search((result) => {
if (result) {
count++;
}
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(count, 5);
done();
});
});
test('Files: *.js (Files as roots)', function(done: () => void) {
let engine = new FileSearchEngine({
rootPaths: [require.toUrl('./fixtures/examples/company.js'), require.toUrl('./fixtures/examples/small.js')],
......
......@@ -6,7 +6,7 @@
'use strict';
import * as assert from 'assert';
import {TestKeybindingService, TestContextService, TestStorageService, TestEventService, TestEditorService, TestQuickOpenService} from 'vs/workbench/test/browser/servicesTestUtils';
import {TestKeybindingService, TestConfigurationService, TestContextService, TestStorageService, TestEventService, TestEditorService, TestQuickOpenService} from 'vs/workbench/test/browser/servicesTestUtils';
import {Registry} from 'vs/platform/platform';
import {EditorHistoryModel, EditorHistoryEntry} from 'vs/workbench/browser/parts/quickopen/editorHistoryModel';
import {QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions as QuickOpenExtensions} from 'vs/workbench/browser/quickopen';
......
......@@ -27,6 +27,7 @@ import Severity from 'vs/base/common/severity';
import Arrays = require('vs/base/common/arrays');
import Errors = require('vs/base/common/errors');
import http = require('vs/base/common/http');
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage';
import UntitledEditorService = require('vs/workbench/services/untitled/browser/untitledEditorService');
import WorkbenchEditorService = require('vs/workbench/services/editor/common/editorService');
......@@ -71,7 +72,11 @@ export class TestContextService implements WorkspaceContextService.IWorkspaceCon
constructor(workspace: any = TestWorkspace, configuration: any = TestConfiguration, options: any = null) {
this.workspace = workspace;
this.configuration = configuration;
this.options = options;
this.options = options || {
globalSettings: {
settings: {}
}
};
}
public getWorkspace(): IWorkspace {
......@@ -466,7 +471,7 @@ export class TestQuickOpenService implements QuickOpenService.IQuickOpenService
private callback: (prefix: string) => void;
constructor(callback: (prefix: string) => void) {
constructor(callback?: (prefix: string) => void) {
this.callback = callback;
}
......@@ -483,7 +488,7 @@ export class TestQuickOpenService implements QuickOpenService.IQuickOpenService
}
show(prefix?: string, quickNavigateConfiguration?: any): Promise {
this.callback(prefix);
this.callback && this.callback(prefix);
return Promise.as(true);
}
......@@ -531,3 +536,15 @@ export const TestFileService = {
});
}
}
export class TestConfigurationService extends EventEmitter.EventEmitter implements IConfigurationService {
public serviceId = IConfigurationService;
public loadConfiguration(section?:string):TPromise<any> {
return TPromise.as({});
}
public hasWorkspaceConfiguration():boolean {
return false;
}
}
\ No newline at end of file
......@@ -32,11 +32,7 @@ define([
'vs/workbench/browser/actions/showPerformanceBox',
'vs/workbench/browser/actions/openSettings',
'vs/workbench/parts/quickopen/browser/gotoSymbolHandler',
'vs/workbench/parts/quickopen/browser/commandsHandler',
'vs/workbench/parts/quickopen/browser/gotoLineHandler',
'vs/workbench/parts/quickopen/browser/helpHandler',
'vs/workbench/parts/quickopen/browser/markersHandler',
'vs/workbench/parts/quickopen/browser/quickopen.contribution',
'vs/workbench/parts/files/browser/explorerViewlet',
'vs/workbench/parts/files/browser/fileActions.contribution',
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册