Path: blob/main/extensions/copilot/src/platform/parser/node/indentationStructure.ts
13401 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 type * as vscode from 'vscode';6import { CharCode } from '../../../util/vs/base/common/charCode';7import { BugIndicatingError } from '../../../util/vs/base/common/errors';8import { Position } from '../../../vscodeTypes';9import { AbstractDocument } from '../../editing/common/abstractText';10import { OverlayNode } from './nodes';1112export function getStructureUsingIndentation(13document: AbstractDocument,14languageId: string,15formattingOptions: vscode.FormattingOptions | undefined16): OverlayNode {17const lines = document.getText().split(/\r\n|\r|\n/g);18const opts = formattingOptions || { tabSize: 4 };19const simpleModel = {20getLineCount: () => lines.length,21getLineContent: (lineNumber: number) => lines[lineNumber - 1],22getOptions: () => opts23} satisfies ISimpleTextModel;2425try {26const regions = generateFoldingRegions(simpleModel, languageId);27const [foldingRanges,] = createFoldingRangeTree(document, regions, undefined);28foldingRanges.adjust(document, isOffSide(languageId));29return foldingRanges.toOverlayNode(document, true);30} catch (err) {31const foldingRanges = new FoldingRangeNode(1, document.getLineCount(), []);32return foldingRanges.toOverlayNode(document, true);33}34}3536function createFoldingRangeTree(doc: AbstractDocument, regions: FoldingRegions, regionIndex: number | undefined): [FoldingRangeNode, number] {37if (typeof regionIndex !== 'undefined' && regionIndex >= regions.length) {38throw new Error(`Invalid region index ${regionIndex}`);39}4041const regionStartLineNumber = (typeof regionIndex === 'undefined' ? 1 : regions.getStartLineNumber(regionIndex));42const regionEndLineNumber = (typeof regionIndex === 'undefined' ? doc.getLineCount() : regions.getEndLineNumber(regionIndex));43const children: FoldingRangeNode[] = [];44let childNode: FoldingRangeNode | null = null;4546regionIndex = (typeof regionIndex === 'undefined' ? 0 : regionIndex + 1);47while (regionIndex < regions.length) {48const startLineNumber = regions.getStartLineNumber(regionIndex);49const endLineNumber = regions.getEndLineNumber(regionIndex);5051if (startLineNumber > regionEndLineNumber || endLineNumber > regionEndLineNumber) {52// We are done with children of this region53break;54}5556const prevChildNode = childNode;57[childNode, regionIndex] = createFoldingRangeTree(doc, regions, regionIndex);58if (prevChildNode && childNode.startLineNumber <= prevChildNode.endLineNumber) {59throw new BugIndicatingError('Invalid Folding Ranges: overlapping children');60}61if (childNode.startLineNumber < regionStartLineNumber) {62throw new BugIndicatingError('Invalid Folding Ranges: child starts before parent');63}64children.push(childNode);65}6667return [68new FoldingRangeNode(regionStartLineNumber, regionEndLineNumber, children),69regionIndex,70];71}7273class FoldingRangeNode {7475constructor(76public startLineNumber: number,77public endLineNumber: number,78readonly children: FoldingRangeNode[]79) {80if (startLineNumber > endLineNumber) {81throw new BugIndicatingError('Invalid Folding Ranges: startLineNumber > endLineNumber');82}83}8485public adjust(document: AbstractDocument, isOffside: boolean): void {86if (isOffside) {87this._adjustOffside();88} else {89this._adjustRegular(document, document.getLineCount());90}91}9293private _adjustOffside(): void {94this.startLineNumber++;95for (const child of this.children) {96child._adjustOffside();97}98}99100private _adjustRegular(document: AbstractDocument, maxEndLineNumber: number): void {101if (this.endLineNumber < maxEndLineNumber) {102const nextLine = document.getLineText(this.endLineNumber).trim();103const isClosingBracket = /^[\}\]\)];?$/.test(nextLine);104const isClosingTag = /^<\/\w/.test(nextLine);105if (isClosingBracket || isClosingTag) {106this.endLineNumber++;107}108}109110for (let i = this.children.length - 1; i >= 0; i--) {111const child = this.children[i];112const childMaxEndLineNumber = (i + 1 < this.children.length ? this.children[i + 1].startLineNumber - 1 : maxEndLineNumber);113child._adjustRegular(document, childMaxEndLineNumber);114}115}116117toOverlayNode(document: AbstractDocument, isRoot: boolean): OverlayNode {118const children: OverlayNode[] = [];119let nextLineNumber = (isRoot && this.startLineNumber === 1 ? 1 : this.startLineNumber + 1);120121// for (let lineNumber = this.startLineNumber + 1;)122for (const child of this.children) {123124// Generate a node for each skipped line125for (let lineNumber = nextLineNumber; lineNumber < child.startLineNumber; lineNumber++) {126const node = createOverlayNode(document, lineNumber, lineNumber, 'LINE', []);127if (node) {128children.push(node);129}130}131132children.push(child.toOverlayNode(document, false));133134nextLineNumber = child.endLineNumber + 1;135}136137// Generate a node for each skipped line138for (let lineNumber = nextLineNumber; lineNumber < this.endLineNumber; lineNumber++) {139const node = createOverlayNode(document, lineNumber, lineNumber, 'LINE', []);140if (node) {141children.push(node);142}143}144145return createOverlayNode(document, this.startLineNumber, this.endLineNumber, 'FOLD', children);146}147}148149function createOverlayNode(doc: AbstractDocument, startLineNumber: number, endLineNumber: number, kind: string, children: OverlayNode[]): OverlayNode {150const startOffset = doc.getOffsetAtPosition(new Position(startLineNumber - 1, 0));151const endPosition = (152endLineNumber < doc.getLineCount()153? new Position(endLineNumber, 0)154: new Position(endLineNumber - 1, doc.getLineLength(endLineNumber - 1))155);156const endOffset = doc.getOffsetAtPosition(endPosition);157return new OverlayNode(158startOffset,159endOffset,160kind,161children162);163}164165166export interface ISimpleTextModel {167getLineCount(): number;168getLineContent(lineNumber: number): string;169getOptions(): { tabSize: number };170}171172function generateFoldingRegions(model: ISimpleTextModel, languageId: string): FoldingRegions {173return _computeRanges(model, isOffSide(languageId));174}175176function isOffSide(languageId: string): boolean {177return ['clojure', 'coffeescript', 'fsharp', 'latex', 'markdown', 'pug', 'python', 'sql', 'yaml'].includes(languageId);178}179180function _computeRanges(model: ISimpleTextModel, offSide: boolean): FoldingRegions {181const tabSize = model.getOptions().tabSize;182const result = new RangesCollector();183184const previousRegions: PreviousRegion[] = [];185const line = model.getLineCount() + 1;186previousRegions.push({ indent: -1, endAbove: line, line }); // sentinel, to make sure there's at least one entry187188for (let line = model.getLineCount(); line > 0; line--) {189const lineContent = model.getLineContent(line);190const indent = computeIndentLevel(lineContent, tabSize);191let previous = previousRegions[previousRegions.length - 1];192if (indent === -1) {193if (offSide) {194// for offSide languages, empty lines are associated to the previous block195// note: the next block is already written to the results, so this only196// impacts the end position of the block before197previous.endAbove = line;198}199continue; // only whitespace200}201if (previous.indent > indent) {202// discard all regions with larger indent203do {204previousRegions.pop();205previous = previousRegions[previousRegions.length - 1];206} while (previous.indent > indent);207208// new folding range209const endLineNumber = previous.endAbove - 1;210if (endLineNumber - line >= 1) { // needs at east size 1211result.insertFirst(line, endLineNumber, indent);212}213}214if (previous.indent === indent) {215previous.endAbove = line;216} else { // previous.indent < indent217// new region with a bigger indent218previousRegions.push({ indent, endAbove: line, line });219}220}221return result.toIndentRanges();222}223224interface PreviousRegion {225indent: number; // indent or -2 if a marker226endAbove: number; // end line number for the region above227line: number; // start line of the region. Only used for marker regions.228}229230const MAX_FOLDING_REGIONS = 0xFFFF;231const MAX_LINE_NUMBER = 0xFFFFFF;232const MASK_INDENT = 0xFF000000;233234class RangesCollector {235private readonly _startIndexes: number[];236private readonly _endIndexes: number[];237private readonly _indentOccurrences: number[];238private _length: number;239240constructor() {241this._startIndexes = [];242this._endIndexes = [];243this._indentOccurrences = [];244this._length = 0;245}246247public insertFirst(startLineNumber: number, endLineNumber: number, indent: number) {248if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) {249return;250}251const index = this._length;252this._startIndexes[index] = startLineNumber;253this._endIndexes[index] = endLineNumber;254this._length++;255if (indent < 1000) {256this._indentOccurrences[indent] = (this._indentOccurrences[indent] || 0) + 1;257}258}259260public toIndentRanges() {261// reverse and create arrays of the exact length262const startIndexes = new Uint32Array(this._length);263const endIndexes = new Uint32Array(this._length);264for (let i = this._length - 1, k = 0; i >= 0; i--, k++) {265startIndexes[k] = this._startIndexes[i];266endIndexes[k] = this._endIndexes[i];267}268return new FoldingRegions(startIndexes, endIndexes);269}270}271272/**273* Returns:274* - -1 => the line consists of whitespace275* - otherwise => the indent level is returned value276*/277function computeIndentLevel(line: string, tabSize: number): number {278let indent = 0;279let i = 0;280const len = line.length;281282while (i < len) {283const chCode = line.charCodeAt(i);284if (chCode === CharCode.Space) {285indent++;286} else if (chCode === CharCode.Tab) {287indent = indent - indent % tabSize + tabSize;288} else {289break;290}291i++;292}293294if (i === len) {295return -1; // line only consists of whitespace296}297298return indent;299}300301class FoldingRegions {302private readonly _startIndexes: Uint32Array;303private readonly _endIndexes: Uint32Array;304305private _parentsComputed: boolean;306307constructor(startIndexes: Uint32Array, endIndexes: Uint32Array) {308this._startIndexes = startIndexes;309this._endIndexes = endIndexes;310this._parentsComputed = false;311// this.ensureParentIndices();312}313314private ensureParentIndices() {315if (!this._parentsComputed) {316this._parentsComputed = true;317const parentIndexes: number[] = [];318const isInsideLast = (startLineNumber: number, endLineNumber: number) => {319const index = parentIndexes[parentIndexes.length - 1];320return this.getStartLineNumber(index) <= startLineNumber && this.getEndLineNumber(index) >= endLineNumber;321};322for (let i = 0, len = this._startIndexes.length; i < len; i++) {323const startLineNumber = this._startIndexes[i];324const endLineNumber = this._endIndexes[i];325if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) {326throw new Error('startLineNumber or endLineNumber must not exceed ' + MAX_LINE_NUMBER);327}328while (parentIndexes.length > 0 && !isInsideLast(startLineNumber, endLineNumber)) {329parentIndexes.pop();330}331const parentIndex = parentIndexes.length > 0 ? parentIndexes[parentIndexes.length - 1] : -1;332parentIndexes.push(i);333this._startIndexes[i] = startLineNumber + ((parentIndex & 0xFF) << 24);334this._endIndexes[i] = endLineNumber + ((parentIndex & 0xFF00) << 16);335}336}337}338339public get length(): number {340return this._startIndexes.length;341}342343public getStartLineNumber(index: number): number {344return this._startIndexes[index] & MAX_LINE_NUMBER;345}346347public getEndLineNumber(index: number): number {348return this._endIndexes[index] & MAX_LINE_NUMBER;349}350351public getParentIndex(index: number) {352this.ensureParentIndices();353const parent = ((this._startIndexes[index] & MASK_INDENT) >>> 24) + ((this._endIndexes[index] & MASK_INDENT) >>> 16);354if (parent === MAX_FOLDING_REGIONS) {355return -1;356}357return parent;358}359360public contains(index: number, line: number) {361return this.getStartLineNumber(index) <= line && this.getEndLineNumber(index) >= line;362}363364private findIndex(line: number) {365let low = 0, high = this._startIndexes.length;366if (high === 0) {367return -1; // no children368}369while (low < high) {370const mid = Math.floor((low + high) / 2);371if (line < this.getStartLineNumber(mid)) {372high = mid;373} else {374low = mid + 1;375}376}377return low - 1;378}379380public findRange(line: number): number {381let index = this.findIndex(line);382if (index >= 0) {383const endLineNumber = this.getEndLineNumber(index);384if (endLineNumber >= line) {385return index;386}387index = this.getParentIndex(index);388while (index !== -1) {389if (this.contains(index, line)) {390return index;391}392index = this.getParentIndex(index);393}394}395return -1;396}397}398399400