diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 3d766b78764da..41ad643ecc39c 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -18,6 +18,7 @@ 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'; export interface IViewZoneData { viewZoneId: string; @@ -955,6 +956,12 @@ export class MouseTargetFactory { } else if ((document).caretPositionFromPoint) { result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates()); } + if (result.type === HitTestResultType.Content) { + const normalizedPosition = ctx.model.normalizePosition(result.position, PositionAffinity.None); + if (!normalizedPosition.equals(result.position)) { + result = new ContentHitTestResult(normalizedPosition, result.spanNode); + } + } // Snap to the nearest soft tab boundary if atomic soft tabs are enabled. if (result.type === HitTestResultType.Content && ctx.stickyTabStops) { result = new ContentHitTestResult(this._snapToSoftTabBoundary(result.position, ctx.model), result.spanNode); diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 2ec7d252ac207..5236d2bd23048 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -144,7 +144,7 @@ export class CursorsController extends Disposable { this._knownModelVersionId = this._model.getVersionId(); this._viewModel = viewModel; this._coordinatesConverter = coordinatesConverter; - this.context = new CursorContext(this._model, this._coordinatesConverter, cursorConfig); + this.context = new CursorContext(this._model, this._viewModel, this._coordinatesConverter, cursorConfig); this._cursors = new CursorCollection(this.context); this._hasFocus = false; @@ -163,7 +163,7 @@ export class CursorsController extends Disposable { } public updateConfiguration(cursorConfig: CursorConfiguration): void { - this.context = new CursorContext(this._model, this._coordinatesConverter, cursorConfig); + this.context = new CursorContext(this._model, this._viewModel, this._coordinatesConverter, cursorConfig); this._cursors.updateContext(this.context); } diff --git a/src/vs/editor/common/controller/cursorCommon.ts b/src/vs/editor/common/controller/cursorCommon.ts index a387696118c5a..ae17e57f6f230 100644 --- a/src/vs/editor/common/controller/cursorCommon.ts +++ b/src/vs/editor/common/controller/cursorCommon.ts @@ -11,7 +11,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { ICommand, IConfiguration } from 'vs/editor/common/editorCommon'; -import { ITextModel, PositionNormalizationAffinity, TextModelResolvedOptions } from 'vs/editor/common/model'; +import { ITextModel, PositionAffinity, TextModelResolvedOptions } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { AutoClosingPairs, IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration'; @@ -221,7 +221,8 @@ export interface ICursorSimpleModel { getLineMaxColumn(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; - normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position; + normalizePosition(position: Position, affinity: PositionAffinity): Position; + /** * Gets the column at which indentation stops at a given line. * @internal @@ -321,11 +322,13 @@ export class CursorContext { _cursorContextBrand: void = undefined; public readonly model: ITextModel; + public readonly viewModel: ICursorSimpleModel; public readonly coordinatesConverter: ICoordinatesConverter; public readonly cursorConfig: CursorConfiguration; - constructor(model: ITextModel, coordinatesConverter: ICoordinatesConverter, cursorConfig: CursorConfiguration) { + constructor(model: ITextModel, viewModel: ICursorSimpleModel, coordinatesConverter: ICoordinatesConverter, cursorConfig: CursorConfiguration) { this.model = model; + this.viewModel = viewModel; this.coordinatesConverter = coordinatesConverter; this.cursorConfig = cursorConfig; } diff --git a/src/vs/editor/common/controller/cursorMoveOperations.ts b/src/vs/editor/common/controller/cursorMoveOperations.ts index 2190fea68ebe4..938780996372c 100644 --- a/src/vs/editor/common/controller/cursorMoveOperations.ts +++ b/src/vs/editor/common/controller/cursorMoveOperations.ts @@ -9,7 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import * as strings from 'vs/base/common/strings'; import { Constants } from 'vs/base/common/uint'; import { AtomicTabMoveOperations, Direction } from 'vs/editor/common/controller/cursorAtomicMoveOperations'; -import { PositionNormalizationAffinity } from 'vs/editor/common/model'; +import { PositionAffinity } from 'vs/editor/common/model'; export class CursorPosition { _cursorPositionBrand: void = undefined; @@ -75,7 +75,7 @@ export class MoveOperations { const pos = cursor.position.delta(undefined, -(noOfColumns - 1)); // We clip the position before normalization, as normalization is not defined // for possibly negative columns. - const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionNormalizationAffinity.Left); + const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionAffinity.Left); const p = MoveOperations.left(config, model, normalizedPos); lineNumber = p.lineNumber; @@ -144,7 +144,7 @@ export class MoveOperations { column = cursor.selection.endColumn; } else { const pos = cursor.position.delta(undefined, noOfColumns - 1); - const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionNormalizationAffinity.Right); + const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionAffinity.Right); const r = MoveOperations.right(config, model, normalizedPos); lineNumber = r.lineNumber; column = r.column; diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index f32a1b43c6479..bb07a4da7172c 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CursorContext, CursorState, SingleCursorState } from 'vs/editor/common/controller/cursorCommon'; +import { CursorContext, CursorState, ICursorSimpleModel, SingleCursorState } from 'vs/editor/common/controller/cursorCommon'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection, SelectionDirection } from 'vs/editor/common/core/selection'; -import { TrackedRangeStickiness } from 'vs/editor/common/model'; +import { PositionAffinity, TrackedRangeStickiness } from 'vs/editor/common/model'; /** * Represents a single cursor. @@ -77,7 +77,40 @@ export class Cursor { this._setState(context, modelState, viewState); } + private static _validatePositionWithCache(viewModel: ICursorSimpleModel, position: Position, cacheInput: Position, cacheOutput: Position): Position { + if (position.equals(cacheInput)) { + return cacheOutput; + } + return viewModel.normalizePosition(position, PositionAffinity.None); + } + + private static _validateViewState(viewModel: ICursorSimpleModel, viewState: SingleCursorState): SingleCursorState { + const position = viewState.position; + const sStartPosition = viewState.selectionStart.getStartPosition(); + const sEndPosition = viewState.selectionStart.getEndPosition(); + + const validPosition = viewModel.normalizePosition(position, PositionAffinity.None); + const validSStartPosition = this._validatePositionWithCache(viewModel, sStartPosition, position, validPosition); + const validSEndPosition = this._validatePositionWithCache(viewModel, sEndPosition, sStartPosition, validSStartPosition); + + if (position.equals(validPosition) && sStartPosition.equals(validSStartPosition) && sEndPosition.equals(validSEndPosition)) { + // fast path: the state is valid + return viewState; + } + + return new SingleCursorState( + Range.fromPositions(validSStartPosition, validSEndPosition), + viewState.selectionStartLeftoverVisibleColumns + sStartPosition.column - validSStartPosition.column, + validPosition, + viewState.leftoverVisibleColumns + position.column - validPosition.column, + ); + } + private _setState(context: CursorContext, modelState: SingleCursorState | null, viewState: SingleCursorState | null): void { + if (viewState) { + viewState = Cursor._validateViewState(context.viewModel, viewState); + } + if (!modelState) { if (!viewState) { return; diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 88e3f01a75a85..e33a53efea312 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1293,9 +1293,16 @@ export interface ITextModel { /** * Among all positions that are projected to the same position in the underlying text model as * the given position, select a unique position as indicated by the affinity. + * + * PositionAffinity.Left: + * The normalized position must be equal or left to the requested position. + * + * PositionAffinity.Right: + * The normalized position must be equal or right to the requested position. + * * @internal */ - normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position; + normalizePosition(position: Position, affinity: PositionAffinity): Position; /** * Gets the column at which indentation stops at a given line. @@ -1307,15 +1314,21 @@ export interface ITextModel { /** * @internal */ -export const enum PositionNormalizationAffinity { +export const enum PositionAffinity { /** * Prefers the left most position. */ Left = 0, + /** * Prefers the right most position. */ Right = 1, + + /** + * No preference. + */ + None = 2, } /** diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 967a60375d00d..747b4becd2bd5 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -3122,7 +3122,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } //#endregion - normalizePosition(position: Position, affinity: model.PositionNormalizationAffinity): Position { + normalizePosition(position: Position, affinity: model.PositionAffinity): Position { return position; } diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 005c364358ffc..1010899cc6197 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -8,7 +8,7 @@ import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { IViewLineTokens, LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, ITextModel, PositionNormalizationAffinity } from 'vs/editor/common/model'; +import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, ITextModel, PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; @@ -45,9 +45,9 @@ export interface ISplitLine { getViewLinesData(model: ISimpleModel, modelLineNumber: number, fromOuputLineIndex: number, toOutputLineIndex: number, globalStartIndex: number, needed: boolean[], result: Array): void; getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number; - getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position; + getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity?: PositionAffinity): Position; getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number; - normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position; + normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position; } export interface IViewModelLinesCollection extends IDisposable { @@ -78,7 +78,7 @@ export interface IViewModelLinesCollection extends IDisposable { getAllOverviewRulerDecorations(ownerId: number, filterOutValidation: boolean, theme: EditorTheme): IOverviewRulerDecorations; getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): IModelDecoration[]; - normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position; + normalizePosition(position: Position, affinity: PositionAffinity): Position; /** * Gets the column at which indentation stops at a given line. * @internal @@ -853,7 +853,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return new Range(start.lineNumber, start.column, end.lineNumber, end.column); } - public convertModelPositionToViewPosition(_modelLineNumber: number, _modelColumn: number): Position { + public convertModelPositionToViewPosition(_modelLineNumber: number, _modelColumn: number, affinity: PositionAffinity = PositionAffinity.None): Position { const validPosition = this.model.validatePosition(new Position(_modelLineNumber, _modelColumn)); const inputLineNumber = validPosition.lineNumber; @@ -873,9 +873,9 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let r: Position; if (lineIndexChanged) { - r = this.lines[lineIndex].getViewPositionOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1)); + r = this.lines[lineIndex].getViewPositionOfModelPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1), affinity); } else { - r = this.lines[inputLineNumber - 1].getViewPositionOfModelPosition(deltaLineNumber, inputColumn); + r = this.lines[inputLineNumber - 1].getViewPositionOfModelPosition(deltaLineNumber, inputColumn, affinity); } // console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + r.lineNumber + ',' + r); @@ -883,14 +883,11 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public convertModelRangeToViewRange(modelRange: Range): Range { - let start = this.convertModelPositionToViewPosition(modelRange.startLineNumber, modelRange.startColumn); - let end = this.convertModelPositionToViewPosition(modelRange.endLineNumber, modelRange.endColumn); - if (modelRange.startLineNumber === modelRange.endLineNumber && start.lineNumber !== end.lineNumber) { - // This is a single line range that ends up taking more lines due to wrapping - if (end.column === this.getViewLineMinColumn(end.lineNumber)) { - // the end column lands on the first column of the next line - return new Range(start.lineNumber, start.column, end.lineNumber - 1, this.getViewLineMaxColumn(end.lineNumber - 1)); - } + const start = this.convertModelPositionToViewPosition(modelRange.startLineNumber, modelRange.startColumn, PositionAffinity.Right); + let end = this.convertModelPositionToViewPosition(modelRange.endLineNumber, modelRange.endColumn, PositionAffinity.Left); + if (end.isBefore(start)) { + // If the range is empty, we don't want the range to get expanded just by converting to a view range + end = start; } return new Range(start.lineNumber, start.column, end.lineNumber, end.column); } @@ -1000,7 +997,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return finalResult; } - normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position { + normalizePosition(position: Position, affinity: PositionAffinity): Position { const viewLineNumber = this._toValidViewLineNumber(position.lineNumber); const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); const lineIndex = r.index; @@ -1101,7 +1098,7 @@ class VisibleIdentitySplitLine implements ISplitLine { return deltaLineNumber; } - public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position { + public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position { return outputPosition; } } @@ -1167,7 +1164,7 @@ class InvisibleIdentitySplitLine implements ISplitLine { throw new Error('Not supported'); } - public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position { + public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position { throw new Error('Not supported'); } } @@ -1217,14 +1214,26 @@ export class SplitLine implements ISplitLine { if (!this._isVisible) { throw new Error('Not supported'); } - let startOffset = this.getInputStartOffsetOfOutputLineIndex(outputLineIndex); - let endOffset = this.getInputEndOffsetOfOutputLineIndex(model, modelLineNumber, outputLineIndex); - let r = model.getValueInRange({ - startLineNumber: modelLineNumber, - startColumn: startOffset + 1, - endLineNumber: modelLineNumber, - endColumn: endOffset + 1 - }); + + // These offsets refer to model text with injected text. + const startOffset = outputLineIndex > 0 ? this._lineBreakData.breakOffsets[outputLineIndex - 1] : 0; + const endOffset = outputLineIndex < this._lineBreakData.breakOffsets.length + ? this._lineBreakData.breakOffsets[outputLineIndex] + // This case might not be possible anyway, but we clamp the value to be on the safe side. + : this._lineBreakData.breakOffsets[this._lineBreakData.breakOffsets.length - 1]; + + let r: string; + if (this._lineBreakData.injectionOffsets !== null) { + const injectedTexts = this._lineBreakData.injectionOffsets.map((offset, idx) => new LineInjectedText(0, 0, offset, this._lineBreakData.injectionOptions![idx], 0)); + r = LineInjectedText.applyInjectedText(model.getLineContent(modelLineNumber), injectedTexts).substring(startOffset, endOffset); + } else { + r = model.getValueInRange({ + startLineNumber: modelLineNumber, + startColumn: startOffset + 1, + endLineNumber: modelLineNumber, + endColumn: endOffset + 1 + }); + } if (outputLineIndex > 0) { r = spaces(this._lineBreakData.wrappedTextIndentLength) + r; @@ -1280,7 +1289,6 @@ export class SplitLine implements ISplitLine { if (!this._isVisible) { throw new Error('Not supported'); } - const lineBreakData = this._lineBreakData; const deltaStartIndex = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0); @@ -1306,8 +1314,9 @@ export class SplitLine implements ISplitLine { let totalInjectedTextLengthBefore = 0; for (let i = 0; i < injectionOffsets.length; i++) { + const length = injectionOptions![i].content.length; const injectedTextStartOffsetInUnwrappedLine = injectionOffsets[i] + totalInjectedTextLengthBefore; - const injectedTextEndOffsetInUnwrappedLine = injectionOffsets[i] + totalInjectedTextLengthBefore + injectionOptions![i].content.length; + const injectedTextEndOffsetInUnwrappedLine = injectionOffsets[i] + totalInjectedTextLengthBefore + length; if (injectedTextStartOffsetInUnwrappedLine > lineEndOffsetInUnwrappedLine) { // Injected text only starts in later wrapped lines. @@ -1316,17 +1325,18 @@ export class SplitLine implements ISplitLine { if (lineStartOffsetInUnwrappedLine < injectedTextEndOffsetInUnwrappedLine) { // Injected text ends after or in this line (but also starts in or before this line). - const length = injectionOptions![i].content.length; const inlineClassName = injectionOptions![i].inlineClassName; if (inlineClassName) { - inlineDecorations.push(new SingleLineInlineDecoration( - Math.max(injectedTextStartOffsetInUnwrappedLine - lineStartOffsetInUnwrappedLine, 0), - Math.min(injectedTextEndOffsetInUnwrappedLine - lineStartOffsetInUnwrappedLine, lineEndOffsetInUnwrappedLine), - inlineClassName - )); + const offset = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0); + const start = offset + Math.max(injectedTextStartOffsetInUnwrappedLine - lineStartOffsetInUnwrappedLine, 0); + const end = offset + Math.min(injectedTextEndOffsetInUnwrappedLine - lineStartOffsetInUnwrappedLine, lineEndOffsetInUnwrappedLine); + if (start !== end) { + inlineDecorations.push(new SingleLineInlineDecoration(start, end, inlineClassName)); + } } - totalInjectedTextLengthBefore += length; } + + totalInjectedTextLengthBefore += length; } } else { const startOffset = this.getInputStartOffsetOfOutputLineIndex(outputLineIndex); @@ -1392,11 +1402,11 @@ export class SplitLine implements ISplitLine { return this._lineBreakData.getInputOffsetOfOutputPosition(outputLineIndex, adjustedColumn) + 1; } - public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position { + public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number, affinity: PositionAffinity = PositionAffinity.None): Position { if (!this._isVisible) { throw new Error('Not supported'); } - let r = this._lineBreakData.getOutputPositionOfInputOffset(inputColumn - 1); + let r = this._lineBreakData.getOutputPositionOfInputOffset(inputColumn - 1, affinity); let outputLineIndex = r.outputLineIndex; let outputColumn = r.outputOffset + 1; @@ -1416,18 +1426,29 @@ export class SplitLine implements ISplitLine { return (deltaLineNumber + r.outputLineIndex); } - public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position { - if (affinity === PositionNormalizationAffinity.Left) { + public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionAffinity): Position { + if (this._lineBreakData.injectionOffsets !== null) { + const baseViewLineNumber = outputPosition.lineNumber - outputLineIndex; + const offsetInUnwrappedLine = this._lineBreakData.outputPositionToOffsetInUnwrappedLine(outputLineIndex, outputPosition.column - 1); + const normalizedOffsetInUnwrappedLine = this._lineBreakData.normalizeOffsetAroundInjections(offsetInUnwrappedLine, affinity); + if (normalizedOffsetInUnwrappedLine !== offsetInUnwrappedLine) { + // injected text caused a change + return this._lineBreakData.getOutputPositionOfOffsetInUnwrappedLine(normalizedOffsetInUnwrappedLine, affinity).toPosition(baseViewLineNumber, this._lineBreakData.wrappedTextIndentLength); + } + } + + if (affinity === PositionAffinity.Left) { if (outputLineIndex > 0 && outputPosition.column === this._getViewLineMinColumn(outputLineIndex)) { return new Position(outputPosition.lineNumber - 1, this.getViewLineMaxColumn(model, modelLineNumber, outputLineIndex - 1)); } } - else if (affinity === PositionNormalizationAffinity.Right) { + else if (affinity === PositionAffinity.Right) { const maxOutputLineIndex = this.getViewLineCount() - 1; if (outputLineIndex < maxOutputLineIndex && outputPosition.column === this.getViewLineMaxColumn(model, modelLineNumber, outputLineIndex)) { return new Position(outputPosition.lineNumber + 1, this._getViewLineMinColumn(outputLineIndex + 1)); } } + return outputPosition; } } @@ -1666,7 +1687,7 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return this.model.getDecorationsInRange(range, ownerId, filterOutValidation); } - normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position { + normalizePosition(position: Position, affinity: PositionAffinity): Position { return this.model.normalizePosition(position, affinity); } diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index f201f605fde56..3b4825d1c1dfc 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -9,7 +9,7 @@ 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 } from 'vs/editor/common/model'; +import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions, ITextModel, InjectedTextOptions, PositionAffinity } 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'; @@ -100,6 +100,11 @@ export class OutputPosition { toString(): string { return `${this.outputLineIndex}:${this.outputOffset}`; } + + toPosition(baseLineNumber: number, wrappedTextIndentLength: number): Position { + const delta = (this.outputLineIndex > 0 ? wrappedTextIndentLength : 0); + return new Position(baseLineNumber + this.outputLineIndex, delta + this.outputOffset + 1); + } } export class LineBreakData { @@ -137,18 +142,27 @@ export class LineBreakData { return inputOffset; } - public getOutputPositionOfInputOffset(inputOffset: number): OutputPosition { + public getOutputPositionOfInputOffset(inputOffset: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition { let delta = 0; if (this.injectionOffsets !== null) { for (let i = 0; i < this.injectionOffsets.length; i++) { - if (inputOffset <= this.injectionOffsets[i]) { + if (inputOffset < this.injectionOffsets[i]) { + break; + } + + if (affinity !== PositionAffinity.Right && inputOffset === this.injectionOffsets[i]) { break; } + delta += this.injectionOptions![i].content.length; } } inputOffset += delta; + return this.getOutputPositionOfOffsetInUnwrappedLine(inputOffset, affinity); + } + + public getOutputPositionOfOffsetInUnwrappedLine(inputOffset: number, affinity: PositionAffinity = PositionAffinity.None): OutputPosition { let low = 0; let high = this.breakOffsets.length - 1; let mid = 0; @@ -160,17 +174,105 @@ export class LineBreakData { const midStop = this.breakOffsets[mid]; midStart = mid > 0 ? this.breakOffsets[mid - 1] : 0; - if (inputOffset < midStart) { - high = mid - 1; - } else if (inputOffset >= midStop) { - low = mid + 1; + if (affinity === PositionAffinity.Left) { + if (inputOffset <= midStart) { + high = mid - 1; + } else if (inputOffset > midStop) { + low = mid + 1; + } else { + break; + } } else { - break; + if (inputOffset < midStart) { + high = mid - 1; + } else if (inputOffset >= midStop) { + low = mid + 1; + } else { + break; + } } } return new OutputPosition(mid, inputOffset - midStart); } + + public outputPositionToOffsetInUnwrappedLine(outputLineIndex: number, outputOffset: number): number { + let result = (outputLineIndex > 0 ? this.breakOffsets[outputLineIndex - 1] : 0) + outputOffset; + if (outputLineIndex > 0) { + result -= this.wrappedTextIndentLength; + } + return result; + } + + public normalizeOffsetAroundInjections(offsetInUnwrappedLine: number, affinity: PositionAffinity): number { + const injectedText = this.getInjectedTextAt(offsetInUnwrappedLine); + if (!injectedText) { + return offsetInUnwrappedLine; + } + + if (affinity === PositionAffinity.None) { + if (offsetInUnwrappedLine === injectedText.offsetInUnwrappedLine + injectedText.length) { + // go to the end of this injected text + return injectedText.offsetInUnwrappedLine + injectedText.length; + } else { + // go to the start of this injected text + return injectedText.offsetInUnwrappedLine; + } + } + + if (affinity === PositionAffinity.Right) { + let result = injectedText.offsetInUnwrappedLine + injectedText.length; + let index = injectedText.injectedTextIndex; + // traverse all injected text that touch eachother + 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.offsetInUnwrappedLine; + let index = injectedText.injectedTextIndex; + // traverse all injected text that touch eachother + while (index - 1 >= 0 && this.injectionOffsets![index - 1] === this.injectionOffsets![index]) { + result -= this.injectionOptions![index - 1].content.length; + index++; + } + return result; + } + + private getInjectedTextAt(offsetInUnwrappedLine: number): { injectedTextIndex: number, offsetInUnwrappedLine: 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 injectedTextStartOffsetInUnwrappedLine = injectionOffsets[i] + totalInjectedTextLengthBefore; + const injectedTextEndOffsetInUnwrappedLine = injectionOffsets[i] + totalInjectedTextLengthBefore + length; + + if (injectedTextStartOffsetInUnwrappedLine > offsetInUnwrappedLine) { + // Injected text starts later. + break; // All later injected texts have an even larger offset. + } + + if (offsetInUnwrappedLine <= injectedTextEndOffsetInUnwrappedLine) { + // Injected text ends after or with the given position (but also starts with or before it). + return { + injectedTextIndex: i, + offsetInUnwrappedLine: injectedTextStartOffsetInUnwrappedLine, + length + }; + } + + totalInjectedTextLengthBefore += length; + } + } + + return undefined; + } } export interface ILineBreaksComputer { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 9e3e4f0b8f1d7..dc410ae2ca944 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -12,7 +12,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IConfiguration, IViewState, ScrollType, ICursorState, ICommand, INewScrollPosition } from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, IActiveIndentGuideInfo, ITextModel, TrackedRangeStickiness, TextModelResolvedOptions, IIdentifiedSingleEditOperation, ICursorStateComputer, PositionNormalizationAffinity } from 'vs/editor/common/model'; +import { EndOfLinePreference, IActiveIndentGuideInfo, ITextModel, TrackedRangeStickiness, TextModelResolvedOptions, IIdentifiedSingleEditOperation, ICursorStateComputer, PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationOverviewRulerOptions, ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ColorId, LanguageId, TokenizationRegistry } from 'vs/editor/common/modes'; @@ -1062,7 +1062,7 @@ export class ViewModel extends Disposable implements IViewModel { } } - normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position { + normalizePosition(position: Position, affinity: PositionAffinity): Position { return this._lines.normalizePosition(position, affinity); } diff --git a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts index ccc275b94569d..dd451072be27f 100644 --- a/src/vs/editor/test/common/viewModel/lineBreakData.test.ts +++ b/src/vs/editor/test/common/viewModel/lineBreakData.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert = require('assert'); +import { PositionAffinity } from 'vs/editor/common/model'; import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel'; import { LineBreakData } from 'vs/editor/common/viewModel/viewModel'; @@ -33,8 +34,8 @@ suite('Editor ViewModel - LineBreakData', () => { return sequence(11).map(i => data.getInputOffsetOfOutputPosition(outputLineIdx, i)); } - function getOutputOffsets(data: LineBreakData): string[] { - return sequence(25).map(i => data.getOutputPositionOfInputOffset(i).toString()); + function getOutputOffsets(data: LineBreakData, affinity: PositionAffinity): string[] { + return sequence(25).map(i => data.getOutputPositionOfInputOffset(i, affinity).toString()); } function mapTextToInjectedTextOptions(arr: string[]): ModelDecorationInjectedTextOptions[] { @@ -52,10 +53,20 @@ suite('Editor ViewModel - LineBreakData', () => { test('getOutputPositionOfInputOffset', () => { data.getOutputPositionOfInputOffset(20); - assert.deepStrictEqual(getOutputOffsets(data), [ + assert.deepStrictEqual(getOutputOffsets(data, PositionAffinity.None), [ '0:0', '0:1', '0:2', '0:4', '0:7', '0:8', '0:9', '1:0', '1:1', '1:2', '1:3', '1:7', '1:8', '1:9', '1:10', '1:11', '1:12', '1:13', '1:14', '1:15', '1:16', '1:17', '1:18', '1:19', '1:20', ]); + + assert.deepStrictEqual(getOutputOffsets(data, PositionAffinity.Left), [ + '0:0', '0:1', '0:2', '0:4', '0:7', '0:8', '0:9', '0:10', + '1:1', '1:2', '1:3', '1:7', '1:8', '1:9', '1:10', '1:11', '1:12', '1:13', '1:14', '1:15', '1:16', '1:17', '1:18', '1:19', '1:20', + ]); + + assert.deepStrictEqual(getOutputOffsets(data, PositionAffinity.Right), [ + '0:0', '0:1', '0:3', '0:6', '0:7', '0:8', '0:9', + '1:0', '1:1', '1:2', '1:6', '1:7', '1:8', '1:9', '1:10', '1:11', '1:12', '1:13', '1:14', '1:15', '1:16', '1:17', '1:18', '1:19', '1:20', + ]); }); test('getInputOffsetOfOutputPosition is inverse of getOutputPositionOfInputOffset', () => { diff --git a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts index 0e9f99aba50cd..ef9866ae107fb 100644 --- a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts @@ -5,10 +5,11 @@ import * as assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; -import { EndOfLineSequence } from 'vs/editor/common/model'; +import { EndOfLineSequence, PositionAffinity } from 'vs/editor/common/model'; import { testViewModel } from 'vs/editor/test/common/viewModel/testViewModel'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; import { ViewEvent } from 'vs/editor/common/view/viewEvents'; +import { Position } from 'vs/editor/common/core/position'; suite('ViewModel', () => { @@ -294,4 +295,55 @@ suite('ViewModel', () => { } ); }); + + test('normalizePosition with multiple touching injected text', () => { + testViewModel( + [ + 'just some text' + ], + {}, + (viewModel, model) => { + model.deltaDecorations([], [ + { + range: new Range(1, 8, 1, 8), + options: { + description: 'test', + before: { + content: 'bar' + } + } + }, + { + range: new Range(1, 8, 1, 8), + options: { + description: 'test', + before: { + content: 'bz' + } + } + }, + ]); + + // just sobarbzme text + + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 8), PositionAffinity.None), new Position(1, 8)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 9), PositionAffinity.None), new Position(1, 8)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 11), PositionAffinity.None), new Position(1, 11)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 12), PositionAffinity.None), new Position(1, 11)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 13), PositionAffinity.None), new Position(1, 13)); + + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 8), PositionAffinity.Left), new Position(1, 8)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 9), PositionAffinity.Left), new Position(1, 8)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 11), PositionAffinity.Left), new Position(1, 8)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 12), PositionAffinity.Left), new Position(1, 8)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 13), PositionAffinity.Left), new Position(1, 8)); + + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 8), PositionAffinity.Right), new Position(1, 13)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 9), PositionAffinity.Right), new Position(1, 13)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 11), PositionAffinity.Right), new Position(1, 13)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 12), PositionAffinity.Right), new Position(1, 13)); + assert.deepStrictEqual(viewModel.normalizePosition(new Position(1, 13), PositionAffinity.Right), new Position(1, 13)); + } + ); + }); });