Path: blob/main/src/vs/editor/common/cursor/cursorMoveOperations.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 * as strings from '../../../base/common/strings.js';6import { Constants } from '../../../base/common/uint.js';7import { CursorColumns } from '../core/cursorColumns.js';8import { Position } from '../core/position.js';9import { Range } from '../core/range.js';10import { AtomicTabMoveOperations, Direction } from './cursorAtomicMoveOperations.js';11import { CursorConfiguration, ICursorSimpleModel, SelectionStartKind, SingleCursorState } from '../cursorCommon.js';12import { PositionAffinity } from '../model.js';1314export class CursorPosition {15_cursorPositionBrand: void = undefined;1617public readonly lineNumber: number;18public readonly column: number;19public readonly leftoverVisibleColumns: number;2021constructor(lineNumber: number, column: number, leftoverVisibleColumns: number) {22this.lineNumber = lineNumber;23this.column = column;24this.leftoverVisibleColumns = leftoverVisibleColumns;25}26}2728export class MoveOperations {29public static leftPosition(model: ICursorSimpleModel, position: Position): Position {30if (position.column > model.getLineMinColumn(position.lineNumber)) {31return position.delta(undefined, -strings.prevCharLength(model.getLineContent(position.lineNumber), position.column - 1));32} else if (position.lineNumber > 1) {33const newLineNumber = position.lineNumber - 1;34return new Position(newLineNumber, model.getLineMaxColumn(newLineNumber));35} else {36return position;37}38}3940private static leftPositionAtomicSoftTabs(model: ICursorSimpleModel, position: Position, tabSize: number): Position {41if (position.column <= model.getLineIndentColumn(position.lineNumber)) {42const minColumn = model.getLineMinColumn(position.lineNumber);43const lineContent = model.getLineContent(position.lineNumber);44const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, Direction.Left);45if (newPosition !== -1 && newPosition + 1 >= minColumn) {46return new Position(position.lineNumber, newPosition + 1);47}48}49return this.leftPosition(model, position);50}5152private static left(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): CursorPosition {53const pos = config.stickyTabStops54? MoveOperations.leftPositionAtomicSoftTabs(model, position, config.tabSize)55: MoveOperations.leftPosition(model, position);56return new CursorPosition(pos.lineNumber, pos.column, 0);57}5859/**60* @param noOfColumns Must be either `1`61* or `Math.round(viewModel.getLineContent(viewLineNumber).length / 2)` (for half lines).62*/63public static moveLeft(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, noOfColumns: number): SingleCursorState {64let lineNumber: number,65column: number;6667if (cursor.hasSelection() && !inSelectionMode) {68// If the user has a selection and does not want to extend it,69// put the cursor at the beginning of the selection.70lineNumber = cursor.selection.startLineNumber;71column = cursor.selection.startColumn;72} else {73// This has no effect if noOfColumns === 1.74// It is ok to do so in the half-line scenario.75const pos = cursor.position.delta(undefined, -(noOfColumns - 1));76// We clip the position before normalization, as normalization is not defined77// for possibly negative columns.78const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionAffinity.Left);79const p = MoveOperations.left(config, model, normalizedPos);8081lineNumber = p.lineNumber;82column = p.column;83}8485return cursor.move(inSelectionMode, lineNumber, column, 0);86}8788/**89* Adjusts the column so that it is within min/max of the line.90*/91private static clipPositionColumn(position: Position, model: ICursorSimpleModel): Position {92return new Position(93position.lineNumber,94MoveOperations.clipRange(position.column, model.getLineMinColumn(position.lineNumber),95model.getLineMaxColumn(position.lineNumber))96);97}9899private static clipRange(value: number, min: number, max: number): number {100if (value < min) {101return min;102}103if (value > max) {104return max;105}106return value;107}108109public static rightPosition(model: ICursorSimpleModel, lineNumber: number, column: number): Position {110if (column < model.getLineMaxColumn(lineNumber)) {111column = column + strings.nextCharLength(model.getLineContent(lineNumber), column - 1);112} else if (lineNumber < model.getLineCount()) {113lineNumber = lineNumber + 1;114column = model.getLineMinColumn(lineNumber);115}116return new Position(lineNumber, column);117}118119public static rightPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number, indentSize: number): Position {120if (column < model.getLineIndentColumn(lineNumber)) {121const lineContent = model.getLineContent(lineNumber);122const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - 1, tabSize, Direction.Right);123if (newPosition !== -1) {124return new Position(lineNumber, newPosition + 1);125}126}127return this.rightPosition(model, lineNumber, column);128}129130public static right(config: CursorConfiguration, model: ICursorSimpleModel, position: Position): CursorPosition {131const pos = config.stickyTabStops132? MoveOperations.rightPositionAtomicSoftTabs(model, position.lineNumber, position.column, config.tabSize, config.indentSize)133: MoveOperations.rightPosition(model, position.lineNumber, position.column);134return new CursorPosition(pos.lineNumber, pos.column, 0);135}136137public static moveRight(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, noOfColumns: number): SingleCursorState {138let lineNumber: number,139column: number;140141if (cursor.hasSelection() && !inSelectionMode) {142// If we are in selection mode, move right without selection cancels selection and puts cursor at the end of the selection143lineNumber = cursor.selection.endLineNumber;144column = cursor.selection.endColumn;145} else {146const pos = cursor.position.delta(undefined, noOfColumns - 1);147const normalizedPos = model.normalizePosition(MoveOperations.clipPositionColumn(pos, model), PositionAffinity.Right);148const r = MoveOperations.right(config, model, normalizedPos);149lineNumber = r.lineNumber;150column = r.column;151}152153return cursor.move(inSelectionMode, lineNumber, column, 0);154}155156public static vertical(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, newLineNumber: number, allowMoveOnEdgeLine: boolean, normalizationAffinity?: PositionAffinity): CursorPosition {157const currentVisibleColumn = CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize) + leftoverVisibleColumns;158const lineCount = model.getLineCount();159const wasOnFirstPosition = (lineNumber === 1 && column === 1);160const wasOnLastPosition = (lineNumber === lineCount && column === model.getLineMaxColumn(lineNumber));161const wasAtEdgePosition = (newLineNumber < lineNumber ? wasOnFirstPosition : wasOnLastPosition);162163lineNumber = newLineNumber;164if (lineNumber < 1) {165lineNumber = 1;166if (allowMoveOnEdgeLine) {167column = model.getLineMinColumn(lineNumber);168} else {169column = Math.min(model.getLineMaxColumn(lineNumber), column);170}171} else if (lineNumber > lineCount) {172lineNumber = lineCount;173if (allowMoveOnEdgeLine) {174column = model.getLineMaxColumn(lineNumber);175} else {176column = Math.min(model.getLineMaxColumn(lineNumber), column);177}178} else {179column = config.columnFromVisibleColumn(model, lineNumber, currentVisibleColumn);180}181182if (wasAtEdgePosition) {183leftoverVisibleColumns = 0;184} else {185leftoverVisibleColumns = currentVisibleColumn - CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize);186}187188if (normalizationAffinity !== undefined) {189const position = new Position(lineNumber, column);190const newPosition = model.normalizePosition(position, normalizationAffinity);191leftoverVisibleColumns = leftoverVisibleColumns + (column - newPosition.column);192lineNumber = newPosition.lineNumber;193column = newPosition.column;194}195return new CursorPosition(lineNumber, column, leftoverVisibleColumns);196}197198public static down(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnLastLine: boolean): CursorPosition {199return this.vertical(config, model, lineNumber, column, leftoverVisibleColumns, lineNumber + count, allowMoveOnLastLine, PositionAffinity.RightOfInjectedText);200}201202public static moveDown(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, linesCount: number): SingleCursorState {203let lineNumber: number,204column: number;205206if (cursor.hasSelection() && !inSelectionMode) {207// If we are in selection mode, move down acts relative to the end of selection208lineNumber = cursor.selection.endLineNumber;209column = cursor.selection.endColumn;210} else {211lineNumber = cursor.position.lineNumber;212column = cursor.position.column;213}214215let i = 0;216let r: CursorPosition;217do {218r = MoveOperations.down(config, model, lineNumber + i, column, cursor.leftoverVisibleColumns, linesCount, true);219const np = model.normalizePosition(new Position(r.lineNumber, r.column), PositionAffinity.None);220if (np.lineNumber > lineNumber) {221break;222}223} while (i++ < 10 && lineNumber + i < model.getLineCount());224225return cursor.move(inSelectionMode, r.lineNumber, r.column, r.leftoverVisibleColumns);226}227228public static translateDown(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState): SingleCursorState {229const selection = cursor.selection;230231const selectionStart = MoveOperations.down(config, model, selection.selectionStartLineNumber, selection.selectionStartColumn, cursor.selectionStartLeftoverVisibleColumns, 1, false);232const position = MoveOperations.down(config, model, selection.positionLineNumber, selection.positionColumn, cursor.leftoverVisibleColumns, 1, false);233234return new SingleCursorState(235new Range(selectionStart.lineNumber, selectionStart.column, selectionStart.lineNumber, selectionStart.column),236SelectionStartKind.Simple,237selectionStart.leftoverVisibleColumns,238new Position(position.lineNumber, position.column),239position.leftoverVisibleColumns240);241}242243public static up(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnFirstLine: boolean): CursorPosition {244return this.vertical(config, model, lineNumber, column, leftoverVisibleColumns, lineNumber - count, allowMoveOnFirstLine, PositionAffinity.LeftOfInjectedText);245}246247public static moveUp(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, linesCount: number): SingleCursorState {248let lineNumber: number,249column: number;250251if (cursor.hasSelection() && !inSelectionMode) {252// If we are in selection mode, move up acts relative to the beginning of selection253lineNumber = cursor.selection.startLineNumber;254column = cursor.selection.startColumn;255} else {256lineNumber = cursor.position.lineNumber;257column = cursor.position.column;258}259260const r = MoveOperations.up(config, model, lineNumber, column, cursor.leftoverVisibleColumns, linesCount, true);261262return cursor.move(inSelectionMode, r.lineNumber, r.column, r.leftoverVisibleColumns);263}264265public static translateUp(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState): SingleCursorState {266267const selection = cursor.selection;268269const selectionStart = MoveOperations.up(config, model, selection.selectionStartLineNumber, selection.selectionStartColumn, cursor.selectionStartLeftoverVisibleColumns, 1, false);270const position = MoveOperations.up(config, model, selection.positionLineNumber, selection.positionColumn, cursor.leftoverVisibleColumns, 1, false);271272return new SingleCursorState(273new Range(selectionStart.lineNumber, selectionStart.column, selectionStart.lineNumber, selectionStart.column),274SelectionStartKind.Simple,275selectionStart.leftoverVisibleColumns,276new Position(position.lineNumber, position.column),277position.leftoverVisibleColumns278);279}280281private static _isBlankLine(model: ICursorSimpleModel, lineNumber: number): boolean {282if (model.getLineFirstNonWhitespaceColumn(lineNumber) === 0) {283// empty or contains only whitespace284return true;285}286return false;287}288289public static moveToPrevBlankLine(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState {290let lineNumber = cursor.position.lineNumber;291292// If our current line is blank, move to the previous non-blank line293while (lineNumber > 1 && this._isBlankLine(model, lineNumber)) {294lineNumber--;295}296297// Find the previous blank line298while (lineNumber > 1 && !this._isBlankLine(model, lineNumber)) {299lineNumber--;300}301302return cursor.move(inSelectionMode, lineNumber, model.getLineMinColumn(lineNumber), 0);303}304305public static moveToNextBlankLine(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState {306const lineCount = model.getLineCount();307let lineNumber = cursor.position.lineNumber;308309// If our current line is blank, move to the next non-blank line310while (lineNumber < lineCount && this._isBlankLine(model, lineNumber)) {311lineNumber++;312}313314// Find the next blank line315while (lineNumber < lineCount && !this._isBlankLine(model, lineNumber)) {316lineNumber++;317}318319return cursor.move(inSelectionMode, lineNumber, model.getLineMinColumn(lineNumber), 0);320}321322public static moveToBeginningOfLine(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState {323const lineNumber = cursor.position.lineNumber;324const minColumn = model.getLineMinColumn(lineNumber);325const firstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(lineNumber) || minColumn;326327let column: number;328329const relevantColumnNumber = cursor.position.column;330if (relevantColumnNumber === firstNonBlankColumn) {331column = minColumn;332} else {333column = firstNonBlankColumn;334}335336return cursor.move(inSelectionMode, lineNumber, column, 0);337}338339public static moveToEndOfLine(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean, sticky: boolean): SingleCursorState {340const lineNumber = cursor.position.lineNumber;341const maxColumn = model.getLineMaxColumn(lineNumber);342return cursor.move(inSelectionMode, lineNumber, maxColumn, sticky ? Constants.MAX_SAFE_SMALL_INTEGER - maxColumn : 0);343}344345public static moveToBeginningOfBuffer(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState {346return cursor.move(inSelectionMode, 1, 1, 0);347}348349public static moveToEndOfBuffer(config: CursorConfiguration, model: ICursorSimpleModel, cursor: SingleCursorState, inSelectionMode: boolean): SingleCursorState {350const lastLineNumber = model.getLineCount();351const lastColumn = model.getLineMaxColumn(lastLineNumber);352353return cursor.move(inSelectionMode, lastLineNumber, lastColumn, 0);354}355}356357358