Repository: microsoft/vscode (TypeScript) — Bug Fix

Issue:
Title Case functionality with apostrophe
<!-- ⚠️⚠️ 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.61.2
- OS Version: Windows 10 Pro x64

Steps to Reproduce:

1.  Use Title Case function with list that has apostrophe

If there is an apostrophe when using Title Case function, it will capitalize the S. 

For example:

`Physician's Assistant` becomes `Physician'S Assistant`



Discussion:

Resolving patch:
diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts
--- a/src/vs/editor/contrib/linesOperations/linesOperations.ts
+++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts
@@ -1076,43 +1076,6 @@ export class LowerCaseAction extends AbstractCaseAction {
 	}
 }
 
-export class TitleCaseAction extends AbstractCaseAction {
-	constructor() {
-		super({
-			id: 'editor.action.transformToTitlecase',
-			label: nls.localize('editor.transformToTitlecase', "Transform to Title Case"),
-			alias: 'Transform to Title Case',
-			precondition: EditorContextKeys.writable
-		});
-	}
-
-	protected _modifyText(text: string, wordSeparators: string): string {
-		const separators = '\r\n\t ' + wordSeparators;
-		const excludedChars = separators.split('');
-
-		let title = '';
-		let startUpperCase = true;
-
-		for (let i = 0; i < text.length; i++) {
-			let currentChar = text[i];
-
-			if (excludedChars.indexOf(currentChar) >= 0) {
-				startUpperCase = true;
-
-				title += currentChar;
-			} else if (startUpperCase) {
-				startUpperCase = false;
-
-				title += currentChar.toLocaleUpperCase();
-			} else {
-				title += currentChar.toLocaleLowerCase();
-			}
-		}
-
-		return title;
-	}
-}
-
 class BackwardsCompatibleRegExp {
 
 	private _actual: RegExp | null;
@@ -1143,10 +1106,35 @@ class BackwardsCompatibleRegExp {
 	}
 }
 
+export class TitleCaseAction extends AbstractCaseAction {
+
+	public static titleBoundary = new BackwardsCompatibleRegExp('(^|[^\\p{L}\\p{N}\']|((^|\\P{L})\'))\\p{L}', 'gmu');
+
+	constructor() {
+		super({
+			id: 'editor.action.transformToTitlecase',
+			label: nls.localize('editor.transformToTitlecase', "Transform to Title Case"),
+			alias: 'Transform to Title Case',
+			precondition: EditorContextKeys.writable
+		});
+	}
+
+	protected _modifyText(text: string, wordSeparators: string): string {
+		const titleBoundary = TitleCaseAction.titleBoundary.get();
+		if (!titleBoundary) {
+			// cannot support this
+			return text;
+		}
+		return text
+			.toLocaleLowerCase()
+			.replace(titleBoundary, (b) => b.toLocaleUpperCase());
+	}
+}
+
 export class SnakeCaseAction extends AbstractCaseAction {
 
-	public static regExp1 = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');
-	public static regExp2 = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu})(\\p{Ll})', 'gmu');
+	public static caseBoundary = new BackwardsCompatibleRegExp('(\\p{Ll})(\\p{Lu})', 'gmu');
+	public static singleLetters = new BackwardsCompatibleRegExp('(\\p{Lu}|\\p{N})(\\p{Lu})(\\p{Ll})', 'gmu');
 
 	constructor() {
 		super({
@@ -1158,15 +1146,15 @@ export class SnakeCaseAction extends AbstractCaseAction {
 	}
 
 	protected _modifyText(text: string, wordSeparators: string): string {
-		const regExp1 = SnakeCaseAction.regExp1.get();
-		const regExp2 = SnakeCaseAction.regExp2.get();
-		if (!regExp1 || !regExp2) {
+		const caseBoundary = SnakeCaseAction.caseBoundary.get();
+		const singleLetters = SnakeCaseAction.singleLetters.get();
+		if (!caseBoundary || !singleLetters) {
 			// cannot support this
 			return text;
 		}
 		return (text
-			.replace(regExp1, '$1_$2')
-			.replace(regExp2, '$1_$2$3')
+			.replace(caseBoundary, '$1_$2')
+			.replace(singleLetters, '$1_$2$3')
 			.toLocaleLowerCase()
 		);
 	}
@@ -1192,8 +1180,10 @@ registerEditorAction(JoinLinesAction);
 registerEditorAction(TransposeAction);
 registerEditorAction(UpperCaseAction);
 registerEditorAction(LowerCaseAction);
-registerEditorAction(TitleCaseAction);
 
-if (SnakeCaseAction.regExp1.isSupported() && SnakeCaseAction.regExp2.isSupported()) {
+if (SnakeCaseAction.caseBoundary.isSupported() && SnakeCaseAction.singleLetters.isSupported()) {
 	registerEditorAction(SnakeCaseAction);
 }
+if (TitleCaseAction.titleBoundary.isSupported()) {
+	registerEditorAction(TitleCaseAction);
+}


Test patch:
diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts
--- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts
+++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts
@@ -748,7 +748,8 @@ suite('Editor Contrib - Line Operations', () => {
 				'foO[baR]BaZ',
 				'foO`baR~BaZ',
 				'foO^baR%BaZ',
-				'foO$baR!BaZ'
+				'foO$baR!BaZ',
+				'\'physician\'s assistant\''
 			], {}, (editor) => {
 				let model = editor.getModel()!;
 				let titlecaseAction = new TitleCaseAction();
@@ -759,7 +760,7 @@ suite('Editor Contrib - Line Operations', () => {
 
 				editor.setSelection(new Selection(2, 1, 2, 12));
 				executeAction(titlecaseAction, editor);
-				assert.strictEqual(model.getLineContent(2), 'Foo\'Bar\'Baz');
+				assert.strictEqual(model.getLineContent(2), 'Foo\'bar\'baz');
 
 				editor.setSelection(new Selection(3, 1, 3, 12));
 				executeAction(titlecaseAction, editor);
@@ -776,6 +777,10 @@ suite('Editor Contrib - Line Operations', () => {
 				editor.setSelection(new Selection(6, 1, 6, 12));
 				executeAction(titlecaseAction, editor);
 				assert.strictEqual(model.getLineContent(6), 'Foo$Bar!Baz');
+
+				editor.setSelection(new Selection(7, 1, 7, 23));
+				executeAction(titlecaseAction, editor);
+				assert.strictEqual(model.getLineContent(7), '\'Physician\'s Assistant\'');
 			}
 		);