Repository: microsoft/vscode (TypeScript)
Date: 2022-08-20 15:25:01+00:00
Task type: Bug Fix

Issue:
`cursorWordPartLeft` doesn't stop at `-` when it is excluded from `editor.wordSeparators`
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->
- VS Code Version: 1.70.1
- OS Version: Windows 11

Steps to Reproduce:

1. Change the "editor.wordSeparators" setting to exclude dash character (e.g. `{ "editor.wordSeparators": "~!@#$%^&*()=+[{]}\\|;:'\",.<>/?" }`) — this is actually a desirable setting in e.g. html/css
2. Assign a keybinding to "cursorWordPartLeft"/"cursorWordPartRight" commands
3. Observe how the definition of a "word part" differs between invocation of "editor.action.smartSelect.expand" (default kb is "shift+alt+right") and "cursorWordPart*"; "cursorWordPart*" does not behave as expected for a string `mat-button` (should have "word breaks" at the same spots as it does in `mat_button`)
`cursorWordPartLeft` doesn't stop at `-` when it is excluded from `editor.wordSeparators`
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->
- VS Code Version: 1.70.1
- OS Version: Windows 11

Steps to Reproduce:

1. Change the "editor.wordSeparators" setting to exclude dash character (e.g. `{ "editor.wordSeparators": "~!@#$%^&*()=+[{]}\\|;:'\",.<>/?" }`) — this is actually a desirable setting in e.g. html/css
2. Assign a keybinding to "cursorWordPartLeft"/"cursorWordPartRight" commands
3. Observe how the definition of a "word part" differs between invocation of "editor.action.smartSelect.expand" (default kb is "shift+alt+right") and "cursorWordPart*"; "cursorWordPart*" does not behave as expected for a string `mat-button` (should have "word breaks" at the same spots as it does in `mat_button`)


Issue discussion:
Thanks for creating this issue! It looks like you may be using an old version of VS Code, the latest stable release is 1.70.2. Please try upgrading to the latest version and checking whether this issue remains.

Happy Coding!
The "proper" solution for this would seem to be to use the same `WordSelectionRangeProvider` for both commands (presently, "cursorWordPartLeft" and "editor.action.smartSelect.expand" do not use the same logic to find next caret position,) but I intend to make a PR with a simpler solution shortly...
Thanks for creating this issue! It looks like you may be using an old version of VS Code, the latest stable release is 1.70.2. Please try upgrading to the latest version and checking whether this issue remains.

Happy Coding!
The "proper" solution for this would seem to be to use the same `WordSelectionRangeProvider` for both commands (presently, "cursorWordPartLeft" and "editor.action.smartSelect.expand" do not use the same logic to find next caret position,) but I intend to make a PR with a simpler solution shortly...

Solution patch:
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
@@ -246,6 +246,11 @@ export class WordOperations {
 				return new Position(lineNumber, column);
 			}
 
