mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-20 02:34:37 +01:00
quick access - first cut open anything
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
||||
|
||||
+4
-4
@@ -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),
|
||||
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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);
|
||||
|
||||
|
||||
+2
-8
@@ -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> {
|
||||
Reference in New Issue
Block a user