Skip to content

Commit

Permalink
Merge pull request #107126 from Timmmm/atomic_tabs
Browse files Browse the repository at this point in the history
Add atomic tabs option
  • Loading branch information
alexdima authored Nov 21, 2020
2 parents 88856f1 + 45ec698 commit fb80c0e
Show file tree
Hide file tree
Showing 8 changed files with 611 additions and 240 deletions.
42 changes: 28 additions & 14 deletions src/vs/editor/browser/controller/mouseTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ViewContext } from 'vs/editor/common/view/viewContext';
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';

export interface IViewZoneData {
viewZoneId: string;
Expand Down Expand Up @@ -239,6 +240,7 @@ export class HitTestContext {
public readonly layoutInfo: EditorLayoutInfo;
public readonly viewDomNode: HTMLElement;
public readonly lineHeight: number;
public readonly atomicSoftTabs: boolean;
public readonly typicalHalfwidthCharacterWidth: number;
public readonly lastRenderData: PointerHandlerLastRenderData;

Expand All @@ -251,6 +253,7 @@ export class HitTestContext {
this.layoutInfo = options.get(EditorOption.layoutInfo);
this.viewDomNode = viewHelper.viewDomNode;
this.lineHeight = options.get(EditorOption.lineHeight);
this.atomicSoftTabs = options.get(EditorOption.atomicSoftTabs);
this.typicalHalfwidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth;
this.lastRenderData = lastRenderData;
this._context = context;
Expand Down Expand Up @@ -1010,6 +1013,17 @@ export class MouseTargetFactory {
};
}

private static _snapToSoftTabBoundary(position: Position, viewModel: IViewModel): Position {
const minColumn = viewModel.getLineMinColumn(position.lineNumber);
const lineContent = viewModel.getLineContent(position.lineNumber);
const { tabSize } = viewModel.getTextModelOptions();
const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - minColumn, tabSize, Direction.Nearest);
if (newPosition !== -1) {
return new Position(position.lineNumber, newPosition + minColumn);
}
return position;
}

private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): IHitTestResult {
// State of the art (18.10.2012):
// The spec says browsers should support document.caretPositionFromPoint, but nobody implemented it (http://dev.w3.org/csswg/cssom-view/)
Expand All @@ -1028,24 +1042,24 @@ export class MouseTargetFactory {

// Thank you browsers for making this so 'easy' :)

let result: IHitTestResult;
if (typeof document.caretRangeFromPoint === 'function') {

return this._doHitTestWithCaretRangeFromPoint(ctx, request);

result = this._doHitTestWithCaretRangeFromPoint(ctx, request);
} else if ((<any>document).caretPositionFromPoint) {

return this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates());

result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates());
} else if ((<any>document.body).createTextRange) {

return this._doHitTestWithMoveToPoint(ctx, request.pos.toClientCoordinates());

result = this._doHitTestWithMoveToPoint(ctx, request.pos.toClientCoordinates());
} else {
result = {
position: null,
hitTarget: null
};
}

