src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts/NotebookCellOutline/_recomputeState
class NotebookCellOutline:	private _recomputeState(): void {
		this._entriesDisposables.clear();
		this._activeEntry = undefined;
		this._entries.length = 0;
		this._uri = undefined;

		const notebookEditorControl = this._editor.getControl();

		if (!notebookEditorControl) {
			return;
		}

		if (!notebookEditorControl.hasModel()) {
			return;
		}

		this._uri = notebookEditorControl.textModel.uri;

		const notebookEditorWidget: IActiveNotebookEditor = notebookEditorControl;

		if (notebookEditorWidget.getLength() === 0) {
			return;
		}

		let includeCodeCells = true;
		if (this._target === OutlineTarget.OutlinePane) {
			includeCodeCells = this._configurationService.getValue<boolean>('notebook.outline.showCodeCells');
		} else if (this._target === OutlineTarget.Breadcrumbs) {
			includeCodeCells = this._configurationService.getValue<boolean>('notebook.breadcrumbs.showCodeCells');
		}

		const focusedCellIndex = notebookEditorWidget.getFocus().start;
		const focused = notebookEditorWidget.cellAt(focusedCellIndex)?.handle;
		const entries: OutlineEntry[] = [];

		for (let i = 0; i < notebookEditorWidget.getLength(); i++) {
			const cell = notebookEditorWidget.cellAt(i);
			const isMarkdown = cell.cellKind === CellKind.Markup;
			if (!isMarkdown && !includeCodeCells) {
				continue;
			}

			// cap the amount of characters that we look at and use the following logic
			// - for MD prefer headings (each header is an entry)
			// - otherwise use the first none-empty line of the cell (MD or code)
			let content = this._getCellFirstNonEmptyLine(cell);
			let hasHeader = false;

			if (isMarkdown) {
				const fullContent = cell.getText().substring(0, 10_000);
				for (const token of marked.lexer(fullContent, { gfm: true })) {
					if (token.type === 'heading') {
						hasHeader = true;
						entries.push(new OutlineEntry(entries.length, token.depth, cell, renderMarkdownAsPlaintext({ value: token.text }).trim(), false, false));
					}
				}
				if (!hasHeader) {
					// no markdown syntax headers, try to find html tags
					const match = fullContent.match(/<h([1-6]).*>(.*)<\/h\1>/i);
					if (match) {
						hasHeader = true;
						const level = parseInt(match[1]);
						const text = match[2].trim();
						entries.push(new OutlineEntry(entries.length, level, cell, text, false, false));
					}
				}

				if (!hasHeader) {
					content = renderMarkdownAsPlaintext({ value: content });
				}
			}

			if (!hasHeader) {
				let preview = content.trim();
				if (preview.length === 0) {
					// empty or just whitespace
					preview = localize('empty', "empty cell");
				}

				const exeState = !isMarkdown && this._notebookExecutionStateService.getCellExecution(cell.uri);
				entries.push(new OutlineEntry(entries.length, 7, cell, preview, !!exeState, exeState ? exeState.isPaused : false));
			}

			if (cell.handle === focused) {
				this._activeEntry = entries[entries.length - 1];
			}

			// send an event whenever any of the cells change
			this._entriesDisposables.add(cell.model.onDidChangeContent(() => {
				this._recomputeState();
				this._onDidChange.fire({});
			}));
		}

		// build a tree from the list of entries
		if (entries.length > 0) {
			const result: OutlineEntry[] = [entries[0]];
			const parentStack: OutlineEntry[] = [entries[0]];

			for (let i = 1; i < entries.length; i++) {
				const entry = entries[i];

				while (true) {
					const len = parentStack.length;
					if (len === 0) {
						// root node
						result.push(entry);
						parentStack.push(entry);
						break;

					} else {
						const parentCandidate = parentStack[len - 1];
						if (parentCandidate.level < entry.level) {
							parentCandidate.addChild(entry);
							parentStack.push(entry);
							break;
						} else {
							parentStack.pop();
						}
					}
				}
			}
			this._entries = result;
		}

		// feature: show markers with each cell
		const markerServiceListener = new MutableDisposable();
		this._entriesDisposables.add(markerServiceListener);
		const updateMarkerUpdater = () => {
			const doUpdateMarker = (clear: boolean) => {
				for (const entry of this._entries) {
					if (clear) {
						entry.clearMarkers();
					} else {
						entry.updateMarkers(this._markerService);
					}
				}
			};
			if (this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) {
				markerServiceListener.value = this._markerService.onMarkerChanged(e => {
					if (e.some(uri => notebookEditorWidget.getCellsInRange().some(cell => isEqual(cell.uri, uri)))) {
						doUpdateMarker(false);
						this._onDidChange.fire({});
					}
				});
				doUpdateMarker(false);
			} else {
				markerServiceListener.clear();
				doUpdateMarker(true);
			}
		};
		updateMarkerUpdater();
		this._entriesDisposables.add(this._configurationService.onDidChangeConfiguration(e => {
			if (e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) {
				updateMarkerUpdater();
				this._onDidChange.fire({});
			}
		}));

		this._onDidChange.fire({});
	}