diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index c85081502dd39a2d28fe2abed6b45634649c30e0..aa9f6433bf4fd90a0f413c54c863f58e53d85fa8 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -445,4 +445,38 @@ export class OutlineModel extends TreeElement { group.updateMarker(marker.slice(0)); } } + + asListOfDocumentSymbols(): DocumentSymbol[] { + const roots: DocumentSymbol[] = []; + for (const child of this.children.values()) { + if (child instanceof OutlineElement) { + roots.push(child.symbol); + } else { + roots.push(...Iterable.map(child.children.values(), child => child.symbol)); + } + } + const bucket: DocumentSymbol[] = []; + OutlineModel._flattenDocumentSymbols(bucket, roots, ''); + return bucket; + } + + private static _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) { + OutlineModel._flattenDocumentSymbols(bucket, entry.children, entry.name); + } + } + } } diff --git a/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts b/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts index 51702b59e92229af50d0ea63c0962297b9561c44..4d9a68f5ccfae7537201c3df4eeae57f6b5d6cbf 100644 --- a/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts +++ b/src/vs/editor/contrib/gotoSymbol/documentSymbols.ts @@ -4,62 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { DocumentSymbol } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { assertType } from 'vs/base/common/types'; -import { Iterable } from 'vs/base/common/iterator'; export async function getDocumentSymbols(document: ITextModel, flat: boolean, token: CancellationToken): Promise { - const model = await OutlineModel.create(document, token); - const roots: DocumentSymbol[] = []; - for (const child of model.children.values()) { - if (child instanceof OutlineElement) { - roots.push(child.symbol); - } else { - roots.push(...Iterable.map(child.children.values(), child => child.symbol)); - } - } - - let flatEntries: DocumentSymbol[] = []; - if (token.isCancellationRequested) { - return flatEntries; - } - if (flat) { - flatten(flatEntries, roots, ''); - } else { - flatEntries = roots; - } - - return flatEntries.sort(compareEntriesUsingStart); -} - -function compareEntriesUsingStart(a: DocumentSymbol, b: DocumentSymbol): number { - return Range.compareRangesUsingStarts(a.range, b.range); -} - -function flatten(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void { - for (let 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... - }); - if (entry.children) { - flatten(bucket, entry.children, entry.name); - } - } + return model.asListOfDocumentSymbols(); } CommandsRegistry.registerCommand('_executeDocumentSymbolProvider', async function (accessor, ...args) { diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index 9fb3e3d3c6566aa453db6c9c1a954934de2da3d6..96c0a9be6e946b1d154c9acb31e2efb3b62f97ec 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -12,11 +12,10 @@ import { ITextModel } from 'vs/editor/common/model'; import { IRange, Range } from 'vs/editor/common/core/range'; import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes'; -import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; +import { OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { trim, format } from 'vs/base/common/strings'; import { prepareQuery, IPreparedQuery, pieceToQuery, scoreFuzzy2 } from 'vs/base/common/fuzzyScorer'; import { IMatch } from 'vs/base/common/filters'; -import { Iterable } from 'vs/base/common/iterator'; import { Codicon } from 'vs/base/common/codicons'; export interface IGotoSymbolQuickPickItem extends IQuickPickItem { @@ -144,7 +143,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // Resolve symbols from document once and reuse this // request for all filtering and typing then on - const symbolsPromise = this.getDocumentSymbols(model, true, token); + const symbolsPromise = this.getDocumentSymbols(model, token); // Set initial picks and update on type let picksCts: CancellationTokenSource | undefined = undefined; @@ -418,49 +417,9 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit return result; } - protected async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise { + protected async getDocumentSymbols(document: ITextModel, token: CancellationToken): Promise { const model = await OutlineModel.create(document, token); - if (token.isCancellationRequested) { - return []; - } - - const roots: DocumentSymbol[] = []; - for (const child of model.children.values()) { - if (child instanceof OutlineElement) { - roots.push(child.symbol); - } else { - roots.push(...Iterable.map(child.children.values(), 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); - } - } + return token.isCancellationRequested ? [] : model.asListOfDocumentSymbols(); } } diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts index 54d7897636db350aaefd062fe63e7549d11f7077..c40d5e780290d45ec751cd1d8861d8eb5052aae5 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsModel.ts @@ -105,7 +105,7 @@ export class BreadcrumbsModel { const { activeEntry } = this._currentOutline; if (activeEntry) { - for (let element of this._currentOutline.treeConfig.parentChainProvider.getBreadcrumbElements(activeEntry)) { + for (let element of this._currentOutline.treeConfig.breadcrumbsDataSource.getBreadcrumbElements(activeEntry)) { result.push(new OutlineElement2(element, this._currentOutline)); } } else if (!this._currentOutline.isEmpty) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index add20721ccbe17d91a5fe3b4145b817112571e1e..bb3f0dd550e4f23a197b54f150bfa8fe1a286499 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -60,6 +60,9 @@ class DocumentSymbolsOutline implements IOutline { { getBreadcrumbElements: () => this._outlineElementChain.filter(element => !(element instanceof OutlineModel)) }, + { + getQuickPickElements: () => { throw new Error('not implemented'); } + }, { getChildren: (parent) => { if (parent instanceof OutlineElement || parent instanceof OutlineGroup) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 427116923a6b73e6145a205659d59467379f23a1..4b853f1739227c744533ac970552ef2f069d766f 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -12,9 +12,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions as QuickaccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IWorkbenchEditorConfiguration, IEditorPane } from 'vs/workbench/common/editor'; +import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { ITextModel } from 'vs/editor/common/model'; -import { DisposableStore, IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable, Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; @@ -23,10 +23,11 @@ import { prepareQuery } from 'vs/base/common/fuzzyScorer'; import { SymbolKind } from 'vs/editor/common/modes'; import { fuzzyScore, createMatches } from 'vs/base/common/filters'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess'; +import { IOutlineService } from 'vs/workbench/services/outline/browser/outline'; +import { isCompositeEditor } from 'vs/editor/browser/editorBrowser'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { @@ -34,7 +35,8 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess constructor( @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IOutlineService private readonly outlineService: IOutlineService, ) { super({ openSideBySideDirection: () => this.configuration.openSideBySideDirection @@ -43,14 +45,6 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#region DocumentSymbols (text editor required) - protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick, token: CancellationToken): IDisposable { - if (this.canPickFromTableOfContents()) { - return this.doGetTableOfContentsPicks(picker); - } - - return super.provideWithTextEditor(context, picker, token); - } - private get configuration() { const editorConfig = this.configurationService.getValue().workbench?.editor; @@ -61,6 +55,10 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess } protected get activeTextEditorControl() { + if (isCompositeEditor(this.editorService.activeEditorPane?.getControl())) { + // TODO@bpasero adopt IOutlineService for "normal" document symbols. + return undefined; + } return this.editorService.activeTextEditorControl; } @@ -104,7 +102,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return []; } - return this.doGetSymbolPicks(this.getDocumentSymbols(model, true, token), prepareQuery(filter), options, token); + return this.doGetSymbolPicks(this.getDocumentSymbols(model, token), prepareQuery(filter), options, token); } addDecorations(editor: IEditor, range: IRange): void { @@ -118,22 +116,21 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#endregion protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { - if (this.canPickFromTableOfContents()) { - return this.doGetTableOfContentsPicks(picker); + if (this.canPickWithOutlineService()) { + return this.doGetOutlinePicks(picker); } return super.provideWithoutTextEditor(picker); } - private canPickFromTableOfContents(): boolean { - return this.editorService.activeEditorPane ? TableOfContentsProviderRegistry.has(this.editorService.activeEditorPane.getId()) : false; + private canPickWithOutlineService(): boolean { + return this.editorService.activeEditorPane ? this.outlineService.canCreateOutline(this.editorService.activeEditorPane) : false; } - private doGetTableOfContentsPicks(picker: IQuickPick): IDisposable { + private doGetOutlinePicks(picker: IQuickPick): IDisposable { const pane = this.editorService.activeEditorPane; if (!pane) { return Disposable.None; } - const provider = TableOfContentsProviderRegistry.get(pane.getId())!; const cts = new CancellationTokenSource(); const disposables = new DisposableStore(); @@ -141,30 +138,37 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess picker.busy = true; - provider.provideTableOfContents(pane, { disposables }, cts.token).then(entries => { + this.outlineService.createOutline(pane, cts.token).then(outline => { - picker.busy = false; - - if (cts.token.isCancellationRequested || !entries || entries.length === 0) { + if (!outline) { + return; + } + if (cts.token.isCancellationRequested) { + outline.dispose(); return; } + disposables.add(outline); + + const entries = Array.from(outline.treeConfig.quickPickDataSource.getQuickPickElements()); + const items: IGotoSymbolQuickPickItem[] = entries.map((entry, idx) => { return { kind: SymbolKind.File, index: idx, score: 0, - label: entry.icon ? `$(${entry.icon.id}) ${entry.label}` : entry.label, - ariaLabel: entry.detail ? `${entry.label}, ${entry.detail}` : entry.label, - detail: entry.detail, + label: entry.label, description: entry.description, + ariaLabel: entry.ariaLabel, }; }); disposables.add(picker.onDidAccept(() => { picker.hide(); const [entry] = picker.selectedItems; - entries[entry.index]?.pick(); + if (entry && entries[entry.index]) { + outline.revealInEditor(entries[entry.index].element, {}, false); + } })); const updatePickerItems = () => { @@ -194,16 +198,23 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess updatePickerItems(); disposables.add(picker.onDidChangeValue(updatePickerItems)); + const previewDisposable = new MutableDisposable(); + disposables.add(previewDisposable); + disposables.add(picker.onDidChangeActive(() => { const [entry] = picker.activeItems; - if (entry) { - entries[entry.index]?.preview(); + if (entry && entries[entry.index]) { + previewDisposable.value = outline.previewInEditor(entries[entry.index].element); + } else { + previewDisposable.clear(); } })); }).catch(err => { onUnexpectedError(err); picker.hide(); + }).finally(() => { + picker.busy = false; }); return disposables; @@ -243,45 +254,3 @@ registerAction2(class GotoSymbolAction extends Action2 { accessor.get(IQuickInputService).quickAccess.show(GotoSymbolQuickAccessProvider.PREFIX); } }); - -//#region toc definition and logic - -export interface ITableOfContentsEntry { - icon?: ThemeIcon; - label: string; - detail?: string; - description?: string; - pick(): any; - preview(): any; -} - -export interface ITableOfContentsProvider { - - provideTableOfContents(editor: T, context: { disposables: DisposableStore }, token: CancellationToken): Promise; -} - -class ProviderRegistry { - - private readonly _provider = new Map(); - - register(type: string, provider: ITableOfContentsProvider): IDisposable { - this._provider.set(type, provider); - return toDisposable(() => { - if (this._provider.get(type) === provider) { - this._provider.delete(type); - } - }); - } - - get(type: string): ITableOfContentsProvider | undefined { - return this._provider.get(type); - } - - has(type: string): boolean { - return this._provider.has(type); - } -} - -export const TableOfContentsProviderRegistry = new ProviderRegistry(); - -//#endregion diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebooksOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebooksOutline.ts index f4d2d925cf281afc65fb0d1ff8d120b06abec90f..f372a4653e772a570bd44554f3e682ec4e9e8749 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebooksOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebooksOutline.ts @@ -133,6 +133,7 @@ class NotebookCellOutline implements IOutline { this.treeConfig = new OutlineTreeConfiguration( { getBreadcrumbElements: (element) => Iterable.single(element) }, + { getQuickPickElements: () => this._entries.map(entry => ({ element: entry, label: `$(${entry.icon.id}) ${entry.label}`, ariaLabel: entry.label })) }, { getChildren: parent => parent === this ? this._entries : [] }, new NotebookOutlineVirtualDelegate(), [new NotebookOutlineRenderer()], diff --git a/src/vs/workbench/services/outline/browser/outline.ts b/src/vs/workbench/services/outline/browser/outline.ts index bfc98a4f839a6e26d7171dd431777ae231c2771d..f1463aea919dd9c53ce08656685a1d3833949126 100644 --- a/src/vs/workbench/services/outline/browser/outline.ts +++ b/src/vs/workbench/services/outline/browser/outline.ts @@ -9,6 +9,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { FuzzyScore } from 'vs/base/common/filters'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { SymbolKind } from 'vs/editor/common/modes'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkbenchDataTreeOptions } from 'vs/platform/list/browser/listService'; @@ -19,6 +20,7 @@ export const IOutlineService = createDecorator('IOutlineService export interface IOutlineService { _serviceBrand: undefined; onDidChange: Event; + canCreateOutline(editor: IEditorPane): boolean; createOutline(editor: IEditorPane, token: CancellationToken): Promise | undefined>; registerOutlineCreator(creator: IOutlineCreator): IDisposable; } @@ -28,13 +30,18 @@ export interface IOutlineCreator

{ createOutline(editor: P, token: CancellationToken): Promise | undefined>; } -export interface IParentChainProvider { +export interface IBreadcrumbsDataSource { getBreadcrumbElements(element: E): Iterable; } +export interface IQuickPickDataSource { + getQuickPickElements(): Iterable<{ element: E, kind?: SymbolKind, label: string, ariaLabel?: string, description?: string }>; +} + export class OutlineTreeConfiguration { constructor( - readonly parentChainProvider: IParentChainProvider, + readonly breadcrumbsDataSource: IBreadcrumbsDataSource, + readonly quickPickDataSource: IQuickPickDataSource, readonly treeDataSource: IDataSource, E>, readonly delegate: IListVirtualDelegate, readonly renderers: ITreeRenderer[], diff --git a/src/vs/workbench/services/outline/browser/outlineService.ts b/src/vs/workbench/services/outline/browser/outlineService.ts index a3e575234930d61a888df3a5c7b6e9e6c08b8ccc..b96ae46926a9aa829c986aa91a1da9c325ffa1c5 100644 --- a/src/vs/workbench/services/outline/browser/outlineService.ts +++ b/src/vs/workbench/services/outline/browser/outlineService.ts @@ -20,6 +20,15 @@ class OutlineService implements IOutlineService { private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; + canCreateOutline(pane: IEditorPane): boolean { + for (let factory of this._factories) { + if (factory.matches(pane)) { + return true; + } + } + return false; + } + async createOutline(pane: IEditorPane, token: CancellationToken): Promise | undefined> { for (let factory of this._factories) { if (factory.matches(pane)) {