Fix transform-case actions changing strings and comments#313559
Fix transform-case actions changing strings and comments#313559maruthang wants to merge 1 commit intomicrosoft:mainfrom
Conversation
Adds an opt-in editor.transformCase.skipStringsAndComments setting (default false) that excludes string and comment ranges from the case transformation actions (Upper/Lower/Title/Snake/Camel/Pascal/Kebab). Default behavior is unchanged. Fixes microsoft#149616
There was a problem hiding this comment.
Pull request overview
Adds an opt-in editor setting to make case-transformation actions skip tokens classified as strings/comments by the active tokenizer, preserving existing behavior when disabled.
Changes:
- Introduces
editor.transformCase.skipStringsAndComments(defaultfalse) via a new structured editor option group. - Updates
AbstractCaseAction.run()to split selections around string/comment token spans (and no-op when the cursor word is inside them). - Adds unit tests with a lightweight tokenizer to validate skip behavior for strings/comments.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/vs/editor/common/config/editorOptions.ts | Registers the new structured transformCase editor option and setting key. |
| src/vs/editor/contrib/linesOperations/browser/linesOperations.ts | Implements skip logic for transform-case actions based on tokenizer StandardTokenType. |
| src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts | Adds unit tests (with a custom tokenizer) covering skip behavior and default behavior. |
| withTestCodeEditor(model, { transformCase: { skipStringsAndComments: options.skipStringsAndComments } }, (editor) => { | ||
| callback(editor, model); | ||
| }); | ||
| disposables.dispose(); |
There was a problem hiding this comment.
withTokenizedTestEditor disposes the DisposableStore only after withTestCodeEditor returns. If an assertion in callback throws, disposables.dispose() will be skipped, which can leak the language/tokenization registrations and make subsequent tests flaky. Wrap the withTestCodeEditor/callback invocation in a try/finally and dispose in the finally block.
| withTestCodeEditor(model, { transformCase: { skipStringsAndComments: options.skipStringsAndComments } }, (editor) => { | |
| callback(editor, model); | |
| }); | |
| disposables.dispose(); | |
| try { | |
| withTestCodeEditor(model, { transformCase: { skipStringsAndComments: options.skipStringsAndComments } }, editor => { | |
| callback(editor, model); | |
| }); | |
| } finally { | |
| disposables.dispose(); | |
| } |
| } else if (skipStringsAndComments) { | ||
| const codeRanges = splitRangeAroundStringsAndComments(model, selection); | ||
| for (const codeRange of codeRanges) { | ||
| const text = model.getValueInRange(codeRange); | ||
| textEdits.push(EditOperation.replace(codeRange, this._modifyText(text, wordSeparators))); |
There was a problem hiding this comment.
When skipStringsAndComments is enabled, it's possible for a selection/word to be entirely inside a string/comment, resulting in textEdits staying empty (e.g. due to the continue/no codeRanges). The action currently still calls pushUndoStop()/executeEdits() which can introduce undo stack elements even though no text changed. Consider early-returning when textEdits.length === 0 (before pushing undo stops) to avoid no-op undo stops.
Summary
Adds a new opt-in editor setting
editor.transformCase.skipStringsAndComments(boolean, defaultfalse). When enabled, the case-transformation actions — Transform to Uppercase, Lowercase, Title Case, Snake Case, Camel Case, Pascal Case, Kebab Case — leave ranges classified as strings or comments by the active language's tokenizer untouched.Default behavior is preserved: with the setting off, the actions take the original code path and produce byte-identical output to before.
Fixes #149616
Implementation
editor.transformCaseinsrc/vs/editor/common/config/editorOptions.ts, mirroring theeditor.comments.*group so future sub-keys can be added without churn.AbstractCaseAction.run()insrc/vs/editor/contrib/linesOperations/browser/linesOperations.ts. Two helpers:isRangeInStringOrComment— used when the user invokes the action with an empty selection (cursor in a word). Returns true only when every covered token isStandardTokenType.StringorStandardTokenType.Comment.splitRangeAroundStringsAndComments— used for non-empty selections. Walks the line tokens and emits a list of "code" sub-ranges that exclude any string/comment spans. Each surviving sub-range is transformed independently via the existing_modifyText(text, wordSeparators)hook, which means every concrete action — Upper/Lower/Title/Snake/Camel/Pascal/Kebab — inherits the new behavior without per-action plumbing.model.tokenization.forceTokenization+findTokenIndexAtOffset, the same pattern asTrimTrailingWhitespaceCommand.Test plan
linesOperations.test.tscovering:"editor.transformCase.skipStringsAndComments": true, select code, run "Transform to Uppercase" — strings and comments untouched.The custom mocha tests use a tiny tokenizer (
'…'= string,//…EOL= comment) so the test is focused on the skip behavior rather than language plumbing.