Skip to content

Commit

Permalink
Fixes microsoft#105730. Word wrapping did not work well with left/rig…
Browse files Browse the repository at this point in the history
…ht cursor movement if there is a selection.
  • Loading branch information
hediet committed May 10, 2021
1 parent ea727e9 commit 67456c1
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 78 deletions.
3 changes: 2 additions & 1 deletion src/vs/editor/common/controller/cursorCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, TextModelResolvedOptions } from 'vs/editor/common/model';
import { ITextModel, PositionNormalizationAffinity, 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';
Expand Down Expand Up @@ -221,6 +221,7 @@ export interface ICursorSimpleModel {
getLineMaxColumn(lineNumber: number): number;
getLineFirstNonWhitespaceColumn(lineNumber: number): number;
getLineLastNonWhitespaceColumn(lineNumber: number): number;
normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/common/controller/cursorDeleteOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class DeleteOperations {

if (deleteSelection.isEmpty()) {
let position = selection.getPosition();
let rightOfPosition = MoveOperations.right(config, model, position.lineNumber, position.column);
let rightOfPosition = MoveOperations.right(config, model, position);
deleteSelection = new Range(
rightOfPosition.lineNumber,
rightOfPosition.column,
Expand Down
56 changes: 10 additions & 46 deletions src/vs/editor/common/controller/cursorMoveCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,29 +419,11 @@ export class CursorMoveCommands {
}

private static _moveLeft(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] {
const hasMultipleCursors = (cursors.length > 1);
let result: PartialCursorState[] = [];
for (let i = 0, len = cursors.length; i < len; i++) {
const cursor = cursors[i];
const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection();
let newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns);

if (skipWrappingPointStop
&& noOfColumns === 1
&& cursor.viewState.position.column === viewModel.getLineMinColumn(cursor.viewState.position.lineNumber)
&& newViewState.position.lineNumber !== cursor.viewState.position.lineNumber
) {
// moved over to the previous view line
const newViewModelPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position);
if (newViewModelPosition.lineNumber === cursor.modelState.position.lineNumber) {
// stayed on the same model line => pass wrapping point where 2 view positions map to a single model position
newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, newViewState, inSelectionMode, 1);
}
}

result[i] = CursorState.fromViewState(newViewState);
}
return result;
return cursors.map(cursor =>
CursorState.fromViewState(
MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns)
)
);
}

private static _moveHalfLineLeft(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] {
Expand All @@ -456,29 +438,11 @@ export class CursorMoveCommands {
}

private static _moveRight(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] {
const hasMultipleCursors = (cursors.length > 1);
let result: PartialCursorState[] = [];
for (let i = 0, len = cursors.length; i < len; i++) {
const cursor = cursors[i];
const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection();
let newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns);

if (skipWrappingPointStop
&& noOfColumns === 1
&& cursor.viewState.position.column === viewModel.getLineMaxColumn(cursor.viewState.position.lineNumber)
&& newViewState.position.lineNumber !== cursor.viewState.position.lineNumber
) {
// moved over to the next view line
const newViewModelPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position);
if (newViewModelPosition.lineNumber === cursor.modelState.position.lineNumber) {
// stayed on the same model line => pass wrapping point where 2 view positions map to a single model position
newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, newViewState, inSelectionMode, 1);
}
}

result[i] = CursorState.fromViewState(newViewState);
}
return result;
return cursors.map(cursor =>
CursorState.fromViewState(
MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns)
)
);
}

