dedupe workspace symbols results

This commit is contained in:
Johannes Rieken 2021-11-16 14:07:03 +01:00
parent 5c52ca4dac
commit 409041b7ff
No known key found for this signature in database
GPG key ID: 96634B5AF12F8798
9 changed files with 205 additions and 135 deletions

View file

@ -384,27 +384,26 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
// --- navigate type
$registerNavigateTypeSupport(handle: number): void {
$registerNavigateTypeSupport(handle: number, supportsResolve: boolean): void {
let lastResultId: number | undefined;
this._registrations.set(handle, search.WorkspaceSymbolProviderRegistry.register(<search.IWorkspaceSymbolProvider>{
provideWorkspaceSymbols: (search: string, token: CancellationToken): Promise<search.IWorkspaceSymbol[]> => {
return this._proxy.$provideWorkspaceSymbols(handle, search, token).then(result => {
if (lastResultId !== undefined) {
this._proxy.$releaseWorkspaceSymbols(handle, lastResultId);
}
lastResultId = result._id;
return MainThreadLanguageFeatures._reviveWorkspaceSymbolDto(result.symbols);
});
},
resolveWorkspaceSymbol: (item: search.IWorkspaceSymbol, token: CancellationToken): Promise<search.IWorkspaceSymbol | undefined> => {
return this._proxy.$resolveWorkspaceSymbol(handle, item, token).then(i => {
if (i) {
return MainThreadLanguageFeatures._reviveWorkspaceSymbolDto(i);
}
return undefined;
});
const provider: search.IWorkspaceSymbolProvider = {
provideWorkspaceSymbols: async (search: string, token: CancellationToken): Promise<search.IWorkspaceSymbol[]> => {
const result = await this._proxy.$provideWorkspaceSymbols(handle, search, token);
if (lastResultId !== undefined) {
this._proxy.$releaseWorkspaceSymbols(handle, lastResultId);
}
lastResultId = result._id;
return MainThreadLanguageFeatures._reviveWorkspaceSymbolDto(result.symbols);
}
}));
};
if (supportsResolve) {
provider.resolveWorkspaceSymbol = async (item: search.IWorkspaceSymbol, token: CancellationToken): Promise<search.IWorkspaceSymbol | undefined> => {
const resolvedItem = await this._proxy.$resolveWorkspaceSymbol(handle, item, token);
return resolvedItem && MainThreadLanguageFeatures._reviveWorkspaceSymbolDto(resolvedItem);
};
}
this._registrations.set(handle, search.WorkspaceSymbolProviderRegistry.register(provider));
}
// --- rename

View file

@ -405,7 +405,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable {
$registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void;
$registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void;
$registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void;
$registerNavigateTypeSupport(handle: number): void;
$registerNavigateTypeSupport(handle: number, supportsResolve: boolean): void;
$registerRenameSupport(handle: number, selector: IDocumentFilterDto[], supportsResolveInitialValues: boolean): void;
$registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend, eventHandle: number | undefined): void;
$emitDocumentSemanticTokensEvent(eventHandle: number): void;

View file

@ -129,14 +129,8 @@ const newCommands: ApiCommand[] = [
new ApiCommand(
'vscode.executeWorkspaceSymbolProvider', '_executeWorkspaceSymbolProvider', 'Execute all workspace symbol providers.',
[ApiCommandArgument.String.with('query', 'Search string')],
new ApiCommandResult<[search.IWorkspaceSymbolProvider, search.IWorkspaceSymbol[]][], types.SymbolInformation[]>('A promise that resolves to an array of SymbolInformation-instances.', value => {
const result: types.SymbolInformation[] = [];
if (Array.isArray(value)) {
for (let tuple of value) {
result.push(...tuple[1].map(typeConverters.WorkspaceSymbol.to));
}
}
return result;
new ApiCommandResult<search.IWorkspaceSymbol[], types.SymbolInformation[]>('A promise that resolves to an array of SymbolInformation-instances.', value => {
return value.map(typeConverters.WorkspaceSymbol.to);
})
),
// --- call hierarchy

View file

@ -1845,7 +1845,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
registerWorkspaceSymbolProvider(extension: IExtensionDescription, provider: vscode.WorkspaceSymbolProvider): vscode.Disposable {
const handle = this._addNewAdapter(new NavigateTypeAdapter(provider, this._logService), extension);
this._proxy.$registerNavigateTypeSupport(handle);
this._proxy.$registerNavigateTypeSupport(handle, typeof provider.resolveWorkspaceSymbol === 'function');
return this._createDisposable(handle);
}

View file

@ -46,7 +46,7 @@ import { registerContributions as searchWidgetContributions } from 'vs/workbench
import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
import { resolveResourcesForSearchIncludes } from 'vs/workbench/contrib/search/common/queryBuilder';
import { getWorkspaceSymbols, SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search';
import { getWorkspaceSymbols, IWorkspaceSymbol, SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search';
import { ISearchHistoryService, SearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService';
import { FileMatch, FileMatchOrMatch, FolderMatch, ISearchWorkbenchService, Match, RenderableMatch, SearchWorkbenchService } from 'vs/workbench/contrib/search/common/searchModel';
import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants';
@ -1025,10 +1025,11 @@ configurationRegistry.registerConfiguration({
}
});
CommandsRegistry.registerCommand('_executeWorkspaceSymbolProvider', function (accessor, ...args) {
CommandsRegistry.registerCommand('_executeWorkspaceSymbolProvider', async function (accessor, ...args): Promise<IWorkspaceSymbol[]> {
const [query] = args;
assertType(typeof query === 'string');
return getWorkspaceSymbols(query);
const result = await getWorkspaceSymbols(query);
return result.map(item => item.symbol);
});
// Go to menu

View file

@ -119,102 +119,101 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
// Convert to symbol picks and apply filtering
const openSideBySideDirection = this.configuration.openSideBySideDirection;
for (const [provider, symbols] of workspaceSymbols) {
for (const symbol of symbols) {
for (const { symbol, provider } of workspaceSymbols) {
// Depending on the workspace symbols filter setting, skip over symbols that:
// - do not have a container
// - and are not treated explicitly as global symbols (e.g. classes)
if (options?.skipLocal && !SymbolsQuickAccessProvider.TREAT_AS_GLOBAL_SYMBOL_TYPES.has(symbol.kind) && !!symbol.containerName) {
// Depending on the workspace symbols filter setting, skip over symbols that:
// - do not have a container
// - and are not treated explicitly as global symbols (e.g. classes)
if (options?.skipLocal && !SymbolsQuickAccessProvider.TREAT_AS_GLOBAL_SYMBOL_TYPES.has(symbol.kind) && !!symbol.containerName) {
continue;
}
const symbolLabel = symbol.name;
const symbolLabelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${symbolLabel}`;
const symbolLabelIconOffset = symbolLabelWithIcon.length - symbolLabel.length;
// Score by symbol label if searching
let symbolScore: number | undefined = undefined;
let symbolMatches: IMatch[] | undefined = undefined;
let skipContainerQuery = false;
if (symbolQuery.original.length > 0) {
// First: try to score on the entire query, it is possible that
// the symbol matches perfectly (e.g. searching for "change log"
// can be a match on a markdown symbol "change log"). In that
// case we want to skip the container query altogether.
if (symbolQuery !== query) {
[symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, { ...query, values: undefined /* disable multi-query support */ }, 0, symbolLabelIconOffset);
if (typeof symbolScore === 'number') {
skipContainerQuery = true; // since we consumed the query, skip any container matching
}
}
// Otherwise: score on the symbol query and match on the container later
if (typeof symbolScore !== 'number') {
[symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, symbolQuery, 0, symbolLabelIconOffset);
if (typeof symbolScore !== 'number') {
continue;
}
}
}
const symbolUri = symbol.location.uri;
let containerLabel: string | undefined = undefined;
if (symbolUri) {
const containerPath = this.labelService.getUriLabel(symbolUri, { relative: true });
if (symbol.containerName) {
containerLabel = `${symbol.containerName}${containerPath}`;
} else {
containerLabel = containerPath;
}
}
// Score by container if specified and searching
let containerScore: number | undefined = undefined;
let containerMatches: IMatch[] | undefined = undefined;
if (!skipContainerQuery && containerQuery && containerQuery.original.length > 0) {
if (containerLabel) {
[containerScore, containerMatches] = scoreFuzzy2(containerLabel, containerQuery);
}
if (typeof containerScore !== 'number') {
continue;
}
const symbolLabel = symbol.name;
const symbolLabelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${symbolLabel}`;
const symbolLabelIconOffset = symbolLabelWithIcon.length - symbolLabel.length;
// Score by symbol label if searching
let symbolScore: number | undefined = undefined;
let symbolMatches: IMatch[] | undefined = undefined;
let skipContainerQuery = false;
if (symbolQuery.original.length > 0) {
// First: try to score on the entire query, it is possible that
// the symbol matches perfectly (e.g. searching for "change log"
// can be a match on a markdown symbol "change log"). In that
// case we want to skip the container query altogether.
if (symbolQuery !== query) {
[symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, { ...query, values: undefined /* disable multi-query support */ }, 0, symbolLabelIconOffset);
if (typeof symbolScore === 'number') {
skipContainerQuery = true; // since we consumed the query, skip any container matching
}
}
// Otherwise: score on the symbol query and match on the container later
if (typeof symbolScore !== 'number') {
[symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, symbolQuery, 0, symbolLabelIconOffset);
if (typeof symbolScore !== 'number') {
continue;
}
}
if (typeof symbolScore === 'number') {
symbolScore += containerScore; // boost symbolScore by containerScore
}
const symbolUri = symbol.location.uri;
let containerLabel: string | undefined = undefined;
if (symbolUri) {
const containerPath = this.labelService.getUriLabel(symbolUri, { relative: true });
if (symbol.containerName) {
containerLabel = `${symbol.containerName}${containerPath}`;
} else {
containerLabel = containerPath;
}
}
// Score by container if specified and searching
let containerScore: number | undefined = undefined;
let containerMatches: IMatch[] | undefined = undefined;
if (!skipContainerQuery && containerQuery && containerQuery.original.length > 0) {
if (containerLabel) {
[containerScore, containerMatches] = scoreFuzzy2(containerLabel, containerQuery);
}
if (typeof containerScore !== 'number') {
continue;
}
if (typeof symbolScore === 'number') {
symbolScore += containerScore; // boost symbolScore by containerScore
}
}
const deprecated = symbol.tags ? symbol.tags.indexOf(SymbolTag.Deprecated) >= 0 : false;
symbolPicks.push({
symbol,
resource: symbolUri,
score: symbolScore,
label: symbolLabelWithIcon,
ariaLabel: symbolLabel,
highlights: deprecated ? undefined : {
label: symbolMatches,
description: containerMatches
},
description: containerLabel,
strikethrough: deprecated,
buttons: [
{
iconClass: openSideBySideDirection === 'right' ? Codicon.splitHorizontal.classNames : Codicon.splitVertical.classNames,
tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom")
}
],
trigger: (buttonIndex, keyMods) => {
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, forcePinned: event.inBackground }),
});
}
const deprecated = symbol.tags ? symbol.tags.indexOf(SymbolTag.Deprecated) >= 0 : false;
symbolPicks.push({
symbol,
resource: symbolUri,
score: symbolScore,
label: symbolLabelWithIcon,
ariaLabel: symbolLabel,
highlights: deprecated ? undefined : {
label: symbolMatches,
description: containerMatches
},
description: containerLabel,
strikethrough: deprecated,
buttons: [
{
iconClass: openSideBySideDirection === 'right' ? Codicon.splitHorizontal.classNames : Codicon.splitVertical.classNames,
tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom")
}
],
trigger: (buttonIndex, keyMods) => {
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, forcePinned: event.inBackground }),
});
}
// Sort picks (unless disabled)

View file

@ -14,9 +14,11 @@ 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 { IRange, Range } from 'vs/editor/common/core/range';
import { isNumber } from 'vs/base/common/types';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { compare } from 'vs/base/common/strings';
import { groupBy } from 'vs/base/common/arrays';
export interface IWorkspaceSymbol {
name: string;
@ -59,19 +61,62 @@ export namespace WorkspaceSymbolProviderRegistry {
}
}
export function getWorkspaceSymbols(query: string, token: CancellationToken = CancellationToken.None): Promise<[IWorkspaceSymbolProvider, IWorkspaceSymbol[]][]> {
export class WorkspaceSymbolItem {
constructor(readonly symbol: IWorkspaceSymbol, readonly provider: IWorkspaceSymbolProvider) { }
}
const result: [IWorkspaceSymbolProvider, IWorkspaceSymbol[]][] = [];
export async function getWorkspaceSymbols(query: string, token: CancellationToken = CancellationToken.None): Promise<WorkspaceSymbolItem[]> {
const promises = WorkspaceSymbolProviderRegistry.all().map(support => {
return Promise.resolve(support.provideWorkspaceSymbols(query, token)).then(value => {
if (Array.isArray(value)) {
result.push([support, value]);
const all: WorkspaceSymbolItem[] = [];
const promises = WorkspaceSymbolProviderRegistry.all().map(async provider => {
try {
const value = await provider.provideWorkspaceSymbols(query, token);
if (!value) {
return;
}
}, onUnexpectedError);
for (let symbol of value) {
all.push(new WorkspaceSymbolItem(symbol, provider));
}
} catch (err) {
onUnexpectedError(err);
}
});
return Promise.all(promises).then(_ => result);
await Promise.all(promises);
if (token.isCancellationRequested) {
return [];
}
// de-duplicate entries
function compareItems(a: WorkspaceSymbolItem, b: WorkspaceSymbolItem): number {
let res = compare(a.symbol.name, b.symbol.name);
if (res === 0) {
res = a.symbol.kind - b.symbol.kind;
}
if (res === 0) {
res = compare(a.symbol.location.uri.toString(), b.symbol.location.uri.toString());
}
if (res === 0) {
if (a.symbol.location.range && b.symbol.location.range) {
if (!Range.areIntersecting(a.symbol.location.range, b.symbol.location.range)) {
res = Range.compareRangesUsingStarts(a.symbol.location.range, b.symbol.location.range);
}
} else if (a.provider.resolveWorkspaceSymbol && !b.provider.resolveWorkspaceSymbol) {
res = -1;
} else if (!a.provider.resolveWorkspaceSymbol && b.provider.resolveWorkspaceSymbol) {
res = 1;
}
}
if (res === 0) {
res = compare(a.symbol.containerName ?? '', b.symbol.containerName ?? '');
}
return res;
}
return groupBy(all, compareItems).map(group => group[0]).flat();
}
export interface IWorkbenchSearchConfigurationProperties extends ISearchConfigurationProperties {

View file

@ -201,12 +201,12 @@ suite('ExtHostLanguageFeatureCommands', function () {
return rpcProtocol.sync().then(() => {
return commands.executeCommand<vscode.SymbolInformation[]>('vscode.executeWorkspaceSymbolProvider', 'testing').then(value => {
assert.strictEqual(value.length, 2); // de-duped
for (let info of value) {
assert.strictEqual(info instanceof types.SymbolInformation, true);
assert.strictEqual(info.name, 'testing');
assert.strictEqual(info.kind, types.SymbolKind.Array);
}
assert.strictEqual(value.length, 3);
});
});
});

View file

@ -681,9 +681,41 @@ suite('ExtHostLanguageFeatures', function () {
let value = await getWorkspaceSymbols('');
assert.strictEqual(value.length, 1);
const [first] = value;
const [, symbols] = first;
assert.strictEqual(symbols.length, 1);
assert.strictEqual(symbols[0].name, 'testing');
assert.strictEqual(first.symbol.name, 'testing');
});
test('Navigate types, de-duplicate results', async () => {
const uri = URI.from({ scheme: 'foo', path: '/some/path' });
disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider {
provideWorkspaceSymbols(): any {
return [new types.SymbolInformation('ONE', types.SymbolKind.Array, undefined, new types.Location(uri, new types.Range(0, 0, 1, 1)))];
}
}));
disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider {
provideWorkspaceSymbols(): any {
return [new types.SymbolInformation('ONE', types.SymbolKind.Array, undefined, new types.Location(uri, new types.Range(0, 0, 1, 1)))]; // get de-duped
}
}));
disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider {
provideWorkspaceSymbols(): any {
return [new types.SymbolInformation('ONE', types.SymbolKind.Array, undefined, new types.Location(uri, undefined!))]; // NO dedupe because of resolve
}
resolveWorkspaceSymbol(a: vscode.SymbolInformation) {
return a;
}
}));
disposables.push(extHost.registerWorkspaceSymbolProvider(defaultExtension, new class implements vscode.WorkspaceSymbolProvider {
provideWorkspaceSymbols(): any {
return [new types.SymbolInformation('ONE', types.SymbolKind.Struct, undefined, new types.Location(uri, new types.Range(0, 0, 1, 1)))]; // NO dedupe because of kind
}
}));
await rpcProtocol.sync();
let value = await getWorkspaceSymbols('');
assert.strictEqual(value.length, 3);
});
// --- rename