提交 633e228d 编写于 作者: B Benjamin Pasero

quick access - first cut goto symbols

上级 14b5afc0
......@@ -61,6 +61,7 @@ export namespace QuickOutlineNLS {
export const entryAriaLabel = nls.localize('entryAriaLabel', "{0}, symbols");
export const quickOutlineActionInput = nls.localize('quickOutlineActionInput', "Type the name of an identifier you wish to navigate to");
export const quickOutlineActionLabel = nls.localize('quickOutlineActionLabel', "Go to Symbol...");
export const quickOutlineByCategoryActionLabel = nls.localize('quickOutlineByCategoryActionLabel', "Go to Symbol by Category...");
export const _symbols_ = nls.localize('symbols', "symbols ({0})");
export const _modules_ = nls.localize('modules', "modules ({0})");
export const _class_ = nls.localize('class', "classes ({0})");
......
......@@ -53,7 +53,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
}
private doProvideWithoutTextEditor(picker: IQuickPick<IGotoLineQuickPickItem>): IDisposable {
const label = localize('cannotRunGotoLine', "Open a text file first to go to a line.");
const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line.");
picker.items = [{ label }];
picker.ariaLabel = label;
......
/*---------------------------------------------------------------------------------------------
* 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 { IQuickPick, IQuickPickItem, IKeyMods, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { DisposableStore, toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { once } from 'vs/base/common/functional';
import { IEditor, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IRange, Range } from 'vs/editor/common/core/range';
import { withNullAsUndefined } from 'vs/base/common/types';
import { AbstractEditorQuickAccessProvider } from 'vs/editor/contrib/quickAccess/quickAccess';
import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes';
import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { values } from 'vs/base/common/collections';
import { trim, format } from 'vs/base/common/strings';
import { fuzzyScore, FuzzyScore, createMatches } from 'vs/base/common/filters';
interface IGotoSymbolQuickPickItem extends IQuickPickItem {
kind: SymbolKind,
index: number,
score?: FuzzyScore;
range?: { decoration: IRange, selection: IRange },
}
export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEditorQuickAccessProvider {
static PREFIX = '@';
static SCOPE_PREFIX = ':';
static PREFIX_BY_CATEGORY = `${AbstractGotoSymbolQuickAccessProvider.PREFIX}${AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX}`;
provide(picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable {
const disposables = new DisposableStore();
// Disable filtering & sorting, we control the results
picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false;
// Provide based on current active editor
let pickerDisposable = this.doProvide(picker, token);
disposables.add(toDisposable(() => pickerDisposable.dispose()));
// Re-create whenever the active editor changes
disposables.add(this.onDidActiveTextEditorControlChange(() => {
pickerDisposable.dispose();
pickerDisposable = this.doProvide(picker, token);
}));
return disposables;
}
private doProvide(picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable {
const activeTextEditorControl = this.activeTextEditorControl;
// With text control
if (activeTextEditorControl) {
const model = this.getModel(activeTextEditorControl);
if (model && DocumentSymbolProviderRegistry.has(model)) {
return this.doProvideWithSymbols(activeTextEditorControl, model, picker, token);
}
}
// Without text control
return this.doProvideWithoutSymbols(picker);
}
private doProvideWithoutSymbols(picker: IQuickPick<IGotoSymbolQuickPickItem>): IDisposable {
const label = localize('cannotRunGotoSymbol', "Open a text editor with symbol information first to go to a symbol.");
picker.items = [{ label, index: 0, kind: SymbolKind.String }];
picker.ariaLabel = label;
return Disposable.None;
}
private doProvideWithSymbols(editor: IEditor, model: ITextModel, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable {
const disposables = new DisposableStore();
// Restore any view state if this picker was closed
// without actually going to a symbol
const lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState());
once(token.onCancellationRequested)(() => {
if (lastKnownEditorViewState) {
editor.restoreViewState(lastKnownEditorViewState);
}
});
// Goto symbol once picked
disposables.add(picker.onDidAccept(() => {
const [item] = picker.selectedItems;
if (item && item.range) {
this.gotoSymbol(editor, item.range.selection, picker.keyMods);
picker.hide();
}
}));
// Set initial picks and update on type
let picksCts: CancellationTokenSource | undefined = undefined;
const updatePickerItems = async () => {
// Cancel any previous ask for picks and busy
picksCts?.dispose(true);
picker.busy = false;
// Create new cancellation source for this run
picksCts = new CancellationTokenSource(token);
// Collect symbol picks
picker.busy = true;
try {
const items = await this.getSymbolPicks(model, picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim(), picksCts.token);
if (token.isCancellationRequested) {
return;
}
picker.items = items;
} finally {
if (!token.isCancellationRequested) {
picker.busy = false;
}
}
};
disposables.add(picker.onDidChangeValue(() => updatePickerItems()));
updatePickerItems();
// Reveal and decorate when active item changes
disposables.add(picker.onDidChangeActive(() => {
const [item] = picker.activeItems;
if (item && item.range) {
// Reveal
editor.revealRangeInCenter(item.range.selection, ScrollType.Smooth);
// Decorate
this.addDecorations(editor, item.range.decoration);
}
}));
// Clean up decorations on dispose
disposables.add(toDisposable(() => this.clearDecorations(editor)));
return disposables;
}
private async getSymbolPicks(model: ITextModel, filter: string, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
// Resolve symbols from document
const symbols = await this.getDocumentSymbols(model, true, token);
if (token.isCancellationRequested) {
return [];
}
// Normalize filter
const filterBySymbolKind = filter.indexOf(AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX) === 0;
const filterLow = filter.toLowerCase();
const filterPos = filterBySymbolKind ? 1 : 0;
// Convert to symbol picks and apply filtering
const filteredSymbolPicks: IGotoSymbolQuickPickItem[] = [];
for (let index = 0; index < symbols.length; index++) {
const symbol = symbols[index];
const label = trim(symbol.name);
const deprecated = symbol.tags && symbol.tags.indexOf(SymbolTag.Deprecated) >= 0;
let score: FuzzyScore | undefined = undefined;
let includeSymbol = true;
if (filter.length > filterPos) {
score = fuzzyScore(filter, filterLow, filterPos, label, label.toLowerCase(), 0, true);
includeSymbol = !!score;
}
if (includeSymbol) {
const labelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${label}`;
// Readjust matches to account for codicons
const labelOffset = labelWithIcon.length - label.length;
const matches = createMatches(score);
if (matches) {
for (const match of matches) {
match.start += labelOffset;
match.end += labelOffset;
}
}
filteredSymbolPicks.push({
index,
kind: symbol.kind,
score,
label: labelWithIcon,
ariaLabel: localize('symbolsAriaLabel', "{0}, symbols picker", label),
description: symbol.containerName,
highlights: deprecated ? undefined : { label: matches },
range: {
selection: Range.collapseToStart(symbol.selectionRange),
decoration: symbol.range
},
italic: deprecated
});
}
}
// Sort by score
const sortedFilteredSymbolPicks = filteredSymbolPicks.sort((symbolA, symbolB) => filterBySymbolKind ?
this.compareByKindAndScore(symbolA, symbolB) :
this.compareByScore(symbolA, symbolB)
);
// Add separator for types
// - @ only total number of symbols
// - @: grouped by symbol kind
let symbolPicks: Array<IGotoSymbolQuickPickItem | IQuickPickSeparator> = [];
if (filterBySymbolKind) {
let lastSymbolKind: SymbolKind | undefined = undefined;
let lastSeparator: IQuickPickSeparator | undefined = undefined;
let lastSymbolKindCounter = 0;
function updateLastSeparatorLabel(): void {
if (lastSeparator && typeof lastSymbolKind === 'number' && lastSymbolKindCounter > 0) {
lastSeparator.label = format(NLS_SYMBOL_KIND_CACHE[lastSymbolKind] || FALLBACK_NLS_SYMBOL_KIND, lastSymbolKindCounter);
}
}
for (const symbolPick of sortedFilteredSymbolPicks) {
// Found new kind
if (lastSymbolKind !== symbolPick.kind) {
// Update last separator with number of symbols we found for kind
updateLastSeparatorLabel();
lastSymbolKind = symbolPick.kind;
lastSymbolKindCounter = 1;
// Add new separator for new kind
lastSeparator = { type: 'separator' };
symbolPicks.push(lastSeparator);
}
// Existing kind, keep counting
else {
lastSymbolKindCounter++;
}
// Add to final result
symbolPicks.push(symbolPick);
}
// Update last separator with number of symbols we found for kind
updateLastSeparatorLabel();
} else {
symbolPicks = [
{ label: localize('symbols', "symbols ({0})", filteredSymbolPicks.length), type: 'separator' },
...sortedFilteredSymbolPicks
];
}
return symbolPicks;
}
private compareByScore(symbolA: IGotoSymbolQuickPickItem, symbolB: IGotoSymbolQuickPickItem): number {
if (!symbolA.score && symbolB.score) {
return 1;
} else if (symbolA.score && !symbolB.score) {
return -1;
}
if (symbolA.score && symbolB.score) {
if (symbolA.score[0] > symbolB.score[0]) {
return -1;
} else if (symbolA.score[0] < symbolB.score[0]) {
return 1;
}
}
if (symbolA.index < symbolB.index) {
return -1;
} else if (symbolA.index > symbolB.index) {
return 1;
}
return 0;
}
private compareByKindAndScore(symbolA: IGotoSymbolQuickPickItem, symbolB: IGotoSymbolQuickPickItem): number {
const kindA = NLS_SYMBOL_KIND_CACHE[symbolA.kind] || FALLBACK_NLS_SYMBOL_KIND;
const kindB = NLS_SYMBOL_KIND_CACHE[symbolB.kind] || FALLBACK_NLS_SYMBOL_KIND;
// Sort by type first if scoped search
const result = kindA.localeCompare(kindB);
if (result === 0) {
return this.compareByScore(symbolA, symbolB);
}
return result;
}
private async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise<DocumentSymbol[]> {
const model = await OutlineModel.create(document, token);
if (token.isCancellationRequested) {
return [];
}
const roots: DocumentSymbol[] = [];
for (const child of values(model.children)) {
if (child instanceof OutlineElement) {
roots.push(child.symbol);
} else {
roots.push(...values(child.children).map(child => child.symbol));
}
}
let flatEntries: DocumentSymbol[] = [];
if (flatten) {
this.flattenDocumentSymbols(flatEntries, roots, '');
} else {
flatEntries = roots;
}
return flatEntries.sort((symbolA, symbolB) => Range.compareRangesUsingStarts(symbolA.range, symbolB.range));
}
private flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void {
for (const entry of entries) {
bucket.push({
kind: entry.kind,
tags: entry.tags,
name: entry.name,
detail: entry.detail,
containerName: entry.containerName || overrideContainerLabel,
range: entry.range,
selectionRange: entry.selectionRange,
children: undefined, // we flatten it...
});
// Recurse over children
if (entry.children) {
this.flattenDocumentSymbols(bucket, entry.children, entry.name);
}
}
}
private getModel(editor: IEditor | IDiffEditor): ITextModel | undefined {
return isDiffEditor(editor) ?
editor.getModel()?.modified :
editor.getModel() as ITextModel;
}
protected gotoSymbol(editor: IEditor, range: IRange, keyMods: IKeyMods): void {
editor.setSelection(range);
editor.revealRangeInCenter(range, ScrollType.Smooth);
editor.focus();
}
}
// #region NLS Helpers
const FALLBACK_NLS_SYMBOL_KIND = localize('property', "properties ({0})");
const NLS_SYMBOL_KIND_CACHE: { [type: number]: string } = {
[SymbolKind.Method]: localize('method', "methods ({0})"),
[SymbolKind.Function]: localize('function', "functions ({0})"),
[SymbolKind.Constructor]: localize('_constructor', "constructors ({0})"),
[SymbolKind.Variable]: localize('variable', "variables ({0})"),
[SymbolKind.Class]: localize('class', "classes ({0})"),
[SymbolKind.Struct]: localize('struct', "structs ({0})"),
[SymbolKind.Event]: localize('event', "events ({0})"),
[SymbolKind.Operator]: localize('operator', "operators ({0})"),
[SymbolKind.Interface]: localize('interface', "interfaces ({0})"),
[SymbolKind.Namespace]: localize('namespace', "namespaces ({0})"),
[SymbolKind.Package]: localize('package', "packages ({0})"),
[SymbolKind.TypeParameter]: localize('typeParameter', "type parameters ({0})"),
[SymbolKind.Module]: localize('modules', "modules ({0})"),
[SymbolKind.Property]: localize('property', "properties ({0})"),
[SymbolKind.Enum]: localize('enum', "enumerations ({0})"),
[SymbolKind.EnumMember]: localize('enumMember', "enumeration members ({0})"),
[SymbolKind.String]: localize('string', "strings ({0})"),
[SymbolKind.File]: localize('file', "files ({0})"),
[SymbolKind.Array]: localize('array', "arrays ({0})"),
[SymbolKind.Number]: localize('number', "numbers ({0})"),
[SymbolKind.Boolean]: localize('boolean', "booleans ({0})"),
[SymbolKind.Object]: localize('object', "objects ({0})"),
[SymbolKind.Key]: localize('key', "keys ({0})"),
[SymbolKind.Field]: localize('field', "fields ({0})"),
[SymbolKind.Constant]: localize('constant', "constants ({0})")
};
//#endregion
......@@ -12,6 +12,7 @@ import 'vs/editor/standalone/browser/quickOpen/quickCommand';
import 'vs/editor/standalone/browser/quickOpen/quickOutline';
import 'vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess';
import 'vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess';
import 'vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess';
import 'vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess';
import 'vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch';
import 'vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast';
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AbstractGotoSymbolQuickAccessProvider } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess';
import { Registry } from 'vs/platform/registry/common/platform';
import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { withNullAsUndefined } from 'vs/base/common/types';
import { QuickOutlineNLS } from 'vs/editor/common/standaloneStrings';
import { Event } from 'vs/base/common/event';
export class StandaloneGotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider {
readonly onDidActiveTextEditorControlChange = Event.None;
constructor(@ICodeEditorService private readonly editorService: ICodeEditorService) {
super();
}
get activeTextEditorControl() {
return withNullAsUndefined(this.editorService.getFocusedCodeEditor());
}
}
Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess).registerQuickAccessProvider({
ctor: StandaloneGotoSymbolQuickAccessProvider,
prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX,
helpEntries: [
{ description: QuickOutlineNLS.quickOutlineActionLabel, prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, needsEditor: true },
{ description: QuickOutlineNLS.quickOutlineByCategoryActionLabel, prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY, needsEditor: true }
]
});
......@@ -10,6 +10,7 @@ import './inspectKeybindings';
import './largeFileOptimizations';
import './inspectEditorTokens/inspectEditorTokens';
import './quickaccess/gotoLineQuickAccess';
import './quickaccess/gotoSymbolAccess';
import './saveParticipants';
import './toggleColumnSelection';
import './toggleMinimap';
......
/*---------------------------------------------------------------------------------------------
* 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 { IKeyMods } from 'vs/platform/quickinput/common/quickInput';
import { IEditor } from 'vs/editor/common/editorCommon';
import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
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';
export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider {
readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange;
constructor(@IEditorService private readonly editorService: IEditorService) {
super();
}
get activeTextEditorControl() {
return this.editorService.activeTextEditorControl;
}
protected gotoSymbol(editor: IEditor, range: IRange, keyMods: IKeyMods): void {
// Check for sideBySide use
if (keyMods.ctrlCmd && this.editorService.activeEditor) {
this.editorService.openEditor(this.editorService.activeEditor, { selection: range, pinned: keyMods.alt }, SIDE_GROUP);
}
// Otherwise let parent handle it
else {
super.gotoSymbol(editor, range, keyMods);
}
}
}
Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess).registerQuickAccessProvider({
ctor: GotoSymbolQuickAccessProvider,
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 }
]
});
......@@ -10,7 +10,6 @@ import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/plat
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { CancellationToken } from 'vs/base/common/cancellation';
import { timeout } from 'vs/base/common/async';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { DisposableStore, toDisposable, dispose } from 'vs/base/common/lifecycle';
import { AbstractEditorCommandsQuickAccessProvider } from 'vs/editor/contrib/quickAccess/commandsQuickAccess';
......@@ -39,7 +38,6 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
@IEditorService private readonly editorService: IEditorService,
@IMenuService private readonly menuService: IMenuService,
@IExtensionService private readonly extensionService: IExtensionService,
@IEnvironmentService environmentService: IEnvironmentService,
@IInstantiationService instantiationService: IInstantiationService,
@IKeybindingService keybindingService: IKeybindingService,
@ICommandService commandService: ICommandService,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册