Path: blob/main/src/vs/editor/common/modelLineProjectionData.ts
3292 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 { assertNever } from '../../base/common/assert.js';6import { WrappingIndent } from './config/editorOptions.js';7import { FontInfo } from './config/fontInfo.js';8import { Position } from './core/position.js';9import { InjectedTextCursorStops, InjectedTextOptions, PositionAffinity } from './model.js';10import { LineInjectedText } from './textModelEvents.js';1112/**13* *input*:14* ```15* xxxxxxxxxxxxxxxxxxxxxxxxxxx16* ```17*18* -> Applying injections `[i...i]`, *inputWithInjections*:19* ```20* xxxxxx[iiiiiiiiii]xxxxxxxxxxxxxxxxx[ii]xxxx21* ```22*23* -> breaking at offsets `|` in `xxxxxx[iiiiiii|iii]xxxxxxxxxxx|xxxxxx[ii]xxxx|`:24* ```25* xxxxxx[iiiiiii26* iii]xxxxxxxxxxx27* xxxxxx[ii]xxxx28* ```29*30* -> applying wrappedTextIndentLength, *output*:31* ```32* xxxxxx[iiiiiii33* iii]xxxxxxxxxxx34* xxxxxx[ii]xxxx35* ```36*/37export class ModelLineProjectionData {38constructor(39public injectionOffsets: number[] | null,40/**41* `injectionOptions.length` must equal `injectionOffsets.length`42*/43public injectionOptions: InjectedTextOptions[] | null,44/**45* Refers to offsets after applying injections to the source.46* The last break offset indicates the length of the source after applying injections.47*/48public breakOffsets: number[],49/**50* Refers to offsets after applying injections51*/52public breakOffsetsVisibleColumn: number[],53public wrappedTextIndentLength: number54) {55}5657public getOutputLineCount(): number {58return this.breakOffsets.length;59}6061public getMinOutputOffset(outputLineIndex: number): number {62if (outputLineIndex > 0) {63return this.wrappedTextIndentLength;64}65return 0;66}6768public getLineLength(outputLineIndex: number): number {69// These offsets refer to model text with injected text.70const startOffset = outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0;71const endOffset = this.breakOffsets[outputLineIndex];7273let lineLength = endOffset - startOffset;74if (outputLineIndex > 0) {75lineLength += this.wrappedTextIndentLength;76}77return lineLength;78}7980public getMaxOutputOffset(outputLineIndex: number): number {81return this.getLineLength(outputLineIndex);82}8384public translateToInputOffset(outputLineIndex: number, outputOffset: number): number {85if (outputLineIndex > 0) {86outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength);87}8889const offsetInInputWithInjection = outputLineIndex === 0 ? outputOffset : this.breakOffsets[outputLineIndex - 1] + outputOffset;90let offsetInInput = offsetInInputWithInjection;9192if (this.injectionOffsets !== null) {93for (let i = 0; i < this.injectionOffsets.length; i++) {94if (offsetInInput > this.injectionOffsets[i]) {95if (offsetInInput < this.injectionOffsets[i] + this.injectionOptions![i].content.length) {96// `inputOffset` is within injected text97offsetInInput = this.injectionOffsets[i];98} else {99offsetInInput -= this.injectionOptions![i].content.length;100}101} else {102break;103}104}105}106107return offsetInInput;108}109110public translateToOutputPosition(inputOffset: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition {111let inputOffsetInInputWithInjection = inputOffset;112if (this.injectionOffsets !== null) {113for (let i = 0; i < this.injectionOffsets.length; i++) {114if (inputOffset < this.injectionOffsets[i]) {115break;116}117118if (affinity !== PositionAffinity.Right && inputOffset === this.injectionOffsets[i]) {119break;120}121122inputOffsetInInputWithInjection += this.injectionOptions![i].content.length;123}124}125126return this.offsetInInputWithInjectionsToOutputPosition(inputOffsetInInputWithInjection, affinity);127}128129private offsetInInputWithInjectionsToOutputPosition(offsetInInputWithInjections: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition {130let low = 0;131let high = this.breakOffsets.length - 1;132let mid = 0;133let midStart = 0;134135while (low <= high) {136mid = low + ((high - low) / 2) | 0;137138const midStop = this.breakOffsets[mid];139midStart = mid > 0 ? this.breakOffsets[mid - 1] : 0;140141if (affinity === PositionAffinity.Left) {142if (offsetInInputWithInjections <= midStart) {143high = mid - 1;144} else if (offsetInInputWithInjections > midStop) {145low = mid + 1;146} else {147break;148}149} else {150if (offsetInInputWithInjections < midStart) {151high = mid - 1;152} else if (offsetInInputWithInjections >= midStop) {153low = mid + 1;154} else {155break;156}157}158}159160let outputOffset = offsetInInputWithInjections - midStart;161if (mid > 0) {162outputOffset += this.wrappedTextIndentLength;163}164165return new OutputPosition(mid, outputOffset);166}167168public normalizeOutputPosition(outputLineIndex: number, outputOffset: number, affinity: PositionAffinity): OutputPosition {169if (this.injectionOffsets !== null) {170const offsetInInputWithInjections = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset);171const normalizedOffsetInUnwrappedLine = this.normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections, affinity);172if (normalizedOffsetInUnwrappedLine !== offsetInInputWithInjections) {173// injected text caused a change174return this.offsetInInputWithInjectionsToOutputPosition(normalizedOffsetInUnwrappedLine, affinity);175}176}177178if (affinity === PositionAffinity.Left) {179if (outputLineIndex > 0 && outputOffset === this.getMinOutputOffset(outputLineIndex)) {180return new OutputPosition(outputLineIndex - 1, this.getMaxOutputOffset(outputLineIndex - 1));181}182}183else if (affinity === PositionAffinity.Right) {184const maxOutputLineIndex = this.getOutputLineCount() - 1;185if (outputLineIndex < maxOutputLineIndex && outputOffset === this.getMaxOutputOffset(outputLineIndex)) {186return new OutputPosition(outputLineIndex + 1, this.getMinOutputOffset(outputLineIndex + 1));187}188}189190return new OutputPosition(outputLineIndex, outputOffset);191}192193private outputPositionToOffsetInInputWithInjections(outputLineIndex: number, outputOffset: number): number {194if (outputLineIndex > 0) {195outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength);196}197const result = (outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0) + outputOffset;198return result;199}200201private normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections: number, affinity: PositionAffinity): number {202const injectedText = this.getInjectedTextAtOffset(offsetInInputWithInjections);203if (!injectedText) {204return offsetInInputWithInjections;205}206207if (affinity === PositionAffinity.None) {208if (offsetInInputWithInjections === injectedText.offsetInInputWithInjections + injectedText.length209&& hasRightCursorStop(this.injectionOptions![injectedText.injectedTextIndex].cursorStops)) {210return injectedText.offsetInInputWithInjections + injectedText.length;211} else {212let result = injectedText.offsetInInputWithInjections;213if (hasLeftCursorStop(this.injectionOptions![injectedText.injectedTextIndex].cursorStops)) {214return result;215}216217let index = injectedText.injectedTextIndex - 1;218while (index >= 0 && this.injectionOffsets![index] === this.injectionOffsets![injectedText.injectedTextIndex]) {219if (hasRightCursorStop(this.injectionOptions![index].cursorStops)) {220break;221}222result -= this.injectionOptions![index].content.length;223if (hasLeftCursorStop(this.injectionOptions![index].cursorStops)) {224break;225}226index--;227}228229return result;230}231} else if (affinity === PositionAffinity.Right || affinity === PositionAffinity.RightOfInjectedText) {232let result = injectedText.offsetInInputWithInjections + injectedText.length;233let index = injectedText.injectedTextIndex;234// traverse all injected text that touch each other235while (index + 1 < this.injectionOffsets!.length && this.injectionOffsets![index + 1] === this.injectionOffsets![index]) {236result += this.injectionOptions![index + 1].content.length;237index++;238}239return result;240} else if (affinity === PositionAffinity.Left || affinity === PositionAffinity.LeftOfInjectedText) {241// affinity is left242let result = injectedText.offsetInInputWithInjections;243let index = injectedText.injectedTextIndex;244// traverse all injected text that touch each other245while (index - 1 >= 0 && this.injectionOffsets![index - 1] === this.injectionOffsets![index]) {246result -= this.injectionOptions![index - 1].content.length;247index--;248}249return result;250}251252assertNever(affinity);253}254255public getInjectedText(outputLineIndex: number, outputOffset: number): InjectedText | null {256const offset = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset);257const injectedText = this.getInjectedTextAtOffset(offset);258if (!injectedText) {259return null;260}261return {262options: this.injectionOptions![injectedText.injectedTextIndex]263};264}265266private getInjectedTextAtOffset(offsetInInputWithInjections: number): { injectedTextIndex: number; offsetInInputWithInjections: number; length: number } | undefined {267const injectionOffsets = this.injectionOffsets;268const injectionOptions = this.injectionOptions;269270if (injectionOffsets !== null) {271let totalInjectedTextLengthBefore = 0;272for (let i = 0; i < injectionOffsets.length; i++) {273const length = injectionOptions![i].content.length;274const injectedTextStartOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore;275const injectedTextEndOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore + length;276277if (injectedTextStartOffsetInInputWithInjections > offsetInInputWithInjections) {278// Injected text starts later.279break; // All later injected texts have an even larger offset.280}281282if (offsetInInputWithInjections <= injectedTextEndOffsetInInputWithInjections) {283// Injected text ends after or with the given position (but also starts with or before it).284return {285injectedTextIndex: i,286offsetInInputWithInjections: injectedTextStartOffsetInInputWithInjections,287length288};289}290291totalInjectedTextLengthBefore += length;292}293}294295return undefined;296}297}298299function hasRightCursorStop(cursorStop: InjectedTextCursorStops | null | undefined): boolean {300if (cursorStop === null || cursorStop === undefined) { return true; }301return cursorStop === InjectedTextCursorStops.Right || cursorStop === InjectedTextCursorStops.Both;302}303function hasLeftCursorStop(cursorStop: InjectedTextCursorStops | null | undefined): boolean {304if (cursorStop === null || cursorStop === undefined) { return true; }305return cursorStop === InjectedTextCursorStops.Left || cursorStop === InjectedTextCursorStops.Both;306}307308export class InjectedText {309constructor(public readonly options: InjectedTextOptions) { }310}311312export class OutputPosition {313outputLineIndex: number;314outputOffset: number;315316constructor(outputLineIndex: number, outputOffset: number) {317this.outputLineIndex = outputLineIndex;318this.outputOffset = outputOffset;319}320321toString(): string {322return `${this.outputLineIndex}:${this.outputOffset}`;323}324325toPosition(baseLineNumber: number): Position {326return new Position(baseLineNumber + this.outputLineIndex, this.outputOffset + 1);327}328}329330export interface ILineBreaksComputerFactory {331createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer;332}333334export interface ILineBreaksComputer {335/**336* Pass in `previousLineBreakData` if the only difference is in breaking columns!!!337*/338addRequest(lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null): void;339finalize(): (ModelLineProjectionData | null)[];340}341342343