return {
position: null,
hitTarget: null
};
// Snap to the nearest soft tab boundary if atomic soft tabs are enabled.
if (result.position && ctx.atomicSoftTabs) {
result.position = this._snapToSoftTabBoundary(result.position, ctx.model);
}
return result;
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@ export interface IEditorOptions {
* Defaults to advanced.
*/
autoIndent?: 'none' | 'keep' | 'brackets' | 'advanced' | 'full';
/**
* Emulate selection behaviour of hard tabs when using soft tabs (spaces) for indentation.
* This means selection will snap to indentation boundaries.
*/
atomicSoftTabs?: boolean;
/**
* Enable format on type.
* Defaults to false.
Expand Down Expand Up @@ -3631,6 +3636,7 @@ export const enum EditorOption {
autoIndent,
automaticLayout,
autoSurround,
atomicSoftTabs,
codeLens,
codeLensFontFamily,
codeLensFontSize,
Expand Down Expand Up @@ -3859,6 +3865,10 @@ export const EditorOptions = {
description: nls.localize('autoSurround', "Controls whether the editor should automatically surround selections when typing quotes or brackets.")
}
)),
atomicSoftTabs: register(new EditorBooleanOption(
EditorOption.atomicSoftTabs, 'atomicSoftTabs', false,
{ description: nls.localize('atomicSoftTabs', "Emulate selection behaviour of hard tabs when using soft tabs (spaces) for indentation. This means selection will snap to indentation boundaries.") }
)),
codeLens: register(new EditorBooleanOption(
EditorOption.codeLens, 'codeLens', true,
{ description: nls.localize('codeLens', "Controls whether the editor shows CodeLens.") }
Expand Down
160 changes: 160 additions & 0 deletions src/vs/editor/common/controller/cursorAtomicMoveOperations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CharCode } from 'vs/base/common/charCode';
import { CursorColumns } from 'vs/editor/common/controller/cursorCommon';

export const enum Direction {
Left,
Right,
Nearest,
}

export class AtomicTabMoveOperations {
/**
* Get the visible column at the position. If we get to a non-whitespace character first
* or past the end of string then return -1.
*
* **Note** `position` and the return value are 0-based.
*/
public static whitespaceVisibleColumn(lineContent: string, position: number, tabSize: number): [number, number, number] {
const lineLength = lineContent.length;
let visibleColumn = 0;
let prevTabStopPosition = -1;
let prevTabStopVisibleColumn = -1;
for (let i = 0; i < lineLength; i++) {
if (i === position) {
return [prevTabStopPosition, prevTabStopVisibleColumn, visibleColumn];
}
if (visibleColumn % tabSize === 0) {
prevTabStopPosition = i;
prevTabStopVisibleColumn = visibleColumn;
}
const chCode = lineContent.charCodeAt(i);
switch (chCode) {
case CharCode.Space:
visibleColumn += 1;
break;
case CharCode.Tab:
// Skip to the next multiple of tabSize.
visibleColumn = CursorColumns.nextRenderTabStop(visibleColumn, tabSize);
break;
default:
return [-1, -1, -1];
}
}
if (position === lineLength) {
return [prevTabStopPosition, prevTabStopVisibleColumn, visibleColumn];
}
return [-1, -1, -1];
}

/**
* Return the position that should result from a move left, right or to the
* nearest tab, if atomic tabs are enabled. Left and right are used for the
* arrow key movements, nearest is used for mouse selection. It returns
* -1 if atomic tabs are not relevant and you should fall back to normal
* behaviour.
*
* **Note**: `position` and the return value are 0-based.
*/
public static atomicPosition(lineContent: string, position: number, tabSize: number, direction: Direction): number {
const lineLength = lineContent.length;

// Get the 0-based visible column corresponding to the position, or return
// -1 if it is not in the initial whitespace.
const [prevTabStopPosition, prevTabStopVisibleColumn, visibleColumn] = AtomicTabMoveOperations.whitespaceVisibleColumn(lineContent, position, tabSize);

if (visibleColumn === -1) {
return -1;
}

// Is the output left or right of the current position. The case for nearest
// where it is the same as the current position is handled in the switch.
let left: boolean;
switch (direction) {
case Direction.Left:
left = true;
break;
case Direction.Right:
left = false;
break;
case Direction.Nearest:
// The code below assumes the output position is either left or right
// of the input position. If it is the same, return immediately.
if (visibleColumn % tabSize === 0) {
return position;
}
// Go to the nearest indentation.
left = visibleColumn % tabSize <= (tabSize / 2);
break;
}

// If going left, we can just use the info about the last tab stop position and
// last tab stop visible column that we computed in the first walk over the whitespace.
if (left) {
if (prevTabStopPosition === -1) {
return -1;
}
// If the direction is left, we need to keep scanning right to ensure
// that targetVisibleColumn + tabSize is before non-whitespace.
// This is so that when we press left at the end of a partial
// indentation it only goes one character. For example ' foo' with
// tabSize 4, should jump from position 6 to position 5, not 4.
let currentVisibleColumn = prevTabStopVisibleColumn;
for (let i = prevTabStopPosition; i < lineLength; ++i) {
if (currentVisibleColumn === prevTabStopVisibleColumn + tabSize) {
// It is a full indentation.
return prevTabStopPosition;
}

const chCode = lineContent.charCodeAt(i);
switch (chCode) {
case CharCode.Space:
currentVisibleColumn += 1;
break;
case CharCode.Tab:
currentVisibleColumn = CursorColumns.nextRenderTabStop(currentVisibleColumn, tabSize);
break;
default:
return -1;
}
}
if (currentVisibleColumn === prevTabStopVisibleColumn + tabSize) {
return prevTabStopPosition;
}
// It must have been a partial indentation.
return -1;
}

// We are going right.
const targetVisibleColumn = CursorColumns.nextRenderTabStop(visibleColumn, tabSize);

// We can just continue from where whitespaceVisibleColumn got to.
let currentVisibleColumn = visibleColumn;
for (let i = position; i < lineLength; i++) {
if (currentVisibleColumn === targetVisibleColumn) {
return i;
}

const chCode = lineContent.charCodeAt(i);
switch (chCode) {
case CharCode.Space:
currentVisibleColumn += 1;
break;
case CharCode.Tab:
currentVisibleColumn = CursorColumns.nextRenderTabStop(currentVisibleColumn, tabSize);
break;
default:
return -1;
}
}
// This condition handles when the target column is at the end of the line.
if (currentVisibleColumn === targetVisibleColumn) {
return lineLength;
}
return -1;
}
}
6 changes: 4 additions & 2 deletions src/vs/editor/common/controller/cursorCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class CursorConfiguration {
public readonly tabSize: number;
public readonly indentSize: number;
public readonly insertSpaces: boolean;
public readonly atomicSoftTabs: boolean;
public readonly pageSize: number;
public readonly lineHeight: number;
public readonly useTabStops: boolean;
Expand Down Expand Up @@ -113,6 +114,7 @@ export class CursorConfiguration {
this.tabSize = modelOptions.tabSize;
this.indentSize = modelOptions.indentSize;
this.insertSpaces = modelOptions.insertSpaces;
this.atomicSoftTabs = options.get(EditorOption.atomicSoftTabs);
this.lineHeight = options.get(EditorOption.lineHeight);
this.pageSize = Math.max(1, Math.floor(layoutInfo.height / this.lineHeight) - 2);
this.useTabStops = options.get(EditorOption.useTabStops);
Expand Down Expand Up @@ -554,14 +556,14 @@ export class CursorColumns {
}

/**
* ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns)
* ATTENTION: This works with 0-based columns (as opposed to the regular 1-based columns)
*/
public static prevRenderTabStop(column: number, tabSize: number): number {
return column - 1 - (column - 1) % tabSize;
}

/**
* ATTENTION: This works with 0-based columns (as oposed to the regular 1-based columns)
* ATTENTION: This works with 0-based columns (as opposed to the regular 1-based columns)
*/
public static prevIndentTabStop(column: number, indentSize: number): number {
return column - 1 - (column - 1) % indentSize;
Expand Down
29 changes: 27 additions & 2 deletions src/vs/editor/common/controller/cursorMoveOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Position } from 'vs/editor/common/core/position';
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';

export class CursorPosition {
_cursorPositionBrand: void;
Expand Down Expand Up @@ -35,8 +36,20 @@ export class MoveOperations {
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 - minColumn, tabSize, Direction.Left);
if (newPosition === -1) {
return this.leftPosition(model, lineNumber, column);
}
return new Position(lineNumber, minColumn + newPosition);
}

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

Expand Down Expand Up @@ -67,8 +80,20 @@ export class MoveOperations {
return new Position(lineNumber, column);
}

public static rightPositionAtomicSoftTabs(model: ICursorSimpleModel, lineNumber: number, column: number, tabSize: number, indentSize: number): Position {
const minColumn = model.getLineMinColumn(lineNumber);
const lineContent = model.getLineContent(lineNumber);
const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, column - minColumn, tabSize, Direction.Right);
if (newPosition === -1) {
return this.rightPosition(model, lineNumber, column);
}
return new Position(lineNumber, minColumn + newPosition);
}

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

Expand Down
Loading

0 comments on commit fb80c0e

Please sign in to comment.