Path: blob/main/src/vs/workbench/contrib/outline/browser/outlinePane.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './outlinePane.css';6import * as dom from '../../../../base/browser/dom.js';7import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js';8import { TimeoutTimer, timeout } from '../../../../base/common/async.js';9import { IDisposable, toDisposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';10import { LRUCache } from '../../../../base/common/map.js';11import { localize } from '../../../../nls.js';12import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';13import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';14import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';15import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';16import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';17import { WorkbenchDataTree } from '../../../../platform/list/browser/listService.js';18import { IStorageService } from '../../../../platform/storage/common/storage.js';19import { IThemeService } from '../../../../platform/theme/common/themeService.js';20import { ViewPane } from '../../../browser/parts/views/viewPane.js';21import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';22import { IEditorService } from '../../../services/editor/common/editorService.js';23import { FuzzyScore } from '../../../../base/common/filters.js';24import { basename } from '../../../../base/common/resources.js';25import { IViewDescriptorService } from '../../../common/views.js';26import { IOpenerService } from '../../../../platform/opener/common/opener.js';27import { OutlineViewState } from './outlineViewState.js';28import { IOutline, IOutlineComparator, IOutlineService, OutlineTarget } from '../../../services/outline/browser/outline.js';29import { EditorResourceAccessor, IEditorPane } from '../../../common/editor.js';30import { CancellationTokenSource } from '../../../../base/common/cancellation.js';31import { Event } from '../../../../base/common/event.js';32import { ITreeSorter } from '../../../../base/browser/ui/tree/tree.js';33import { AbstractTreeViewState, IAbstractTreeViewState, TreeFindMode } from '../../../../base/browser/ui/tree/abstractTree.js';34import { URI } from '../../../../base/common/uri.js';35import { ctxAllCollapsed, ctxFilterOnType, ctxFocused, ctxFollowsCursor, ctxSortMode, IOutlinePane, OutlineSortOrder } from './outline.js';36import { defaultProgressBarStyles } from '../../../../platform/theme/browser/defaultStyles.js';37import { IHoverService } from '../../../../platform/hover/browser/hover.js';3839class OutlineTreeSorter<E> implements ITreeSorter<E> {4041constructor(42private _comparator: IOutlineComparator<E>,43public order: OutlineSortOrder44) { }4546compare(a: E, b: E): number {47if (this.order === OutlineSortOrder.ByKind) {48return this._comparator.compareByType(a, b);49} else if (this.order === OutlineSortOrder.ByName) {50return this._comparator.compareByName(a, b);51} else {52return this._comparator.compareByPosition(a, b);53}54}55}5657export class OutlinePane extends ViewPane implements IOutlinePane {5859static readonly Id = 'outline';6061private readonly _disposables = new DisposableStore();6263private readonly _editorControlDisposables = new DisposableStore();64private readonly _editorPaneDisposables = new DisposableStore();65private readonly _outlineViewState = new OutlineViewState();6667private readonly _editorListener = new MutableDisposable();6869private _domNode!: HTMLElement;70private _message!: HTMLDivElement;71private _progressBar!: ProgressBar;72private _treeContainer!: HTMLElement;73private _tree?: WorkbenchDataTree<IOutline<any> | undefined, any, FuzzyScore>;74private _treeDimensions?: dom.Dimension;75private _treeStates = new LRUCache<string, IAbstractTreeViewState>(10);7677private _ctxFollowsCursor!: IContextKey<boolean>;78private _ctxFilterOnType!: IContextKey<boolean>;79private _ctxSortMode!: IContextKey<OutlineSortOrder>;80private _ctxAllCollapsed!: IContextKey<boolean>;8182constructor(83options: IViewletViewOptions,84@IOutlineService private readonly _outlineService: IOutlineService,85@IInstantiationService private readonly _instantiationService: IInstantiationService,86@IViewDescriptorService viewDescriptorService: IViewDescriptorService,87@IStorageService private readonly _storageService: IStorageService,88@IEditorService private readonly _editorService: IEditorService,89@IConfigurationService configurationService: IConfigurationService,90@IKeybindingService keybindingService: IKeybindingService,91@IContextKeyService contextKeyService: IContextKeyService,92@IContextMenuService contextMenuService: IContextMenuService,93@IOpenerService openerService: IOpenerService,94@IThemeService themeService: IThemeService,95@IHoverService hoverService: IHoverService,96) {97super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, hoverService);98this._outlineViewState.restore(this._storageService);99this._disposables.add(this._outlineViewState);100101contextKeyService.bufferChangeEvents(() => {102this._ctxFollowsCursor = ctxFollowsCursor.bindTo(contextKeyService);103this._ctxFilterOnType = ctxFilterOnType.bindTo(contextKeyService);104this._ctxSortMode = ctxSortMode.bindTo(contextKeyService);105this._ctxAllCollapsed = ctxAllCollapsed.bindTo(contextKeyService);106});107108const updateContext = () => {109this._ctxFollowsCursor.set(this._outlineViewState.followCursor);110this._ctxFilterOnType.set(this._outlineViewState.filterOnType);111this._ctxSortMode.set(this._outlineViewState.sortBy);112};113updateContext();114this._disposables.add(this._outlineViewState.onDidChange(updateContext));115}116117override dispose(): void {118this._disposables.dispose();119this._editorPaneDisposables.dispose();120this._editorControlDisposables.dispose();121this._editorListener.dispose();122super.dispose();123}124125override focus(): void {126this._editorControlChangePromise.then(() => {127super.focus();128this._tree?.domFocus();129});130}131132protected override renderBody(container: HTMLElement): void {133super.renderBody(container);134135this._domNode = container;136container.classList.add('outline-pane');137138const progressContainer = dom.$('.outline-progress');139this._message = dom.$('.outline-message');140141this._progressBar = new ProgressBar(progressContainer, defaultProgressBarStyles);142143this._treeContainer = dom.$('.outline-tree');144dom.append(container, progressContainer, this._message, this._treeContainer);145146this._disposables.add(this.onDidChangeBodyVisibility(visible => {147if (!visible) {148// stop everything when not visible149this._editorListener.clear();150this._editorPaneDisposables.clear();151this._editorControlDisposables.clear();152153} else if (!this._editorListener.value) {154const event = Event.any(this._editorService.onDidActiveEditorChange, this._outlineService.onDidChange);155this._editorListener.value = event(() => this._handleEditorChanged(this._editorService.activeEditorPane));156this._handleEditorChanged(this._editorService.activeEditorPane);157}158}));159}160161protected override layoutBody(height: number, width: number): void {162super.layoutBody(height, width);163this._tree?.layout(height, width);164this._treeDimensions = new dom.Dimension(width, height);165}166167collapseAll(): void {168this._tree?.collapseAll();169}170171expandAll(): void {172this._tree?.expandAll();173}174175get outlineViewState() {176return this._outlineViewState;177}178179private _showMessage(message: string) {180this._domNode.classList.add('message');181this._progressBar.stop().hide();182this._message.textContent = message;183}184185private _captureViewState(uri?: URI): boolean {186if (this._tree) {187const oldOutline = this._tree.getInput();188if (!uri) {189uri = oldOutline?.uri;190}191if (oldOutline && uri) {192this._treeStates.set(`${oldOutline.outlineKind}/${uri}`, this._tree.getViewState());193return true;194}195}196return false;197}198199private _editorControlChangePromise: Promise<void> = Promise.resolve();200private _handleEditorChanged(pane: IEditorPane | undefined): void {201this._editorPaneDisposables.clear();202203if (pane) {204// react to control changes from within pane (https://github.com/microsoft/vscode/issues/134008)205this._editorPaneDisposables.add(pane.onDidChangeControl(() => {206this._editorControlChangePromise = this._handleEditorControlChanged(pane);207}));208}209210this._editorControlChangePromise = this._handleEditorControlChanged(pane);211}212213private async _handleEditorControlChanged(pane: IEditorPane | undefined): Promise<void> {214215// persist state216const resource = EditorResourceAccessor.getOriginalUri(pane?.input);217const didCapture = this._captureViewState();218219this._editorControlDisposables.clear();220221if (!pane || !this._outlineService.canCreateOutline(pane) || !resource) {222return this._showMessage(localize('no-editor', "The active editor cannot provide outline information."));223}224225let loadingMessage: IDisposable | undefined;226if (!didCapture) {227loadingMessage = new TimeoutTimer(() => {228this._showMessage(localize('loading', "Loading document symbols for '{0}'...", basename(resource)));229}, 100);230}231232this._progressBar.infinite().show(500);233234const cts = new CancellationTokenSource();235this._editorControlDisposables.add(toDisposable(() => cts.dispose(true)));236237const newOutline = await this._outlineService.createOutline(pane, OutlineTarget.OutlinePane, cts.token);238loadingMessage?.dispose();239240if (!newOutline) {241return;242}243244if (cts.token.isCancellationRequested) {245newOutline?.dispose();246return;247}248249this._editorControlDisposables.add(newOutline);250this._progressBar.stop().hide();251252const sorter = new OutlineTreeSorter(newOutline.config.comparator, this._outlineViewState.sortBy);253254const tree = this._instantiationService.createInstance(255WorkbenchDataTree<IOutline<any> | undefined, any, FuzzyScore>,256'OutlinePane',257this._treeContainer,258newOutline.config.delegate,259newOutline.config.renderers,260newOutline.config.treeDataSource,261{262...newOutline.config.options,263sorter,264expandOnDoubleClick: false,265expandOnlyOnTwistieClick: true,266multipleSelectionSupport: false,267hideTwistiesOfChildlessElements: true,268defaultFindMode: this._outlineViewState.filterOnType ? TreeFindMode.Filter : TreeFindMode.Highlight,269overrideStyles: this.getLocationBasedColors().listOverrideStyles270}271);272273ctxFocused.bindTo(tree.contextKeyService);274275// update tree, listen to changes276const updateTree = () => {277if (newOutline.isEmpty) {278// no more elements279this._showMessage(localize('no-symbols', "No symbols found in document '{0}'", basename(resource)));280this._captureViewState(resource);281tree.setInput(undefined);282283} else if (!tree.getInput()) {284// first: init tree285this._domNode.classList.remove('message');286const state = this._treeStates.get(`${newOutline.outlineKind}/${newOutline.uri}`);287tree.setInput(newOutline, state && AbstractTreeViewState.lift(state));288289} else {290// update: refresh tree291this._domNode.classList.remove('message');292tree.updateChildren();293}294};295updateTree();296this._editorControlDisposables.add(newOutline.onDidChange(updateTree));297tree.findMode = this._outlineViewState.filterOnType ? TreeFindMode.Filter : TreeFindMode.Highlight;298299// feature: apply panel background to tree300this._editorControlDisposables.add(this.viewDescriptorService.onDidChangeLocation(({ views }) => {301if (views.some(v => v.id === this.id)) {302tree.updateOptions({ overrideStyles: this.getLocationBasedColors().listOverrideStyles });303}304}));305306// feature: filter on type - keep tree and menu in sync307this._editorControlDisposables.add(tree.onDidChangeFindMode(mode => this._outlineViewState.filterOnType = mode === TreeFindMode.Filter));308309// feature: reveal outline selection in editor310// on change -> reveal/select defining range311let idPool = 0;312this._editorControlDisposables.add(tree.onDidOpen(async e => {313const myId = ++idPool;314const isDoubleClick = e.browserEvent?.type === 'dblclick';315if (!isDoubleClick) {316// workaround for https://github.com/microsoft/vscode/issues/206424317await timeout(150);318if (myId !== idPool) {319return;320}321}322await newOutline.reveal(e.element, e.editorOptions, e.sideBySide, isDoubleClick);323}));324// feature: reveal editor selection in outline325const revealActiveElement = () => {326if (!this._outlineViewState.followCursor || !newOutline.activeElement) {327return;328}329let item = newOutline.activeElement;330while (item) {331const top = tree.getRelativeTop(item);332if (top === null) {333// not visible -> reveal334tree.reveal(item, 0.5);335}336if (tree.getRelativeTop(item) !== null) {337tree.setFocus([item]);338tree.setSelection([item]);339break;340}341// STILL not visible -> try parent342item = tree.getParentElement(item);343}344};345revealActiveElement();346this._editorControlDisposables.add(newOutline.onDidChange(revealActiveElement));347348// feature: update view when user state changes349this._editorControlDisposables.add(this._outlineViewState.onDidChange((e: { followCursor?: boolean; sortBy?: boolean; filterOnType?: boolean }) => {350this._outlineViewState.persist(this._storageService);351if (e.filterOnType) {352tree.findMode = this._outlineViewState.filterOnType ? TreeFindMode.Filter : TreeFindMode.Highlight;353}354if (e.followCursor) {355revealActiveElement();356}357if (e.sortBy) {358sorter.order = this._outlineViewState.sortBy;359tree.resort();360}361}));362363// feature: expand all nodes when filtering (not when finding)364let viewState: AbstractTreeViewState | undefined;365this._editorControlDisposables.add(tree.onDidChangeFindPattern(pattern => {366if (tree.findMode === TreeFindMode.Highlight) {367return;368}369if (!viewState && pattern) {370viewState = tree.getViewState();371tree.expandAll();372} else if (!pattern && viewState) {373tree.setInput(tree.getInput()!, viewState);374viewState = undefined;375}376}));377378// feature: update all-collapsed context key379const updateAllCollapsedCtx = () => {380this._ctxAllCollapsed.set(tree.getNode(null).children.every(node => !node.collapsible || node.collapsed));381};382this._editorControlDisposables.add(tree.onDidChangeCollapseState(updateAllCollapsedCtx));383this._editorControlDisposables.add(tree.onDidChangeModel(updateAllCollapsedCtx));384updateAllCollapsedCtx();385386// last: set tree property and wire it up to one of our context keys387tree.layout(this._treeDimensions?.height, this._treeDimensions?.width);388this._tree = tree;389this._editorControlDisposables.add(toDisposable(() => {390tree.dispose();391this._tree = undefined;392}));393}394}395396397