diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index be1f71119d3..e6dc5ca034a 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -14,11 +14,12 @@ import { Position } from 'vs/editor/common/core/position'; import { Range as EditorRange } from 'vs/editor/common/core/range'; import { HorizontalPosition } from 'vs/editor/common/view/renderingContext'; import { ViewContext } from 'vs/editor/common/view/viewContext'; -import { InjectedText, IViewModel } from 'vs/editor/common/viewModel/viewModel'; +import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; import * as dom from 'vs/base/browser/dom'; import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/controller/cursorAtomicMoveOperations'; import { PositionAffinity } from 'vs/editor/common/model'; +import { InjectedText } from 'vs/editor/common/viewModel/modelLineProjectionData'; export interface IViewZoneData { viewZoneId: string; diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 3e1c460a4b6..68b9aab8b09 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -9,9 +9,9 @@ import { createStringBuilder, IStringBuilder } from 'vs/editor/common/core/strin import { CharCode } from 'vs/base/common/charCode'; import * as strings from 'vs/base/common/strings'; import { Configuration } from 'vs/editor/browser/config/configuration'; -import { ILineBreaksComputer, ILineBreaksComputerFactory, ModelLineProjectionData } from 'vs/editor/common/viewModel/viewModel'; import { LineInjectedText } from 'vs/editor/common/model/textModelEvents'; import { InjectedTextOptions } from 'vs/editor/common/model'; +import { ILineBreaksComputer, ILineBreaksComputerFactory, ModelLineProjectionData } from 'vs/editor/common/viewModel/modelLineProjectionData'; const ttPolicy = window.trustedTypes?.createPolicy('domLineBreaksComputer', { createHTML: value => value }); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 666b4beb2f1..61c4755ee6a 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -32,7 +32,7 @@ import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations'; import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/linesLayout'; -import { ILineBreaksComputer, InlineDecoration, InlineDecorationType, IViewModel, ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; +import { InlineDecoration, InlineDecorationType, IViewModel, ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -52,6 +52,7 @@ import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { ILineBreaksComputer } from 'vs/editor/common/viewModel/modelLineProjectionData'; export interface IDiffCodeEditorWidgetOptions { originalEditor?: ICodeEditorWidgetOptions; diff --git a/src/vs/editor/common/viewModel/modelLineProjection.ts b/src/vs/editor/common/viewModel/modelLineProjection.ts index 5a1398b762c..390fb638bbb 100644 --- a/src/vs/editor/common/viewModel/modelLineProjection.ts +++ b/src/vs/editor/common/viewModel/modelLineProjection.ts @@ -8,7 +8,8 @@ import { Position } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { EndOfLinePreference, ITextModel, PositionAffinity } from 'vs/editor/common/model'; import { LineInjectedText } from 'vs/editor/common/model/textModelEvents'; -import { InjectedText, ModelLineProjectionData, SingleLineInlineDecoration, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; +import { InjectedText, ModelLineProjectionData } from 'vs/editor/common/viewModel/modelLineProjectionData'; +import { SingleLineInlineDecoration, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; export interface IModelLineProjection { isVisible(): boolean; diff --git a/src/vs/editor/common/viewModel/modelLineProjectionData.ts b/src/vs/editor/common/viewModel/modelLineProjectionData.ts new file mode 100644 index 00000000000..a786ebfbff0 --- /dev/null +++ b/src/vs/editor/common/viewModel/modelLineProjectionData.ts @@ -0,0 +1,316 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; +import { FontInfo } from 'vs/editor/common/config/fontInfo'; +import { Position } from 'vs/editor/common/core/position'; +import { InjectedTextOptions, PositionAffinity } from 'vs/editor/common/model'; +import { LineInjectedText } from 'vs/editor/common/model/textModelEvents'; + +/** + * *input*: + * ``` + * xxxxxxxxxxxxxxxxxxxxxxxxxxx + * ``` + * + * -> Applying injections `[i...i]`, *inputWithInjections*: + * ``` + * xxxxxx[iiiiiiiiii]xxxxxxxxxxxxxxxxx[ii]xxxx + * ``` + * + * -> breaking at offsets `|` in `xxxxxx[iiiiiii|iii]xxxxxxxxxxx|xxxxxx[ii]xxxx|`: + * ``` + * xxxxxx[iiiiiii + * iii]xxxxxxxxxxx + * xxxxxx[ii]xxxx + * ``` + * + * -> applying wrappedTextIndentLength, *output*: + * ``` + * xxxxxx[iiiiiii + * iii]xxxxxxxxxxx + * xxxxxx[ii]xxxx + * ``` + */ +export class ModelLineProjectionData { + constructor( + public injectionOffsets: number[] | null, + /** + * `injectionOptions.length` must equal `injectionOffsets.length` + */ + public injectionOptions: InjectedTextOptions[] | null, + /** + * Refers to offsets after applying injections to the source. + * The last break offset indicates the length of the source after applying injections. + */ + public breakOffsets: number[], + /** + * Refers to offsets after applying injections + */ + public breakOffsetsVisibleColumn: number[], + public wrappedTextIndentLength: number + ) { + } + + public getOutputLineCount(): number { + return this.breakOffsets.length; + } + + public getMinOutputOffset(outputLineIndex: number): number { + if (outputLineIndex > 0) { + return this.wrappedTextIndentLength; + } + return 0; + } + + public getLineLength(outputLineIndex: number): number { + // These offsets refer to model text with injected text. + const startOffset = outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0; + const endOffset = this.breakOffsets[outputLineIndex]; + + let lineLength = endOffset - startOffset; + if (outputLineIndex > 0) { + lineLength += this.wrappedTextIndentLength; + } + return lineLength; + } + + public getMaxOutputOffset(outputLineIndex: number): number { + return this.getLineLength(outputLineIndex); + } + + public translateToInputOffset(outputLineIndex: number, outputOffset: number): number { + if (outputLineIndex > 0) { + outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength); + } + + const offsetInInputWithInjection = outputLineIndex === 0 ? outputOffset : this.breakOffsets[outputLineIndex - 1] + outputOffset; + let offsetInInput = offsetInInputWithInjection; + + if (this.injectionOffsets !== null) { + for (let i = 0; i < this.injectionOffsets.length; i++) { + if (offsetInInput > this.injectionOffsets[i]) { + if (offsetInInput < this.injectionOffsets[i] + this.injectionOptions![i].content.length) { + // `inputOffset` is within injected text + offsetInInput = this.injectionOffsets[i]; + } else { + offsetInInput -= this.injectionOptions![i].content.length; + } + } else { + break; + } + } + } + + return offsetInInput; + } + + public translateToOutputPosition(inputOffset: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition { + let inputOffsetInInputWithInjection = inputOffset; + if (this.injectionOffsets !== null) { + for (let i = 0; i < this.injectionOffsets.length; i++) { + if (inputOffset < this.injectionOffsets[i]) { + break; + } + + if (affinity !== PositionAffinity.Right && inputOffset === this.injectionOffsets[i]) { + break; + } + + inputOffsetInInputWithInjection += this.injectionOptions![i].content.length; + } + } + + return this.offsetInInputWithInjectionsToOutputPosition(inputOffsetInInputWithInjection, affinity); + } + + private offsetInInputWithInjectionsToOutputPosition(offsetInInputWithInjections: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition { + let low = 0; + let high = this.breakOffsets.length - 1; + let mid = 0; + let midStart = 0; + + while (low <= high) { + mid = low + ((high - low) / 2) | 0; + + const midStop = this.breakOffsets[mid]; + midStart = mid > 0 ? this.breakOffsets[mid - 1] : 0; + + if (affinity === PositionAffinity.Left) { + if (offsetInInputWithInjections <= midStart) { + high = mid - 1; + } else if (offsetInInputWithInjections > midStop) { + low = mid + 1; + } else { + break; + } + } else { + if (offsetInInputWithInjections < midStart) { + high = mid - 1; + } else if (offsetInInputWithInjections >= midStop) { + low = mid + 1; + } else { + break; + } + } + } + + let outputOffset = offsetInInputWithInjections - midStart; + if (mid > 0) { + outputOffset += this.wrappedTextIndentLength; + } + + return new OutputPosition(mid, outputOffset); + } + + public normalizeOutputPosition(outputLineIndex: number, outputOffset: number, affinity: PositionAffinity): OutputPosition { + if (this.injectionOffsets !== null) { + const offsetInInputWithInjections = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset); + const normalizedOffsetInUnwrappedLine = this.normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections, affinity); + if (normalizedOffsetInUnwrappedLine !== offsetInInputWithInjections) { + // injected text caused a change + return this.offsetInInputWithInjectionsToOutputPosition(normalizedOffsetInUnwrappedLine, affinity); + } + } + + if (affinity === PositionAffinity.Left) { + if (outputLineIndex > 0 && outputOffset === this.getMinOutputOffset(outputLineIndex)) { + return new OutputPosition(outputLineIndex - 1, this.getMaxOutputOffset(outputLineIndex - 1)); + } + } + else if (affinity === PositionAffinity.Right) { + const maxOutputLineIndex = this.getOutputLineCount() - 1; + if (outputLineIndex < maxOutputLineIndex && outputOffset === this.getMaxOutputOffset(outputLineIndex)) { + return new OutputPosition(outputLineIndex + 1, this.getMinOutputOffset(outputLineIndex + 1)); + } + } + + return new OutputPosition(outputLineIndex, outputOffset); + } + + private outputPositionToOffsetInInputWithInjections(outputLineIndex: number, outputOffset: number): number { + if (outputLineIndex > 0) { + outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength); + } + const result = (outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0) + outputOffset; + return result; + } + + private normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections: number, affinity: PositionAffinity): number { + const injectedText = this.getInjectedTextAtOffset(offsetInInputWithInjections); + if (!injectedText) { + return offsetInInputWithInjections; + } + + if (affinity === PositionAffinity.None) { + if (offsetInInputWithInjections === injectedText.offsetInInputWithInjections + injectedText.length) { + // go to the end of this injected text + return injectedText.offsetInInputWithInjections + injectedText.length; + } else { + // go to the start of this injected text + return injectedText.offsetInInputWithInjections; + } + } + + if (affinity === PositionAffinity.Right) { + let result = injectedText.offsetInInputWithInjections + injectedText.length; + let index = injectedText.injectedTextIndex; + // traverse all injected text that touch each other + while (index + 1 < this.injectionOffsets!.length && this.injectionOffsets![index + 1] === this.injectionOffsets![index]) { + result += this.injectionOptions![index + 1].content.length; + index++; + } + return result; + } + + // affinity is left + let result = injectedText.offsetInInputWithInjections; + let index = injectedText.injectedTextIndex; + // traverse all injected text that touch each other + while (index - 1 >= 0 && this.injectionOffsets![index - 1] === this.injectionOffsets![index]) { + result -= this.injectionOptions![index - 1].content.length; + index++; + } + return result; + } + + public getInjectedText(outputLineIndex: number, outputOffset: number): InjectedText | null { + const offset = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset); + const injectedText = this.getInjectedTextAtOffset(offset); + if (!injectedText) { + return null; + } + return { + options: this.injectionOptions![injectedText.injectedTextIndex] + }; + } + + private getInjectedTextAtOffset(offsetInInputWithInjections: number): { injectedTextIndex: number, offsetInInputWithInjections: number, length: number } | undefined { + const injectionOffsets = this.injectionOffsets; + const injectionOptions = this.injectionOptions; + + if (injectionOffsets !== null) { + let totalInjectedTextLengthBefore = 0; + for (let i = 0; i < injectionOffsets.length; i++) { + const length = injectionOptions![i].content.length; + const injectedTextStartOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore; + const injectedTextEndOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore + length; + + if (injectedTextStartOffsetInInputWithInjections > offsetInInputWithInjections) { + // Injected text starts later. + break; // All later injected texts have an even larger offset. + } + + if (offsetInInputWithInjections <= injectedTextEndOffsetInInputWithInjections) { + // Injected text ends after or with the given position (but also starts with or before it). + return { + injectedTextIndex: i, + offsetInInputWithInjections: injectedTextStartOffsetInInputWithInjections, + length + }; + } + + totalInjectedTextLengthBefore += length; + } + } + + return undefined; + } +} + +export class InjectedText { + constructor(public readonly options: InjectedTextOptions) { } +} + +export class OutputPosition { + outputLineIndex: number; + outputOffset: number; + + constructor(outputLineIndex: number, outputOffset: number) { + this.outputLineIndex = outputLineIndex; + this.outputOffset = outputOffset; + } + + toString(): string { + return `${this.outputLineIndex}:${this.outputOffset}`; + } + + toPosition(baseLineNumber: number): Position { + return new Position(baseLineNumber + this.outputLineIndex, this.outputOffset + 1); + } +} + +export interface ILineBreaksComputerFactory { + createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent): ILineBreaksComputer; +} + +export interface ILineBreaksComputer { + /** + * Pass in `previousLineBreakData` if the only difference is in breaking columns!!! + */ + addRequest(lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null): void; + finalize(): (ModelLineProjectionData | null)[]; +} diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 057d25bb6c2..dd6d1437b0c 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -8,9 +8,9 @@ import * as strings from 'vs/base/common/strings'; import { WrappingIndent, IComputedEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; -import { ILineBreaksComputer, ILineBreaksComputerFactory, ModelLineProjectionData } from 'vs/editor/common/viewModel/viewModel'; import { LineInjectedText } from 'vs/editor/common/model/textModelEvents'; import { InjectedTextOptions } from 'vs/editor/common/model'; +import { ILineBreaksComputerFactory, ILineBreaksComputer, ModelLineProjectionData } from 'vs/editor/common/viewModel/modelLineProjectionData'; const enum CharacterClass { NONE = 0, diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index b705c57538f..f11a0f62ea5 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -5,21 +5,19 @@ import { IScrollPosition, Scrollable } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; +import { CursorConfiguration, CursorState, EditOperationType, IColumnSelectData, ICursorSimpleModel, PartialCursorState } from 'vs/editor/common/controller/cursorCommon'; +import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { INewScrollPosition, ScrollType } from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions, ITextModel, InjectedTextOptions, PositionAffinity, IndentGuide, BracketGuideOptions } from 'vs/editor/common/model'; -import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; -import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; -import { IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; +import { BracketGuideOptions, EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, IndentGuide, ITextModel, PositionAffinity, TextModelResolvedOptions } from 'vs/editor/common/model'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; -import { ICursorSimpleModel, PartialCursorState, CursorState, IColumnSelectData, EditOperationType, CursorConfiguration } from 'vs/editor/common/controller/cursorCommon'; -import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; +import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; +import { IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; +import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; +import { ILineBreaksComputer, InjectedText } from 'vs/editor/common/viewModel/modelLineProjectionData'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; -import { LineInjectedText } from 'vs/editor/common/model/textModelEvents'; -import { FontInfo } from 'vs/editor/common/config/fontInfo'; -import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; export interface IViewWhitespaceViewportData { readonly id: string; @@ -94,307 +92,6 @@ export interface ICoordinatesConverter { getViewLineNumberOfModelPosition(modelLineNumber: number, modelColumn: number): number; } -export class OutputPosition { - outputLineIndex: number; - outputOffset: number; - - constructor(outputLineIndex: number, outputOffset: number) { - this.outputLineIndex = outputLineIndex; - this.outputOffset = outputOffset; - } - - toString(): string { - return `${this.outputLineIndex}:${this.outputOffset}`; - } - - toPosition(baseLineNumber: number): Position { - return new Position(baseLineNumber + this.outputLineIndex, this.outputOffset + 1); - } -} - -/** - * *input*: - * ``` - * xxxxxxxxxxxxxxxxxxxxxxxxxxx - * ``` - * - * -> Applying injections `[i...i]`, *inputWithInjections*: - * ``` - * xxxxxx[iiiiiiiiii]xxxxxxxxxxxxxxxxx[ii]xxxx - * ``` - * - * -> breaking at offsets `|` in `xxxxxx[iiiiiii|iii]xxxxxxxxxxx|xxxxxx[ii]xxxx|`: - * ``` - * xxxxxx[iiiiiii - * iii]xxxxxxxxxxx - * xxxxxx[ii]xxxx - * ``` - * - * -> applying wrappedTextIndentLength, *output*: - * ``` - * xxxxxx[iiiiiii - * iii]xxxxxxxxxxx - * xxxxxx[ii]xxxx - * ``` - */ -export class ModelLineProjectionData { - constructor( - public injectionOffsets: number[] | null, - /** - * `injectionOptions.length` must equal `injectionOffsets.length` - */ - public injectionOptions: InjectedTextOptions[] | null, - /** - * Refers to offsets after applying injections to the source. - * The last break offset indicates the length of the source after applying injections. - */ - public breakOffsets: number[], - /** - * Refers to offsets after applying injections - */ - public breakOffsetsVisibleColumn: number[], - public wrappedTextIndentLength: number - ) { - } - - public getOutputLineCount(): number { - return this.breakOffsets.length; - } - - public getMinOutputOffset(outputLineIndex: number): number { - if (outputLineIndex > 0) { - return this.wrappedTextIndentLength; - } - return 0; - } - - public getLineLength(outputLineIndex: number): number { - // These offsets refer to model text with injected text. - const startOffset = outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0; - const endOffset = this.breakOffsets[outputLineIndex]; - - let lineLength = endOffset - startOffset; - if (outputLineIndex > 0) { - lineLength += this.wrappedTextIndentLength; - } - return lineLength; - } - - public getMaxOutputOffset(outputLineIndex: number): number { - return this.getLineLength(outputLineIndex); - } - - public translateToInputOffset(outputLineIndex: number, outputOffset: number): number { - if (outputLineIndex > 0) { - outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength); - } - - const offsetInInputWithInjection = outputLineIndex === 0 ? outputOffset : this.breakOffsets[outputLineIndex - 1] + outputOffset; - let offsetInInput = offsetInInputWithInjection; - - if (this.injectionOffsets !== null) { - for (let i = 0; i < this.injectionOffsets.length; i++) { - if (offsetInInput > this.injectionOffsets[i]) { - if (offsetInInput < this.injectionOffsets[i] + this.injectionOptions![i].content.length) { - // `inputOffset` is within injected text - offsetInInput = this.injectionOffsets[i]; - } else { - offsetInInput -= this.injectionOptions![i].content.length; - } - } else { - break; - } - } - } - - return offsetInInput; - } - - public translateToOutputPosition(inputOffset: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition { - let inputOffsetInInputWithInjection = inputOffset; - if (this.injectionOffsets !== null) { - for (let i = 0; i < this.injectionOffsets.length; i++) { - if (inputOffset < this.injectionOffsets[i]) { - break; - } - - if (affinity !== PositionAffinity.Right && inputOffset === this.injectionOffsets[i]) { - break; - } - - inputOffsetInInputWithInjection += this.injectionOptions![i].content.length; - } - } - - return this.offsetInInputWithInjectionsToOutputPosition(inputOffsetInInputWithInjection, affinity); - } - - private offsetInInputWithInjectionsToOutputPosition(offsetInInputWithInjections: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition { - let low = 0; - let high = this.breakOffsets.length - 1; - let mid = 0; - let midStart = 0; - - while (low <= high) { - mid = low + ((high - low) / 2) | 0; - - const midStop = this.breakOffsets[mid]; - midStart = mid > 0 ? this.breakOffsets[mid - 1] : 0; - - if (affinity === PositionAffinity.Left) { - if (offsetInInputWithInjections <= midStart) { - high = mid - 1; - } else if (offsetInInputWithInjections > midStop) { - low = mid + 1; - } else { - break; - } - } else { - if (offsetInInputWithInjections < midStart) { - high = mid - 1; - } else if (offsetInInputWithInjections >= midStop) { - low = mid + 1; - } else { - break; - } - } - } - - let outputOffset = offsetInInputWithInjections - midStart; - if (mid > 0) { - outputOffset += this.wrappedTextIndentLength; - } - - return new OutputPosition(mid, outputOffset); - } - - public normalizeOutputPosition(outputLineIndex: number, outputOffset: number, affinity: PositionAffinity): OutputPosition { - if (this.injectionOffsets !== null) { - const offsetInInputWithInjections = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset); - const normalizedOffsetInUnwrappedLine = this.normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections, affinity); - if (normalizedOffsetInUnwrappedLine !== offsetInInputWithInjections) { - // injected text caused a change - return this.offsetInInputWithInjectionsToOutputPosition(normalizedOffsetInUnwrappedLine, affinity); - } - } - - if (affinity === PositionAffinity.Left) { - if (outputLineIndex > 0 && outputOffset === this.getMinOutputOffset(outputLineIndex)) { - return new OutputPosition(outputLineIndex - 1, this.getMaxOutputOffset(outputLineIndex - 1)); - } - } - else if (affinity === PositionAffinity.Right) { - const maxOutputLineIndex = this.getOutputLineCount() - 1; - if (outputLineIndex < maxOutputLineIndex && outputOffset === this.getMaxOutputOffset(outputLineIndex)) { - return new OutputPosition(outputLineIndex + 1, this.getMinOutputOffset(outputLineIndex + 1)); - } - } - - return new OutputPosition(outputLineIndex, outputOffset); - } - - private outputPositionToOffsetInInputWithInjections(outputLineIndex: number, outputOffset: number): number { - if (outputLineIndex > 0) { - outputOffset = Math.max(0, outputOffset - this.wrappedTextIndentLength); - } - const result = (outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0) + outputOffset; - return result; - } - - private normalizeOffsetInInputWithInjectionsAroundInjections(offsetInInputWithInjections: number, affinity: PositionAffinity): number { - const injectedText = this.getInjectedTextAtOffset(offsetInInputWithInjections); - if (!injectedText) { - return offsetInInputWithInjections; - } - - if (affinity === PositionAffinity.None) { - if (offsetInInputWithInjections === injectedText.offsetInInputWithInjections + injectedText.length) { - // go to the end of this injected text - return injectedText.offsetInInputWithInjections + injectedText.length; - } else { - // go to the start of this injected text - return injectedText.offsetInInputWithInjections; - } - } - - if (affinity === PositionAffinity.Right) { - let result = injectedText.offsetInInputWithInjections + injectedText.length; - let index = injectedText.injectedTextIndex; - // traverse all injected text that touch each other - while (index + 1 < this.injectionOffsets!.length && this.injectionOffsets![index + 1] === this.injectionOffsets![index]) { - result += this.injectionOptions![index + 1].content.length; - index++; - } - return result; - } - - // affinity is left - let result = injectedText.offsetInInputWithInjections; - let index = injectedText.injectedTextIndex; - // traverse all injected text that touch each other - while (index - 1 >= 0 && this.injectionOffsets![index - 1] === this.injectionOffsets![index]) { - result -= this.injectionOptions![index - 1].content.length; - index++; - } - return result; - } - - public getInjectedText(outputLineIndex: number, outputOffset: number): InjectedText | null { - const offset = this.outputPositionToOffsetInInputWithInjections(outputLineIndex, outputOffset); - const injectedText = this.getInjectedTextAtOffset(offset); - if (!injectedText) { - return null; - } - return { - options: this.injectionOptions![injectedText.injectedTextIndex] - }; - } - - private getInjectedTextAtOffset(offsetInInputWithInjections: number): { injectedTextIndex: number, offsetInInputWithInjections: number, length: number } | undefined { - const injectionOffsets = this.injectionOffsets; - const injectionOptions = this.injectionOptions; - - if (injectionOffsets !== null) { - let totalInjectedTextLengthBefore = 0; - for (let i = 0; i < injectionOffsets.length; i++) { - const length = injectionOptions![i].content.length; - const injectedTextStartOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore; - const injectedTextEndOffsetInInputWithInjections = injectionOffsets[i] + totalInjectedTextLengthBefore + length; - - if (injectedTextStartOffsetInInputWithInjections > offsetInInputWithInjections) { - // Injected text starts later. - break; // All later injected texts have an even larger offset. - } - - if (offsetInInputWithInjections <= injectedTextEndOffsetInInputWithInjections) { - // Injected text ends after or with the given position (but also starts with or before it). - return { - injectedTextIndex: i, - offsetInInputWithInjections: injectedTextStartOffsetInInputWithInjections, - length - }; - } - - totalInjectedTextLengthBefore += length; - } - } - - return undefined; - } -} - -export interface ILineBreaksComputerFactory { - createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent): ILineBreaksComputer; -} - -export interface ILineBreaksComputer { - /** - * Pass in `previousLineBreakData` if the only difference is in breaking columns!!! - */ - addRequest(lineText: string, injectedText: LineInjectedText[] | null, previousLineBreakData: ModelLineProjectionData | null): void; - finalize(): (ModelLineProjectionData | null)[]; -} - export interface IViewModel extends ICursorSimpleModel { readonly model: ITextModel; @@ -487,10 +184,6 @@ export interface IViewModel extends ICursorSimpleModel { //#endregion } -export class InjectedText { - constructor(public readonly options: InjectedTextOptions) { } -} - export class MinimapLinesRenderingData { public readonly tabSize: number; public readonly data: Array; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 73408764b5c..5dd89a09c2a 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -21,7 +21,7 @@ import { MinimapTokensColorTracker } from 'vs/editor/common/viewModel/minimapTok import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout'; import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProjectedModel } from 'vs/editor/common/viewModel/viewModelLines'; -import { ICoordinatesConverter, InjectedText, ILineBreaksComputer, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration, OverviewRulerDecorationsGroup, ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/viewModel'; +import { ICoordinatesConverter, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration, OverviewRulerDecorationsGroup } from 'vs/editor/common/viewModel/viewModel'; import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations'; import { RunOnceScheduler } from 'vs/base/common/async'; import * as platform from 'vs/base/common/platform'; @@ -34,6 +34,7 @@ import { ViewModelEventDispatcher, OutgoingViewModelEvent, FocusChangedEvent, Sc import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { ArrayQueue } from 'vs/base/common/arrays'; +import { ILineBreaksComputerFactory, ILineBreaksComputer, InjectedText } from 'vs/editor/common/viewModel/modelLineProjectionData'; const USE_IDENTITY_LINES_COLLECTION = true; diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index d51f3d3aa53..c79bc0c5b1d 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -14,8 +14,9 @@ import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { LineInjectedText } from 'vs/editor/common/model/textModelEvents'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { createModelLineProjection, IModelLineProjection } from 'vs/editor/common/viewModel/modelLineProjection'; +import { ILineBreaksComputer, ModelLineProjectionData, InjectedText, ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/modelLineProjectionData'; import { ConstantTimePrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ICoordinatesConverter, ILineBreaksComputer, ILineBreaksComputerFactory, InjectedText, ModelLineProjectionData, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; +import { ICoordinatesConverter, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; export interface IViewModelLines extends IDisposable { createCoordinatesConverter(): ICoordinatesConverter; diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index 20705b9396c..6ff5c565287 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -6,7 +6,7 @@ import assert = require('assert'); import { PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel'; -import { ModelLineProjectionData } from 'vs/editor/common/viewModel/viewModel'; +import { ModelLineProjectionData } from 'vs/editor/common/viewModel/modelLineProjectionData'; suite('Editor ViewModel - LineBreakData', () => { test('Basic', () => { diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index dd7f378e6a0..bb53c937bbc 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { WrappingIndent, EditorOptions } from 'vs/editor/common/config/editorOptions'; import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; -import { ILineBreaksComputerFactory, ModelLineProjectionData } from 'vs/editor/common/viewModel/viewModel'; +import { ModelLineProjectionData, ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/modelLineProjectionData'; function parseAnnotatedText(annotatedText: string): { text: string; indices: number[]; } { let text = ''; diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index dac9403c5ce..636d0f18a83 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -15,11 +15,12 @@ import * as modes from 'vs/editor/common/modes'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; import { ViewModelLinesFromProjectedModel } from 'vs/editor/common/viewModel/viewModelLines'; -import { ModelLineProjectionData, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; +import { ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { ISimpleModel, IModelLineProjection, createModelLineProjection } from 'vs/editor/common/viewModel/modelLineProjection'; +import { ModelLineProjectionData } from 'vs/editor/common/viewModel/modelLineProjectionData'; suite('Editor ViewModel - SplitLinesCollection', () => { test('SplitLine', () => {