mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-28 19:27:05 +01:00
quick access - first cut workspace symbols
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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<TriggerAction>;
|
||||
trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise<TriggerAction>;
|
||||
}
|
||||
|
||||
export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem> implements IQuickAccessProvider {
|
||||
@@ -232,7 +236,7 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
|
||||
const [item] = picker.selectedItems;
|
||||
if (typeof item?.accept === 'function') {
|
||||
picker.hide();
|
||||
item.accept();
|
||||
item.accept(picker.keyMods);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -241,7 +245,7 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
|
||||
if (typeof item.trigger === 'function') {
|
||||
const buttonIndex = item.buttons?.indexOf(button) ?? -1;
|
||||
if (buttonIndex >= 0) {
|
||||
const result = item.trigger(buttonIndex);
|
||||
const result = item.trigger(buttonIndex, picker.keyMods);
|
||||
const action = (typeof result === 'number') ? result : await result;
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
|
||||
@@ -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<IWorkbenchEditorConfiguration>().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
|
||||
|
||||
@@ -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<IWorkbenchEditorConfiguration>().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<IQuickAccessRegistry>(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 }
|
||||
]
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider<IPi
|
||||
highlights: { label: highlights },
|
||||
buttons: [{
|
||||
iconClass: 'codicon-gear',
|
||||
tooltip: localize('customizeTask', "Configure Launch Configuration")
|
||||
tooltip: localize('customizeLaunchConfig', "Configure Launch Configuration")
|
||||
}],
|
||||
trigger: () => {
|
||||
config.launch.openConfigFile(false, false);
|
||||
|
||||
@@ -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<IQuickOpenRegistry>(QuickOpenExtensions.Quickopen).registerQuickOpen
|
||||
)
|
||||
);
|
||||
|
||||
// Register Quick Access Handler
|
||||
|
||||
Registry.as<IQuickAccessRegistry>(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<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
|
||||
@@ -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<ISymbolsQuickPickItem> {
|
||||
|
||||
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);
|
||||
|
||||
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<IWorkbenchEditorConfiguration>().workbench.editor;
|
||||
|
||||
return {
|
||||
openEditorPinned: !editorConfig.enablePreviewFromQuickOpen,
|
||||
openSideBySideDirection: editorConfig.openSideBySideDirection
|
||||
};
|
||||
}
|
||||
|
||||
protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise<Array<ISymbolsQuickPickItem>> {
|
||||
return this.delayer.trigger(async () => {
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.doGetSymbolPicks(filter, token);
|
||||
});
|
||||
}
|
||||
|
||||
private async doGetSymbolPicks(filter: string, token: CancellationToken): Promise<Array<ISymbolsQuickPickItem>> {
|
||||
const workspaceSymbols = await getWorkspaceSymbols(filter, token);
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const symbolPicks: Array<ISymbolsQuickPickItem> = [];
|
||||
|
||||
// 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<void> {
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user