Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.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 { Emitter, Event } from '../../../../../base/common/event.js';6import { DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';7import { isEqual } from '../../../../../base/common/resources.js';8import { URI } from '../../../../../base/common/uri.js';9import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';10import { IMarkerService } from '../../../../../platform/markers/common/markers.js';11import { IActiveNotebookEditor, INotebookEditor } from '../notebookBrowser.js';12import { CellKind } from '../../common/notebookCommon.js';13import { OutlineChangeEvent, OutlineConfigKeys } from '../../../../services/outline/browser/outline.js';14import { OutlineEntry } from './OutlineEntry.js';15import { CancellationToken } from '../../../../../base/common/cancellation.js';16import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './notebookOutlineEntryFactory.js';1718export interface INotebookCellOutlineDataSource {19readonly activeElement: OutlineEntry | undefined;20readonly entries: OutlineEntry[];21}2223export class NotebookCellOutlineDataSource implements INotebookCellOutlineDataSource {2425private readonly _disposables = new DisposableStore();2627private readonly _onDidChange = new Emitter<OutlineChangeEvent>();28readonly onDidChange: Event<OutlineChangeEvent> = this._onDidChange.event;2930private _uri: URI | undefined;31private _entries: OutlineEntry[] = [];32private _activeEntry?: OutlineEntry;3334constructor(35private readonly _editor: INotebookEditor,36@IMarkerService private readonly _markerService: IMarkerService,37@IConfigurationService private readonly _configurationService: IConfigurationService,38@INotebookOutlineEntryFactory private readonly _outlineEntryFactory: NotebookOutlineEntryFactory39) {40this.recomputeState();41}4243get activeElement(): OutlineEntry | undefined {44return this._activeEntry;45}46get entries(): OutlineEntry[] {47return this._entries;48}49get isEmpty(): boolean {50return this._entries.length === 0;51}52get uri() {53return this._uri;54}5556public async computeFullSymbols(cancelToken: CancellationToken) {57try {58const notebookEditorWidget = this._editor;5960const notebookCells = notebookEditorWidget?.getViewModel()?.viewCells.filter((cell) => cell.cellKind === CellKind.Code);6162if (notebookCells) {63const promises: Promise<void>[] = [];64// limit the number of cells so that we don't resolve an excessive amount of text models65for (const cell of notebookCells.slice(0, 50)) {66// gather all symbols asynchronously67promises.push(this._outlineEntryFactory.cacheSymbols(cell, cancelToken));68}69await Promise.allSettled(promises);70}71this.recomputeState();72} catch (err) {73console.error('Failed to compute notebook outline symbols:', err);74// Still recompute state with whatever symbols we have75this.recomputeState();76}77}7879public recomputeState(): void {80this._disposables.clear();81this._activeEntry = undefined;82this._uri = undefined;8384if (!this._editor.hasModel()) {85return;86}8788this._uri = this._editor.textModel.uri;8990const notebookEditorWidget: IActiveNotebookEditor = this._editor;9192if (notebookEditorWidget.getLength() === 0) {93return;94}9596const notebookCells = notebookEditorWidget.getViewModel().viewCells;9798const entries: OutlineEntry[] = [];99for (const cell of notebookCells) {100entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, entries.length));101}102103// build a tree from the list of entries104if (entries.length > 0) {105const result: OutlineEntry[] = [entries[0]];106const parentStack: OutlineEntry[] = [entries[0]];107108for (let i = 1; i < entries.length; i++) {109const entry = entries[i];110111while (true) {112const len = parentStack.length;113if (len === 0) {114// root node115result.push(entry);116parentStack.push(entry);117break;118119} else {120const parentCandidate = parentStack[len - 1];121if (parentCandidate.level < entry.level) {122parentCandidate.addChild(entry);123parentStack.push(entry);124break;125} else {126parentStack.pop();127}128}129}130}131this._entries = result;132}133134// feature: show markers with each cell135const markerServiceListener = new MutableDisposable();136this._disposables.add(markerServiceListener);137const updateMarkerUpdater = () => {138if (notebookEditorWidget.isDisposed) {139return;140}141142const doUpdateMarker = (clear: boolean) => {143for (const entry of this._entries) {144if (clear) {145entry.clearMarkers();146} else {147entry.updateMarkers(this._markerService);148}149}150};151const problem = this._configurationService.getValue('problems.visibility');152if (problem === undefined) {153return;154}155156const config = this._configurationService.getValue(OutlineConfigKeys.problemsEnabled);157158if (problem && config) {159markerServiceListener.value = this._markerService.onMarkerChanged(e => {160if (notebookEditorWidget.isDisposed) {161console.error('notebook editor is disposed');162return;163}164165if (e.some(uri => notebookEditorWidget.getCellsInRange().some(cell => isEqual(cell.uri, uri)))) {166doUpdateMarker(false);167this._onDidChange.fire({});168}169});170doUpdateMarker(false);171} else {172markerServiceListener.clear();173doUpdateMarker(true);174}175};176updateMarkerUpdater();177this._disposables.add(this._configurationService.onDidChangeConfiguration(e => {178if (e.affectsConfiguration('problems.visibility') || e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) {179updateMarkerUpdater();180this._onDidChange.fire({});181}182}));183184const { changeEventTriggered } = this.recomputeActive();185if (!changeEventTriggered) {186this._onDidChange.fire({});187}188}189190public recomputeActive(): { changeEventTriggered: boolean } {191let newActive: OutlineEntry | undefined;192const notebookEditorWidget = this._editor;193194if (notebookEditorWidget) {//TODO don't check for widget, only here if we do have195if (notebookEditorWidget.hasModel() && notebookEditorWidget.getLength() > 0) {196const cell = notebookEditorWidget.cellAt(notebookEditorWidget.getFocus().start);197if (cell) {198for (const entry of this._entries) {199newActive = entry.find(cell, []);200if (newActive) {201break;202}203}204}205}206}207208if (newActive !== this._activeEntry) {209this._activeEntry = newActive;210this._onDidChange.fire({ affectOnlyActiveElement: true });211return { changeEventTriggered: true };212}213return { changeEventTriggered: false };214}215216dispose(): void {217this._entries.length = 0;218this._activeEntry = undefined;219this._disposables.dispose();220}221}222223224