Repository: microsoft/vscode (TypeScript)
Date: 2021-05-04 12:26:08+00:00
Task type: Bug Fix

Issue:
Left delete behavior in some languages is changed (when useTabStops: false)
This is a follow up of #84897.
When `useTabStops: false` is configured, the original bug still occurs.

Left delete behavior in some languages is changed (when useTabStops: false)
This is a follow up of #84897.
When `useTabStops: false` is configured, the original bug still occurs.



Issue discussion:

Solution patch:
diff --git a/src/vs/editor/common/controller/cursorDeleteOperations.ts b/src/vs/editor/common/controller/cursorDeleteOperations.ts
--- a/src/vs/editor/common/controller/cursorDeleteOperations.ts
+++ b/src/vs/editor/common/controller/cursorDeleteOperations.ts
@@ -12,6 +12,7 @@ import { Range } from 'vs/editor/common/core/range';
 import { Selection } from 'vs/editor/common/core/selection';
 import { ICommand } from 'vs/editor/common/editorCommon';
 import { StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
+import { Position } from 'vs/editor/common/core/position';
 
 export class DeleteOperations {
 
@@ -141,63 +142,70 @@ export class DeleteOperations {
 	}
 
 	public static deleteLeft(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[], autoClosedCharacters: Range[]): [boolean, Array<ICommand | null>] {
-
 		if (this.isAutoClosingPairDelete(config.autoClosingDelete, config.autoClosingBrackets, config.autoClosingQuotes, config.autoClosingPairs.autoClosingPairsOpenByEnd, model, selections, autoClosedCharacters)) {
 			return this._runAutoClosingPairDelete(config, model, selections);
 		}
 
-		let commands: Array<ICommand | null> = [];
+		const commands: Array<ICommand | null> = [];
 		let shouldPushStackElementBefore = (prevEditOperationType !== EditOperationType.DeletingLeft);
 		for (let i = 0, len = selections.length; i < len; i++) {
-			const selection = selections[i];
-
-			let deleteSelection: Range = selection;
-
-			if (deleteSelection.isEmpty()) {
-				let position = selection.getPosition();
-
-				if (config.useTabStops && position.column > 1) {
-					let lineContent = model.getLineContent(position.lineNumber);
-
-					let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
-					let lastIndentationColumn = (
-						firstNonWhitespaceIndex === -1
-							? /* entire string is whitespace */lineContent.length + 1
-							: firstNonWhitespaceIndex + 1
-					);
-
-					if (position.column <= lastIndentationColumn) {
-						let fromVisibleColumn = CursorColumns.visibleColumnFromColumn2(config, model, position);
-						let toVisibleColumn = CursorColumns.prevIndentTabStop(fromVisibleColumn, config.indentSize);
-						let toColumn = CursorColumns.columnFromVisibleColumn2(config, model, position.lineNumber, toVisibleColumn);
-						deleteSelection = new Range(position.lineNumber, toColumn, position.lineNumber, position.column);
-					} else {
-						deleteSelection = new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column);
-					}
-				} else {
-					let leftOfPosition = MoveOperations.left(config, model, position.lineNumber, position.column);
-					deleteSelection = new Range(
-						leftOfPosition.lineNumber,
-						leftOfPosition.column,
-						position.lineNumber,
-						position.column
-					);
-				}
-			}
+			let deleteRange = DeleteOperations.getDeleteRange(selections[i], model, config);
 
-			if (deleteSelection.isEmpty()) {
-				// Probably at beginning of file => ignore
+			// Ignore empty delete ranges, as they have no effect
+			// They happen if the cursor is at the beginning of the file.
+			if (deleteRange.isEmpty()) {
 				commands[i] = null;
 				continue;
 			}
 
-			if (deleteSelection.startLineNumber !== deleteSelection.endLineNumber) {
+			if (deleteRange.startLineNumber !== deleteRange.endLineNumber) {
 				shouldPushStackElementBefore = true;
 			}
 
-			commands[i] = new ReplaceCommand(deleteSelection, '');
+			commands[i] = new ReplaceCommand(deleteRange, '');
 		}
 		return [shouldPushStackElementBefore, commands];
+
+	}
+
+	private static getDeleteRange(selection: Selection, model: ICursorSimpleModel, config: CursorConfiguration,): Range {
+		if (!selection.isEmpty()) {
+			return selection;
+		}
+
+		const position = selection.getPosition();
+
+		// Unintend when using tab stops and cursor is within indentation
+		if (config.useTabStops && position.column > 1) {
+			const lineContent = model.getLineContent(position.lineNumber);
+
+			const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
+			const lastIndentationColumn = (
+				firstNonWhitespaceIndex === -1
+					? /* entire string is whitespace */ lineContent.length + 1
+					: firstNonWhitespaceIndex + 1
+			);
+
+			if (position.column <= lastIndentationColumn) {
+				const fromVisibleColumn = CursorColumns.visibleColumnFromColumn2(config, model, position);
+				const toVisibleColumn = CursorColumns.prevIndentTabStop(fromVisibleColumn, config.indentSize);
+				const toColumn = CursorColumns.columnFromVisibleColumn2(config, model, position.lineNumber, toVisibleColumn);
+				return new Range(position.lineNumber, toColumn, position.lineNumber, position.column);
+			}
+		}
+
+		return Range.fromPositions(DeleteOperations.decreasePositionInModelBy1Column(position, model) || position, position);
+	}
+
+	private static decreasePositionInModelBy1Column(position: Position, model: ICursorSimpleModel): Position | undefined {
+		if (position.column > 1) {
+			return position.delta(0, -1);
+		} else if (position.lineNumber > 1) {
+			const newLine = position.lineNumber - 1;
+			return new Position(newLine, model.getLineMaxColumn(newLine));
+		} else {
+			return undefined;
+		}
 	}
 
 	public static cut(config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): EditOperationResult {


Test patch:
diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts
--- a/src/vs/editor/test/browser/controller/cursor.test.ts
+++ b/src/vs/editor/test/browser/controller/cursor.test.ts
@@ -2608,6 +2608,40 @@ suite('Editor Controller - Regression tests', () => {
 
 		model.dispose();
 	});
+
+	test('issue #122914: Left delete behavior in some languages is changed (useTabStops: false)', () => {
+		let model = createTextModel(
+			[
+				'สวัสดี'
+			].join('\n')
+		);
+
+		withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, viewModel) => {
+			editor.setSelections([
+				new Selection(1, 7, 1, 7)
+			]);
+
+			CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null);
+			assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัสด');
+
+			CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null);
+			assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวัส');
+
+			CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null);
+			assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สวั');
+
+			CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null);
+			assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'สว');
+
+			CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null);
+			assert.strictEqual(model.getValue(EndOfLinePreference.LF), 'ส');
+
+			CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null);
+			assert.strictEqual(model.getValue(EndOfLinePreference.LF), '');
+		});
+
+		model.dispose();
+	});
 });
 
 suite('Editor Controller - Cursor Configuration', () => {