提交 8eb4e841 编写于 作者: B Benjamin Pasero

quick access - first cut open anything

上级 b7bfce30
......@@ -5,7 +5,7 @@
import { localize } from 'vs/nls';
import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { PickerQuickAccessProvider, IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { PickerQuickAccessProvider, IPickerQuickAccessItem, IPickerQuickAccessProviderOptions } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { distinct } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle';
......@@ -30,7 +30,7 @@ export interface ICommandQuickPick extends IPickerQuickAccessItem {
commandAlias: string | undefined;
}
export interface ICommandsQuickAccessOptions {
export interface ICommandsQuickAccessOptions extends IPickerQuickAccessProviderOptions {
showAlias: boolean;
}
......@@ -43,14 +43,14 @@ export abstract class AbstractCommandsQuickAccessProvider extends PickerQuickAcc
private readonly commandsHistory = this._register(this.instantiationService.createInstance(CommandsHistory));
constructor(
private options: ICommandsQuickAccessOptions,
protected options: ICommandsQuickAccessOptions,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@ICommandService private readonly commandService: ICommandService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@INotificationService private readonly notificationService: INotificationService
) {
super(AbstractCommandsQuickAccessProvider.PREFIX);
super(AbstractCommandsQuickAccessProvider.PREFIX, options);
}
protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<ICommandQuickPick | IQuickPickSeparator>> {
......
......@@ -53,17 +53,21 @@ export interface IPickerQuickAccessItem extends IQuickPickItem {
trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise<TriggerAction>;
}
export interface IPickerQuickAccessProviderOptions {
canAcceptInBackground?: boolean;
}
export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem> extends Disposable implements IQuickAccessProvider {
constructor(private prefix: string) {
constructor(private prefix: string, protected options?: IPickerQuickAccessProviderOptions) {
super();
}
provide(picker: IQuickPick<T>, token: CancellationToken): IDisposable {
const disposables = new DisposableStore();
// Allow subclasses to configure picker
this.configure(picker);
// Apply options if any
picker.canAcceptInBackground = !!this.options?.canAcceptInBackground;
// Disable filtering & sorting, we control the results
picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false;
......@@ -142,13 +146,6 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
return disposables;
}
/**
* Subclasses can override this method to configure the picker before showing it.
*
* @param picker the picker instance used for the quick access before it opens.
*/
protected configure(picker: IQuickPick<T>): void { }
/**
* Returns an array of picks and separators as needed. If the picks are resolved
* long running, the provided cancellation token should be used to cancel the
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { IQuickPickSeparator, quickPickItemScorerAccessor, IQuickPickItemWithResource, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
import { IQuickPickSeparator, quickPickItemScorerAccessor, IQuickPickItemWithResource } from 'vs/platform/quickinput/common/quickInput';
import { PickerQuickAccessProvider, IPickerQuickAccessItem, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { EditorsOrder, IEditorIdentifier, toResource, SideBySideEditor } from 'vs/workbench/common/editor';
......@@ -25,13 +25,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService
) {
super(prefix);
}
protected configure(picker: IQuickPick<IEditorQuickPickItem>): void {
// Allow to open editors in background without closing picker
picker.canAcceptInBackground = true;
super(prefix, { canAcceptInBackground: true });
}
protected getPicks(filter: string): Array<IEditorQuickPickItem | IQuickPickSeparator> {
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IQuickPickSeparator, IQuickInputButton, IKeyMods, quickPickItemScorerAccessor } from 'vs/platform/quickinput/common/quickInput';
import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { prepareQuery, IPreparedQuery, compareItemsByScore, scoreItem } from 'vs/base/common/fuzzyScorer';
import { IFileQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { getOutOfWorkspaceEditorResources, extractRangeFromFilter, IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search';
import { ISearchService, IFileMatch } from 'vs/workbench/services/search/common/search';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { untildify } from 'vs/base/common/labels';
import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService';
import { URI } from 'vs/base/common/uri';
import { toLocalResource, basename, dirname } from 'vs/base/common/resources';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IFileService } from 'vs/platform/files/common/files';
import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { ILabelService } from 'vs/platform/label/common/label';
import { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { localize } from 'vs/nls';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { Range, IRange } from 'vs/editor/common/core/range';
import { ThrottledDelayer } from 'vs/base/common/async';
import { top } from 'vs/base/common/arrays';
import { FileQueryCacheState } from 'vs/workbench/contrib/search/common/cacheState';
interface IAnythingQuickPickItem extends IPickerQuickAccessItem {
resource: URI;
}
export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnythingQuickPickItem> {
static PREFIX = '';
private static readonly MAX_RESULTS = 512;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ISearchService private readonly searchService: ISearchService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IRemotePathService private readonly remotePathService: IRemotePathService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IFileService private readonly fileService: IFileService,
@ILabelService private readonly labelService: ILabelService,
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService
) {
super(AnythingQuickAccessProvider.PREFIX, { canAcceptInBackground: true });
}
private get configuration() {
const editorConfig = this.configurationService.getValue<IWorkbenchEditorConfiguration>().workbench.editor;
const searchConfig = this.configurationService.getValue<IWorkbenchSearchConfiguration>();
return {
openEditorPinned: !editorConfig.enablePreviewFromQuickOpen,
openSideBySideDirection: editorConfig.openSideBySideDirection,
includeSymbols: searchConfig.search.quickOpen.includeSymbols
};
}
protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<IAnythingQuickPickItem | IQuickPickSeparator>> {
// TODO this should run just once when picker opens
this.warmUpFileQueryCache();
// Find a suitable range from the pattern looking for ":", "#" or ","
let range: IRange | undefined = undefined;
const filterWithRange = extractRangeFromFilter(filter);
if (filterWithRange) {
filter = filterWithRange.filter;
range = filterWithRange.range;
}
const query = prepareQuery(filter);
// TODO include history results
// TODO exclude duplicates from editor history!
// TODO groups ("recently opened", "file results", "file and symbol results")
// Resolve file and symbol picks (if enabled)
const [filePicks, symbolPicks] = await Promise.all([
this.getFilePicks(query, range, token),
this.getSymbolPicks(query, range, token)
]);
if (token.isCancellationRequested) {
return [];
}
// Sort top 512 items by score
const scorerCache = Object.create(null); // TODO should keep this for as long as the picker is opened (also check other pickers)
const sortedAnythingPicks = top(
[...filePicks, ...symbolPicks],
(anyPickA, anyPickB) => compareItemsByScore(anyPickA, anyPickB, query, true, quickPickItemScorerAccessor, scorerCache),
AnythingQuickAccessProvider.MAX_RESULTS
);
// Adjust highlights
for (const anythingPick of sortedAnythingPicks) {
const { labelMatch, descriptionMatch } = scoreItem(anythingPick, query, true, quickPickItemScorerAccessor, scorerCache);
anythingPick.highlights = {
label: labelMatch,
description: descriptionMatch
};
}
return sortedAnythingPicks;
}
//#region Editor History
protected getHistoryPicks(filter: string): Array<IAnythingQuickPickItem> {
return [];
}
//#endregion
//# File Search
private static readonly FILE_QUERY_DELAY = 200; // this delay accommodates for the user typing a word and then stops typing to start searching
private fileQueryDelayer = this._register(new ThrottledDelayer<IFileMatch[]>(AnythingQuickAccessProvider.FILE_QUERY_DELAY));
private fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder);
private fileQueryCacheState: FileQueryCacheState | undefined;
private warmUpFileQueryCache(): void {
this.fileQueryCacheState = new FileQueryCacheState(
cacheKey => this.fileQueryBuilder.file(this.contextService.getWorkspace().folders, this.getFileQueryOptions({ cacheKey })),
query => this.searchService.fileSearch(query),
cacheKey => this.searchService.clearCache(cacheKey),
this.fileQueryCacheState
);
this.fileQueryCacheState.load();
}
protected async getFilePicks(query: IPreparedQuery, range: IRange | undefined, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
if (!query.value) {
return [];
}
// Absolute path result
const absolutePathResult = await this.getAbsolutePathFileResult(query, token);
if (token.isCancellationRequested) {
return [];
}
// Use absolute path result as only results if present
let fileMatches: Array<IFileMatch<URI>>;
if (absolutePathResult) {
fileMatches = [{ resource: absolutePathResult }];
}
// Otherwise run the file search (with a delayer if cache is not ready yet)
else {
if (this.fileQueryCacheState?.isLoaded) {
fileMatches = await this.doFileSearch(query, token);
} else {
fileMatches = await this.fileQueryDelayer.trigger(() => this.doFileSearch(query, token));
}
}
if (token.isCancellationRequested) {
return [];
}
// Convert to picks
return fileMatches.map(fileMatch => this.createFilePick(fileMatch.resource, range, false));
}
private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise<IFileMatch[]> {
if (token.isCancellationRequested) {
return [];
}
const { results } = await this.searchService.fileSearch(
this.fileQueryBuilder.file(
this.contextService.getWorkspace().folders,
this.getFileQueryOptions({
filePattern: query.original,
cacheKey: this.fileQueryCacheState?.cacheKey,
maxResults: AnythingQuickAccessProvider.MAX_RESULTS
})
), token);
return results;
}
private createFilePick(resource: URI, range: IRange | undefined, isHistoryResult: boolean): IAnythingQuickPickItem {
const label = basename(resource);
const description = this.labelService.getUriLabel(dirname(resource), { relative: true });
const isDirty = this.workingCopyService.isDirty(resource);
const openSideBySideDirection = this.configuration.openSideBySideDirection;
return {
resource,
label,
ariaLabel: localize('filePickAriaLabel', "{0}, file picker", label),
description,
iconClasses: getIconClasses(this.modelService, this.modeService, resource), // TODO force 'file' icon if symbols are merged in for better looks
buttonsAlwaysVisible: isDirty,
buttons: (() => {
const buttons: IQuickInputButton[] = [];
// Open to side / below
buttons.push({
iconClass: openSideBySideDirection === 'right' ? 'codicon-split-horizontal' : 'codicon-split-vertical',
tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom")
});
// Remove from History
if (isHistoryResult) {
buttons.push({
iconClass: isDirty ? 'codicon-circle-filled' : 'codicon-close',
tooltip: localize('closeEditor', "Close Editor")
});
}
// Dirty indicator
else if (isDirty) {
buttons.push({
iconClass: 'codicon-circle-filled',
tooltip: localize('dirtyFile', "Dirty File")
});
}
return buttons;
})(),
trigger: async (buttonIndex, keyMods) => {
switch (buttonIndex) {
// Open to side / below
case 0:
this.openFile(resource, { keyMods, range, forceOpenSideBySide: true });
return TriggerAction.CLOSE_PICKER;
// Remove from History / Dirty Indicator
case 1:
//TODO
return TriggerAction.REFRESH_PICKER;
}
return TriggerAction.NO_ACTION;
},
accept: (keyMods, event) => this.openFile(resource, { keyMods, range, preserveFocus: event.inBackground })
};
}
private async openFile(resource: URI, options: { keyMods?: IKeyMods, preserveFocus?: boolean, range?: IRange, forceOpenSideBySide?: boolean }): Promise<void> {
await this.editorService.openEditor({
resource,
options: {
preserveFocus: options.preserveFocus,
pinned: options.keyMods?.alt || this.configuration.openEditorPinned,
selection: options.range ? Range.collapseToStart(options.range) : undefined
}
}, options.keyMods?.ctrlCmd || options.forceOpenSideBySide ? SIDE_GROUP : ACTIVE_GROUP);
}
private getFileQueryOptions(input: { filePattern?: string, cacheKey?: string, maxResults?: number }): IFileQueryBuilderOptions {
const fileQueryOptions: IFileQueryBuilderOptions = {
_reason: 'openFileHandler',
extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
filePattern: input.filePattern || '',
cacheKey: input.cacheKey,
maxResults: input.maxResults || 0,
sortByScore: true
};
return fileQueryOptions;
}
private async getAbsolutePathFileResult(query: IPreparedQuery, token: CancellationToken): Promise<URI | undefined> {
const detildifiedQuery = untildify(query.original, (await this.remotePathService.userHome).path);
if (token.isCancellationRequested) {
return;
}
const isAbsolutePathQuery = (await this.remotePathService.path).isAbsolute(detildifiedQuery);
if (token.isCancellationRequested) {
return;
}
if (isAbsolutePathQuery) {
const resource = toLocalResource(
await this.remotePathService.fileURI(detildifiedQuery),
this.environmentService.configuration.remoteAuthority
);
if (token.isCancellationRequested) {
return;
}
try {
return (await this.fileService.resolve(resource)).isDirectory ? undefined : resource;
} catch (error) {
// ignore
}
}
return;
}
//#endregion
//#region Symbols (if enabled)
protected async getSymbolPicks(query: IPreparedQuery, range: IRange | undefined, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
if (
!query.value || // we need a value for search for
!this.configuration.includeSymbols || // we need to enable symbols in search
range // a range is an indicator for just searching for files
) {
return [];
}
return [];
}
//#endregion
}
......@@ -57,6 +57,7 @@ import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEd
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess';
import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess';
import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess';
registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true);
registerSingleton(ISearchHistoryService, SearchHistoryService, true);
......@@ -654,8 +655,17 @@ Registry.as<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpen
);
// Register Quick Access Handler
const quickAccessRegistry = Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess);
Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({
quickAccessRegistry.registerQuickAccessProvider({
ctor: AnythingQuickAccessProvider,
prefix: AnythingQuickAccessProvider.PREFIX,
placeholder: nls.localize('anythingQuickAccessPlaceholder', "Type '?' to get help on the actions you can take from here"),
contextKey: 'inFilesPicker',
helpEntries: [{ description: nls.localize('anythingQuickAccess', "Go to File"), needsEditor: false }]
});
quickAccessRegistry.registerQuickAccessProvider({
ctor: SymbolsQuickAccessProvider,
prefix: SymbolsQuickAccessProvider.PREFIX,
placeholder: nls.localize('symbolsQuickAccessPlaceholder', "Type the name of a symbol to open."),
......
......@@ -19,7 +19,7 @@ import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/
import { Range } from 'vs/editor/common/core/range';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
import { IKeyMods, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
import { IKeyMods } from 'vs/platform/quickinput/common/quickInput';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { createResourceExcludeMatcher } from 'vs/workbench/services/search/common/search';
import { ResourceMap } from 'vs/base/common/map';
......@@ -35,7 +35,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
private static readonly TYPING_SEARCH_DELAY = 200; // this delay accommodates for the user typing a word and then stops typing to start searching
private delayer = new ThrottledDelayer<ISymbolsQuickPickItem[]>(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY);
private delayer = this._register(new ThrottledDelayer<ISymbolsQuickPickItem[]>(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY));
private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService));
......@@ -46,13 +46,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
@IConfigurationService private readonly configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super(SymbolsQuickAccessProvider.PREFIX);
}
protected configure(picker: IQuickPick<ISymbolsQuickPickItem>): void {
// Allow to open symbols in background without closing picker
picker.canAcceptInBackground = true;
super(SymbolsQuickAccessProvider.PREFIX, { canAcceptInBackground: true });
}
private get configuration() {
......@@ -156,12 +150,12 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom")
}
],
accept: async (keyMods, event) => this.openSymbol(provider, symbol, token, keyMods, { preserveFocus: event.inBackground }),
trigger: (buttonIndex, keyMods) => {
this.openSymbol(provider, symbol, token, keyMods, { forceOpenSideBySide: true });
this.openSymbol(provider, symbol, token, { keyMods, forceOpenSideBySide: true });
return TriggerAction.CLOSE_PICKER;
}
},
accept: async (keyMods, event) => this.openSymbol(provider, symbol, token, { keyMods, preserveFocus: event.inBackground }),
});
}
}
......@@ -172,7 +166,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
return symbolPicks;
}
private async openSymbol(provider: IWorkspaceSymbolProvider, symbol: IWorkspaceSymbol, token: CancellationToken, keyMods: IKeyMods, options: { forceOpenSideBySide?: boolean, preserveFocus?: boolean }): Promise<void> {
private async openSymbol(provider: IWorkspaceSymbolProvider, symbol: IWorkspaceSymbol, token: CancellationToken, options: { keyMods: IKeyMods, forceOpenSideBySide?: boolean, preserveFocus?: boolean }): Promise<void> {
// Resolve actual symbol to open for providers that can resolve
let symbolToOpen = symbol;
......@@ -195,10 +189,10 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
resource: symbolToOpen.location.uri,
options: {
preserveFocus: options?.preserveFocus,
pinned: keyMods.alt || this.configuration.openEditorPinned,
pinned: options.keyMods.alt || this.configuration.openEditorPinned,
selection: symbolToOpen.location.range ? Range.collapseToStart(symbolToOpen.location.range) : undefined
}
}, keyMods.ctrlCmd || options?.forceOpenSideBySide ? SIDE_GROUP : ACTIVE_GROUP);
}, options.keyMods.ctrlCmd || options?.forceOpenSideBySide ? SIDE_GROUP : ACTIVE_GROUP);
}
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { IFileQuery } from 'vs/workbench/services/search/common/search';
import { assign, equals } from 'vs/base/common/objects';
enum LoadingPhase {
Created = 1,
Loading = 2,
Loaded = 3,
Errored = 4,
Disposed = 5
}
export class FileQueryCacheState {
private readonly _cacheKey = defaultGenerator.nextId();
get cacheKey(): string {
if (this.loadingPhase === LoadingPhase.Loaded || !this.previousCacheState) {
return this._cacheKey;
}
return this.previousCacheState.cacheKey;
}
get isLoaded(): boolean {
const isLoaded = this.loadingPhase === LoadingPhase.Loaded;
return isLoaded || !this.previousCacheState ? isLoaded : this.previousCacheState.isLoaded;
}
get isUpdating(): boolean {
const isUpdating = this.loadingPhase === LoadingPhase.Loading;
return isUpdating || !this.previousCacheState ? isUpdating : this.previousCacheState.isUpdating;
}
private readonly query = this.cacheQuery(this._cacheKey);
private loadingPhase = LoadingPhase.Created;
private loadPromise: Promise<void> | undefined;
constructor(
private cacheQuery: (cacheKey: string) => IFileQuery,
private loadFn: (query: IFileQuery) => Promise<any>,
private disposeFn: (cacheKey: string) => Promise<void>,
private previousCacheState: FileQueryCacheState | undefined
) {
if (this.previousCacheState) {
const current = assign({}, this.query, { cacheKey: null });
const previous = assign({}, this.previousCacheState.query, { cacheKey: null });
if (!equals(current, previous)) {
this.previousCacheState.dispose();
this.previousCacheState = undefined;
}
}
}
load(): void {
if (this.isUpdating) {
return;
}
this.loadingPhase = LoadingPhase.Loading;
this.loadPromise = (async () => {
try {
await this.loadFn(this.query);
this.loadingPhase = LoadingPhase.Loaded;
if (this.previousCacheState) {
this.previousCacheState.dispose();
this.previousCacheState = undefined;
}
} catch (error) {
this.loadingPhase = LoadingPhase.Errored;
throw error;
}
})();
}
dispose(): void {
if (this.loadPromise) {
(async () => {
try {
await this.loadPromise;
} catch (error) {
// ignore
}
this.loadingPhase = LoadingPhase.Disposed;
this.disposeFn(this._cacheKey);
})();
} else {
this.loadingPhase = LoadingPhase.Disposed;
}
if (this.previousCacheState) {
this.previousCacheState.dispose();
this.previousCacheState = undefined;
}
}
}
......@@ -14,6 +14,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { CancellationToken } from 'vs/base/common/cancellation';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IFileService } from 'vs/platform/files/common/files';
import { IRange } from 'vs/editor/common/core/range';
import { isNumber } from 'vs/base/common/types';
export interface IWorkspaceSymbol {
name: string;
......@@ -95,3 +97,62 @@ export function getOutOfWorkspaceEditorResources(accessor: ServicesAccessor): UR
return resources as URI[];
}
// Supports patterns of <path><#|:|(><line><#|:|,><col?>
const LINE_COLON_PATTERN = /\s?[#:\(](\d*)([#:,](\d*))?\)?\s*$/;
export function extractRangeFromFilter(filter: string): { filter: string, range: IRange } | undefined {
if (!filter) {
return undefined;
}
let range: IRange | undefined = undefined;
// Find Line/Column number from search value using RegExp
const patternMatch = LINE_COLON_PATTERN.exec(filter);
if (patternMatch && patternMatch.length > 1) {
const startLineNumber = parseInt(patternMatch[1], 10);
// Line Number
if (isNumber(startLineNumber)) {
range = {
startLineNumber: startLineNumber,
startColumn: 1,
endLineNumber: startLineNumber,
endColumn: 1
};
// Column Number
if (patternMatch.length > 3) {
const startColumn = parseInt(patternMatch[3], 10);
if (isNumber(startColumn)) {
range = {
startLineNumber: range.startLineNumber,
startColumn: startColumn,
endLineNumber: range.endLineNumber,
endColumn: startColumn
};
}
}
}
// User has typed "something:" or "something#" without a line number, in this case treat as start of file
else if (patternMatch[1] === '') {
range = {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 1
};
}
}
if (patternMatch && range) {
return {
filter: filter.substr(0, patternMatch.index), // clear range suffix from search value
range: range
};
}
return undefined;
}
......@@ -6,11 +6,11 @@
import * as assert from 'assert';
import * as errors from 'vs/base/common/errors';
import * as objects from 'vs/base/common/objects';
import { CacheState } from 'vs/workbench/contrib/search/browser/openFileHandler';
import { DeferredPromise } from 'vs/base/test/common/utils';
import { QueryType, IFileQuery } from 'vs/workbench/services/search/common/search';
import { FileQueryCacheState } from 'vs/workbench/contrib/search/common/cacheState';
suite('CacheState', () => {
suite('FileQueryCacheState', () => {
test('reuse old cacheKey until new cache is loaded', async function () {
......@@ -162,8 +162,8 @@ suite('CacheState', () => {
assert.strictEqual(third.cacheKey, thirdKey); // recover with next successful load
});
function createCacheState(cache: MockCache, previous?: CacheState): CacheState {
return new CacheState(
function createCacheState(cache: MockCache, previous?: FileQueryCacheState): FileQueryCacheState {
return new FileQueryCacheState(
cacheKey => cache.query(cacheKey),
query => cache.load(query),
cacheKey => cache.dispose(cacheKey),
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { extractRangeFromFilter } from 'vs/workbench/contrib/search/common/search';
suite('extractRangeFromFilter', () => {
test('basics', async function () {
assert.ok(!extractRangeFromFilter(''));
assert.ok(!extractRangeFromFilter('/some/path'));
assert.ok(!extractRangeFromFilter('/some/path/file.txt'));
for (const lineSep of [':', '#', '(']) {
for (const colSep of [':', '#', ',']) {
const base = '/some/path/file.txt';
let res = extractRangeFromFilter(`${base}${lineSep}20`);
assert.equal(res?.filter, base);
assert.equal(res?.range.startLineNumber, 20);
assert.equal(res?.range.startColumn, 1);
res = extractRangeFromFilter(`${base}${lineSep}20${colSep}`);
assert.equal(res?.filter, base);
assert.equal(res?.range.startLineNumber, 20);
assert.equal(res?.range.startColumn, 1);
res = extractRangeFromFilter(`${base}${lineSep}20${colSep}3`);
assert.equal(res?.filter, base);
assert.equal(res?.range.startLineNumber, 20);
assert.equal(res?.range.startColumn, 3);
}
}
});
test('allow space after path', async function () {
let res = extractRangeFromFilter('/some/path/file.txt (19,20)');
assert.equal(res?.filter, '/some/path/file.txt');
assert.equal(res?.range.startLineNumber, 19);
assert.equal(res?.range.startColumn, 20);
});
});
......@@ -40,7 +40,7 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess';
import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminaQuickAccess';
import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalsQuickAccess';
registerSingleton(ITerminalService, TerminalService, true);
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { IQuickPickSeparator, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { matchesFuzzy } from 'vs/base/common/filters';
import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
......@@ -19,13 +19,7 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider<IPick
@ITerminalService private readonly terminalService: ITerminalService,
@ICommandService private readonly commandService: ICommandService,
) {
super(TerminalQuickAccessProvider.PREFIX);
}
protected configure(picker: IQuickPick<IPickerQuickAccessItem>): void {
// Allow to open terminals in background without closing picker
picker.canAcceptInBackground = true;
super(TerminalQuickAccessProvider.PREFIX, { canAcceptInBackground: true });
}
protected getPicks(filter: string): Array<IPickerQuickAccessItem | IQuickPickSeparator> {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册