Repository: microsoft/vscode (TypeScript)
Date: 2022-12-10 21:23:59+00:00
Task type: Bug Fix

Issue:
Mac/UI-consisteny: shift-continuing a double/triple-click'n drag selection does not remember its starting mode

Issue Type: <b>Bug</b>

### Summary
On the Mac, when a text selection is **initiated** by double/triple click (to get word/line-wise selection), this should be **remembered** when **continuing** this selection by shift-click and drag.

### Description
When you **double/triple** click somewhere, the clicked **word/line** is selected.
Also, when you keep the mouse down after double/triple clicking and start **dragging**, the selection starts with the clicked word/line, and as you drag along it also **grows** by words/lines (depending on number of initial clicks).

Both of these behaviours are entirely correct.

BUT, when you now release the mouse, hold down shift, and then start dragging again, VSCode forgot how the selection was started, and expands by letters only.

### To reproduce
- double click a word with the mouse to select it, OR
- double click and drag several words to select them, then
- release the mouse,
- hold down shift,
- click'n drag with the mouse somewhere outside the existing selection,

-> the selection now grows by **individual letters**, and not by **words** anymore.

Same thing for **triple** click and selecting **lines**.

### Expected behaviour
The shift-click and drag should remember if the initial selection was started with a single, double or triple click, and expand the selection accordingly. Try the same steps with _TextEdit_ or a _Mail_ Compose window or even the Github textarea input field in Safari where I'm currently entering this bug report.

VS Code version: Code 1.51.1 (e5a624b788d92b8d34d1392e4c4d9789406efe8f, 2020-11-11T01:11:34.018Z)
OS version: Darwin x64 19.6.0

<details>
<summary>System Info</summary>

|Item|Value|
|---|---|
|CPUs|Intel(R) Core(TM) i5-8500B CPU @ 3.00GHz (6 x 3000)|
|GPU Status|2d_canvas: enabled<br>flash_3d: enabled<br>flash_stage3d: enabled<br>flash_stage3d_baseline: enabled<br>gpu_compositing: enabled<br>metal: disabled_off<br>multiple_raster_threads: enabled_on<br>oop_rasterization: enabled<br>opengl: enabled_on<br>protected_video_decode: unavailable_off<br>rasterization: enabled<br>skia_renderer: disabled_off_ok<br>video_decode: enabled<br>webgl: enabled<br>webgl2: enabled|
|Load (avg)|2, 1, 1|
|Memory (System)|32.00GB (1.11GB free)|
|Process Argv|-psn_0_1061123 --crash-reporter-id 942e714c-4201-410a-9a28-266cd696ccc5|
|Screen Reader|no|
|VM|0%|
</details><details><summary>Extensions (30)</summary>

Extension|Author (truncated)|Version
---|---|---
aws-toolkit-vscode|ama|1.16.0
path-intellisense|chr|2.3.0
bracket-pair-colorizer-2|Coe|0.2.0
vscode-quick-select|dba|0.2.9
vscode-npm-script|eg2|0.3.13
auto-close-tag|for|0.5.9
auto-complete-tag|for|0.1.0
auto-rename-tag|for|0.1.5
todo-tree|Gru|0.0.188
terraform|has|2.3.0
vscode-auto-open-markdown-preview|hnw|0.0.4
vscode-power-mode|hoo|2.2.0
vscode-hacker-typer|jev|0.1.1
cypress-fixture-intellisense|Jos|1.2.0
vscode-map-preview|jum|0.5.6
hg|mrc|1.7.1
vscode-docker|ms-|1.8.1
cpptools|ms-|1.1.3
indent-rainbow|ode|7.4.0
geo-data-viewer|Ran|2.3.0
vscode-xml|red|0.14.0
preview-vscode|sea|2.2.5
code-settings-sync|Sha|3.4.3
vscode-cy-helper|She|1.0.1
ascii-plist|spe|1.0.3
code-spell-checker|str|1.10.2
html-preview-vscode|tht|0.2.5
vscodeintellicode|Vis|1.2.10
quokka-vscode|Wal|1.0.335
markdown-all-in-one|yzh|3.4.0


</details>
<!-- generated by issue reporter -->


