UNPKG

monaco-editor-core

Version:

A browser based code editor

530 lines (529 loc) • 23.8 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter } from '../../../../base/common/event.js'; import { FoldingRegions } from './foldingRanges.js'; import { hash } from '../../../../base/common/hash.js'; export class FoldingModel { get regions() { return this._regions; } get textModel() { return this._textModel; } constructor(textModel, decorationProvider) { this._updateEventEmitter = new Emitter(); this.onDidChange = this._updateEventEmitter.event; this._textModel = textModel; this._decorationProvider = decorationProvider; this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0)); this._editorDecorationIds = []; } toggleCollapseState(toggledRegions) { if (!toggledRegions.length) { return; } toggledRegions = toggledRegions.sort((r1, r2) => r1.regionIndex - r2.regionIndex); const processed = {}; this._decorationProvider.changeDecorations(accessor => { let k = 0; // index from [0 ... this.regions.length] let dirtyRegionEndLine = -1; // end of the range where decorations need to be updated let lastHiddenLine = -1; // the end of the last hidden lines const updateDecorationsUntil = (index) => { while (k < index) { const endLineNumber = this._regions.getEndLineNumber(k); const isCollapsed = this._regions.isCollapsed(k); if (endLineNumber <= dirtyRegionEndLine) { const isManual = this.regions.getSource(k) !== 0 /* FoldSource.provider */; accessor.changeDecorationOptions(this._editorDecorationIds[k], this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual)); } if (isCollapsed && endLineNumber > lastHiddenLine) { lastHiddenLine = endLineNumber; } k++; } }; for (const region of toggledRegions) { const index = region.regionIndex; const editorDecorationId = this._editorDecorationIds[index]; if (editorDecorationId && !processed[editorDecorationId]) { processed[editorDecorationId] = true; updateDecorationsUntil(index); // update all decorations up to current index using the old dirtyRegionEndLine const newCollapseState = !this._regions.isCollapsed(index); this._regions.setCollapsed(index, newCollapseState); dirtyRegionEndLine = Math.max(dirtyRegionEndLine, this._regions.getEndLineNumber(index)); } } updateDecorationsUntil(this._regions.length); }); this._updateEventEmitter.fire({ model: this, collapseStateChanged: toggledRegions }); } removeManualRanges(ranges) { const newFoldingRanges = new Array(); const intersects = (foldRange) => { for (const range of ranges) { if (!(range.startLineNumber > foldRange.endLineNumber || foldRange.startLineNumber > range.endLineNumber)) { return true; } } return false; }; for (let i = 0; i < this._regions.length; i++) { const foldRange = this._regions.toFoldRange(i); if (foldRange.source === 0 /* FoldSource.provider */ || !intersects(foldRange)) { newFoldingRanges.push(foldRange); } } this.updatePost(FoldingRegions.fromFoldRanges(newFoldingRanges)); } update(newRegions, selection) { const foldedOrManualRanges = this._currentFoldedOrManualRanges(selection); const newRanges = FoldingRegions.sanitizeAndMerge(newRegions, foldedOrManualRanges, this._textModel.getLineCount(), selection); this.updatePost(FoldingRegions.fromFoldRanges(newRanges)); } updatePost(newRegions) { const newEditorDecorations = []; let lastHiddenLine = -1; for (let index = 0, limit = newRegions.length; index < limit; index++) { const startLineNumber = newRegions.getStartLineNumber(index); const endLineNumber = newRegions.getEndLineNumber(index); const isCollapsed = newRegions.isCollapsed(index); const isManual = newRegions.getSource(index) !== 0 /* FoldSource.provider */; const decorationRange = { startLineNumber: startLineNumber, startColumn: this._textModel.getLineMaxColumn(startLineNumber), endLineNumber: endLineNumber, endColumn: this._textModel.getLineMaxColumn(endLineNumber) + 1 }; newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine, isManual) }); if (isCollapsed && endLineNumber > lastHiddenLine) { lastHiddenLine = endLineNumber; } } this._decorationProvider.changeDecorations(accessor => this._editorDecorationIds = accessor.deltaDecorations(this._editorDecorationIds, newEditorDecorations)); this._regions = newRegions; this._updateEventEmitter.fire({ model: this }); } _currentFoldedOrManualRanges(selection) { const foldedRanges = []; for (let i = 0, limit = this._regions.length; i < limit; i++) { let isCollapsed = this.regions.isCollapsed(i); const source = this.regions.getSource(i); if (isCollapsed || source !== 0 /* FoldSource.provider */) { const foldRange = this._regions.toFoldRange(i); const decRange = this._textModel.getDecorationRange(this._editorDecorationIds[i]); if (decRange) { if (isCollapsed && selection?.startsInside(decRange.startLineNumber + 1, decRange.endLineNumber)) { isCollapsed = false; // uncollapse is the range is blocked } foldedRanges.push({ startLineNumber: decRange.startLineNumber, endLineNumber: decRange.endLineNumber, type: foldRange.type, isCollapsed, source }); } } } return foldedRanges; } /** * Collapse state memento, for persistence only */ getMemento() { const foldedOrManualRanges = this._currentFoldedOrManualRanges(); const result = []; const maxLineNumber = this._textModel.getLineCount(); for (let i = 0, limit = foldedOrManualRanges.length; i < limit; i++) { const range = foldedOrManualRanges[i]; if (range.startLineNumber >= range.endLineNumber || range.startLineNumber < 1 || range.endLineNumber > maxLineNumber) { continue; } const checksum = this._getLinesChecksum(range.startLineNumber + 1, range.endLineNumber); result.push({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, isCollapsed: range.isCollapsed, source: range.source, checksum: checksum }); } return (result.length > 0) ? result : undefined; } /** * Apply persisted state, for persistence only */ applyMemento(state) { if (!Array.isArray(state)) { return; } const rangesToRestore = []; const maxLineNumber = this._textModel.getLineCount(); for (const range of state) { if (range.startLineNumber >= range.endLineNumber || range.startLineNumber < 1 || range.endLineNumber > maxLineNumber) { continue; } const checksum = this._getLinesChecksum(range.startLineNumber + 1, range.endLineNumber); if (!range.checksum || checksum === range.checksum) { rangesToRestore.push({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, type: undefined, isCollapsed: range.isCollapsed ?? true, source: range.source ?? 0 /* FoldSource.provider */ }); } } const newRanges = FoldingRegions.sanitizeAndMerge(this._regions, rangesToRestore, maxLineNumber); this.updatePost(FoldingRegions.fromFoldRanges(newRanges)); } _getLinesChecksum(lineNumber1, lineNumber2) { const h = hash(this._textModel.getLineContent(lineNumber1) + this._textModel.getLineContent(lineNumber2)); return h % 1000000; // 6 digits is plenty } dispose() { this._decorationProvider.removeDecorations(this._editorDecorationIds); } getAllRegionsAtLine(lineNumber, filter) { const result = []; if (this._regions) { let index = this._regions.findRange(lineNumber); let level = 1; while (index >= 0) { const current = this._regions.toRegion(index); if (!filter || filter(current, level)) { result.push(current); } level++; index = current.parentIndex; } } return result; } getRegionAtLine(lineNumber) { if (this._regions) { const index = this._regions.findRange(lineNumber); if (index >= 0) { return this._regions.toRegion(index); } } return null; } getRegionsInside(region, filter) { const result = []; const index = region ? region.regionIndex + 1 : 0; const endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE; if (filter && filter.length === 2) { const levelStack = []; for (let i = index, len = this._regions.length; i < len; i++) { const current = this._regions.toRegion(i); if (this._regions.getStartLineNumber(i) < endLineNumber) { while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) { levelStack.pop(); } levelStack.push(current); if (filter(current, levelStack.length)) { result.push(current); } } else { break; } } } else { for (let i = index, len = this._regions.length; i < len; i++) { const current = this._regions.toRegion(i); if (this._regions.getStartLineNumber(i) < endLineNumber) { if (!filter || filter(current)) { result.push(current); } } else { break; } } } return result; } } /** * Collapse or expand the regions at the given locations * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels. * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model. */ export function toggleCollapseState(foldingModel, levels, lineNumbers) { const toToggle = []; for (const lineNumber of lineNumbers) { const region = foldingModel.getRegionAtLine(lineNumber); if (region) { const doCollapse = !region.isCollapsed; toToggle.push(region); if (levels > 1) { const regionsInside = foldingModel.getRegionsInside(region, (r, level) => r.isCollapsed !== doCollapse && level < levels); toToggle.push(...regionsInside); } } } foldingModel.toggleCollapseState(toToggle); } /** * Collapse or expand the regions at the given locations including all children. * @param doCollapse Whether to collapse or expand * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels. * @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model. */ export function setCollapseStateLevelsDown(foldingModel, doCollapse, levels = Number.MAX_VALUE, lineNumbers) { const toToggle = []; if (lineNumbers && lineNumbers.length > 0) { for (const lineNumber of lineNumbers) { const region = foldingModel.getRegionAtLine(lineNumber); if (region) { if (region.isCollapsed !== doCollapse) { toToggle.push(region); } if (levels > 1) { const regionsInside = foldingModel.getRegionsInside(region, (r, level) => r.isCollapsed !== doCollapse && level < levels); toToggle.push(...regionsInside); } } } } else { const regionsInside = foldingModel.getRegionsInside(null, (r, level) => r.isCollapsed !== doCollapse && level < levels); toToggle.push(...regionsInside); } foldingModel.toggleCollapseState(toToggle); } /** * Collapse or expand the regions at the given locations including all parents. * @param doCollapse Whether to collapse or expand * @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels. * @param lineNumbers the location of the regions to collapse or expand. */ export function setCollapseStateLevelsUp(foldingModel, doCollapse, levels, lineNumbers) { const toToggle = []; for (const lineNumber of lineNumbers) { const regions = foldingModel.getAllRegionsAtLine(lineNumber, (region, level) => region.isCollapsed !== doCollapse && level <= levels); toToggle.push(...regions); } foldingModel.toggleCollapseState(toToggle); } /** * Collapse or expand a region at the given locations. If the inner most region is already collapsed/expanded, uses the first parent instead. * @param doCollapse Whether to collapse or expand * @param lineNumbers the location of the regions to collapse or expand. */ export function setCollapseStateUp(foldingModel, doCollapse, lineNumbers) { const toToggle = []; for (const lineNumber of lineNumbers) { const regions = foldingModel.getAllRegionsAtLine(lineNumber, (region) => region.isCollapsed !== doCollapse); if (regions.length > 0) { toToggle.push(regions[0]); } } foldingModel.toggleCollapseState(toToggle); } /** * Folds or unfolds all regions that have a given level, except if they contain one of the blocked lines. * @param foldLevel level. Level == 1 is the top level * @param doCollapse Whether to collapse or expand */ export function setCollapseStateAtLevel(foldingModel, foldLevel, doCollapse, blockedLineNumbers) { const filter = (region, level) => level === foldLevel && region.isCollapsed !== doCollapse && !blockedLineNumbers.some(line => region.containsLine(line)); const toToggle = foldingModel.getRegionsInside(null, filter); foldingModel.toggleCollapseState(toToggle); } /** * Folds or unfolds all regions, except if they contain or are contained by a region of one of the blocked lines. * @param doCollapse Whether to collapse or expand * @param blockedLineNumbers the location of regions to not collapse or expand */ export function setCollapseStateForRest(foldingModel, doCollapse, blockedLineNumbers) { const filteredRegions = []; for (const lineNumber of blockedLineNumbers) { const regions = foldingModel.getAllRegionsAtLine(lineNumber, undefined); if (regions.length > 0) { filteredRegions.push(regions[0]); } } const filter = (region) => filteredRegions.every((filteredRegion) => !filteredRegion.containedBy(region) && !region.containedBy(filteredRegion)) && region.isCollapsed !== doCollapse; const toToggle = foldingModel.getRegionsInside(null, filter); foldingModel.toggleCollapseState(toToggle); } /** * Folds all regions for which the lines start with a given regex * @param foldingModel the folding model */ export function setCollapseStateForMatchingLines(foldingModel, regExp, doCollapse) { const editorModel = foldingModel.textModel; const regions = foldingModel.regions; const toToggle = []; for (let i = regions.length - 1; i >= 0; i--) { if (doCollapse !== regions.isCollapsed(i)) { const startLineNumber = regions.getStartLineNumber(i); if (regExp.test(editorModel.getLineContent(startLineNumber))) { toToggle.push(regions.toRegion(i)); } } } foldingModel.toggleCollapseState(toToggle); } /** * Folds all regions of the given type * @param foldingModel the folding model */ export function setCollapseStateForType(foldingModel, type, doCollapse) { const regions = foldingModel.regions; const toToggle = []; for (let i = regions.length - 1; i >= 0; i--) { if (doCollapse !== regions.isCollapsed(i) && type === regions.getType(i)) { toToggle.push(regions.toRegion(i)); } } foldingModel.toggleCollapseState(toToggle); } /** * Get line to go to for parent fold of current line * @param lineNumber the current line number * @param foldingModel the folding model * * @return Parent fold start line */ export function getParentFoldLine(lineNumber, foldingModel) { let startLineNumber = null; const foldingRegion = foldingModel.getRegionAtLine(lineNumber); if (foldingRegion !== null) { startLineNumber = foldingRegion.startLineNumber; // If current line is not the start of the current fold, go to top line of current fold. If not, go to parent fold if (lineNumber === startLineNumber) { const parentFoldingIdx = foldingRegion.parentIndex; if (parentFoldingIdx !== -1) { startLineNumber = foldingModel.regions.getStartLineNumber(parentFoldingIdx); } else { startLineNumber = null; } } } return startLineNumber; } /** * Get line to go to for previous fold at the same level of current line * @param lineNumber the current line number * @param foldingModel the folding model * * @return Previous fold start line */ export function getPreviousFoldLine(lineNumber, foldingModel) { let foldingRegion = foldingModel.getRegionAtLine(lineNumber); // If on the folding range start line, go to previous sibling. if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) { // If current line is not the start of the current fold, go to top line of current fold. If not, go to previous fold. if (lineNumber !== foldingRegion.startLineNumber) { return foldingRegion.startLineNumber; } else { // Find min line number to stay within parent. const expectedParentIndex = foldingRegion.parentIndex; let minLineNumber = 0; if (expectedParentIndex !== -1) { minLineNumber = foldingModel.regions.getStartLineNumber(foldingRegion.parentIndex); } // Find fold at same level. while (foldingRegion !== null) { if (foldingRegion.regionIndex > 0) { foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex - 1); // Keep at same level. if (foldingRegion.startLineNumber <= minLineNumber) { return null; } else if (foldingRegion.parentIndex === expectedParentIndex) { return foldingRegion.startLineNumber; } } else { return null; } } } } else { // Go to last fold that's before the current line. if (foldingModel.regions.length > 0) { foldingRegion = foldingModel.regions.toRegion(foldingModel.regions.length - 1); while (foldingRegion !== null) { // Found fold before current line. if (foldingRegion.startLineNumber < lineNumber) { return foldingRegion.startLineNumber; } if (foldingRegion.regionIndex > 0) { foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex - 1); } else { foldingRegion = null; } } } } return null; } /** * Get line to go to next fold at the same level of current line * @param lineNumber the current line number * @param foldingModel the folding model * * @return Next fold start line */ export function getNextFoldLine(lineNumber, foldingModel) { let foldingRegion = foldingModel.getRegionAtLine(lineNumber); // If on the folding range start line, go to next sibling. if (foldingRegion !== null && foldingRegion.startLineNumber === lineNumber) { // Find max line number to stay within parent. const expectedParentIndex = foldingRegion.parentIndex; let maxLineNumber = 0; if (expectedParentIndex !== -1) { maxLineNumber = foldingModel.regions.getEndLineNumber(foldingRegion.parentIndex); } else if (foldingModel.regions.length === 0) { return null; } else { maxLineNumber = foldingModel.regions.getEndLineNumber(foldingModel.regions.length - 1); } // Find fold at same level. while (foldingRegion !== null) { if (foldingRegion.regionIndex < foldingModel.regions.length) { foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex + 1); // Keep at same level. if (foldingRegion.startLineNumber >= maxLineNumber) { return null; } else if (foldingRegion.parentIndex === expectedParentIndex) { return foldingRegion.startLineNumber; } } else { return null; } } } else { // Go to first fold that's after the current line. if (foldingModel.regions.length > 0) { foldingRegion = foldingModel.regions.toRegion(0); while (foldingRegion !== null) { // Found fold after current line. if (foldingRegion.startLineNumber > lineNumber) { return foldingRegion.startLineNumber; } if (foldingRegion.regionIndex < foldingModel.regions.length) { foldingRegion = foldingModel.regions.toRegion(foldingRegion.regionIndex + 1); } else { foldingRegion = null; } } } } return null; }