提交 ae391417 编写于 作者: B Benjamin Pasero

quick access - first cut history results in open anything

上级 bcc488e0
......@@ -57,6 +57,14 @@ 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, protected options?: IPickerQuickAccessProviderOptions) {
......@@ -83,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;
......@@ -159,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,6 +3,7 @@
* 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 } from 'vs/platform/quickinput/common/quickInput';
import { PickerQuickAccessProvider, IPickerQuickAccessItem, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess';
......@@ -99,7 +100,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")
}
],
......
/*---------------------------------------------------------------------------------------------
* 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-editor::before {
content: "\ea76"; /* Close icon flips between black dot and "X" for dirty open editors */
}
......@@ -3,8 +3,9 @@
* 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 } from 'vs/platform/quickinput/common/quickInput';
import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction, FastAndSlowPicksType } 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';
......@@ -14,7 +15,7 @@ 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 { 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';
......@@ -26,15 +27,20 @@ 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 { 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';
interface IAnythingQuickPickItem extends IPickerQuickAccessItem {
resource: URI;
resource: URI | undefined;
}
export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnythingQuickPickItem> {
......@@ -55,7 +61,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
@IModeService private readonly modeService: IModeService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService
@IEditorService private readonly editorService: IEditorService,
@IHistoryService private readonly historyService: IHistoryService,
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService
) {
super(AnythingQuickAccessProvider.PREFIX, { canAcceptInBackground: true });
}
......@@ -67,11 +75,13 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
return {
openEditorPinned: !editorConfig.enablePreviewFromQuickOpen,
openSideBySideDirection: editorConfig.openSideBySideDirection,
includeSymbols: searchConfig.search.quickOpen.includeSymbols
includeSymbols: searchConfig.search.quickOpen.includeSymbols,
includeHistory: searchConfig.search.quickOpen.includeHistory,
shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY
};
}
protected async getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<IAnythingQuickPickItem | IQuickPickSeparator>> {
protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicksType<IAnythingQuickPickItem | IQuickPickSeparator> {
// TODO this should run just once when picker opens
this.warmUpFileQueryCache();
......@@ -86,14 +96,47 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
const query = prepareQuery(filter);
// TODO include history results
// TODO exclude duplicates from editor history!
// TODO groups ("recently opened", "file results", "file and symbol results")
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, token),
this.getSymbolPicks(query, range, token)
this.getFilePicks(query, range, excludes, token),
this.getSymbolPicks(query, range, excludes, token)
]);
if (token.isCancellationRequested) {
......@@ -121,16 +164,50 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
return sortedAnythingPicks;
}
//#region Editor History
protected getHistoryPicks(filter: string): Array<IAnythingQuickPickItem> {
return [];
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));
}
// Otherwise filter and sort by query
const editorHistoryPicks: Array<IAnythingQuickPickItem> = [];
const scorerCache = Object.create(null); // TODO should keep this for as long as the picker is opened (also check other pickers)
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, quickPickItemScorerAccessor, 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, quickPickItemScorerAccessor, scorerCache, () => -1));
}
//#endregion
//# File Search
//#region 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
......@@ -149,7 +226,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
this.fileQueryCacheState.load();
}
protected async getFilePicks(query: IPreparedQuery, range: IRange | undefined, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
protected async getFilePicks(query: IPreparedQuery, range: IRange | undefined, excludes: ResourceMap<boolean>, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
if (!query.value) {
return [];
}
......@@ -179,8 +256,10 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
return [];
}
// Convert to picks
return fileMatches.map(fileMatch => this.createFilePick(fileMatch.resource, range, false));
// 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[]> {
......@@ -201,78 +280,6 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
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',
......@@ -322,7 +329,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
//#region Symbols (if enabled)
protected async getSymbolPicks(query: IPreparedQuery, range: IRange | undefined, token: CancellationToken): Promise<Array<IAnythingQuickPickItem>> {
protected async getSymbolPicks(query: IPreparedQuery, range: IRange | undefined, excludes: ResourceMap<boolean>, 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
......@@ -335,4 +342,108 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
}
//#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), // TODO force 'file' icon if symbols are merged in for better looks
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 */
}
......@@ -76,6 +76,7 @@ export function getWorkspaceSymbols(query: string, token: CancellationToken = Ca
export interface IWorkbenchSearchConfigurationProperties extends ISearchConfigurationProperties {
quickOpen: {
includeSymbols: boolean;
includeHistory: boolean;
};
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册