Issue discussion:

Solution patch:
diff --git a/src/vs/editor/common/cursor/cursorColumnSelection.ts b/src/vs/editor/common/cursor/cursorColumnSelection.ts
--- a/src/vs/editor/common/cursor/cursorColumnSelection.ts
+++ b/src/vs/editor/common/cursor/cursorColumnSelection.ts
@@ -3,7 +3,7 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import { CursorConfiguration, ICursorSimpleModel, SingleCursorState, IColumnSelectData } from 'vs/editor/common/cursorCommon';
+import { CursorConfiguration, ICursorSimpleModel, SingleCursorState, IColumnSelectData, SelectionStartKind } from 'vs/editor/common/cursorCommon';
 import { Position } from 'vs/editor/common/core/position';
 import { Range } from 'vs/editor/common/core/range';
 
@@ -48,7 +48,7 @@ export class ColumnSelection {
 			}
 
 			result.push(new SingleCursorState(
-				new Range(lineNumber, startColumn, lineNumber, startColumn), 0,
+				new Range(lineNumber, startColumn, lineNumber, startColumn), SelectionStartKind.Simple, 0,
 				new Position(lineNumber, endColumn), 0
 			));
 		}
@@ -60,7 +60,7 @@ export class ColumnSelection {
 				const maxColumn = model.getLineMaxColumn(lineNumber);
 
 				result.push(new SingleCursorState(
-					new Range(lineNumber, maxColumn, lineNumber, maxColumn), 0,
+					new Range(lineNumber, maxColumn, lineNumber, maxColumn), SelectionStartKind.Simple, 0,
 					new Position(lineNumber, maxColumn), 0
 				));
 			}
diff --git a/src/vs/editor/common/cursor/cursorMoveCommands.ts b/src/vs/editor/common/cursor/cursorMoveCommands.ts
--- a/src/vs/editor/common/cursor/cursorMoveCommands.ts
+++ b/src/vs/editor/common/cursor/cursorMoveCommands.ts
@@ -4,7 +4,7 @@
  *--------------------------------------------------------------------------------------------*/
 
 import * as types from 'vs/base/common/types';
-import { CursorState, ICursorSimpleModel, PartialCursorState, SingleCursorState } from 'vs/editor/common/cursorCommon';
+import { CursorState, ICursorSimpleModel, PartialCursorState, SelectionStartKind, SingleCursorState } from 'vs/editor/common/cursorCommon';
 import { MoveOperations } from 'vs/editor/common/cursor/cursorMoveOperations';
 import { WordOperations } from 'vs/editor/common/cursor/cursorWordOperations';
 import { IPosition, Position } from 'vs/editor/common/core/position';
@@ -138,7 +138,7 @@ export class CursorMoveCommands {
 			}
 
 			result[i] = CursorState.fromModelState(new SingleCursorState(
-				new Range(startLineNumber, 1, startLineNumber, 1), 0,
+				new Range(startLineNumber, 1, startLineNumber, 1), SelectionStartKind.Simple, 0,
 				new Position(endLineNumber, endColumn), 0
 			));
 		}
@@ -168,7 +168,7 @@ export class CursorMoveCommands {
 		const maxColumn = viewModel.model.getLineMaxColumn(lineCount);
 
 		return CursorState.fromModelState(new SingleCursorState(
-			new Range(1, 1, 1, 1), 0,
+			new Range(1, 1, 1, 1), SelectionStartKind.Simple, 0,
 			new Position(lineCount, maxColumn), 0
 		));
 	}
@@ -193,7 +193,7 @@ export class CursorMoveCommands {
 			}
 
 			return CursorState.fromModelState(new SingleCursorState(
-				new Range(position.lineNumber, 1, selectToLineNumber, selectToColumn), 0,
+				new Range(position.lineNumber, 1, selectToLineNumber, selectToColumn), SelectionStartKind.Line, 0,
 				new Position(selectToLineNumber, selectToColumn), 0
 			));
 		}