+			if (left === CharCode.Dash && right !== CharCode.Dash) {
+				// kebab-case-variables
+				return new Position(lineNumber, column);
+			}
+
 			if ((strings.isLowerAsciiLetter(left) || strings.isAsciiDigit(left)) && strings.isUpperAsciiLetter(right)) {
 				// camelCaseVariables
 				return new Position(lineNumber, column);
@@ -348,6 +353,11 @@ export class WordOperations {
 				return new Position(lineNumber, column);
 			}
 
+			if (left !== CharCode.Dash && right === CharCode.Dash) {
+				// kebab-case-variables
+				return new Position(lineNumber, column);
+			}
+
 			if ((strings.isLowerAsciiLetter(left) || strings.isAsciiDigit(left)) && strings.isUpperAsciiLetter(right)) {
 				// camelCaseVariables
 				return new Position(lineNumber, column);


Test patch:
diff --git a/src/vs/editor/contrib/wordOperations/test/browser/wordTestUtils.ts b/src/vs/editor/contrib/wordOperations/test/browser/wordTestUtils.ts
--- a/src/vs/editor/contrib/wordOperations/test/browser/wordTestUtils.ts
+++ b/src/vs/editor/contrib/wordOperations/test/browser/wordTestUtils.ts
@@ -4,7 +4,7 @@
  *--------------------------------------------------------------------------------------------*/
 
 import { Position } from 'vs/editor/common/core/position';
-import { ITestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
+import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
 
 export function deserializePipePositions(text: string): [string, Position[]] {
 	let resultText = '';
@@ -58,9 +58,9 @@ export function serializePipePositions(text: string, positions: Position[]): str
 	return resultText;
 }
 
-export function testRepeatedActionAndExtractPositions(text: string, initialPosition: Position, action: (editor: ITestCodeEditor) => void, record: (editor: ITestCodeEditor) => Position, stopCondition: (editor: ITestCodeEditor) => boolean): Position[] {
+export function testRepeatedActionAndExtractPositions(text: string, initialPosition: Position, action: (editor: ITestCodeEditor) => void, record: (editor: ITestCodeEditor) => Position, stopCondition: (editor: ITestCodeEditor) => boolean, options: TestCodeEditorInstantiationOptions = {}): Position[] {
 	const actualStops: Position[] = [];
-	withTestCodeEditor(text, {}, (editor) => {
+	withTestCodeEditor(text, options, (editor) => {
 		editor.setPosition(initialPosition);
 		while (true) {
 			action(editor);
diff --git a/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts b/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts
--- a/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts
+++ b/src/vs/editor/contrib/wordPartOperations/test/browser/wordPartOperations.test.ts
@@ -212,4 +212,72 @@ suite('WordPartOperations', () => {
 		const actual = serializePipePositions(text, actualStops);
 		assert.deepStrictEqual(actual, EXPECTED);
 	});
+
+	test('issue #158667: cursorWordPartLeft stops at "-" even when "-" is not in word separators', () => {
+		const EXPECTED = [
+			'|this-|is-|a-|kebab-|case-|var| |THIS-|IS-|CAPS-|KEBAB| |this-|IS|Mixed|Use',
+		].join('\n');
+		const [text,] = deserializePipePositions(EXPECTED);
+		const actualStops = testRepeatedActionAndExtractPositions(
+			text,
+			new Position(1000, 1000),
+			ed => cursorWordPartLeft(ed),
+			ed => ed.getPosition()!,
+			ed => ed.getPosition()!.equals(new Position(1, 1)),
+			{ wordSeparators: "!\"#&'()*+,./:;<=>?@[\\]^`{|}·" } // default characters sans '$-%~' plus '·'
+		);
+		const actual = serializePipePositions(text, actualStops);
+		assert.deepStrictEqual(actual, EXPECTED);
+	});
+
+	test('issue #158667: cursorWordPartRight stops at "-" even when "-" is not in word separators', () => {
+		const EXPECTED = [
+			'this|-is|-a|-kebab|-case|-var| |THIS|-IS|-CAPS|-KEBAB| |this|-IS|Mixed|Use|',
+		].join('\n');
+		const [text,] = deserializePipePositions(EXPECTED);
+		const actualStops = testRepeatedActionAndExtractPositions(
+			text,
+			new Position(1, 1),
+			ed => cursorWordPartRight(ed),
+			ed => ed.getPosition()!,
+			ed => ed.getPosition()!.equals(new Position(1, 60)),
+			{ wordSeparators: "!\"#&'()*+,./:;<=>?@[\\]^`{|}·" } // default characters sans '$-%~' plus '·'
+		);
+		const actual = serializePipePositions(text, actualStops);
+		assert.deepStrictEqual(actual, EXPECTED);
+	});
+
+	test('issue #158667: deleteWordPartLeft stops at "-" even when "-" is not in word separators', () => {
+		const EXPECTED = [
+			'|this-|is-|a-|kebab-|case-|var| |THIS-|IS-|CAPS-|KEBAB| |this-|IS|Mixed|Use',
+		].join(' ');
+		const [text,] = deserializePipePositions(EXPECTED);
+		const actualStops = testRepeatedActionAndExtractPositions(
+			text,
+			new Position(1000, 1000),
+			ed => deleteWordPartLeft(ed),
+			ed => ed.getPosition()!,
+			ed => ed.getValue().length === 0,
+			{ wordSeparators: "!\"#&'()*+,./:;<=>?@[\\]^`{|}·" } // default characters sans '$-%~' plus '·'
+		);
+		const actual = serializePipePositions(text, actualStops);
+		assert.deepStrictEqual(actual, EXPECTED);
+	});
+
+	test('issue #158667: deleteWordPartRight stops at "-" even when "-" is not in word separators', () => {
+		const EXPECTED = [
+			'this|-is|-a|-kebab|-case|-var| |THIS|-IS|-CAPS|-KEBAB| |this|-IS|Mixed|Use|',
+		].join(' ');
+		const [text,] = deserializePipePositions(EXPECTED);
+		const actualStops = testRepeatedActionAndExtractPositions(
+			text,
+			new Position(1, 1),
+			ed => deleteWordPartRight(ed),
+			ed => new Position(1, text.length - ed.getValue().length + 1),
+			ed => ed.getValue().length === 0,
+			{ wordSeparators: "!\"#&'()*+,./:;<=>?@[\\]^`{|}·" } // default characters sans '$-%~' plus '·'
+		);
+		const actual = serializePipePositions(text, actualStops);
+		assert.deepStrictEqual(actual, EXPECTED);
+	});
 });