Path: blob/main/src/vs/workbench/contrib/notebook/browser/viewModel/foldingModel.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 { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';6import { Emitter, Event } from '../../../../../base/common/event.js';7import { DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';8import { marked } from '../../../../../base/common/marked/marked.js';9import { TrackedRangeStickiness } from '../../../../../editor/common/model.js';10import { FoldingLimitReporter } from '../../../../../editor/contrib/folding/browser/folding.js';11import { FoldingRegion, FoldingRegions } from '../../../../../editor/contrib/folding/browser/foldingRanges.js';12import { IFoldingRangeData, sanitizeRanges } from '../../../../../editor/contrib/folding/browser/syntaxRangeProvider.js';13import { INotebookViewModel } from '../notebookBrowser.js';14import { CellKind } from '../../common/notebookCommon.js';15import { cellRangesToIndexes, ICellRange } from '../../common/notebookRange.js';1617type RegionFilter = (r: FoldingRegion) => boolean;18type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean;1920const foldingRangeLimit: FoldingLimitReporter = {21limit: 5000,22update: () => { }23};2425export class FoldingModel implements IDisposable {26private _viewModel: INotebookViewModel | null = null;27private readonly _viewModelStore = new DisposableStore();28private _regions: FoldingRegions;29get regions() {30return this._regions;31}3233private readonly _onDidFoldingRegionChanges = new Emitter<void>();34readonly onDidFoldingRegionChanged: Event<void> = this._onDidFoldingRegionChanges.event;3536private _foldingRangeDecorationIds: string[] = [];3738constructor() {39this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0));40}4142dispose() {43this._onDidFoldingRegionChanges.dispose();44this._viewModelStore.dispose();45}4647detachViewModel() {48this._viewModelStore.clear();49this._viewModel = null;50}5152attachViewModel(model: INotebookViewModel) {53this._viewModel = model;5455this._viewModelStore.add(this._viewModel.onDidChangeViewCells(() => {56this.recompute();57}));5859this._viewModelStore.add(this._viewModel.onDidChangeSelection(() => {60if (!this._viewModel) {61return;62}6364const indexes = cellRangesToIndexes(this._viewModel.getSelections());6566let changed = false;6768indexes.forEach(index => {69let regionIndex = this.regions.findRange(index + 1);7071while (regionIndex !== -1) {72if (this._regions.isCollapsed(regionIndex) && index > this._regions.getStartLineNumber(regionIndex) - 1) {73this._regions.setCollapsed(regionIndex, false);74changed = true;75}76regionIndex = this._regions.getParentIndex(regionIndex);77}78});7980if (changed) {81this._onDidFoldingRegionChanges.fire();82}8384}));8586this.recompute();87}8889getRegionAtLine(lineNumber: number): FoldingRegion | null {90if (this._regions) {91const index = this._regions.findRange(lineNumber);92if (index >= 0) {93return this._regions.toRegion(index);94}95}96return null;97}9899getRegionsInside(region: FoldingRegion | null, filter?: RegionFilter | RegionFilterWithLevel): FoldingRegion[] {100const result: FoldingRegion[] = [];101const index = region ? region.regionIndex + 1 : 0;102const endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;103104if (filter && filter.length === 2) {105const levelStack: FoldingRegion[] = [];106for (let i = index, len = this._regions.length; i < len; i++) {107const current = this._regions.toRegion(i);108if (this._regions.getStartLineNumber(i) < endLineNumber) {109while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {110levelStack.pop();111}112levelStack.push(current);113if (filter(current, levelStack.length)) {114result.push(current);115}116} else {117break;118}119}120} else {121for (let i = index, len = this._regions.length; i < len; i++) {122const current = this._regions.toRegion(i);123if (this._regions.getStartLineNumber(i) < endLineNumber) {124if (!filter || (filter as RegionFilter)(current)) {125result.push(current);126}127} else {128break;129}130}131}132return result;133}134135getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] {136const result: FoldingRegion[] = [];137if (this._regions) {138let index = this._regions.findRange(lineNumber);139let level = 1;140while (index >= 0) {141const current = this._regions.toRegion(index);142if (!filter || filter(current, level)) {143result.push(current);144}145level++;146index = current.parentIndex;147}148}149return result;150}151152setCollapsed(index: number, newState: boolean) {153this._regions.setCollapsed(index, newState);154}155156recompute() {157if (!this._viewModel) {158return;159}160161const viewModel = this._viewModel;162const cells = viewModel.viewCells;163const stack: { index: number; level: number; endIndex: number }[] = [];164165for (let i = 0; i < cells.length; i++) {166const cell = cells[i];167168if (cell.cellKind !== CellKind.Markup || cell.language !== 'markdown') {169continue;170}171172const minDepth = Math.min(7, ...Array.from(getMarkdownHeadersInCell(cell.getText()), header => header.depth));173if (minDepth < 7) {174// header 1 to 6175stack.push({ index: i, level: minDepth, endIndex: 0 });176}177}178179// calculate folding ranges180const rawFoldingRanges: IFoldingRangeData[] = stack.map((entry, startIndex) => {181let end: number | undefined = undefined;182for (let i = startIndex + 1; i < stack.length; ++i) {183if (stack[i].level <= entry.level) {184end = stack[i].index - 1;185break;186}187}188189const endIndex = end !== undefined ? end : cells.length - 1;190191// one based192return {193start: entry.index + 1,194end: endIndex + 1,195rank: 1196};197}).filter(range => range.start !== range.end);198199const newRegions = sanitizeRanges(rawFoldingRanges, foldingRangeLimit);200201// restore collased state202let i = 0;203const nextCollapsed = () => {204while (i < this._regions.length) {205const isCollapsed = this._regions.isCollapsed(i);206i++;207if (isCollapsed) {208return i - 1;209}210}211return -1;212};213214let k = 0;215let collapsedIndex = nextCollapsed();216217while (collapsedIndex !== -1 && k < newRegions.length) {218// get the latest range219const decRange = viewModel.getTrackedRange(this._foldingRangeDecorationIds[collapsedIndex]);220if (decRange) {221const collasedStartIndex = decRange.start;222223while (k < newRegions.length) {224const startIndex = newRegions.getStartLineNumber(k) - 1;225if (collasedStartIndex >= startIndex) {226newRegions.setCollapsed(k, collasedStartIndex === startIndex);227k++;228} else {229break;230}231}232}233collapsedIndex = nextCollapsed();234}235236while (k < newRegions.length) {237newRegions.setCollapsed(k, false);238k++;239}240241const cellRanges: ICellRange[] = [];242for (let i = 0; i < newRegions.length; i++) {243const region = newRegions.toRegion(i);244cellRanges.push({ start: region.startLineNumber - 1, end: region.endLineNumber - 1 });245}246247// remove old tracked ranges and add new ones248// TODO@rebornix, implement delta249this._foldingRangeDecorationIds.forEach(id => viewModel.setTrackedRange(id, null, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter));250this._foldingRangeDecorationIds = cellRanges.map(region => viewModel.setTrackedRange(null, region, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter)).filter(str => str !== null) as string[];251252this._regions = newRegions;253this._onDidFoldingRegionChanges.fire();254}255256getMemento(): ICellRange[] {257const collapsedRanges: ICellRange[] = [];258let i = 0;259while (i < this._regions.length) {260const isCollapsed = this._regions.isCollapsed(i);261262if (isCollapsed) {263const region = this._regions.toRegion(i);264collapsedRanges.push({ start: region.startLineNumber - 1, end: region.endLineNumber - 1 });265}266267i++;268}269270return collapsedRanges;271}272273public applyMemento(state: ICellRange[]): boolean {274if (!this._viewModel) {275return false;276}277278let i = 0;279let k = 0;280281while (k < state.length && i < this._regions.length) {282// get the latest range283const decRange = this._viewModel.getTrackedRange(this._foldingRangeDecorationIds[i]);284if (decRange) {285const collasedStartIndex = state[k].start;286287while (i < this._regions.length) {288const startIndex = this._regions.getStartLineNumber(i) - 1;289if (collasedStartIndex >= startIndex) {290this._regions.setCollapsed(i, collasedStartIndex === startIndex);291i++;292} else {293break;294}295}296}297k++;298}299300while (i < this._regions.length) {301this._regions.setCollapsed(i, false);302i++;303}304305return true;306}307}308309export function updateFoldingStateAtIndex(foldingModel: FoldingModel, index: number, collapsed: boolean) {310const range = foldingModel.regions.findRange(index + 1);311foldingModel.setCollapsed(range, collapsed);312}313314export function* getMarkdownHeadersInCell(cellContent: string): Iterable<{ readonly depth: number; readonly text: string }> {315for (const token of marked.lexer(cellContent, { gfm: true })) {316if (token.type === 'heading') {317yield {318depth: token.depth,319text: renderAsPlaintext({ value: token.raw }).trim()320};321}322}323}324325326