@@ -246,12 +246,20 @@ export class CursorMoveCommands {
 		const column = cursor.viewState.position.column;
 
 		return CursorState.fromViewState(new SingleCursorState(
-			new Range(lineNumber, column, lineNumber, column), 0,
+			new Range(lineNumber, column, lineNumber, column), SelectionStartKind.Simple, 0,
 			new Position(lineNumber, column), 0
 		));
 	}
 
 	public static moveTo(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition | undefined): PartialCursorState {
+		if (inSelectionMode) {
+			if (cursor.modelState.selectionStartKind === SelectionStartKind.Word) {
+				return this.word(viewModel, cursor, inSelectionMode, _position);
+			}
+			if (cursor.modelState.selectionStartKind === SelectionStartKind.Line) {
+				return this.line(viewModel, cursor, inSelectionMode, _position, _viewPosition);
+			}
+		}
 		const position = viewModel.model.validatePosition(_position);
 		const viewPosition = (
 			_viewPosition
diff --git a/src/vs/editor/common/cursor/cursorMoveOperations.ts b/src/vs/editor/common/cursor/cursorMoveOperations.ts
--- a/src/vs/editor/common/cursor/cursorMoveOperations.ts
+++ b/src/vs/editor/common/cursor/cursorMoveOperations.ts
@@ -3,7 +3,7 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import { CursorConfiguration, ICursorSimpleModel, SingleCursorState } from 'vs/editor/common/cursorCommon';
+import { CursorConfiguration, ICursorSimpleModel, SelectionStartKind, SingleCursorState } from 'vs/editor/common/cursorCommon';
 import { CursorColumns } from 'vs/editor/common/core/cursorColumns';
 import { Position } from 'vs/editor/common/core/position';
 import { Range } from 'vs/editor/common/core/range';
@@ -226,6 +226,7 @@ export class MoveOperations {
 
 		return new SingleCursorState(
 			new Range(selectionStart.lineNumber, selectionStart.column, selectionStart.lineNumber, selectionStart.column),
+			SelectionStartKind.Simple,
 			selectionStart.leftoverVisibleColumns,
 			new Position(position.lineNumber, position.column),
 			position.leftoverVisibleColumns
@@ -263,6 +264,7 @@ export class MoveOperations {
 
 		return new SingleCursorState(
 			new Range(selectionStart.lineNumber, selectionStart.column, selectionStart.lineNumber, selectionStart.column),
+			SelectionStartKind.Simple,
 			selectionStart.leftoverVisibleColumns,
 			new Position(position.lineNumber, position.column),
 			position.leftoverVisibleColumns
diff --git a/src/vs/editor/common/cursor/cursorWordOperations.ts b/src/vs/editor/common/cursor/cursorWordOperations.ts
--- a/src/vs/editor/common/cursor/cursorWordOperations.ts
+++ b/src/vs/editor/common/cursor/cursorWordOperations.ts
@@ -6,7 +6,7 @@
 import { CharCode } from 'vs/base/common/charCode';
 import * as strings from 'vs/base/common/strings';
 import { EditorAutoClosingEditStrategy, EditorAutoClosingStrategy } from 'vs/editor/common/config/editorOptions';
-import { CursorConfiguration, ICursorSimpleModel, SingleCursorState } from 'vs/editor/common/cursorCommon';
+import { CursorConfiguration, ICursorSimpleModel, SelectionStartKind, SingleCursorState } from 'vs/editor/common/cursorCommon';
 import { DeleteOperations } from 'vs/editor/common/cursor/cursorDeleteOperations';
 import { WordCharacterClass, WordCharacterClassifier, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier';
 import { Position } from 'vs/editor/common/core/position';
@@ -734,7 +734,7 @@ export class WordOperations {
 			}
 
 			return new SingleCursorState(
-				new Range(position.lineNumber, startColumn, position.lineNumber, endColumn), 0,
+				new Range(position.lineNumber, startColumn, position.lineNumber, endColumn), SelectionStartKind.Word, 0,
 				new Position(position.lineNumber, endColumn), 0
 			);
 		}
diff --git a/src/vs/editor/common/cursor/oneCursor.ts b/src/vs/editor/common/cursor/oneCursor.ts
--- a/src/vs/editor/common/cursor/oneCursor.ts
+++ b/src/vs/editor/common/cursor/oneCursor.ts
@@ -3,7 +3,7 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
-import { CursorState, ICursorSimpleModel, SingleCursorState } from 'vs/editor/common/cursorCommon';
+import { CursorState, ICursorSimpleModel, SelectionStartKind, SingleCursorState } from 'vs/editor/common/cursorCommon';
 import { CursorContext } from 'vs/editor/common/cursor/cursorContext';
 import { Position } from 'vs/editor/common/core/position';
 import { Range } from 'vs/editor/common/core/range';
@@ -27,8 +27,8 @@ export class Cursor {
 
 		this._setState(
 			context,
-			new SingleCursorState(new Range(1, 1, 1, 1), 0, new Position(1, 1), 0),
-			new SingleCursorState(new Range(1, 1, 1, 1), 0, new Position(1, 1), 0)
+			new SingleCursorState(new Range(1, 1, 1, 1), SelectionStartKind.Simple, 0, new Position(1, 1), 0),
+			new SingleCursorState(new Range(1, 1, 1, 1), SelectionStartKind.Simple, 0, new Position(1, 1), 0)
 		);
 	}
 
@@ -98,6 +98,7 @@ export class Cursor {
 
 		return new SingleCursorState(
 			Range.fromPositions(validSStartPosition, validSEndPosition),
+			viewState.selectionStartKind,
 			viewState.selectionStartLeftoverVisibleColumns + sStartPosition.column - validSStartPosition.column,
 			validPosition,
 			viewState.leftoverVisibleColumns + position.column - validPosition.column,
@@ -122,7 +123,7 @@ export class Cursor {
 				context.coordinatesConverter.convertViewPositionToModelPosition(viewState.position)
 			);
 
-			modelState = new SingleCursorState(selectionStart, viewState.selectionStartLeftoverVisibleColumns, position, viewState.leftoverVisibleColumns);
+			modelState = new SingleCursorState(selectionStart, viewState.selectionStartKind, viewState.selectionStartLeftoverVisibleColumns, position, viewState.leftoverVisibleColumns);
 		} else {
 			// Validate new model state
 			const selectionStart = context.model.validateRange(modelState.selectionStart);
@@ -133,7 +134,7 @@ export class Cursor {
 			);
 			const leftoverVisibleColumns = modelState.position.equals(position) ? modelState.leftoverVisibleColumns : 0;
 
-			modelState = new SingleCursorState(selectionStart, selectionStartLeftoverVisibleColumns, position, leftoverVisibleColumns);
+			modelState = new SingleCursorState(selectionStart, modelState.selectionStartKind, selectionStartLeftoverVisibleColumns, position, leftoverVisibleColumns);
 		}
 
 		if (!viewState) {
@@ -142,12 +143,12 @@ export class Cursor {
 			const viewSelectionStart2 = context.coordinatesConverter.convertModelPositionToViewPosition(new Position(modelState.selectionStart.endLineNumber, modelState.selectionStart.endColumn));
 			const viewSelectionStart = new Range(viewSelectionStart1.lineNumber, viewSelectionStart1.column, viewSelectionStart2.lineNumber, viewSelectionStart2.column);
 			const viewPosition = context.coordinatesConverter.convertModelPositionToViewPosition(modelState.position);
-			viewState = new SingleCursorState(viewSelectionStart, modelState.selectionStartLeftoverVisibleColumns, viewPosition, modelState.leftoverVisibleColumns);
+			viewState = new SingleCursorState(viewSelectionStart, modelState.selectionStartKind, modelState.selectionStartLeftoverVisibleColumns, viewPosition, modelState.leftoverVisibleColumns);
 		} else {
 			// Validate new view state
 			const viewSelectionStart = context.coordinatesConverter.validateViewRange(viewState.selectionStart, modelState.selectionStart);
 			const viewPosition = context.coordinatesConverter.validateViewPosition(viewState.position, modelState.position);
-			viewState = new SingleCursorState(viewSelectionStart, modelState.selectionStartLeftoverVisibleColumns, viewPosition, modelState.leftoverVisibleColumns);
+			viewState = new SingleCursorState(viewSelectionStart, modelState.selectionStartKind, modelState.selectionStartLeftoverVisibleColumns, viewPosition, modelState.leftoverVisibleColumns);
 		}
 
 		this.modelState = modelState;
diff --git a/src/vs/editor/common/cursorCommon.ts b/src/vs/editor/common/cursorCommon.ts
--- a/src/vs/editor/common/cursorCommon.ts
+++ b/src/vs/editor/common/cursorCommon.ts
@@ -261,7 +261,7 @@ export class CursorState {
 		const selection = Selection.liftSelection(modelSelection);
 		const modelState = new SingleCursorState(
 			Range.fromPositions(selection.getSelectionStart()),
-			0,
+			SelectionStartKind.Simple, 0,
 			selection.getPosition(), 0
 		);
 		return CursorState.fromModelState(modelState);
@@ -308,29 +308,27 @@ export class PartialViewCursorState {
 	}
 }
 
+export const enum SelectionStartKind {
+	Simple,
+	Word,
+	Line
+}
+
 /**
  * Represents the cursor state on either the model or on the view model.
  */
 export class SingleCursorState {
 	_singleCursorStateBrand: void = undefined;
 
-	// --- selection can start as a range (think double click and drag)
-	public readonly selectionStart: Range;
-	public readonly selectionStartLeftoverVisibleColumns: number;
-	public readonly position: Position;
-	public readonly leftoverVisibleColumns: number;
 	public readonly selection: Selection;
 
 	constructor(
-		selectionStart: Range,
-		selectionStartLeftoverVisibleColumns: number,
-		position: Position,
-		leftoverVisibleColumns: number,
+		public readonly selectionStart: Range,
+		public readonly selectionStartKind: SelectionStartKind,
+		public readonly selectionStartLeftoverVisibleColumns: number,
+		public readonly position: Position,
+		public readonly leftoverVisibleColumns: number,
 	) {
-		this.selectionStart = selectionStart;
-		this.selectionStartLeftoverVisibleColumns = selectionStartLeftoverVisibleColumns;
-		this.position = position;
-		this.leftoverVisibleColumns = leftoverVisibleColumns;
 		this.selection = SingleCursorState._computeSelection(this.selectionStart, this.position);
 	}
 
@@ -338,6 +336,7 @@ export class SingleCursorState {
 		return (
 			this.selectionStartLeftoverVisibleColumns === other.selectionStartLeftoverVisibleColumns
 			&& this.leftoverVisibleColumns === other.leftoverVisibleColumns
+			&& this.selectionStartKind === other.selectionStartKind
 			&& this.position.equals(other.position)
 			&& this.selectionStart.equalsRange(other.selectionStart)
 		);
@@ -352,6 +351,7 @@ export class SingleCursorState {
 			// move just position
 			return new SingleCursorState(
 				this.selectionStart,
+				this.selectionStartKind,
 				this.selectionStartLeftoverVisibleColumns,
 				new Position(lineNumber, column),
 				leftoverVisibleColumns
@@ -360,6 +360,7 @@ export class SingleCursorState {
 			// move everything
 			return new SingleCursorState(
 				new Range(lineNumber, column, lineNumber, column),
+				SelectionStartKind.Simple,
 				leftoverVisibleColumns,
 				new Position(lineNumber, column),
 				leftoverVisibleColumns


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
@@ -6109,6 +6109,26 @@ suite('Editor Controller', () => {
 		});
 	});
 
+	test('issue #112039: shift-continuing a double/triple-click and drag selection does not remember its starting mode', () => {
+		const model = createTextModel(
+			[
+				'just some text',
+				'and another line',
+				'and another one',
+			].join('\n')
+		);
+
+		withTestCodeEditor(model, {}, (editor, viewModel) => {
+			CoreNavigationCommands.WordSelect.runEditorCommand(null, editor, {
+				position: new Position(2, 6)
+			});
+			CoreNavigationCommands.MoveToSelect.runEditorCommand(null, editor, {
+				position: new Position(1, 8),
+			});
+			assertCursor(viewModel, new Selection(2, 12, 1, 6));
+		});
+	});
+
 	test('issue #158236: Shift click selection does not work on line number indicator', () => {
 		const model = createTextModel(
 			[