Skip to content

Commit 9102848

Browse files
authored
Cache outline headers & ref count outline provider (microsoft#210213)
* Cache outline headers & ref count outline provider * Fix tests * Remove handle * Oops * Simpler cachine
1 parent c002405 commit 9102848

File tree

10 files changed

+150
-87
lines changed

10 files changed

+150
-87
lines changed

src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
1212
import { IDataSource, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
1313
import { Emitter, Event } from 'vs/base/common/event';
1414
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
15-
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
15+
import { Disposable, DisposableStore, IDisposable, toDisposable, type IReference } from 'vs/base/common/lifecycle';
1616
import { ThemeIcon } from 'vs/base/common/themables';
1717
import { URI } from 'vs/base/common/uri';
1818
import { getIconClassesForLanguageId } from 'vs/editor/common/services/getIconClasses';
@@ -51,6 +51,7 @@ import { IOutlinePane } from 'vs/workbench/contrib/outline/browser/outline';
5151
import { Codicon } from 'vs/base/common/codicons';
5252
import { NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys';
5353
import { NotebookOutlineConstants } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory';
54+
import { INotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory';
5455

5556
class NotebookOutlineTemplate {
5657

@@ -337,7 +338,7 @@ export class NotebookCellOutline implements IOutline<OutlineEntry> {
337338
readonly onDidChange: Event<OutlineChangeEvent> = this._onDidChange.event;
338339

339340
get entries(): OutlineEntry[] {
340-
return this._outlineProvider?.entries ?? [];
341+
return this._outlineProviderReference?.object?.entries ?? [];
341342
}
342343

343344
private readonly _entriesDisposables = new DisposableStore();
@@ -347,10 +348,10 @@ export class NotebookCellOutline implements IOutline<OutlineEntry> {
347348
readonly outlineKind = 'notebookCells';
348349

349350
get activeElement(): OutlineEntry | undefined {
350-
return this._outlineProvider?.activeElement;
351+
return this._outlineProviderReference?.object?.activeElement;
351352
}
352353

353-
private _outlineProvider: NotebookCellOutlineProvider | undefined;
354+
private _outlineProviderReference: IReference<NotebookCellOutlineProvider> | undefined;
354355
private readonly _localDisposables = new DisposableStore();
355356

356357
constructor(
@@ -363,14 +364,14 @@ export class NotebookCellOutline implements IOutline<OutlineEntry> {
363364
const installSelectionListener = () => {
364365
const notebookEditor = _editor.getControl();
365366
if (!notebookEditor?.hasModel()) {
366-
this._outlineProvider?.dispose();
367-
this._outlineProvider = undefined;
367+
this._outlineProviderReference?.dispose();
368+
this._outlineProviderReference = undefined;
368369
this._localDisposables.clear();
369370
} else {
370-
this._outlineProvider?.dispose();
371+
this._outlineProviderReference?.dispose();
371372
this._localDisposables.clear();
372-
this._outlineProvider = instantiationService.createInstance(NotebookCellOutlineProvider, notebookEditor, _target);
373-
this._localDisposables.add(this._outlineProvider.onDidChange(e => {
373+
this._outlineProviderReference = instantiationService.invokeFunction((accessor) => accessor.get(INotebookCellOutlineProviderFactory).getOrCreate(notebookEditor, _target));
374+
this._localDisposables.add(this._outlineProviderReference.object.onDidChange(e => {
374375
this._onDidChange.fire(e);
375376
}));
376377
}
@@ -411,7 +412,7 @@ export class NotebookCellOutline implements IOutline<OutlineEntry> {
411412
return result;
412413
}
413414
},
414-
quickPickDataSource: instantiationService.createInstance(NotebookQuickPickProvider, () => (this._outlineProvider?.entries ?? [])),
415+
quickPickDataSource: instantiationService.createInstance(NotebookQuickPickProvider, () => (this._outlineProviderReference?.object?.entries ?? [])),
415416
treeDataSource,
416417
delegate,
417418
renderers,
@@ -425,7 +426,7 @@ export class NotebookCellOutline implements IOutline<OutlineEntry> {
425426
const showCodeCellSymbols = configurationService.getValue<boolean>(NotebookSetting.outlineShowCodeCellSymbols);
426427
const showMarkdownHeadersOnly = configurationService.getValue<boolean>(NotebookSetting.outlineShowMarkdownHeadersOnly);
427428

428-
for (const entry of parent instanceof NotebookCellOutline ? (this._outlineProvider?.entries ?? []) : parent.children) {
429+
for (const entry of parent instanceof NotebookCellOutline ? (this._outlineProviderReference?.object?.entries ?? []) : parent.children) {
429430
if (entry.cell.cellKind === CellKind.Markup) {
430431
if (!showMarkdownHeadersOnly) {
431432
yield entry;
@@ -444,14 +445,14 @@ export class NotebookCellOutline implements IOutline<OutlineEntry> {
444445
}
445446

446447
async setFullSymbols(cancelToken: CancellationToken) {
447-
await this._outlineProvider?.setFullSymbols(cancelToken);
448+
await this._outlineProviderReference?.object?.setFullSymbols(cancelToken);
448449
}
449450

450451
get uri(): URI | undefined {
451-
return this._outlineProvider?.uri;
452+
return this._outlineProviderReference?.object?.uri;
452453
}
453454
get isEmpty(): boolean {
454-
return this._outlineProvider?.isEmpty ?? true;
455+
return this._outlineProviderReference?.object?.isEmpty ?? true;
455456
}
456457
async reveal(entry: OutlineEntry, options: IEditorOptions, sideBySide: boolean): Promise<void> {
457458
await this._editorService.openEditor({
@@ -530,7 +531,7 @@ export class NotebookCellOutline implements IOutline<OutlineEntry> {
530531
this._onDidChange.dispose();
531532
this._dispoables.dispose();
532533
this._entriesDisposables.dispose();
533-
this._outlineProvider?.dispose();
534+
this._outlineProviderReference?.dispose();
534535
this._localDisposables.dispose();
535536
}
536537
}

src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
5757
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
5858
import { NotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/browser/services/notebookRendererMessagingServiceImpl';
5959
import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService';
60+
import { INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProviderFactory';
6061

6162
// Editor Controller
6263
import 'vs/workbench/contrib/notebook/browser/controller/coreActions';
@@ -755,6 +756,7 @@ registerSingleton(INotebookExecutionStateService, NotebookExecutionStateService,
755756
registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingService, InstantiationType.Delayed);
756757
registerSingleton(INotebookKeymapService, NotebookKeymapService, InstantiationType.Delayed);
757758
registerSingleton(INotebookLoggingService, NotebookLoggingService, InstantiationType.Delayed);
759+
registerSingleton(INotebookCellOutlineProviderFactory, NotebookCellOutlineProviderFactory, InstantiationType.Delayed);
758760

759761
const schemas: IJSONSchemaMap = {};
760762
function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema }): x is IConfigurationPropertySchema {

src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ export interface ICellViewModel extends IGenericCellViewModel {
260260
focusedOutputId?: string | undefined;
261261
outputIsHovered: boolean;
262262
getText(): string;
263+
getAlternativeId(): number;
263264
getTextLength(): number;
264265
getHeight(lineHeight: number): number;
265266
metadata: NotebookCellMetadata;

src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,8 @@ import { Schemas } from 'vs/base/common/network';
9696
import { DropIntoEditorController } from 'vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController';
9797
import { CopyPasteController } from 'vs/editor/contrib/dropOrPasteInto/browser/copyPasteController';
9898
import { NotebookStickyScroll } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll';
99-
import { NotebookCellOutlineProvider } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider';
10099
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
101100
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
102-
import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline';
103101
import { PixelRatio } from 'vs/base/browser/pixelRatio';
104102
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
105103
import { PreventDefaultContextMenuItemsContextKeyName } from 'vs/workbench/contrib/webview/browser/webview.contribution';
@@ -278,7 +276,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
278276
public readonly scopedContextKeyService: IContextKeyService;
279277
private readonly instantiationService: IInstantiationService;
280278
private readonly _notebookOptions: NotebookOptions;
281-
public readonly _notebookOutline: NotebookCellOutlineProvider;
282279

283280
private _currentProgress: IProgressRunner | undefined;
284281

@@ -335,8 +332,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
335332

336333
this._register(this.instantiationService.createInstance(NotebookEditorContextKeys, this));
337334

338-
this._notebookOutline = this._register(this.instantiationService.createInstance(NotebookCellOutlineProvider, this, OutlineTarget.QuickPick));
339-
340335
this._register(notebookKernelService.onDidChangeSelectedNotebooks(e => {
341336
if (isEqual(e.notebook, this.viewModel?.uri)) {
342337
this._loadKernelPreloads();
@@ -1054,7 +1049,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
10541049
}
10551050

10561051
private _registerNotebookStickyScroll() {
1057-
this._notebookStickyScroll = this._register(this.instantiationService.createInstance(NotebookStickyScroll, this._notebookStickyScrollContainer, this, this._notebookOutline, this._list));
1052+
this._notebookStickyScroll = this._register(this.instantiationService.createInstance(NotebookStickyScroll, this._notebookStickyScrollContainer, this, this._list));
10581053

10591054
const localDisposableStore = this._register(new DisposableStore());
10601055

src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@ export abstract class BaseCellViewModel extends Disposable {
307307
return this.model.getValue();
308308
}
309309

310+
getAlternativeId(): number {
311+
return this.model.alternativeId;
312+
}
313+
310314
getTextLength(): number {
311315
return this.model.getTextLength();
312316
}

src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,25 @@ type entryDesc = {
2727
kind: SymbolKind;
2828
};
2929

30+
function getMarkdownHeadersInCellFallbackToHtmlTags(fullContent: string) {
31+
const headers = Array.from(getMarkdownHeadersInCell(fullContent));
32+
if (headers.length) {
33+
return headers;
34+
}
35+
// no markdown syntax headers, try to find html tags
36+
const match = fullContent.match(/<h([1-6]).*>(.*)<\/h\1>/i);
37+
if (match) {
38+
const level = parseInt(match[1]);
39+
const text = match[2].trim();
40+
headers.push({ depth: level, text });
41+
}
42+
return headers;
43+
}
44+
3045
export class NotebookOutlineEntryFactory {
3146

3247
private cellOutlineEntryCache: Record<string, entryDesc[]> = {};
33-
48+
private readonly cachedMarkdownOutlineEntries = new WeakMap<ICellViewModel, { alternativeId: number; headers: { depth: number, text: string }[] }>();
3449
constructor(
3550
private readonly executionStateService: INotebookExecutionStateService
3651
) { }
@@ -48,22 +63,15 @@ export class NotebookOutlineEntryFactory {
4863

4964
if (isMarkdown) {
5065
const fullContent = cell.getText().substring(0, 10000);
51-
for (const { depth, text } of getMarkdownHeadersInCell(fullContent)) {
66+
const cache = this.cachedMarkdownOutlineEntries.get(cell);
67+
const headers = cache?.alternativeId === cell.getAlternativeId() ? cache.headers : Array.from(getMarkdownHeadersInCellFallbackToHtmlTags(fullContent));
68+
this.cachedMarkdownOutlineEntries.set(cell, { alternativeId: cell.getAlternativeId(), headers });
69+
70+
for (const { depth, text } of headers) {
5271
hasHeader = true;
5372
entries.push(new OutlineEntry(index++, depth, cell, text, false, false));
5473
}
5574

56-
if (!hasHeader) {
57-
// no markdown syntax headers, try to find html tags
58-
const match = fullContent.match(/<h([1-6]).*>(.*)<\/h\1>/i);
59-
if (match) {
60-
hasHeader = true;
61-
const level = parseInt(match[1]);
62-
const text = match[2].trim();
63-
entries.push(new OutlineEntry(index++, level, cell, text, false, false));
64-
}
65-
}
66-
6775
if (!hasHeader) {
6876
content = renderMarkdownAsPlaintext({ value: content });
6977
}

src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineProvider.ts

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { Emitter, Event } from 'vs/base/common/event';
7-
import { DisposableStore, MutableDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
7+
import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
88
import { isEqual } from 'vs/base/common/resources';
99
import { URI } from 'vs/base/common/uri';
1010
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1111
import { IMarkerService } from 'vs/platform/markers/common/markers';
1212
import { IThemeService } from 'vs/platform/theme/common/themeService';
13-
import { IActiveNotebookEditor, ICellViewModel, INotebookEditor, INotebookViewCellsUpdateEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
14-
import { CellKind, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
15-
import { INotebookExecutionStateService, NotebookExecutionType, type ICellExecutionStateChangedEvent, type IExecutionStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
13+
import { IActiveNotebookEditor, ICellViewModel, INotebookEditor, type INotebookViewCellsUpdateEvent } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
14+
import { CellKind, NotebookCellsChangeType, NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
15+
import { INotebookExecutionStateService, NotebookExecutionType } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService';
1616
import { OutlineChangeEvent, OutlineConfigKeys, OutlineTarget } from 'vs/workbench/services/outline/browser/outline';
1717
import { OutlineEntry } from './OutlineEntry';
1818
import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/browser/outlineModel';
1919
import { CancellationToken } from 'vs/base/common/cancellation';
2020
import { NotebookOutlineConstants, NotebookOutlineEntryFactory } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory';
21+
import { Delayer } from 'vs/base/common/async';
2122

2223
export class NotebookCellOutlineProvider {
2324
private readonly _disposables = new DisposableStore();
@@ -41,7 +42,6 @@ export class NotebookCellOutlineProvider {
4142
}
4243

4344
private readonly _outlineEntryFactory: NotebookOutlineEntryFactory;
44-
4545
constructor(
4646
private readonly _editor: INotebookEditor,
4747
private readonly _target: OutlineTarget,
@@ -53,29 +53,34 @@ export class NotebookCellOutlineProvider {
5353
) {
5454
this._outlineEntryFactory = new NotebookOutlineEntryFactory(notebookExecutionStateService);
5555

56-
const selectionListener = new MutableDisposable();
57-
this._disposables.add(selectionListener);
58-
59-
selectionListener.value = combinedDisposable(
60-
Event.debounce<void, void>(
61-
_editor.onDidChangeSelection,
62-
(last, _current) => last,
63-
200
64-
)(this._recomputeActive, this),
65-
Event.debounce<INotebookViewCellsUpdateEvent, INotebookViewCellsUpdateEvent>(
66-
_editor.onDidChangeViewCells,
67-
(last, _current) => last ?? _current,
68-
200
69-
)(this._recomputeState, this)
56+
this._disposables.add(Event.debounce<void, void>(
57+
_editor.onDidChangeSelection,
58+
(last, _current) => last,
59+
200
60+
)(() => {
61+
this._recomputeActive();
62+
}, this))
63+
this._disposables.add(Event.debounce<INotebookViewCellsUpdateEvent, INotebookViewCellsUpdateEvent>(
64+
_editor.onDidChangeViewCells,
65+
(last, _current) => last ?? _current,
66+
200
67+
)(() => {
68+
this._recomputeActive();
69+
}, this)
7070
);
7171

72+
// .3s of a delay is sufficient, 100-200s is too quick and will unnecessarily block the ui thread.
73+
// Given we're only updating the outline when the user types, we can afford to wait a bit.
74+
const delayer = this._disposables.add(new Delayer<void>(300));
75+
const delayedRecompute = () => delayer.trigger(() => this._recomputeState());
76+
7277
this._disposables.add(_configurationService.onDidChangeConfiguration(e => {
7378
if (e.affectsConfiguration(NotebookSetting.outlineShowMarkdownHeadersOnly) ||
7479
e.affectsConfiguration(NotebookSetting.outlineShowCodeCells) ||
7580
e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols) ||
7681
e.affectsConfiguration(NotebookSetting.breadcrumbsShowCodeCells)
7782
) {
78-
this._recomputeState();
83+
delayedRecompute();
7984
}
8085
}));
8186

@@ -84,17 +89,28 @@ export class NotebookCellOutlineProvider {
8489
}));
8590

8691
this._disposables.add(
87-
Event.debounce<ICellExecutionStateChangedEvent | IExecutionStateChangedEvent>(
88-
notebookExecutionStateService.onDidChangeExecution,
89-
(last, _current) => last ?? _current,
90-
200)(e => {
91-
if (e.type === NotebookExecutionType.cell && !!this._editor.textModel && e.affectsNotebook(this._editor.textModel?.uri)) {
92-
this._recomputeState();
93-
}
94-
})
92+
notebookExecutionStateService.onDidChangeExecution(e => {
93+
if (e.type === NotebookExecutionType.cell && !!this._editor.textModel && e.affectsNotebook(this._editor.textModel?.uri)) {
94+
delayedRecompute();
95+
}
96+
})
9597
);
9698

97-
this._recomputeState();
99+
const disposable = this._disposables.add(new DisposableStore());
100+
const monitorModelChanges = () => {
101+
disposable.clear();
102+
if (!this._editor.textModel) {
103+
return;
104+
}
105+
disposable.add(this._editor.textModel.onDidChangeContent(contentChanges => {
106+
if (contentChanges.rawEvents.some(c => c.kind === NotebookCellsChangeType.ChangeCellContent)) {
107+
delayedRecompute();
108+
}
109+
}));
110+
}
111+
this._disposables.add(this._editor.onDidChangeModel(monitorModelChanges));
112+
monitorModelChanges();
113+
this._recomputeState()
98114
}
99115

100116
dispose(): void {
@@ -104,10 +120,6 @@ export class NotebookCellOutlineProvider {
104120
this._disposables.dispose();
105121
}
106122

107-
init(): void {
108-
this._recomputeState();
109-
}
110-
111123
async setFullSymbols(cancelToken: CancellationToken) {
112124
const notebookEditorWidget = this._editor;
113125

@@ -126,7 +138,6 @@ export class NotebookCellOutlineProvider {
126138

127139
this._recomputeState();
128140
}
129-
130141
private _recomputeState(): void {
131142
this._entriesDisposables.clear();
132143
this._activeEntry = undefined;
@@ -159,11 +170,6 @@ export class NotebookCellOutlineProvider {
159170
const entries: OutlineEntry[] = [];
160171
for (const cell of notebookCells) {
161172
entries.push(...this._outlineEntryFactory.getOutlineEntries(cell, this._target, entries.length));
162-
// send an event whenever any of the cells change
163-
this._entriesDisposables.add(cell.model.onDidChangeContent(() => {
164-
this._recomputeState();
165-
this._onDidChange.fire({});
166-
}));
167173
}
168174

169175
// build a tree from the list of entries

0 commit comments

Comments
 (0)