From 0e587865bfbc73c4249bdaf2bac60102012fcce4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 16 Mar 2020 08:46:52 +0100 Subject: [PATCH] quick access - first cut workspace symbols --- .../quickAccess/gotoSymbolQuickAccess.ts | 6 +- .../platform/quickinput/common/quickAccess.ts | 14 +- .../quickaccess/gotoLineQuickAccess.ts | 13 +- .../browser/quickaccess/gotoSymbolAccess.ts | 17 +- .../contrib/debug/browser/debugQuickAccess.ts | 2 +- .../search/browser/search.contribution.ts | 12 ++ .../search/browser/symbolsQuickAccess.ts | 201 ++++++++++++++++++ 7 files changed, 250 insertions(+), 15 deletions(-) create mode 100644 src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index 04c6cb44eaf..1f2dc6c6210 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -196,18 +196,18 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit } if (includeSymbol) { - const labelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${symbolLabel}`; + const symbolLabelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${symbolLabel}`; const deprecated = symbol.tags && symbol.tags.indexOf(SymbolTag.Deprecated) >= 0; filteredSymbolPicks.push({ index, kind: symbol.kind, score: symbolScore, - label: labelWithIcon, + label: symbolLabelWithIcon, ariaLabel: localize('symbolsAriaLabel', "{0}, symbols picker", symbolLabel), description: containerLabel, highlights: deprecated ? undefined : { - label: createMatches(symbolScore, labelWithIcon.length - symbolLabel.length /* Readjust matches to account for codicons in label */), + label: createMatches(symbolScore, symbolLabelWithIcon.length - symbolLabel.length /* Readjust matches to account for codicons in label */), description: createMatches(containerScore) }, range: { diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index 116086d3e4e..a16ca553935 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -10,7 +10,7 @@ import { first } from 'vs/base/common/arrays'; import { startsWith } from 'vs/base/common/strings'; import { assertIsDefined } from 'vs/base/common/types'; import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickSeparator, IKeyMods } from 'vs/base/parts/quickinput/common/quickInput'; export interface IQuickAccessController { @@ -167,8 +167,10 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { /** * A method that will be executed when the pick item is accepted from * the picker. The picker will close automatically before running this. + * + * @param keyMods the state of modifier keys when the item was accepted. */ - accept?(): void; + accept?(keyMods: IKeyMods): void; /** * A method that will be executed when a button of the pick item was @@ -177,10 +179,12 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { * @param buttonIndex index of the button of the item that * was clicked. * + * @param the state of modifier keys when the button was triggered. + * * @returns a value that indicates what should happen after the trigger * which can be a `Promise` for long running operations. */ - trigger?(buttonIndex: number): TriggerAction | Promise; + trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; } export abstract class PickerQuickAccessProvider implements IQuickAccessProvider { @@ -232,7 +236,7 @@ export abstract class PickerQuickAccessProvider= 0) { - const result = item.trigger(buttonIndex); + const result = item.trigger(buttonIndex, picker.keyMods); const action = (typeof result === 'number') ? result : await result; if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index a5cd2bf23f9..b33ec1ba9e2 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -11,12 +11,17 @@ import { IRange } from 'vs/editor/common/core/range'; import { AbstractGotoLineQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoLineQuickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider { protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; - constructor(@IEditorService private readonly editorService: IEditorService) { + constructor( + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { super(); } @@ -25,10 +30,14 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv } protected gotoLine(editor: IEditor, range: IRange, keyMods: IKeyMods): void { + const enablePreviewFromQuickAccess = this.configurationService.getValue().workbench.editor.enablePreviewFromQuickOpen; // Check for sideBySide use if (keyMods.ctrlCmd && this.editorService.activeEditor) { - this.editorService.openEditor(this.editorService.activeEditor, { selection: range, pinned: keyMods.alt }, SIDE_GROUP); + this.editorService.openEditor(this.editorService.activeEditor, { + selection: range, + pinned: keyMods.alt || !enablePreviewFromQuickAccess + }, SIDE_GROUP); } // Otherwise let parent handle it diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolAccess.ts index ef6957e0178..a7f8a7237b5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolAccess.ts @@ -11,12 +11,17 @@ import { IRange } from 'vs/editor/common/core/range'; import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; import { AbstractGotoSymbolQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; - constructor(@IEditorService private readonly editorService: IEditorService) { + constructor( + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { super(); } @@ -25,10 +30,14 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess } protected gotoSymbol(editor: IEditor, range: IRange, keyMods: IKeyMods): void { + const enablePreviewFromQuickAccess = this.configurationService.getValue().workbench.editor.enablePreviewFromQuickOpen; // Check for sideBySide use if (keyMods.ctrlCmd && this.editorService.activeEditor) { - this.editorService.openEditor(this.editorService.activeEditor, { selection: range, pinned: keyMods.alt }, SIDE_GROUP); + this.editorService.openEditor(this.editorService.activeEditor, { + selection: range, + pinned: keyMods.alt || !enablePreviewFromQuickAccess + }, SIDE_GROUP); } // Otherwise let parent handle it @@ -43,7 +52,7 @@ Registry.as(Extensions.Quickaccess).registerQuickAccessPro prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, placeholder: localize('gotoSymbolQuickAccessPlaceholder', "Type the name of a symbol to go to."), helpEntries: [ - { description: localize('gotoSymbolQuickAccess', "Go to Symol in Editor"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, needsEditor: true }, - { description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symol in Editor by Category"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY, needsEditor: true } + { description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, needsEditor: true }, + { description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symbol in Editor by Category"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY, needsEditor: true } ] }); diff --git a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts index 7c39c372d52..6b4f8127c3b 100644 --- a/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts +++ b/src/vs/workbench/contrib/debug/browser/debugQuickAccess.ts @@ -52,7 +52,7 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider { config.launch.openConfigFile(false, false); diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 3d9deddf982..e918cce82d5 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -55,6 +55,8 @@ import { assertType, assertIsDefined } from 'vs/base/common/types'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; 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'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); registerSingleton(ISearchHistoryService, SearchHistoryService, true); @@ -651,6 +653,16 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen ) ); +// Register Quick Access Handler + +Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: SymbolsQuickAccessProvider, + prefix: SymbolsQuickAccessProvider.PREFIX, + placeholder: nls.localize('symbolsQuickAccessPlaceholder', "Type the name of a symbol to open."), + contextKey: 'inWorkspaceSymbolsPicker', + helpEntries: [{ description: nls.localize('symbolsQuickAccess', "Go to Symbol in Workspace"), needsEditor: false }] +}); + // Configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts new file mode 100644 index 00000000000..4f41ad1dffe --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction } from 'vs/platform/quickinput/common/quickAccess'; +import { fuzzyScore, createMatches, FuzzyScore } from 'vs/base/common/filters'; +import { stripWildcards } from 'vs/base/common/strings'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider } from 'vs/workbench/contrib/search/common/search'; +import { SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; +import { basename } from 'vs/base/common/resources'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { Schemas } from 'vs/base/common/network'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +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 } from 'vs/platform/quickinput/common/quickInput'; + +interface ISymbolsQuickPickItem extends IPickerQuickAccessItem { + score: FuzzyScore; + symbol: IWorkspaceSymbol; +} + +export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider { + + 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(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY); + + constructor( + @ILabelService private readonly labelService: ILabelService, + @IOpenerService private readonly openerService: IOpenerService, + @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(SymbolsQuickAccessProvider.PREFIX); + } + + private get configuration() { + const editorConfig = this.configurationService.getValue().workbench.editor; + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection + }; + } + + protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { + return this.delayer.trigger(async () => { + if (token.isCancellationRequested) { + return []; + } + + return this.doGetSymbolPicks(filter, token); + }); + } + + private async doGetSymbolPicks(filter: string, token: CancellationToken): Promise> { + const workspaceSymbols = await getWorkspaceSymbols(filter, token); + if (token.isCancellationRequested) { + return []; + } + + const symbolPicks: Array = []; + + // Normalize filter + const [symbolFilter, containerFilter] = stripWildcards(filter).split(' ') as [string, string | undefined]; + const symbolFilterLow = symbolFilter.toLowerCase(); + const containerFilterLow = containerFilter?.toLowerCase(); + + // Convert to symbol picks and apply filtering + const openSideBySideDirection = this.configuration.openSideBySideDirection; + for (const [provider, symbols] of workspaceSymbols) { + for (const symbol of symbols) { + const symbolLabel = symbol.name; + const symbolLabelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${symbolLabel}`; + + let containerLabel: string | undefined = undefined; + if (symbol.location.uri) { + if (symbol.containerName) { + containerLabel = `${symbol.containerName} — ${basename(symbol.location.uri)}`; + } else { + containerLabel = this.labelService.getUriLabel(symbol.location.uri, { relative: true }); + } + } + + // Score by symbol + const symbolScore = fuzzyScore(symbolFilter, symbolFilterLow, 0, symbolLabel, symbolLabel.toLowerCase(), 0, true); + let containerScore: FuzzyScore | undefined = undefined; + if (!symbolScore) { + continue; + } + + // Score by container if specified + if (containerFilter && containerFilterLow) { + if (containerLabel) { + containerScore = fuzzyScore(containerFilter, containerFilterLow, 0, containerLabel, containerLabel.toLowerCase(), 0, true); + } + + if (!containerScore) { + continue; + } + } + + const deprecated = symbol.tags ? symbol.tags.indexOf(SymbolTag.Deprecated) >= 0 : false; + + symbolPicks.push({ + symbol, + score: symbolScore, + label: symbolLabelWithIcon, + ariaLabel: localize('symbolAriaLabel', "{0}, symbols picker", symbolLabel), + highlights: deprecated ? undefined : { + label: createMatches(symbolScore, symbolLabelWithIcon.length - symbolLabel.length /* Readjust matches to account for codicons in label */), + description: createMatches(containerScore) + }, + description: containerLabel, + strikethrough: deprecated, + buttons: [ + { + iconClass: openSideBySideDirection === 'right' ? 'codicon-split-horizontal' : 'codicon-split-vertical', + tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom") + } + ], + accept: async keyMods => this.openSymbol(provider, symbol, token, keyMods), + trigger: async (buttonIndex, keyMods) => { + this.openSymbol(provider, symbol, token, keyMods, true); + + return TriggerAction.CLOSE_PICKER; + } + }); + } + } + + // Sort picks + symbolPicks.sort((symbolA, symbolB) => this.compareSymbols(symbolA, symbolB)); + + return symbolPicks; + } + + private async openSymbol(provider: IWorkspaceSymbolProvider, symbol: IWorkspaceSymbol, token: CancellationToken, keyMods: IKeyMods, forceOpenSideBySide = false): Promise { + + // Resolve actual symbol to open for providers that can resolve + let symbolToOpen = symbol; + if (typeof provider.resolveWorkspaceSymbol === 'function' && !symbol.location.range) { + symbolToOpen = await provider.resolveWorkspaceSymbol(symbol, token) || symbol; + + if (token.isCancellationRequested) { + return; + } + } + + // Open HTTP(s) links with opener service + if (symbolToOpen.location.uri.scheme === Schemas.http || symbolToOpen.location.uri.scheme === Schemas.https) { + this.openerService.open(symbolToOpen.location.uri, { fromUserGesture: true }); + } + + // Otherwise open as editor + else { + this.editorService.openEditor({ + resource: symbolToOpen.location.uri, + options: { + pinned: keyMods.alt || forceOpenSideBySide || this.configuration.openEditorPinned, + selection: symbolToOpen.location.range ? Range.collapseToStart(symbolToOpen.location.range) : undefined + } + }, keyMods.ctrlCmd || forceOpenSideBySide ? SIDE_GROUP : ACTIVE_GROUP); + } + } + + private compareSymbols(symbolA: ISymbolsQuickPickItem, symbolB: ISymbolsQuickPickItem): number { + + // By score + if (symbolA.score && symbolB.score) { + if (symbolA.score[0] > symbolB.score[0]) { + return -1; + } else if (symbolA.score[0] < symbolB.score[0]) { + return 1; + } + } + + // By name + const symbolAName = symbolA.symbol.name.toLowerCase(); + const symbolBName = symbolB.symbol.name.toLowerCase(); + const res = symbolAName.localeCompare(symbolBName); + if (res !== 0) { + return res; + } + + // By kind + const symbolAKind = SymbolKinds.toCssClassName(symbolA.symbol.kind); + const symbolBKind = SymbolKinds.toCssClassName(symbolB.symbol.kind); + return symbolAKind.localeCompare(symbolBKind); + } +} -- GitLab