Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Emitter, Event } from '../../../../../base/common/event.js';
7
import { DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';
8
import { isEqual } from '../../../../../base/common/resources.js';
9
import { URI } from '../../../../../base/common/uri.js';
10
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
11
import { IMarkerService } from '../../../../../platform/markers/common/markers.js';
12
import { IActiveNotebookEditor, INotebookEditor } from '../notebookBrowser.js';
13
import { CellKind } from '../../common/notebookCommon.js';
14
import { OutlineChangeEvent, OutlineConfigKeys } from '../../../../services/outline/browser/outline.js';
15
import { OutlineEntry } from './OutlineEntry.js';
16
import { CancellationToken } from '../../../../../base/common/cancellation.js';
17
import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './notebookOutlineEntryFactory.js';
18
19
export interface INotebookCellOutlineDataSource {
20
readonly activeElement: OutlineEntry | undefined;
21
readonly entries: OutlineEntry[];
22
}
23
24
export class NotebookCellOutlineDataSource implements INotebookCellOutlineDataSource {
25
26
private readonly _disposables = new DisposableStore();
27
28
private readonly _onDidChange = new Emitter<OutlineChangeEvent>();
29
readonly onDidChange: Event<OutlineChangeEvent> = this._onDidChange.event;
30
31
private _uri: URI | undefined;
32
private _entries: OutlineEntry[] = [];
33
private _activeEntry?: OutlineEntry;
34
35
constructor(
36
private readonly _editor: INotebookEditor,
37
@IMarkerService private readonly _markerService: IMarkerService,
38
@IConfigurationService private readonly _configurationService: IConfigurationService,
39
@INotebookOutlineEntryFactory private readonly _outlineEntryFactory: NotebookOutlineEntryFactory
40
) {
41
this.recomputeState();
42
}
43
44
get activeElement(): OutlineEntry | undefined {
45
return this._activeEntry;
46
}
47
get entries(): OutlineEntry[] {
48
return this._entries;
49
}
50
get isEmpty(): boolean {
51
return this._entries.length === 0;
52
}
53
get uri() {
54
return this._uri;
55
}
56
57
public async computeFullSymbols(cancelToken: CancellationToken) {
58
try {
59
const notebookEditorWidget = this._editor;
60
61
const notebookCells = notebookEditorWidget?.getViewModel()?.viewCells.filter((cell) => cell.cellKind === CellKind.Code);
62
63
if (notebookCells) {
64
const promises: Promise<void>[] = [];
65
// limit the number of cells so that we don't resolve an excessive amount of text models
66
for (const cell of notebookCells.slice(0, 50)) {
67
// gather all symbols asynchronously
68
promises.push(this._outlineEntryFactory.cacheSymbols(cell, cancelToken));
69
}
70
await Promise.allSettled(promises);
71
}
72
this.recomputeState();
73
} catch (err) {
74
console.error('Failed to compute notebook outline symbols:', err);
75
// Still recompute state with whatever symbols we have
76
this.recomputeState();
77
}
78
}
79
80
public recomputeState(): void {
81
this._disposables.clear();
82
this._activeEntry = undefined;
83
this._uri = undefined;
84
85
if (!this._editor.hasModel()) {
86
return;
87
}
88
89
this._uri = this._editor.textModel.uri;
90
91
const notebookEditorWidget: IActiveNotebookEditor = this._editor;
92
93
if (notebookEditorWidget.getLength() === 0) {
94
return;
95
}
96
97
const notebookCells = notebookEditorWidget.getViewModel().viewCells;
98
99
const entries: OutlineEntry[] = [];
100
for (const cell of notebookCells) {
101
entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, entries.length));
102
}
103
104
// build a tree from the list of entries
105
if (entries.length > 0) {
106
const result: OutlineEntry[] = [entries[0]];
107
const parentStack: OutlineEntry[] = [entries[0]];
108
109
for (let i = 1; i < entries.length; i++) {
110
const entry = entries[i];
111
112
while (true) {
113
const len = parentStack.length;
114
if (len === 0) {
115
// root node
116
result.push(entry);
117
parentStack.push(entry);
118
break;
119
120
} else {
121
const parentCandidate = parentStack[len - 1];
122
if (parentCandidate.level < entry.level) {
123
parentCandidate.addChild(entry);
124
parentStack.push(entry);
125
break;
126
} else {
127
parentStack.pop();
128
}
129
}
130
}
131
}
132
this._entries = result;
133
}
134
135
// feature: show markers with each cell
136
const markerServiceListener = new MutableDisposable();
137
this._disposables.add(markerServiceListener);
138
const updateMarkerUpdater = () => {
139
if (notebookEditorWidget.isDisposed) {
140
return;
141
}
142
143
const doUpdateMarker = (clear: boolean) => {
144
for (const entry of this._entries) {
145
if (clear) {
146
entry.clearMarkers();
147
} else {
148
entry.updateMarkers(this._markerService);
149
}
150
}
151
};
152
const problem = this._configurationService.getValue('problems.visibility');
153
if (problem === undefined) {
154
return;
155
}
156
157
const config = this._configurationService.getValue(OutlineConfigKeys.problemsEnabled);
158
159
if (problem && config) {
160
markerServiceListener.value = this._markerService.onMarkerChanged(e => {
161
if (notebookEditorWidget.isDisposed) {
162
console.error('notebook editor is disposed');
163
return;
164
}
165
166
if (e.some(uri => notebookEditorWidget.getCellsInRange().some(cell => isEqual(cell.uri, uri)))) {
167
doUpdateMarker(false);
168
this._onDidChange.fire({});
169
}
170
});
171
doUpdateMarker(false);
172
} else {
173
markerServiceListener.clear();
174
doUpdateMarker(true);
175
}
176
};
177
updateMarkerUpdater();
178
this._disposables.add(this._configurationService.onDidChangeConfiguration(e => {
179
if (e.affectsConfiguration('problems.visibility') || e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) {
180
updateMarkerUpdater();
181
this._onDidChange.fire({});
182
}
183
}));
184
185
const { changeEventTriggered } = this.recomputeActive();
186
if (!changeEventTriggered) {
187
this._onDidChange.fire({});
188
}
189
}
190
191
public recomputeActive(): { changeEventTriggered: boolean } {
192
let newActive: OutlineEntry | undefined;
193
const notebookEditorWidget = this._editor;
194
195
if (notebookEditorWidget) {//TODO don't check for widget, only here if we do have
196
if (notebookEditorWidget.hasModel() && notebookEditorWidget.getLength() > 0) {
197
const cell = notebookEditorWidget.cellAt(notebookEditorWidget.getFocus().start);
198
if (cell) {
199
for (const entry of this._entries) {
200
newActive = entry.find(cell, []);
201
if (newActive) {
202
break;
203
}
204
}
205
}
206
}
207
}
208
209
if (newActive !== this._activeEntry) {
210
this._activeEntry = newActive;
211
this._onDidChange.fire({ affectOnlyActiveElement: true });
212
return { changeEventTriggered: true };
213
}
214
return { changeEventTriggered: false };
215
}
216
217
dispose(): void {
218
this._entries.length = 0;
219
this._activeEntry = undefined;
220
this._disposables.dispose();
221
}
222
}
223
224