diff --git a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts index d3e1a048da2bb26dd679f1fe2e61264a6fdc7cbb..1cebb8652a452a591f582d8f08452d5b1f87c34b 100644 --- a/src/vs/platform/quickinput/browser/commandsQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/commandsQuickAccess.ts @@ -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> { diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index fca14db0d98e6939ebf6d7b61b072dcc2f9480ca..389cac3d346139f569f3c9e5ef0d08e189bbb0c1 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -53,17 +53,21 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; } +export interface IPickerQuickAccessProviderOptions { + canAcceptInBackground?: boolean; +} + export abstract class PickerQuickAccessProvider extends Disposable implements IQuickAccessProvider { - constructor(private prefix: string) { + constructor(private prefix: string, protected options?: IPickerQuickAccessProviderOptions) { super(); } provide(picker: IQuickPick, 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): 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 diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts index 0194b0a3b3b0656c4535b121bcb09c7faa09ac54..6717efcbfe1faba7f5bded0f1afc25d9e14ab598 100644 --- a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -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): void { - - // Allow to open editors in background without closing picker - picker.canAcceptInBackground = true; + super(prefix, { canAcceptInBackground: true }); } protected getPicks(filter: string): Array { diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts new file mode 100644 index 0000000000000000000000000000000000000000..663b4942d37ddbac2da71fe0b3a550dac3a3cc05 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -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 { + + 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().workbench.editor; + const searchConfig = this.configurationService.getValue(); + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection, + includeSymbols: searchConfig.search.quickOpen.includeSymbols + }; + } + + protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + + // 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 { + 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(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> { + 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>; + 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 { + 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 { + 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 { + 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> { + 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 +} diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index b57f2b35e6b6efc0327e5374080d22490b447061..55dc1003bef137f5568431fa4f3865703a1c2994 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -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(QuickOpenExtensions.Quickopen).registerQuickOpen ); // Register Quick Access Handler +const quickAccessRegistry = Registry.as(QuickAccessExtensions.Quickaccess); -Registry.as(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."), diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index 19c7c4d083d57c7ee6d9bef6301703cca2591247..27e57cc75623a27b193369d53afaaaafca3387a0 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -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(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY); + private delayer = this._register(new ThrottledDelayer(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY)); private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService)); @@ -46,13 +46,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider): 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 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 { + private async openSymbol(provider: IWorkspaceSymbolProvider, symbol: IWorkspaceSymbol, token: CancellationToken, options: { keyMods: IKeyMods, forceOpenSideBySide?: boolean, preserveFocus?: boolean }): Promise { // Resolve actual symbol to open for providers that can resolve let symbolToOpen = symbol; @@ -195,10 +189,10 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider | undefined; + + constructor( + private cacheQuery: (cacheKey: string) => IFileQuery, + private loadFn: (query: IFileQuery) => Promise, + private disposeFn: (cacheKey: string) => Promise, + 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; + } + } +} diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index 10e3f0938343af29fe7852a52c5277935ccf458b..3aa857bbeae859039f657fd0781fdb5e3cf7a4df 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -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 <#|:|(><#|:|,> +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; +} diff --git a/src/vs/workbench/contrib/search/test/browser/openFileHandler.test.ts b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts similarity index 96% rename from src/vs/workbench/contrib/search/test/browser/openFileHandler.test.ts rename to src/vs/workbench/contrib/search/test/common/cacheState.test.ts index d6887d88e8284ac09bb20435772d56b1166c2adc..6baa982999aef636a594ed700d26072cab79008d 100644 --- a/src/vs/workbench/contrib/search/test/browser/openFileHandler.test.ts +++ b/src/vs/workbench/contrib/search/test/common/cacheState.test.ts @@ -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), diff --git a/src/vs/workbench/contrib/search/test/common/extractRange.test.ts b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..d508b8be961aeac7ab829cb3ec1043dd6454592a --- /dev/null +++ b/src/vs/workbench/contrib/search/test/common/extractRange.test.ts @@ -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); + }); +}); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 8b354bf3d01b82cdc8d9570fe9df5545f9226b1a..154e738b70c8cf5ecd9d945303c08010b3168bc2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -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); diff --git a/src/vs/workbench/contrib/terminal/browser/terminaQuickAccess.ts b/src/vs/workbench/contrib/terminal/browser/terminalsQuickAccess.ts similarity index 90% rename from src/vs/workbench/contrib/terminal/browser/terminaQuickAccess.ts rename to src/vs/workbench/contrib/terminal/browser/terminalsQuickAccess.ts index e73900ed0a06e1ac544eb61a73eb731a1b170ed0..32546d2eaab485aaa2c6e1ef114e5cfb6d4eb89a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminaQuickAccess.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalsQuickAccess.ts @@ -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): void { - - // Allow to open terminals in background without closing picker - picker.canAcceptInBackground = true; + super(TerminalQuickAccessProvider.PREFIX, { canAcceptInBackground: true }); } protected getPicks(filter: string): Array {