未验证 提交 f68b1bd2 编写于 作者: B Benjamin Pasero 提交者: GitHub

Merge pull request #92907 from microsoft/ben/quick-access-anything

Quick access anything provider
......@@ -308,22 +308,35 @@ export type QuickPickInput<T = IQuickPickItem> = T | IQuickPickSeparator;
export type IQuickPickItemWithResource = IQuickPickItem & { resource: URI | undefined };
export const quickPickItemScorerAccessor = new class implements IItemAccessor<IQuickPickItemWithResource> {
export class QuickPickItemScorerAccessor implements IItemAccessor<IQuickPickItemWithResource> {
constructor(private options?: { skipDescription?: boolean, skipPath?: boolean }) { }
getItemLabel(entry: IQuickPickItemWithResource): string {
return entry.label;
}
getItemDescription(entry: IQuickPickItemWithResource): string | undefined {
if (this.options?.skipDescription) {
return undefined;
}
return entry.description;
}
getItemPath(entry: IQuickPickItemWithResource): string | undefined {
if (this.options?.skipPath) {
return undefined;
}
if (entry.resource?.scheme === Schemas.file) {
return entry.resource.fsPath;
}
return entry.resource?.path;
}
};
}
export const quickPickItemScorerAccessor = new QuickPickItemScorerAccessor();
//#endregion
......@@ -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,29 @@ export interface IPickerQuickAccessItem extends IQuickPickItem {
trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise<TriggerAction>;
}
export interface IPickerQuickAccessProviderOptions {
canAcceptInBackground?: boolean;
}
export type FastAndSlowPicksType<T> = { picks: Array<T | IQuickPickSeparator>, additionalPicks: Promise<Array<T | IQuickPickSeparator>> };
function isFastAndSlowPicksType<T>(obj: unknown): obj is FastAndSlowPicksType<T> {
const candidate = obj as FastAndSlowPicksType<T>;
return Array.isArray(candidate.picks) && candidate.additionalPicks instanceof Promise;
}
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;
......@@ -79,9 +91,26 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
// Create new cancellation source for this run
picksCts = new CancellationTokenSource(token);
// Collect picks and support both long running and short
// Collect picks and support both long running and short or combined
const res = this.getPicks(picker.value.substr(this.prefix.length).trim(), disposables.add(new DisposableStore()), picksCts.token);
if (Array.isArray(res)) {
if (isFastAndSlowPicksType(res)) {
picker.items = res.picks;
picker.busy = true;
try {
const additionalPicks = await res.additionalPicks;
if (token.isCancellationRequested) {
return;
}
if (additionalPicks.length > 0) {
picker.items = [...res.picks, ...additionalPicks];
}
} finally {
if (!token.isCancellationRequested) {
picker.busy = false;
}
}
} else if (Array.isArray(res)) {
picker.items = res;
} else {
picker.busy = true;
......@@ -142,13 +171,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
......@@ -162,6 +184,7 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
* up when the picker closes.
* @param token for long running tasks, implementors need to check on cancellation
* through this token.
* @returns the picks either directly, as promise or combined fast and slow results.
*/
protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array<T | IQuickPickSeparator> | Promise<Array<T | IQuickPickSeparator>>;
protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array<T | IQuickPickSeparator> | Promise<Array<T | IQuickPickSeparator>> | { picks: Array<T | IQuickPickSeparator>, additionalPicks: Promise<Array<T | IQuickPickSeparator>> };
}
......@@ -3,8 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/editorquickaccess';
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,18 +26,14 @@ 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> {
const query = prepareQuery(filter);
const scorerCache = Object.create(null);
// Filtering
const filteredEditorEntries = this.doGetEditorPickItems().filter(entry => {
if (!query.value) {
return true;
......@@ -105,7 +102,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro
buttonsAlwaysVisible: isDirty,
buttons: [
{
iconClass: isDirty ? 'codicon-circle-filled' : 'codicon-close',
iconClass: isDirty ? 'dirty-editor codicon-circle-filled' : 'codicon-close',
tooltip: localize('closeEditor', "Close Editor")
}
],
......
......@@ -3,6 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .codicon-close::before {
content: "\ea71";
.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-editor::before {
content: "\ea76"; /* Close icon flips between black dot and "X" for dirty open editors */
}
......@@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/fileactions';
import * as nls from 'vs/nls';
import { isWindows, isWeb } from 'vs/base/common/platform';
import * as extpath from 'vs/base/common/extpath';
......
......@@ -25,6 +25,10 @@
justify-content: center;
}
.open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .codicon-close::before {
content: "\ea71"; /* Close icon flips between black dot and "X" for dirty open editors */
}
.open-editors .monaco-list .monaco-list-row > .monaco-action-bar .action-close-all-files,
.open-editors .monaco-list .monaco-list-row > .monaco-action-bar .save-all {
width: 23px;
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/anythingQuickAccess';
import { IQuickPickSeparator, IQuickInputButton, IKeyMods, quickPickItemScorerAccessor, QuickPickItemScorerAccessor, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction, FastAndSlowPicksType } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { prepareQuery, IPreparedQuery, compareItemsByScore, scoreItem, ScorerCache } 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, dirname, basenameOrAuthority } 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, IDisposable } 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, IEditorInput, EditorInput } 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';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { Schemas } from 'vs/base/common/network';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { ResourceMap } from 'vs/base/common/map';
import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess';
interface IAnythingQuickPickItem extends IPickerQuickAccessItem {
resource: URI | undefined;
}
export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnythingQuickPickItem> {
static PREFIX = '';
private static readonly MAX_RESULTS = 512;
private static readonly TYPING_SEARCH_DELAY = 200; // this delay accommodates for the user typing a word and then stops typing to start searching
private readonly pickState = new class {
scorerCache: ScorerCache = Object.create(null);
fileQueryCache: FileQueryCacheState | undefined;
constructor(private readonly provider: AnythingQuickAccessProvider) { }
reset(): void {
this.fileQueryCache = this.provider.createFileQueryCache();
this.scorerCache = Object.create(null);
}
}(this);
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,
@IHistoryService private readonly historyService: IHistoryService,
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService
) {
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,
includeHistory: searchConfig.search.quickOpen.includeHistory,
shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY
};
}
provide(picker: IQuickPick<IAnythingQuickPickItem>, token: CancellationToken): IDisposable {
// Reset the pick state for this run
this.pickState.reset();
// Start picker
return super.provide(picker, token);
}
protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicksType<IAnythingQuickPickItem | IQuickPickSeparator> {
// 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);
const historyEditorPicks = this.getEditorHistoryPicks(query, range);
return {
// Fast picks: editor history
picks: historyEditorPicks.length > 0 ?
[
{ type: 'separator', label: localize('recentlyOpenedSeparator', "recently opened") },
...historyEditorPicks
] : [],
// Slow picks: files and symbols
additionalPicks: (async (): Promise<Array<IAnythingQuickPickItem | IQuickPickSeparator>> => {
// Exclude any result that is already present in editor history
const additionalPicksExcludes = new ResourceMap<boolean>();
for (const historyEditorPick of historyEditorPicks) {
if (historyEditorPick.resource) {
additionalPicksExcludes.set(historyEditorPick.resource, true);
}
}
const additionalPicks = await this.getAdditionalPicks(query, range, additionalPicksExcludes, token);
if (token.isCancellationRequested) {
return [];
}
return additionalPicks.length > 0 ? [
{ type: 'separator', label: this.configuration.includeSymbols ? localize('fileAndSymbolResultsSeparator', "file and symbol results") : localize('fileResultsSeparator', "file results") },
...additionalPicks
] : [];
})()
};
}
private async getAdditionalPicks(query: IPreparedQuery, range: IRange | undefined, excludes: ResourceMap<boolean>, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
// Resolve file and symbol picks (if enabled)
const [filePicks, symbolPicks] = await Promise.all([
this.getFilePicks(query, range, excludes, token),
this.getSymbolPicks(query, range, token)
]);
if (token.isCancellationRequested) {
return [];
}
// Sort top 512 items by score
const sortedAnythingPicks = top(
[...filePicks, ...symbolPicks],
(anyPickA, anyPickB) => compareItemsByScore(anyPickA, anyPickB, query, true, quickPickItemScorerAccessor, this.pickState.scorerCache),
AnythingQuickAccessProvider.MAX_RESULTS
);
// Adjust highlights
for (const anythingPick of sortedAnythingPicks) {
if (anythingPick.highlights) {
continue; // preserve any highlights we got already (e.g. symbols)
}
const { labelMatch, descriptionMatch } = scoreItem(anythingPick, query, true, quickPickItemScorerAccessor, this.pickState.scorerCache);
anythingPick.highlights = {
label: labelMatch,
description: descriptionMatch
};
}
return sortedAnythingPicks;
}
//#region Editor History
private readonly labelOnlyEditorHistoryPickAccessor = new QuickPickItemScorerAccessor({ skipDescription: true });
protected getEditorHistoryPicks(query: IPreparedQuery, range: IRange | undefined): Array<IAnythingQuickPickItem> {
if (!this.configuration.includeHistory) {
return []; // disabled
}
// Just return all history entries if not searching
if (!query.value) {
return this.historyService.getHistory().map(editor => this.createAnythingPick(editor, range));
}
// Only match on label of the editor unless the search includes path separators
const editorHistoryScorerAccessor = query.containsPathSeparator ? quickPickItemScorerAccessor : this.labelOnlyEditorHistoryPickAccessor;
// Otherwise filter and sort by query
const editorHistoryPicks: Array<IAnythingQuickPickItem> = [];
for (const editor of this.historyService.getHistory()) {
const resource = editor.resource;
if (!resource || (!this.fileService.canHandleResource(resource) && resource.scheme !== Schemas.untitled)) {
continue; // exclude editors without file resource if we are searching by pattern
}
const editorHistoryPick = this.createAnythingPick(editor, range);
const { score, labelMatch, descriptionMatch } = scoreItem(editorHistoryPick, query, false, editorHistoryScorerAccessor, this.pickState.scorerCache);
if (!score) {
continue; // exclude editors not matching query
}
editorHistoryPick.highlights = {
label: labelMatch,
description: descriptionMatch
};
editorHistoryPicks.push(editorHistoryPick);
}
return editorHistoryPicks.sort((editorA, editorB) => compareItemsByScore(editorA, editorB, query, false, editorHistoryScorerAccessor, this.pickState.scorerCache, () => -1));
}
//#endregion
//#region File Search
private fileQueryDelayer = this._register(new ThrottledDelayer<IFileMatch[]>(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY));
private fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder);
private createFileQueryCache(): FileQueryCacheState {
return new FileQueryCacheState(
cacheKey => this.fileQueryBuilder.file(this.contextService.getWorkspace().folders, this.getFileQueryOptions({ cacheKey })),
query => this.searchService.fileSearch(query),
cacheKey => this.searchService.clearCache(cacheKey),
this.pickState.fileQueryCache
).load();
}
protected async getFilePicks(query: IPreparedQuery, range: IRange | undefined, excludes: ResourceMap<boolean>, 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.pickState.fileQueryCache?.isLoaded) {
fileMatches = await this.doFileSearch(query, token);
} else {
fileMatches = await this.fileQueryDelayer.trigger(async () => {
if (token.isCancellationRequested) {
return [];
}
return this.doFileSearch(query, token);
});
}
}
if (token.isCancellationRequested) {
return [];
}
// Filter excludes & convert to picks
return fileMatches
.filter(fileMatch => !excludes.has(fileMatch.resource))
.map(fileMatch => this.createAnythingPick(fileMatch.resource, range));
}
private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise<IFileMatch[]> {
const { results } = await this.searchService.fileSearch(
this.fileQueryBuilder.file(
this.contextService.getWorkspace().folders,
this.getFileQueryOptions({
filePattern: query.original,
cacheKey: this.pickState.fileQueryCache?.cacheKey,
maxResults: AnythingQuickAccessProvider.MAX_RESULTS
})
), token);
return results;
}
private getFileQueryOptions(input: { filePattern?: string, cacheKey?: string, maxResults?: number }): IFileQueryBuilderOptions {
const fileQueryOptions: IFileQueryBuilderOptions = {
_reason: 'openFileHandler', // used for telemetry - do not change
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)
private symbolsQuickAccess = this._register(this.instantiationService.createInstance(SymbolsQuickAccessProvider));
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 [];
}
// Delegate to the existing symbols quick access
// but skip local results and also do not sort
return this.symbolsQuickAccess.getSymbolPicks(query.value, { skipLocal: true, skipSorting: true, delay: AnythingQuickAccessProvider.TYPING_SEARCH_DELAY }, token);
}
//#endregion
//#region Helpers
private createAnythingPick(resourceOrEditor: URI | IEditorInput | IResourceEditorInput, range: IRange | undefined): IAnythingQuickPickItem {
const isEditorHistoryEntry = !URI.isUri(resourceOrEditor);
let resource: URI | undefined;
let label: string;
let description: string | undefined = undefined;
let isDirty: boolean | undefined = undefined;
if (resourceOrEditor instanceof EditorInput) {
resource = resourceOrEditor.resource;
label = resourceOrEditor.getName();
description = resourceOrEditor.getDescription();
isDirty = resourceOrEditor.isDirty() && !resourceOrEditor.isSaving();
} else {
resource = URI.isUri(resourceOrEditor) ? resourceOrEditor : (resourceOrEditor as IResourceEditorInput).resource;
label = basenameOrAuthority(resource);
description = this.labelService.getUriLabel(dirname(resource), { relative: true });
isDirty = this.workingCopyService.isDirty(resource) && !this.configuration.shortAutoSaveDelay;
}
return {
resource,
label,
ariaLabel: isEditorHistoryEntry ?
localize('historyPickAriaLabel', "{0}, recently opened", label) :
localize('filePickAriaLabel', "{0}, file picker", label),
description,
iconClasses: getIconClasses(this.modelService, this.modeService, resource),
buttonsAlwaysVisible: isDirty,
buttons: (() => {
const openSideBySideDirection = this.configuration.openSideBySideDirection;
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 (isEditorHistoryEntry) {
buttons.push({
iconClass: isDirty ? 'dirty-anything codicon-circle-filled' : 'codicon-close',
tooltip: localize('closeEditor', "Remove from Recently Opened")
});
}
// 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.openAnything(resourceOrEditor, { keyMods, range, forceOpenSideBySide: true });
return TriggerAction.CLOSE_PICKER;
// Remove from History / Dirty Indicator
case 1:
if (!URI.isUri(resourceOrEditor)) {
this.historyService.remove(resourceOrEditor);
return TriggerAction.REFRESH_PICKER;
}
}
return TriggerAction.NO_ACTION;
},
accept: (keyMods, event) => this.openAnything(resourceOrEditor, { keyMods, range, preserveFocus: event.inBackground })
};
}
private async openAnything(resourceOrEditor: URI | IEditorInput | IResourceEditorInput, options: { keyMods?: IKeyMods, preserveFocus?: boolean, range?: IRange, forceOpenSideBySide?: boolean }): Promise<void> {
const editorOptions: ITextEditorOptions = {
preserveFocus: options.preserveFocus,
pinned: options.keyMods?.alt || this.configuration.openEditorPinned,
selection: options.range ? Range.collapseToStart(options.range) : undefined
};
const targetGroup = options.keyMods?.ctrlCmd || options.forceOpenSideBySide ? SIDE_GROUP : ACTIVE_GROUP;
if (resourceOrEditor instanceof EditorInput) {
await this.editorService.openEditor(resourceOrEditor, editorOptions);
} else {
await this.editorService.openEditor({
resource: URI.isUri(resourceOrEditor) ? resourceOrEditor : resourceOrEditor.resource,
options: editorOptions
}, targetGroup);
}
}
//#endregion
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-anything::before {
content: "\ea76"; /* Close icon flips between black dot and "X" for dirty open editors */
}
......@@ -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,23 +19,25 @@ 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';
import { URI } from 'vs/base/common/uri';
interface ISymbolsQuickPickItem extends IPickerQuickAccessItem {
score: FuzzyScore;
interface ISymbolQuickPickItem extends IPickerQuickAccessItem {
resource: URI | undefined;
score: FuzzyScore | undefined;
symbol: IWorkspaceSymbol;
}
export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbolsQuickPickItem> {
export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbolQuickPickItem> {
static PREFIX = '#';
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<ISymbolQuickPickItem[]>(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY));
private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService));
......@@ -46,13 +48,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() {
......@@ -64,23 +60,27 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
};
}
protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<ISymbolsQuickPickItem>> {
protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> {
return this.getSymbolPicks(filter, undefined, token);
}
async getSymbolPicks(filter: string, options: { skipLocal: boolean, skipSorting: boolean, delay: number } | undefined, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> {
return this.delayer.trigger(async () => {
if (token.isCancellationRequested) {
return [];
}
return this.doGetSymbolPicks(filter, token);
});
return this.doGetSymbolPicks(filter, options, token);
}, options?.delay);
}
private async doGetSymbolPicks(filter: string, token: CancellationToken): Promise<Array<ISymbolsQuickPickItem>> {
private async doGetSymbolPicks(filter: string, options: { skipLocal: boolean, skipSorting: boolean } | undefined, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> {
const workspaceSymbols = await getWorkspaceSymbols(filter, token);
if (token.isCancellationRequested) {
return [];
}
const symbolPicks: Array<ISymbolsQuickPickItem> = [];
const symbolPicks: Array<ISymbolQuickPickItem> = [];
// Normalize filter
const [symbolFilter, containerFilter] = stripWildcards(filter).split(' ') as [string, string | undefined];
......@@ -92,6 +92,9 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
const symbolsExcludedByResource = new ResourceMap<boolean>();
for (const [provider, symbols] of workspaceSymbols) {
for (const symbol of symbols) {
if (options?.skipLocal && !!symbol.containerName) {
continue; // ignore local symbols if we are told so
}
// Score by symbol label
const symbolLabel = symbol.name;
......@@ -141,6 +144,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
symbolPicks.push({
symbol,
resource: symbolUri,
score: symbolScore,
label: symbolLabelWithIcon,
ariaLabel: localize('symbolAriaLabel', "{0}, symbols picker", symbolLabel),
......@@ -156,23 +160,25 @@ 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 }),
});
}
}
// Sort picks
symbolPicks.sort((symbolA, symbolB) => this.compareSymbols(symbolA, symbolB));
// Sort picks (unless disabled)
if (!options?.skipSorting) {
symbolPicks.sort((symbolA, symbolB) => this.compareSymbols(symbolA, symbolB));
}
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,14 +201,14 @@ 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);
}
}
private compareSymbols(symbolA: ISymbolsQuickPickItem, symbolB: ISymbolsQuickPickItem): number {
private compareSymbols(symbolA: ISymbolQuickPickItem, symbolB: ISymbolQuickPickItem): number {
// By score
if (symbolA.score && symbolB.score) {
......
/*---------------------------------------------------------------------------------------------
* 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(): FileQueryCacheState {
if (this.isUpdating) {
return this;
}
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;
}
})();
return this;
}
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;
......@@ -74,6 +76,7 @@ export function getWorkspaceSymbols(query: string, token: CancellationToken = Ca
export interface IWorkbenchSearchConfigurationProperties extends ISearchConfigurationProperties {
quickOpen: {
includeSymbols: boolean;
includeHistory: boolean;
};
}
......@@ -95,3 +98,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.
先完成此消息的编辑!
想要评论请 注册