Path: blob/main/src/vs/editor/common/viewModel/modelLineProjection.ts
3294 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 { LineTokens } from '../tokens/lineTokens.js';6import { Position } from '../core/position.js';7import { IRange } from '../core/range.js';8import { EndOfLinePreference, ITextModel, PositionAffinity } from '../model.js';9import { LineInjectedText } from '../textModelEvents.js';10import { InjectedText, ModelLineProjectionData } from '../modelLineProjectionData.js';11import { ViewLineData } from '../viewModel.js';12import { SingleLineInlineDecoration } from './inlineDecorations.js';1314export interface IModelLineProjection {15isVisible(): boolean;1617/**18* This invalidates the current instance (potentially reuses and returns it again).19*/20setVisible(isVisible: boolean): IModelLineProjection;2122getProjectionData(): ModelLineProjectionData | null;23getViewLineCount(): number;24getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string;25getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number;26getViewLineMinColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number;27getViewLineMaxColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number;28getViewLineData(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): ViewLineData;29getViewLinesData(model: ISimpleModel, modelLineNumber: number, outputLineIdx: number, lineCount: number, globalStartIndex: number, needed: boolean[], result: Array<ViewLineData | null>): void;3031getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number;32getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity?: PositionAffinity): Position;33getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number;34normalizePosition(outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position;3536getInjectedTextAt(outputLineIndex: number, column: number): InjectedText | null;37}3839export interface ISimpleModel {40tokenization: {41getLineTokens(lineNumber: number): LineTokens;42};43getLineContent(lineNumber: number): string;44getLineLength(lineNumber: number): number;45getLineMinColumn(lineNumber: number): number;46getLineMaxColumn(lineNumber: number): number;47getValueInRange(range: IRange, eol?: EndOfLinePreference): string;48}4950export function createModelLineProjection(lineBreakData: ModelLineProjectionData | null, isVisible: boolean): IModelLineProjection {51if (lineBreakData === null) {52// No mapping needed53if (isVisible) {54return IdentityModelLineProjection.INSTANCE;55}56return HiddenModelLineProjection.INSTANCE;57} else {58return new ModelLineProjection(lineBreakData, isVisible);59}60}6162/**63* This projection is used to64* * wrap model lines65* * inject text66*/67class ModelLineProjection implements IModelLineProjection {68private readonly _projectionData: ModelLineProjectionData;69private _isVisible: boolean;7071constructor(lineBreakData: ModelLineProjectionData, isVisible: boolean) {72this._projectionData = lineBreakData;73this._isVisible = isVisible;74}7576public isVisible(): boolean {77return this._isVisible;78}7980public setVisible(isVisible: boolean): IModelLineProjection {81this._isVisible = isVisible;82return this;83}8485public getProjectionData(): ModelLineProjectionData | null {86return this._projectionData;87}8889public getViewLineCount(): number {90if (!this._isVisible) {91return 0;92}93return this._projectionData.getOutputLineCount();94}9596public getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string {97this._assertVisible();9899const startOffsetInInputWithInjections = outputLineIndex > 0 ? this._projectionData.breakOffsets[outputLineIndex - 1] : 0;100const endOffsetInInputWithInjections = this._projectionData.breakOffsets[outputLineIndex];101102let r: string;103if (this._projectionData.injectionOffsets !== null) {104const injectedTexts = this._projectionData.injectionOffsets.map(105(offset, idx) => new LineInjectedText(1060,1070,108offset + 1,109this._projectionData.injectionOptions![idx],1100111)112);113const lineWithInjections = LineInjectedText.applyInjectedText(114model.getLineContent(modelLineNumber),115injectedTexts116);117r = lineWithInjections.substring(startOffsetInInputWithInjections, endOffsetInInputWithInjections);118} else {119r = model.getValueInRange({120startLineNumber: modelLineNumber,121startColumn: startOffsetInInputWithInjections + 1,122endLineNumber: modelLineNumber,123endColumn: endOffsetInInputWithInjections + 1124});125}126127if (outputLineIndex > 0) {128r = spaces(this._projectionData.wrappedTextIndentLength) + r;129}130131return r;132}133134public getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number {135this._assertVisible();136return this._projectionData.getLineLength(outputLineIndex);137}138139public getViewLineMinColumn(_model: ITextModel, _modelLineNumber: number, outputLineIndex: number): number {140this._assertVisible();141return this._projectionData.getMinOutputOffset(outputLineIndex) + 1;142}143144public getViewLineMaxColumn(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number {145this._assertVisible();146return this._projectionData.getMaxOutputOffset(outputLineIndex) + 1;147}148149/**150* Try using {@link getViewLinesData} instead.151*/152public getViewLineData(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): ViewLineData {153const arr = new Array<ViewLineData>();154this.getViewLinesData(model, modelLineNumber, outputLineIndex, 1, 0, [true], arr);155return arr[0];156}157158public getViewLinesData(model: ISimpleModel, modelLineNumber: number, outputLineIdx: number, lineCount: number, globalStartIndex: number, needed: boolean[], result: Array<ViewLineData | null>): void {159this._assertVisible();160161const lineBreakData = this._projectionData;162163const injectionOffsets = lineBreakData.injectionOffsets;164const injectionOptions = lineBreakData.injectionOptions;165166let inlineDecorationsPerOutputLine: SingleLineInlineDecoration[][] | null = null;167168if (injectionOffsets) {169inlineDecorationsPerOutputLine = [];170let totalInjectedTextLengthBefore = 0;171let currentInjectedOffset = 0;172173for (let outputLineIndex = 0; outputLineIndex < lineBreakData.getOutputLineCount(); outputLineIndex++) {174const inlineDecorations = new Array<SingleLineInlineDecoration>();175inlineDecorationsPerOutputLine[outputLineIndex] = inlineDecorations;176177const lineStartOffsetInInputWithInjections = outputLineIndex > 0 ? lineBreakData.breakOffsets[outputLineIndex - 1] : 0;178const lineEndOffsetInInputWithInjections = lineBreakData.breakOffsets[outputLineIndex];179180while (currentInjectedOffset < injectionOffsets.length) {181const length = injectionOptions![currentInjectedOffset].content.length;182const injectedTextStartOffsetInInputWithInjections = injectionOffsets[currentInjectedOffset] + totalInjectedTextLengthBefore;183const injectedTextEndOffsetInInputWithInjections = injectedTextStartOffsetInInputWithInjections + length;184185if (injectedTextStartOffsetInInputWithInjections > lineEndOffsetInInputWithInjections) {186// Injected text only starts in later wrapped lines.187break;188}189190if (lineStartOffsetInInputWithInjections < injectedTextEndOffsetInInputWithInjections) {191// Injected text ends after or in this line (but also starts in or before this line).192const options = injectionOptions![currentInjectedOffset];193if (options.inlineClassName) {194const offset = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0);195const start = offset + Math.max(injectedTextStartOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, 0);196const end = offset + Math.min(injectedTextEndOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, lineEndOffsetInInputWithInjections - lineStartOffsetInInputWithInjections);197if (start !== end) {198inlineDecorations.push(new SingleLineInlineDecoration(start, end, options.inlineClassName, options.inlineClassNameAffectsLetterSpacing!));199}200}201}202203if (injectedTextEndOffsetInInputWithInjections <= lineEndOffsetInInputWithInjections) {204totalInjectedTextLengthBefore += length;205currentInjectedOffset++;206} else {207// injected text breaks into next line, process it again208break;209}210}211}212}213214let lineWithInjections: LineTokens;215if (injectionOffsets) {216const tokensToInsert: { offset: number; text: string; tokenMetadata: number }[] = [];217218for (let idx = 0; idx < injectionOffsets.length; idx++) {219const offset = injectionOffsets[idx];220const tokens = injectionOptions![idx].tokens;221if (tokens) {222tokens.forEach((range, info) => {223tokensToInsert.push({224offset,225text: range.substring(injectionOptions![idx].content),226tokenMetadata: info.metadata,227});228});229} else {230tokensToInsert.push({231offset,232text: injectionOptions![idx].content,233tokenMetadata: LineTokens.defaultTokenMetadata,234});235}236}237238lineWithInjections = model.tokenization.getLineTokens(modelLineNumber).withInserted(tokensToInsert);239} else {240lineWithInjections = model.tokenization.getLineTokens(modelLineNumber);241}242243for (let outputLineIndex = outputLineIdx; outputLineIndex < outputLineIdx + lineCount; outputLineIndex++) {244const globalIndex = globalStartIndex + outputLineIndex - outputLineIdx;245if (!needed[globalIndex]) {246result[globalIndex] = null;247continue;248}249result[globalIndex] = this._getViewLineData(lineWithInjections, inlineDecorationsPerOutputLine ? inlineDecorationsPerOutputLine[outputLineIndex] : null, outputLineIndex);250}251}252253private _getViewLineData(lineWithInjections: LineTokens, inlineDecorations: null | SingleLineInlineDecoration[], outputLineIndex: number): ViewLineData {254this._assertVisible();255const lineBreakData = this._projectionData;256const deltaStartIndex = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0);257258const lineStartOffsetInInputWithInjections = outputLineIndex > 0 ? lineBreakData.breakOffsets[outputLineIndex - 1] : 0;259const lineEndOffsetInInputWithInjections = lineBreakData.breakOffsets[outputLineIndex];260const tokens = lineWithInjections.sliceAndInflate(lineStartOffsetInInputWithInjections, lineEndOffsetInInputWithInjections, deltaStartIndex);261262let lineContent = tokens.getLineContent();263if (outputLineIndex > 0) {264lineContent = spaces(lineBreakData.wrappedTextIndentLength) + lineContent;265}266267const minColumn = this._projectionData.getMinOutputOffset(outputLineIndex) + 1;268const maxColumn = lineContent.length + 1;269const continuesWithWrappedLine = (outputLineIndex + 1 < this.getViewLineCount());270const startVisibleColumn = (outputLineIndex === 0 ? 0 : lineBreakData.breakOffsetsVisibleColumn[outputLineIndex - 1]);271272return new ViewLineData(273lineContent,274continuesWithWrappedLine,275minColumn,276maxColumn,277startVisibleColumn,278tokens,279inlineDecorations280);281}282283public getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number {284this._assertVisible();285return this._projectionData.translateToInputOffset(outputLineIndex, outputColumn - 1) + 1;286}287288public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity: PositionAffinity = PositionAffinity.None): Position {289this._assertVisible();290const r = this._projectionData.translateToOutputPosition(inputColumn - 1, affinity);291return r.toPosition(deltaLineNumber);292}293294public getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number {295this._assertVisible();296const r = this._projectionData.translateToOutputPosition(inputColumn - 1);297return deltaLineNumber + r.outputLineIndex;298}299300public normalizePosition(outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position {301const baseViewLineNumber = outputPosition.lineNumber - outputLineIndex;302const normalizedOutputPosition = this._projectionData.normalizeOutputPosition(outputLineIndex, outputPosition.column - 1, affinity);303const result = normalizedOutputPosition.toPosition(baseViewLineNumber);304return result;305}306307public getInjectedTextAt(outputLineIndex: number, outputColumn: number): InjectedText | null {308return this._projectionData.getInjectedText(outputLineIndex, outputColumn - 1);309}310311private _assertVisible() {312if (!this._isVisible) {313throw new Error('Not supported');314}315}316}317318/**319* This projection does not change the model line.320*/321class IdentityModelLineProjection implements IModelLineProjection {322public static readonly INSTANCE = new IdentityModelLineProjection();323324private constructor() { }325326public isVisible(): boolean {327return true;328}329330public setVisible(isVisible: boolean): IModelLineProjection {331if (isVisible) {332return this;333}334return HiddenModelLineProjection.INSTANCE;335}336337public getProjectionData(): ModelLineProjectionData | null {338return null;339}340341public getViewLineCount(): number {342return 1;343}344345public getViewLineContent(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): string {346return model.getLineContent(modelLineNumber);347}348349public getViewLineLength(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): number {350return model.getLineLength(modelLineNumber);351}352353public getViewLineMinColumn(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): number {354return model.getLineMinColumn(modelLineNumber);355}356357public getViewLineMaxColumn(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): number {358return model.getLineMaxColumn(modelLineNumber);359}360361public getViewLineData(model: ISimpleModel, modelLineNumber: number, _outputLineIndex: number): ViewLineData {362const lineTokens = model.tokenization.getLineTokens(modelLineNumber);363const lineContent = lineTokens.getLineContent();364return new ViewLineData(365lineContent,366false,3671,368lineContent.length + 1,3690,370lineTokens.inflate(),371null372);373}374375public getViewLinesData(model: ISimpleModel, modelLineNumber: number, _fromOuputLineIndex: number, _toOutputLineIndex: number, globalStartIndex: number, needed: boolean[], result: Array<ViewLineData | null>): void {376if (!needed[globalStartIndex]) {377result[globalStartIndex] = null;378return;379}380result[globalStartIndex] = this.getViewLineData(model, modelLineNumber, 0);381}382383public getModelColumnOfViewPosition(_outputLineIndex: number, outputColumn: number): number {384return outputColumn;385}386387public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position {388return new Position(deltaLineNumber, inputColumn);389}390391public getViewLineNumberOfModelPosition(deltaLineNumber: number, _inputColumn: number): number {392return deltaLineNumber;393}394395public normalizePosition(outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position {396return outputPosition;397}398399public getInjectedTextAt(_outputLineIndex: number, _outputColumn: number): InjectedText | null {400return null;401}402}403404/**405* This projection hides the model line.406*/407class HiddenModelLineProjection implements IModelLineProjection {408public static readonly INSTANCE = new HiddenModelLineProjection();409410private constructor() { }411412public isVisible(): boolean {413return false;414}415416public setVisible(isVisible: boolean): IModelLineProjection {417if (!isVisible) {418return this;419}420return IdentityModelLineProjection.INSTANCE;421}422423public getProjectionData(): ModelLineProjectionData | null {424return null;425}426427public getViewLineCount(): number {428return 0;429}430431public getViewLineContent(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): string {432throw new Error('Not supported');433}434435public getViewLineLength(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): number {436throw new Error('Not supported');437}438439public getViewLineMinColumn(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): number {440throw new Error('Not supported');441}442443public getViewLineMaxColumn(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): number {444throw new Error('Not supported');445}446447public getViewLineData(_model: ISimpleModel, _modelLineNumber: number, _outputLineIndex: number): ViewLineData {448throw new Error('Not supported');449}450451public getViewLinesData(_model: ISimpleModel, _modelLineNumber: number, _fromOuputLineIndex: number, _toOutputLineIndex: number, _globalStartIndex: number, _needed: boolean[], _result: ViewLineData[]): void {452throw new Error('Not supported');453}454455public getModelColumnOfViewPosition(_outputLineIndex: number, _outputColumn: number): number {456throw new Error('Not supported');457}458459public getViewPositionOfModelPosition(_deltaLineNumber: number, _inputColumn: number): Position {460throw new Error('Not supported');461}462463public getViewLineNumberOfModelPosition(_deltaLineNumber: number, _inputColumn: number): number {464throw new Error('Not supported');465}466467public normalizePosition(outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position {468throw new Error('Not supported');469}470471public getInjectedTextAt(_outputLineIndex: number, _outputColumn: number): InjectedText | null {472throw new Error('Not supported');473}474}475476const _spaces: string[] = [''];477function spaces(count: number): string {478if (count >= _spaces.length) {479for (let i = 1; i <= count; i++) {480_spaces[i] = _makeSpaces(i);481}482}483return _spaces[count];484}485486function _makeSpaces(count: number): string {487return new Array(count + 1).join(' ');488}489490491