提交 54bccc2a 编写于 作者: B Benjamin Pasero

quick access - fix various remaining issues:

- run additional file search over original query if limit hit and multiple inputs
- preserve existing highlights of symbols
- reduce flicker when having fast and slow results
上级 66f0e65e
......@@ -156,7 +156,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
// Collect symbol picks
picker.busy = true;
try {
const items = await this.doGetSymbolPicks(symbolsPromise, prepareQuery(picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim()), picksCts.token);
const items = await this.doGetSymbolPicks(symbolsPromise, prepareQuery(picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim()), undefined, picksCts.token);
if (token.isCancellationRequested) {
return;
}
......@@ -195,7 +195,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
return disposables;
}
protected async doGetSymbolPicks(symbolsPromise: Promise<DocumentSymbol[]>, query: IPreparedQuery, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
protected async doGetSymbolPicks(symbolsPromise: Promise<DocumentSymbol[]>, query: IPreparedQuery, options: { extraContainerLabel?: string } | undefined, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
const symbols = await symbolsPromise;
if (token.isCancellationRequested) {
return [];
......@@ -220,7 +220,13 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit
const symbol = symbols[index];
const symbolLabel = trim(symbol.name);
const containerLabel = symbol.containerName;
let containerLabel = symbol.containerName;
if (containerLabel && options?.extraContainerLabel) {
containerLabel = `${options.extraContainerLabel}${containerLabel}`;
} else {
containerLabel = options?.extraContainerLabel;
}
let symbolScore: FuzzyScore | undefined = undefined;
let containerScore: FuzzyScore | undefined = undefined;
......
......@@ -21,7 +21,7 @@ if (isWeb) {
// Running out of sources
if (Object.keys(product).length === 0) {
assign(product, {
version: '1.43.0-dev',
version: '1.44.0-dev',
nameLong: 'Visual Studio Code Web Dev',
nameShort: 'VSCode Web Dev',
urlProtocol: 'code-oss'
......
......@@ -115,15 +115,27 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
const picksToken = picksCts.token;
const providedPicks = this.getPicks(picker.value.substr(this.prefix.length).trim(), picksDisposables, picksToken);
function applyPicks(picks: Picks<T>): void {
function applyPicks(picks: Picks<T>, skipEmpty?: boolean): boolean {
let items: ReadonlyArray<Pick<T>>;
let activeItem: T | undefined = undefined;
if (isPicksWithActive(picks)) {
picker.items = picks.items;
if (picks.active) {
picker.activeItems = [picks.active];
}
items = picks.items;
activeItem = picks.active;
} else {
picker.items = picks;
items = picks;
}
if (items.length === 0 && skipEmpty) {
return false;
}
picker.items = items;
if (activeItem) {
picker.activeItems = [activeItem];
}
return true;
}
// No Picks
......@@ -133,8 +145,8 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
// Fast and Slow Picks
else if (isFastAndSlowPicks(providedPicks)) {
let fastPicksHandlerDone = false;
let slowPicksHandlerDone = false;
let fastPicksApplied = false;
let slowPicksApplied = false;
await Promise.all([
......@@ -143,17 +155,13 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
// If the slow picks are faster, we reduce the flicker by
// only setting the items once.
(async () => {
try {
await timeout(PickerQuickAccessProvider.FAST_PICKS_RACE_DELAY);
if (picksToken.isCancellationRequested) {
return;
}
await timeout(PickerQuickAccessProvider.FAST_PICKS_RACE_DELAY);
if (picksToken.isCancellationRequested) {
return;
}
if (!slowPicksHandlerDone) {
applyPicks(providedPicks.picks);
}
} finally {
fastPicksHandlerDone = true;
if (!slowPicksApplied) {
fastPicksApplied = applyPicks(providedPicks.picks, true /* skip over empty to reduce flicker */);
}
})(),
......@@ -186,7 +194,7 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
additionalPicks = awaitedAdditionalPicks;
}
if (additionalPicks.length > 0 || !fastPicksHandlerDone) {
if (additionalPicks.length > 0 || !fastPicksApplied) {
applyPicks({
items: [...picks, ...additionalPicks],
active: activePick || additionalActivePick
......@@ -197,7 +205,7 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
picker.busy = false;
}
slowPicksHandlerDone = true;
slowPicksApplied = true;
}
})()
]);
......
......@@ -269,6 +269,6 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
}
}
return false;
return false; // no veto (no dirty)
}
}
......@@ -71,7 +71,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess
private static readonly SYMBOL_PICKS_TIMEOUT = 8000;
async getSymbolPicks(model: ITextModel, filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
async getSymbolPicks(model: ITextModel, filter: string, options: { extraContainerLabel?: string }, disposables: DisposableStore, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
// If the registry does not know the model, we wait for as long as
// the registry knows it. This helps in cases where a language
......@@ -86,7 +86,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess
return [];
}
return this.doGetSymbolPicks(this.getDocumentSymbols(model, true, token), prepareQuery(filter), token);
return this.doGetSymbolPicks(this.getDocumentSymbols(model, true, token), prepareQuery(filter), options, token);
}
addDecorations(editor: IEditor, range: IRange): void {
......
......@@ -10,7 +10,7 @@ import { prepareQuery, IPreparedQuery, compareItemsByScore, scoreItem, ScorerCac
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 } from 'vs/workbench/services/search/common/search';
import { ISearchService, ISearchComplete } 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';
......@@ -48,6 +48,7 @@ import { once } from 'vs/base/common/functional';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { withNullAsUndefined } from 'vs/base/common/types';
import { stripCodicons } from 'vs/base/common/codicons';
interface IAnythingQuickPickItem extends IPickerQuickAccessItem {
resource: URI | undefined;
......@@ -367,17 +368,26 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
// Perform filtering
const filteredAnythingPicks: IAnythingQuickPickItem[] = [];
for (const anythingPick of sortedAnythingPicks) {
const { score, labelMatch, descriptionMatch } = scoreItem(anythingPick, query, true, quickPickItemScorerAccessor, this.pickState.scorerCache);
if (!score) {
continue; // exclude files/symbols not matching query
// Always preserve any existing highlights (e.g. from workspace symbols)
if (anythingPick.highlights) {
filteredAnythingPicks.push(anythingPick);
}
anythingPick.highlights = {
label: labelMatch,
description: descriptionMatch
};
// Otherwise, do the scoring and matching here
else {
const { score, labelMatch, descriptionMatch } = scoreItem(anythingPick, query, true, quickPickItemScorerAccessor, this.pickState.scorerCache);
if (!score) {
continue;
}
anythingPick.highlights = {
label: labelMatch,
description: descriptionMatch
};
filteredAnythingPicks.push(anythingPick);
filteredAnythingPicks.push(anythingPick);
}
}
return filteredAnythingPicks;
......@@ -498,15 +508,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
const [fileSearchResults, relativePathFileResults] = await Promise.all([
// File search: this is a search over all files of the workspace using the provided pattern
this.searchService.fileSearch(
this.fileQueryBuilder.file(
this.contextService.getWorkspace().folders,
this.getFileQueryOptions({
query,
cacheKey: this.pickState.fileQueryCache?.cacheKey,
maxResults: AnythingQuickAccessProvider.MAX_RESULTS
})
), token),
this.getFileSearchResults(query, token),
// Relative path search: we also want to consider results that match files inside the workspace
// by looking for relative paths that the user typed as query. This allows to return even excluded
......@@ -521,7 +523,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
// Return quickly if no relative results are present
if (!relativePathFileResults) {
return fileSearchResults.results.map(result => result.resource);
return fileSearchResults;
}
// Otherwise, make sure to filter relative path results from
......@@ -532,12 +534,12 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
}
return [
...fileSearchResults.results.filter(result => !relativePathFileResultsMap.has(result.resource)).map(result => result.resource),
...fileSearchResults.filter(result => !relativePathFileResultsMap.has(result)),
...relativePathFileResults
];
}
private getFileQueryOptions(input: { query?: IPreparedQuery, cacheKey?: string, maxResults?: number }): IFileQueryBuilderOptions {
private async getFileSearchResults(query: IPreparedQuery, token: CancellationToken): Promise<URI[]> {
// filePattern for search depends on the number of queries in input:
// - with multiple: only take the first one and let the filter later drop non-matching results
......@@ -547,18 +549,61 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
// search results for "someFile" and not both that would normally not match.
//
let filePattern = '';
if (input.query) {
if (input.query.values && input.query.values.length > 1) {
filePattern = input.query.values[0].original;
} else {
filePattern = input.query.original;
if (query.values && query.values.length > 1) {
filePattern = query.values[0].original;
} else {
filePattern = query.original;
}
const fileSearchResults = await this.doGetFileSearchResults(filePattern, token);
if (token.isCancellationRequested) {
return [];
}
// If we detect that the search limit has been hit and we have a query
// that was composed of multiple inputs where we only took the first part
// we run another search with the full original query included to make
// sure we are including all possible results that could match.
if (fileSearchResults.limitHit && query.values && query.values.length > 1) {
const additionalFileSearchResults = await this.doGetFileSearchResults(query.original, token);
if (token.isCancellationRequested) {
return [];
}
// Remember which result we already covered
const existingFileSearchResultsMap = new ResourceMap<boolean>();
for (const fileSearchResult of fileSearchResults.results) {
existingFileSearchResultsMap.set(fileSearchResult.resource, true);
}
// Add all additional results to the original set for inclusion
for (const additionalFileSearchResult of additionalFileSearchResults.results) {
if (!existingFileSearchResultsMap.has(additionalFileSearchResult.resource)) {
fileSearchResults.results.push(additionalFileSearchResult);
}
}
}
return fileSearchResults.results.map(result => result.resource);
}
private doGetFileSearchResults(filePattern: string, token: CancellationToken): Promise<ISearchComplete> {
return this.searchService.fileSearch(
this.fileQueryBuilder.file(
this.contextService.getWorkspace().folders,
this.getFileQueryOptions({
filePattern,
cacheKey: this.pickState.fileQueryCache?.cacheKey,
maxResults: AnythingQuickAccessProvider.MAX_RESULTS
})
), token);
}
private getFileQueryOptions(input: { filePattern?: string, cacheKey?: string, maxResults?: number }): IFileQueryBuilderOptions {
return {
_reason: 'openFileHandler', // used for telemetry - do not change
extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources),
filePattern,
filePattern: input.filePattern || '',
cacheKey: input.cacheKey,
maxResults: input.maxResults || 0,
sortByScore: true
......@@ -570,7 +615,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
return;
}
const detildifiedQuery = untildify(query.value, (await this.remotePathService.userHome).path);
const detildifiedQuery = untildify(query.original, (await this.remotePathService.userHome).path);
if (token.isCancellationRequested) {
return;
}
......@@ -609,7 +654,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
// Convert relative paths to absolute paths over all folders of the workspace
// and return them as results if the absolute paths exist
const isAbsolutePathQuery = (await this.remotePathService.path).isAbsolute(query.value);
const isAbsolutePathQuery = (await this.remotePathService.path).isAbsolute(query.original);
if (!isAbsolutePathQuery) {
const resources: URI[] = [];
for (const folder of this.contextService.getWorkspace().folders) {
......@@ -618,7 +663,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
}
const resource = toLocalResource(
folder.toResource(query.value),
folder.toResource(query.original),
this.environmentService.configuration.remoteAuthority
);
......@@ -654,25 +699,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
return [];
}
// symbolPattern for search depends on the number of queries in input:
// - with multiple: only take the first one and let the filter later drop non-matching results
// - with single: just take the original in full
//
// This enables to e.g. search for "someFile someFolder" by only returning
// symbol results for "someFile" and not both that would normally not match.
//
let symbolPattern = '';
if (query.values && query.values.length > 1) {
symbolPattern = query.values[0].original;
} else {
symbolPattern = query.original;
}
// Delegate to the existing symbols quick access
// but skip local results and also do not score
return this.workspaceSymbolsQuickAccess.getSymbolPicks(symbolPattern, {
return this.workspaceSymbolsQuickAccess.getSymbolPicks(query.original, {
skipLocal: true,
skipScoring: true,
skipSorting: true,
delay: AnythingQuickAccessProvider.TYPING_SEARCH_DELAY
}, token);
}
......@@ -740,7 +771,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
}
// Ask provider for editor symbols
const editorSymbolPicks = (await this.editorSymbolsQuickAccess.getSymbolPicks(model, filter, disposables, token));
const editorSymbolPicks = (await this.editorSymbolsQuickAccess.getSymbolPicks(model, filter, { extraContainerLabel: stripCodicons(activeGlobalPick.label) }, disposables, token));
if (token.isCancellationRequested) {
return [];
}
......@@ -756,7 +787,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
return {
...editorSymbolPick,
resource: activeGlobalResource,
description: editorSymbolPick.description ? `${activeGlobalPick.label}${editorSymbolPick.description}` : activeGlobalPick.label,
description: editorSymbolPick.description,
trigger: (buttonIndex, keyMods) => {
this.openAnything(activeGlobalResource, { keyMods, range: editorSymbolPick.range?.selection, forceOpenSideBySide: true });
......
......@@ -89,7 +89,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
return this.getSymbolPicks(filter, undefined, token);
}
async getSymbolPicks(filter: string, options: { skipLocal?: boolean, skipScoring?: boolean, delay?: number } | undefined, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> {
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 [];
......@@ -99,7 +99,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
}, options?.delay);
}
private async doGetSymbolPicks(query: IPreparedQuery, options: { skipLocal?: boolean, skipScoring?: boolean } | undefined, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> {
private async doGetSymbolPicks(query: IPreparedQuery, options: { skipLocal?: boolean, skipSorting?: boolean } | undefined, token: CancellationToken): Promise<Array<ISymbolQuickPickItem>> {
const workspaceSymbols = await getWorkspaceSymbols(query.original, token);
if (token.isCancellationRequested) {
return [];
......@@ -130,9 +130,9 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
continue;
}
// Score by symbol label (unless disabled)
// Score by symbol label
const symbolLabel = symbol.name;
const symbolScore = options?.skipScoring ? FuzzyScore.Default : fuzzyScore(symbolQuery.original, symbolQuery.originalLowercase, 0, symbolLabel, symbolLabel.toLowerCase(), 0, true);
const symbolScore = fuzzyScore(symbolQuery.original, symbolQuery.originalLowercase, 0, symbolLabel, symbolLabel.toLowerCase(), 0, true);
if (!symbolScore) {
continue;
}
......@@ -148,11 +148,9 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
}
}
// Score by container if specified (unless disabled)
// Score by container if specified
let containerScore: FuzzyScore | undefined = undefined;
if (options?.skipScoring) {
containerScore = FuzzyScore.Default;
} else if (containerQuery) {
if (containerQuery) {
if (containerLabel) {
containerScore = fuzzyScore(containerQuery.original, containerQuery.originalLowercase, 0, containerLabel, containerLabel.toLowerCase(), 0, true);
}
......@@ -184,7 +182,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
score: symbolScore,
label: symbolLabelWithIcon,
ariaLabel: localize('symbolAriaLabel', "{0}, symbols picker", symbolLabel),
highlights: (deprecated || options?.skipScoring) ? undefined : {
highlights: deprecated ? undefined : {
label: createMatches(symbolScore, symbolLabelWithIcon.length - symbolLabel.length /* Readjust matches to account for codicons in label */),
description: createMatches(containerScore)
},
......@@ -207,7 +205,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
}
// Sort picks (unless disabled)
if (!options?.skipScoring) {
if (!options?.skipSorting) {
symbolPicks.sort((symbolA, symbolB) => this.compareSymbols(symbolA, symbolB));
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册