Path: blob/main/src/vs/editor/common/cursor/cursorAtomicMoveOperations.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 { CharCode } from '../../../base/common/charCode.js';6import { CursorColumns } from '../core/cursorColumns.js';78export const enum Direction {9Left,10Right,11Nearest,12}1314export class AtomicTabMoveOperations {15/**16* Get the visible column at the position. If we get to a non-whitespace character first17* or past the end of string then return -1.18*19* **Note** `position` and the return value are 0-based.20*/21public static whitespaceVisibleColumn(lineContent: string, position: number, tabSize: number): [number, number, number] {22const lineLength = lineContent.length;23let visibleColumn = 0;24let prevTabStopPosition = -1;25let prevTabStopVisibleColumn = -1;26for (let i = 0; i < lineLength; i++) {27if (i === position) {28return [prevTabStopPosition, prevTabStopVisibleColumn, visibleColumn];29}30if (visibleColumn % tabSize === 0) {31prevTabStopPosition = i;32prevTabStopVisibleColumn = visibleColumn;33}34const chCode = lineContent.charCodeAt(i);35switch (chCode) {36case CharCode.Space:37visibleColumn += 1;38break;39case CharCode.Tab:40// Skip to the next multiple of tabSize.41visibleColumn = CursorColumns.nextRenderTabStop(visibleColumn, tabSize);42break;43default:44return [-1, -1, -1];45}46}47if (position === lineLength) {48return [prevTabStopPosition, prevTabStopVisibleColumn, visibleColumn];49}50return [-1, -1, -1];51}5253/**54* Return the position that should result from a move left, right or to the55* nearest tab, if atomic tabs are enabled. Left and right are used for the56* arrow key movements, nearest is used for mouse selection. It returns57* -1 if atomic tabs are not relevant and you should fall back to normal58* behaviour.59*60* **Note**: `position` and the return value are 0-based.61*/62public static atomicPosition(lineContent: string, position: number, tabSize: number, direction: Direction): number {63const lineLength = lineContent.length;6465// Get the 0-based visible column corresponding to the position, or return66// -1 if it is not in the initial whitespace.67const [prevTabStopPosition, prevTabStopVisibleColumn, visibleColumn] = AtomicTabMoveOperations.whitespaceVisibleColumn(lineContent, position, tabSize);6869if (visibleColumn === -1) {70return -1;71}7273// Is the output left or right of the current position. The case for nearest74// where it is the same as the current position is handled in the switch.75let left: boolean;76switch (direction) {77case Direction.Left:78left = true;79break;80case Direction.Right:81left = false;82break;83case Direction.Nearest:84// The code below assumes the output position is either left or right85// of the input position. If it is the same, return immediately.86if (visibleColumn % tabSize === 0) {87return position;88}89// Go to the nearest indentation.90left = visibleColumn % tabSize <= (tabSize / 2);91break;92}9394// If going left, we can just use the info about the last tab stop position and95// last tab stop visible column that we computed in the first walk over the whitespace.96if (left) {97if (prevTabStopPosition === -1) {98return -1;99}100// If the direction is left, we need to keep scanning right to ensure101// that targetVisibleColumn + tabSize is before non-whitespace.102// This is so that when we press left at the end of a partial103// indentation it only goes one character. For example ' foo' with104// tabSize 4, should jump from position 6 to position 5, not 4.105let currentVisibleColumn = prevTabStopVisibleColumn;106for (let i = prevTabStopPosition; i < lineLength; ++i) {107if (currentVisibleColumn === prevTabStopVisibleColumn + tabSize) {108// It is a full indentation.109return prevTabStopPosition;110}111112const chCode = lineContent.charCodeAt(i);113switch (chCode) {114case CharCode.Space:115currentVisibleColumn += 1;116break;117case CharCode.Tab:118currentVisibleColumn = CursorColumns.nextRenderTabStop(currentVisibleColumn, tabSize);119break;120default:121return -1;122}123}124if (currentVisibleColumn === prevTabStopVisibleColumn + tabSize) {125return prevTabStopPosition;126}127// It must have been a partial indentation.128return -1;129}130131// We are going right.132const targetVisibleColumn = CursorColumns.nextRenderTabStop(visibleColumn, tabSize);133134// We can just continue from where whitespaceVisibleColumn got to.135let currentVisibleColumn = visibleColumn;136for (let i = position; i < lineLength; i++) {137if (currentVisibleColumn === targetVisibleColumn) {138return i;139}140141const chCode = lineContent.charCodeAt(i);142switch (chCode) {143case CharCode.Space:144currentVisibleColumn += 1;145break;146case CharCode.Tab:147currentVisibleColumn = CursorColumns.nextRenderTabStop(currentVisibleColumn, tabSize);148break;149default:150return -1;151}152}153// This condition handles when the target column is at the end of the line.154if (currentVisibleColumn === targetVisibleColumn) {155return lineLength;156}157return -1;158}159}160161162