Path: blob/main/src/vs/editor/contrib/folding/browser/foldingRanges.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 { SelectedLines } from './folding.js';67export interface ILineRange {8startLineNumber: number;9endLineNumber: number;10}1112export const enum FoldSource {13provider = 0,14userDefined = 1,15recovered = 216}1718export const foldSourceAbbr = {19[FoldSource.provider]: ' ',20[FoldSource.userDefined]: 'u',21[FoldSource.recovered]: 'r',22};2324export interface FoldRange {25startLineNumber: number;26endLineNumber: number;27type: string | undefined;28isCollapsed: boolean;29source: FoldSource;30}3132export const MAX_FOLDING_REGIONS = 0xFFFF;33export const MAX_LINE_NUMBER = 0xFFFFFF;3435const MASK_INDENT = 0xFF000000;3637class BitField {38private readonly _states: Uint32Array;39constructor(size: number) {40const numWords = Math.ceil(size / 32);41this._states = new Uint32Array(numWords);42}4344public get(index: number): boolean {45const arrayIndex = (index / 32) | 0;46const bit = index % 32;47return (this._states[arrayIndex] & (1 << bit)) !== 0;48}4950public set(index: number, newState: boolean) {51const arrayIndex = (index / 32) | 0;52const bit = index % 32;53const value = this._states[arrayIndex];54if (newState) {55this._states[arrayIndex] = value | (1 << bit);56} else {57this._states[arrayIndex] = value & ~(1 << bit);58}59}60}6162export class FoldingRegions {63private readonly _startIndexes: Uint32Array;64private readonly _endIndexes: Uint32Array;65private readonly _collapseStates: BitField;66private readonly _userDefinedStates: BitField;67private readonly _recoveredStates: BitField;6869private _parentsComputed: boolean;70private readonly _types: Array<string | undefined> | undefined;7172constructor(startIndexes: Uint32Array, endIndexes: Uint32Array, types?: Array<string | undefined>) {73if (startIndexes.length !== endIndexes.length || startIndexes.length > MAX_FOLDING_REGIONS) {74throw new Error('invalid startIndexes or endIndexes size');75}76this._startIndexes = startIndexes;77this._endIndexes = endIndexes;78this._collapseStates = new BitField(startIndexes.length);79this._userDefinedStates = new BitField(startIndexes.length);80this._recoveredStates = new BitField(startIndexes.length);81this._types = types;82this._parentsComputed = false;83}8485private ensureParentIndices() {86if (!this._parentsComputed) {87this._parentsComputed = true;88const parentIndexes: number[] = [];89const isInsideLast = (startLineNumber: number, endLineNumber: number) => {90const index = parentIndexes[parentIndexes.length - 1];91return this.getStartLineNumber(index) <= startLineNumber && this.getEndLineNumber(index) >= endLineNumber;92};93for (let i = 0, len = this._startIndexes.length; i < len; i++) {94const startLineNumber = this._startIndexes[i];95const endLineNumber = this._endIndexes[i];96if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) {97throw new Error('startLineNumber or endLineNumber must not exceed ' + MAX_LINE_NUMBER);98}99while (parentIndexes.length > 0 && !isInsideLast(startLineNumber, endLineNumber)) {100parentIndexes.pop();101}102const parentIndex = parentIndexes.length > 0 ? parentIndexes[parentIndexes.length - 1] : -1;103parentIndexes.push(i);104this._startIndexes[i] = startLineNumber + ((parentIndex & 0xFF) << 24);105this._endIndexes[i] = endLineNumber + ((parentIndex & 0xFF00) << 16);106}107}108}109110public get length(): number {111return this._startIndexes.length;112}113114public getStartLineNumber(index: number): number {115return this._startIndexes[index] & MAX_LINE_NUMBER;116}117118public getEndLineNumber(index: number): number {119return this._endIndexes[index] & MAX_LINE_NUMBER;120}121122public getType(index: number): string | undefined {123return this._types ? this._types[index] : undefined;124}125126public hasTypes() {127return !!this._types;128}129130public isCollapsed(index: number): boolean {131return this._collapseStates.get(index);132}133134public setCollapsed(index: number, newState: boolean) {135this._collapseStates.set(index, newState);136}137138private isUserDefined(index: number): boolean {139return this._userDefinedStates.get(index);140}141142private setUserDefined(index: number, newState: boolean) {143return this._userDefinedStates.set(index, newState);144}145146private isRecovered(index: number): boolean {147return this._recoveredStates.get(index);148}149150private setRecovered(index: number, newState: boolean) {151return this._recoveredStates.set(index, newState);152}153154public getSource(index: number): FoldSource {155if (this.isUserDefined(index)) {156return FoldSource.userDefined;157} else if (this.isRecovered(index)) {158return FoldSource.recovered;159}160return FoldSource.provider;161}162163public setSource(index: number, source: FoldSource): void {164if (source === FoldSource.userDefined) {165this.setUserDefined(index, true);166this.setRecovered(index, false);167} else if (source === FoldSource.recovered) {168this.setUserDefined(index, false);169this.setRecovered(index, true);170} else {171this.setUserDefined(index, false);172this.setRecovered(index, false);173}174}175176public setCollapsedAllOfType(type: string, newState: boolean) {177let hasChanged = false;178if (this._types) {179for (let i = 0; i < this._types.length; i++) {180if (this._types[i] === type) {181this.setCollapsed(i, newState);182hasChanged = true;183}184}185}186return hasChanged;187}188189public toRegion(index: number): FoldingRegion {190return new FoldingRegion(this, index);191}192193public getParentIndex(index: number) {194this.ensureParentIndices();195const parent = ((this._startIndexes[index] & MASK_INDENT) >>> 24) + ((this._endIndexes[index] & MASK_INDENT) >>> 16);196if (parent === MAX_FOLDING_REGIONS) {197return -1;198}199return parent;200}201202public contains(index: number, line: number) {203return this.getStartLineNumber(index) <= line && this.getEndLineNumber(index) >= line;204}205206private findIndex(line: number) {207let low = 0, high = this._startIndexes.length;208if (high === 0) {209return -1; // no children210}211while (low < high) {212const mid = Math.floor((low + high) / 2);213if (line < this.getStartLineNumber(mid)) {214high = mid;215} else {216low = mid + 1;217}218}219return low - 1;220}221222public findRange(line: number): number {223let index = this.findIndex(line);224if (index >= 0) {225const endLineNumber = this.getEndLineNumber(index);226if (endLineNumber >= line) {227return index;228}229index = this.getParentIndex(index);230while (index !== -1) {231if (this.contains(index, line)) {232return index;233}234index = this.getParentIndex(index);235}236}237return -1;238}239240241public toString() {242const res: string[] = [];243for (let i = 0; i < this.length; i++) {244res[i] = `[${foldSourceAbbr[this.getSource(i)]}${this.isCollapsed(i) ? '+' : '-'}] ${this.getStartLineNumber(i)}/${this.getEndLineNumber(i)}`;245}246return res.join(', ');247}248249public toFoldRange(index: number): FoldRange {250return {251startLineNumber: this._startIndexes[index] & MAX_LINE_NUMBER,252endLineNumber: this._endIndexes[index] & MAX_LINE_NUMBER,253type: this._types ? this._types[index] : undefined,254isCollapsed: this.isCollapsed(index),255source: this.getSource(index)256};257}258259public static fromFoldRanges(ranges: FoldRange[]): FoldingRegions {260const rangesLength = ranges.length;261const startIndexes = new Uint32Array(rangesLength);262const endIndexes = new Uint32Array(rangesLength);263let types: Array<string | undefined> | undefined = [];264let gotTypes = false;265for (let i = 0; i < rangesLength; i++) {266const range = ranges[i];267startIndexes[i] = range.startLineNumber;268endIndexes[i] = range.endLineNumber;269types.push(range.type);270if (range.type) {271gotTypes = true;272}273}274if (!gotTypes) {275types = undefined;276}277const regions = new FoldingRegions(startIndexes, endIndexes, types);278for (let i = 0; i < rangesLength; i++) {279if (ranges[i].isCollapsed) {280regions.setCollapsed(i, true);281}282regions.setSource(i, ranges[i].source);283}284return regions;285}286287/**288* Two inputs, each a FoldingRegions or a FoldRange[], are merged.289* Each input must be pre-sorted on startLineNumber.290* The first list is assumed to always include all regions currently defined by range providers.291* The second list only contains the previously collapsed and all manual ranges.292* If the line position matches, the range of the new range is taken, and the range is no longer manual293* When an entry in one list overlaps an entry in the other, the second list's entry "wins" and294* overlapping entries in the first list are discarded.295* Invalid entries are discarded. An entry is invalid if:296* the start and end line numbers aren't a valid range of line numbers,297* it is out of sequence or has the same start line as a preceding entry,298* it overlaps a preceding entry and is not fully contained by that entry.299*/300public static sanitizeAndMerge(301rangesA: FoldingRegions | FoldRange[],302rangesB: FoldingRegions | FoldRange[],303maxLineNumber: number | undefined,304selection?: SelectedLines305): FoldRange[] {306307maxLineNumber = maxLineNumber ?? Number.MAX_VALUE;308309const getIndexedFunction = (r: FoldingRegions | FoldRange[], limit: number) => {310return Array.isArray(r)311? ((i: number) => { return (i < limit) ? r[i] : undefined; })312: ((i: number) => { return (i < limit) ? r.toFoldRange(i) : undefined; });313};314const getA = getIndexedFunction(rangesA, rangesA.length);315const getB = getIndexedFunction(rangesB, rangesB.length);316let indexA = 0;317let indexB = 0;318let nextA = getA(0);319let nextB = getB(0);320321const stackedRanges: FoldRange[] = [];322let topStackedRange: FoldRange | undefined;323let prevLineNumber = 0;324const resultRanges: FoldRange[] = [];325326while (nextA || nextB) {327328let useRange: FoldRange | undefined = undefined;329if (nextB && (!nextA || nextA.startLineNumber >= nextB.startLineNumber)) {330if (nextA && nextA.startLineNumber === nextB.startLineNumber) {331if (nextB.source === FoldSource.userDefined) {332// a user defined range (possibly unfolded)333useRange = nextB;334} else {335// a previously folded range or a (possibly unfolded) recovered range336useRange = nextA;337// stays collapsed if the range still has the same number of lines or the selection is not in the range or after it338useRange.isCollapsed = nextB.isCollapsed && (nextA.endLineNumber === nextB.endLineNumber || !selection?.startsInside(nextA.startLineNumber + 1, nextA.endLineNumber + 1));339useRange.source = FoldSource.provider;340}341nextA = getA(++indexA); // not necessary, just for speed342} else {343useRange = nextB;344if (nextB.isCollapsed && nextB.source === FoldSource.provider) {345// a previously collapsed range346useRange.source = FoldSource.recovered;347}348}349nextB = getB(++indexB);350} else {351// nextA is next. The user folded B set takes precedence and we sometimes need to look352// ahead in it to check for an upcoming conflict.353let scanIndex = indexB;354let prescanB = nextB;355while (true) {356if (!prescanB || prescanB.startLineNumber > nextA!.endLineNumber) {357useRange = nextA;358break; // no conflict, use this nextA359}360if (prescanB.source === FoldSource.userDefined && prescanB.endLineNumber > nextA!.endLineNumber) {361// we found a user folded range, it wins362break; // without setting nextResult, so this nextA gets skipped363}364prescanB = getB(++scanIndex);365}366nextA = getA(++indexA);367}368369if (useRange) {370while (topStackedRange371&& topStackedRange.endLineNumber < useRange.startLineNumber) {372topStackedRange = stackedRanges.pop();373}374if (useRange.endLineNumber > useRange.startLineNumber375&& useRange.startLineNumber > prevLineNumber376&& useRange.endLineNumber <= maxLineNumber377&& (!topStackedRange378|| topStackedRange.endLineNumber >= useRange.endLineNumber)) {379resultRanges.push(useRange);380prevLineNumber = useRange.startLineNumber;381if (topStackedRange) {382stackedRanges.push(topStackedRange);383}384topStackedRange = useRange;385}386}387388}389return resultRanges;390}391392}393394export class FoldingRegion {395396constructor(private readonly ranges: FoldingRegions, private index: number) {397}398399public get startLineNumber() {400return this.ranges.getStartLineNumber(this.index);401}402403public get endLineNumber() {404return this.ranges.getEndLineNumber(this.index);405}406407public get regionIndex() {408return this.index;409}410411public get parentIndex() {412return this.ranges.getParentIndex(this.index);413}414415public get isCollapsed() {416return this.ranges.isCollapsed(this.index);417}418419containedBy(range: ILineRange): boolean {420return range.startLineNumber <= this.startLineNumber && range.endLineNumber >= this.endLineNumber;421}422containsLine(lineNumber: number) {423return this.startLineNumber <= lineNumber && lineNumber <= this.endLineNumber;424}425hidesLine(lineNumber: number) {426return this.startLineNumber < lineNumber && lineNumber <= this.endLineNumber;427}428}429430431