private static _moveHalfLineRight(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] {
Expand Down
86 changes: 61 additions & 25 deletions src/vs/editor/common/controller/cursorMoveOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +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';

export class CursorPosition {
_cursorPositionBrand: void;
Expand All @@ -25,51 +26,84 @@ export class CursorPosition {
}

export class MoveOperations {

public static leftPosition(model: ICursorSimpleModel, lineNumber: number, column: number): Position {
if (column > model.getLineMinColumn(lineNumber)) {
column = column - strings.prevCharLength(model.getLineContent(lineNumber), column - 1);
} else if (lineNumber > 1) {
lineNumber = lineNumber - 1;
column = model.getLineMaxColumn(lineNumber);
public static leftPosition(model: ICursorSimpleModel, position: Position): Position {
if (position.column > model.getLineMinColumn(position.lineNumber)) {
return position.delta(undefined, -strings.prevCharLength(model.getLineContent(position.lineNumber), position.column - 1));
} else if (position.lineNumber > 1) {
const newLineNumber = position.lineNumber - 1;
return new Position(newLineNumber, model.getLineMaxColumn(newLineNumber));
} else {
return position;
}
return new Position(lineNumber, column);
}

public static leftPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number): Position {
const minColumn = model.getLineMinColumn(lineNumber);
const lineContent = model.getLineContent(lineNumber);
const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Left);
private static leftPositionAtomicSoftTabs(model: ICursorSimpleModel, position: Position, tabSize: number): Position {
const minColumn = model.getLineMinColumn(position.lineNumber);
const lineContent = model.getLineContent(position.lineNumber);
const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Left);
if (newPosition === -1 || newPosition + 1 < minColumn) {
return this.leftPosition(model, lineNumber, column);
return this.leftPosition(model, position);
}
return new Position(lineNumber, newPosition + 1);
return new Position(position.lineNumber, newPosition + 1);
}

public static left(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition {
private static left(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): CursorPosition {
const pos = config.stickyTabStops
? MoveOperations.leftPositionAtomicSoftTabs(model, lineNumber, column, config.tabSize)
: MoveOperations.leftPosition(model, lineNumber, column);
? MoveOperations.leftPositionAtomicSoftTabs(model, position, config.tabSize)
: MoveOperations.leftPosition(model, position);
return new CursorPosition(pos.lineNumber, pos.column, 0);
}

/**
* @param noOfColumns Must be either `1`
* or `Math.round(viewModel.getLineContent(viewLineNumber).length / 2)` (for half lines).
*/
public static moveLeft(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, noOfColumns: number): SingleCursorState {
let lineNumber: number,
column: number;

if (cursor.hasSelection() && !inSelectionMode) {
// If we are in selection mode, move left without selection cancels selection and puts cursor at the beginning of the selection
// If the user has a selection and does not want to extend it,
// put the cursor at the beginning of the selection.
lineNumber = cursor.selection.startLineNumber;
column = cursor.selection.startColumn;
} else {
let r = MoveOperations.left(config, model, cursor.position.lineNumber, cursor.position.column - (noOfColumns - 1));
lineNumber = r.lineNumber;
column = r.column;
// This has no effect if noOfColumns === 1.
// It is ok to do so in the half-line scenario.
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 p = MoveOperations.left(config, model, normalizedPos);

lineNumber = p.lineNumber;
column = p.column;
}

return cursor.move(inSelectionMode, lineNumber, column, 0);
}

/**
* Adjusts the column so that it is within min/max of the line.
*/
private static clipPositionColumn(position: Position, model: ICursorSimpleModel): Position {
return new Position(
position.lineNumber,
MoveOperations.clipRange(position.column, model.getLineMinColumn(position.lineNumber),
model.getLineMaxColumn(position.lineNumber))
);
}

private static clipRange(value: number, min: number, max: number): number {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}

public static rightPosition(model: ICursorSimpleModel, lineNumber: number, column: number): Position {
if (column < model.getLineMaxColumn(lineNumber)) {
column = column + strings.nextCharLength(model.getLineContent(lineNumber), column - 1);
Expand All @@ -89,10 +123,10 @@ export class MoveOperations {
return new Position(lineNumber, newPosition + 1);
}

public static right(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number): CursorPosition {
public static right(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): CursorPosition {
const pos = config.stickyTabStops
? MoveOperations.rightPositionAtomicSoftTabs(model, lineNumber, column, config.tabSize, config.indentSize)
: MoveOperations.rightPosition(model, lineNumber, column);
? MoveOperations.rightPositionAtomicSoftTabs(model, position.lineNumber, position.column, config.tabSize, config.indentSize)
: MoveOperations.rightPosition(model, position.lineNumber, position.column);
return new CursorPosition(pos.lineNumber, pos.column, 0);
}

Expand All @@ -105,7 +139,9 @@ export class MoveOperations {
lineNumber = cursor.selection.endLineNumber;
column = cursor.selection.endColumn;
} else {
let r = MoveOperations.right(config, model, cursor.position.lineNumber, cursor.position.column + (noOfColumns - 1));
const pos = cursor.position.delta(undefined, noOfColumns - 1);
const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionNormalizationAffinity.Right);
const r = MoveOperations.right(config, model, normalizedPos);
lineNumber = r.lineNumber;
column = r.column;
}
Expand Down
21 changes: 21 additions & 0 deletions src/vs/editor/common/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,27 @@ export interface ITextModel {
* @internal
*/
getAttachedEditorCount(): number;

/**
* 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.
* @internal
*/
normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position;
}

/**
* @internal
*/
export const enum PositionNormalizationAffinity {
/**
* Prefers the left most position.
*/
Left = 0,
/**
* Prefers the right most position.
*/
Right = 1,
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/vs/editor/common/model/textModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3028,6 +3028,9 @@ export class TextModel extends Disposable implements model.ITextModel {
}

//#endregion
normalizePosition(position: Position, affinity: model.PositionNormalizationAffinity): Position {
return position;
}
}

//#region Decorations
Expand Down
47 changes: 45 additions & 2 deletions src/vs/editor/common/viewModel/splitLinesCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { WrappingIndent } from 'vs/editor/common/config/editorOptions';
import { 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 } from 'vs/editor/common/model';
import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, ITextModel, PositionNormalizationAffinity } 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';
Expand Down Expand Up @@ -46,6 +46,7 @@ export interface ISplitLine {
getModelColumnOfViewPosition(outputLineIndex: number, outputColumn: number): number;
getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position;
getViewLineNumberOfModelPosition(deltaLineNumber: number, inputColumn: number): number;
normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position;
}

export interface IViewModelLinesCollection extends IDisposable {
Expand Down Expand Up @@ -75,6 +76,8 @@ 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;
}

export class CoordinatesConverter implements ICoordinatesConverter {
Expand Down Expand Up @@ -971,6 +974,15 @@ export class SplitLinesCollection implements IViewModelLinesCollection {

return finalResult;
}

normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position {
const viewLineNumber = this._toValidViewLineNumber(position.lineNumber);
const r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1);
const lineIndex = r.index;
const remainder = r.remainder;

return this.lines[lineIndex].normalizePosition(this.model, lineIndex + 1, remainder, position, affinity);
}
}

class VisibleIdentitySplitLine implements ISplitLine {
Expand Down Expand Up @@ -1046,6 +1058,10 @@ class VisibleIdentitySplitLine implements ISplitLine {
public getViewLineNumberOfModelPosition(deltaLineNumber: number, _inputColumn: number): number {
return deltaLineNumber;
}

public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position {
return outputPosition;
}
}

class InvisibleIdentitySplitLine implements ISplitLine {
Expand Down Expand Up @@ -1108,6 +1124,10 @@ class InvisibleIdentitySplitLine implements ISplitLine {
public getViewLineNumberOfModelPosition(_deltaLineNumber: number, _inputColumn: number): number {
throw new Error('Not supported');
}

public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position {
throw new Error('Not supported');
}
}

export class SplitLine implements ISplitLine {
Expand Down Expand Up @@ -1190,6 +1210,10 @@ export class SplitLine implements ISplitLine {
if (!this._isVisible) {
throw new Error('Not supported');
}
return this._getViewLineMinColumn(outputLineIndex);
}

private _getViewLineMinColumn(outputLineIndex: number): number {
if (outputLineIndex > 0) {
return this._lineBreakData.wrappedTextIndentLength + 1;
}
Expand All @@ -1200,7 +1224,7 @@ export class SplitLine implements ISplitLine {
if (!this._isVisible) {
throw new Error('Not supported');
}
return this.getViewLineContent(model, modelLineNumber, outputLineIndex).length + 1;
return this.getViewLineLength(model, modelLineNumber, outputLineIndex) + 1;
}

public getViewLineData(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): ViewLineData {
Expand Down Expand Up @@ -1298,6 +1322,21 @@ export class SplitLine implements ISplitLine {
const r = LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets, inputColumn - 1);
return (deltaLineNumber + r.outputLineIndex);
}

public normalizePosition(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number, outputPosition: Position, affinity: PositionNormalizationAffinity): Position {
if (affinity === PositionNormalizationAffinity.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) {
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;
}
}

let _spaces: string[] = [''];
Expand Down Expand Up @@ -1532,6 +1571,10 @@ export class IdentityLinesCollection implements IViewModelLinesCollection {
public getDecorationsInRange(range: Range, ownerId: number, filterOutValidation: boolean): IModelDecoration[] {
return this.model.getDecorationsInRange(range, ownerId, filterOutValidation);
}

normalizePosition(position: Position, affinity: PositionNormalizationAffinity): Position {
return this.model.normalizePosition(position, affinity);
}
}

class OverviewRulerDecorations {
Expand Down
Loading

0 comments on commit 67456c1

Please sign in to comment.