Skip to content

Commit 8b15ad7

Browse files
authored
Merge pull request #79111 from skprabhanjan/fix-78397
Fix-78397 Implement case preservation in search as well
2 parents 17f0eac + dd2a230 commit 8b15ad7

File tree

8 files changed

+126
-19
lines changed

8 files changed

+126
-19
lines changed

src/vs/base/common/search.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as strings from './strings';
7+
8+
export function buildReplaceStringWithCasePreserved(matches: string[] | null, pattern: string): string {
9+
if (matches && (matches[0] !== '')) {
10+
if (matches[0].toUpperCase() === matches[0]) {
11+
return pattern.toUpperCase();
12+
} else if (matches[0].toLowerCase() === matches[0]) {
13+
return pattern.toLowerCase();
14+
} else if (strings.containsUppercaseCharacter(matches[0][0])) {
15+
return pattern[0].toUpperCase() + pattern.substr(1);
16+
} else {
17+
// we don't understand its pattern yet.
18+
return pattern;
19+
}
20+
} else {
21+
return pattern;
22+
}
23+
}

src/vs/editor/contrib/find/replacePattern.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CharCode } from 'vs/base/common/charCode';
7-
import { containsUppercaseCharacter } from 'vs/base/common/strings';
7+
import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search';
88

99
const enum ReplacePatternKind {
1010
StaticValue = 0,
@@ -51,17 +51,8 @@ export class ReplacePattern {
5151

5252
public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string {
5353
if (this._state.kind === ReplacePatternKind.StaticValue) {
54-
if (preserveCase && matches && (matches[0] !== '')) {
55-
if (matches[0].toUpperCase() === matches[0]) {
56-
return this._state.staticValue.toUpperCase();
57-
} else if (matches[0].toLowerCase() === matches[0]) {
58-
return this._state.staticValue.toLowerCase();
59-
} else if (containsUppercaseCharacter(matches[0][0])) {
60-
return this._state.staticValue[0].toUpperCase() + this._state.staticValue.substr(1);
61-
} else {
62-
// we don't understand its pattern yet.
63-
return this._state.staticValue;
64-
}
54+
if (preserveCase) {
55+
return buildReplaceStringWithCasePreserved(matches, this._state.staticValue);
6556
} else {
6657
return this._state.staticValue;
6758
}

src/vs/editor/contrib/find/test/replacePattern.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as assert from 'assert';
77
import { ReplacePattern, ReplacePiece, parseReplaceString } from 'vs/editor/contrib/find/replacePattern';
8+
import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search';
89

910
suite('Replace Pattern test', () => {
1011

@@ -154,6 +155,29 @@ suite('Replace Pattern test', () => {
154155
assert.equal(actual, 'a{}');
155156
});
156157

158+
test('buildReplaceStringWithCasePreserved test', () => {
159+
let replacePattern = 'Def';
160+
let actual: string | string[] = 'abc';
161+
162+
assert.equal(buildReplaceStringWithCasePreserved([actual], replacePattern), 'def');
163+
actual = 'Abc';
164+
assert.equal(buildReplaceStringWithCasePreserved([actual], replacePattern), 'Def');
165+
actual = 'ABC';
166+
assert.equal(buildReplaceStringWithCasePreserved([actual], replacePattern), 'DEF');
167+
168+
actual = ['abc', 'Abc'];
169+
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'def');
170+
actual = ['Abc', 'abc'];
171+
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'Def');
172+
actual = ['ABC', 'abc'];
173+
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'DEF');
174+
175+
actual = ['AbC'];
176+
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'Def');
177+
actual = ['aBC'];
178+
assert.equal(buildReplaceStringWithCasePreserved(actual, replacePattern), 'Def');
179+
});
180+
157181
test('preserve case', () => {
158182
let replacePattern = parseReplaceString('Def');
159183
let actual = replacePattern.buildReplaceString(['abc'], true);

src/vs/workbench/contrib/search/browser/media/searchview.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@
6060
display: inline-flex;
6161
}
6262

63+
.search-view .search-widget .replace-input {
64+
position: relative;
65+
display: flex;
66+
display: -webkit-flex;
67+
vertical-align: middle;
68+
width: auto !important;
69+
}
70+
71+
.search-view .search-widget .replace-input > .controls {
72+
position: absolute;
73+
top: 3px;
74+
right: 2px;
75+
}
76+
6377
.search-view .search-widget .replace-container.disabled {
6478
display: none;
6579
}

src/vs/workbench/contrib/search/browser/searchView.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ export class SearchView extends ViewletPanel {
364364
const searchHistory = history.search || this.viewletState['query.searchHistory'] || [];
365365
const replaceHistory = history.replace || this.viewletState['query.replaceHistory'] || [];
366366
const showReplace = typeof this.viewletState['view.showReplace'] === 'boolean' ? this.viewletState['view.showReplace'] : true;
367+
const preserveCase = this.viewletState['query.preserveCase'] === true;
367368

368369
this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, container, <ISearchWidgetOptions>{
369370
value: contentPattern,
@@ -372,7 +373,8 @@ export class SearchView extends ViewletPanel {
372373
isCaseSensitive: isCaseSensitive,
373374
isWholeWords: isWholeWords,
374375
searchHistory: searchHistory,
375-
replaceHistory: replaceHistory
376+
replaceHistory: replaceHistory,
377+
preserveCase: preserveCase
376378
}));
377379

378380
if (showReplace) {
@@ -390,6 +392,12 @@ export class SearchView extends ViewletPanel {
390392
this.viewModel.replaceActive = state;
391393
this.refreshTree();
392394
}));
395+
396+
this._register(this.searchWidget.onPreserveCaseChange((state) => {
397+
this.viewModel.preserveCase = state;
398+
this.refreshTree();
399+
}));
400+
393401
this._register(this.searchWidget.onReplaceValueChanged((value) => {
394402
this.viewModel.replaceString = this.searchWidget.getReplaceValue();
395403
this.delayedRefresh.trigger(() => this.refreshTree());
@@ -1641,6 +1649,7 @@ export class SearchView extends ViewletPanel {
16411649
const patternExcludes = this.inputPatternExcludes.getValue().trim();
16421650
const patternIncludes = this.inputPatternIncludes.getValue().trim();
16431651
const useExcludesAndIgnoreFiles = this.inputPatternExcludes.useExcludesAndIgnoreFiles();
1652+
const preserveCase = this.viewModel.preserveCase;
16441653

16451654
this.viewletState['query.contentPattern'] = contentPattern;
16461655
this.viewletState['query.regex'] = isRegex;
@@ -1649,6 +1658,7 @@ export class SearchView extends ViewletPanel {
16491658
this.viewletState['query.folderExclusions'] = patternExcludes;
16501659
this.viewletState['query.folderIncludes'] = patternIncludes;
16511660
this.viewletState['query.useExcludesAndIgnoreFiles'] = useExcludesAndIgnoreFiles;
1661+
this.viewletState['query.preserveCase'] = preserveCase;
16521662

16531663
const isReplaceShown = this.searchAndReplaceWidget.isReplaceShown();
16541664
this.viewletState['view.showReplace'] = isReplaceShown;

src/vs/workbench/contrib/search/browser/searchWidget.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
3333
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
3434
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
3535
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
36+
import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
3637

3738
export interface ISearchWidgetOptions {
3839
value?: string;
@@ -42,6 +43,7 @@ export interface ISearchWidgetOptions {
4243
isWholeWords?: boolean;
4344
searchHistory?: string[];
4445
replaceHistory?: string[];
46+
preserveCase?: boolean;
4547
}
4648

4749
class ReplaceAllAction extends Action {
@@ -97,6 +99,7 @@ export class SearchWidget extends Widget {
9799
replaceInputFocusTracker: dom.IFocusTracker;
98100
private replaceInputBoxFocused: IContextKey<boolean>;
99101
private _replaceHistoryDelayer: Delayer<void>;
102+
private _preserveCase: Checkbox;
100103

101104
private ignoreGlobalFindBufferOnNextFocus = false;
102105
private previousGlobalFindBufferValue: string;
@@ -113,6 +116,9 @@ export class SearchWidget extends Widget {
113116
private _onReplaceStateChange = this._register(new Emitter<boolean>());
114117
readonly onReplaceStateChange: Event<boolean> = this._onReplaceStateChange.event;
115118

119+
private _onPreserveCaseChange = this._register(new Emitter<boolean>());
120+
readonly onPreserveCaseChange: Event<boolean> = this._onPreserveCaseChange.event;
121+
116122
private _onReplaceValueChanged = this._register(new Emitter<void>());
117123
readonly onReplaceValueChanged: Event<void> = this._onReplaceValueChanged.event;
118124

@@ -333,13 +339,34 @@ export class SearchWidget extends Widget {
333339

334340
private renderReplaceInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
335341
this.replaceContainer = dom.append(parent, dom.$('.replace-container.disabled'));
336-
const replaceBox = dom.append(this.replaceContainer, dom.$('.input-box'));
342+
const replaceBox = dom.append(this.replaceContainer, dom.$('.replace-input'));
343+
337344
this.replaceInput = this._register(new ContextScopedHistoryInputBox(replaceBox, this.contextViewService, {
338345
ariaLabel: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview or Escape to cancel'),
339346
placeholder: nls.localize('search.replace.placeHolder', "Replace"),
340347
history: options.replaceHistory || [],
341348
flexibleHeight: true
342349
}, this.contextKeyService));
350+
351+
this._preserveCase = this._register(new Checkbox({
352+
actionClassName: 'monaco-preserve-case',
353+
title: nls.localize('label.preserveCaseCheckbox', "Preserve Case"),
354+
isChecked: !!options.preserveCase,
355+
}));
356+
357+
this._register(this._preserveCase.onChange(viaKeyboard => {
358+
if (!viaKeyboard) {
359+
this.replaceInput.focus();
360+
this._onPreserveCaseChange.fire(this._preserveCase.checked);
361+
}
362+
}));
363+
364+
let controls = document.createElement('div');
365+
controls.className = 'controls';
366+
controls.style.display = 'block';
367+
controls.appendChild(this._preserveCase.domNode);
368+
replaceBox.appendChild(controls);
369+
343370
this._register(attachInputBoxStyler(this.replaceInput, this.themeService));
344371
this.onkeydown(this.replaceInput.inputElement, (keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent));
345372
this.replaceInput.value = options.replaceValue || '';

src/vs/workbench/contrib/search/common/searchModel.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,17 @@ export class Match {
103103
}
104104

105105
const fullMatchText = this.fullMatchText();
106-
let replaceString = searchModel.replacePattern.getReplaceString(fullMatchText);
106+
let replaceString = searchModel.replacePattern.getReplaceString(fullMatchText, searchModel.preserveCase);
107107

108108
// If match string is not matching then regex pattern has a lookahead expression
109109
if (replaceString === null) {
110110
const fullMatchTextWithTrailingContent = this.fullMatchText(true);
111-
replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithTrailingContent);
111+
replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithTrailingContent, searchModel.preserveCase);
112112

113113
// Search/find normalize line endings - check whether \r prevents regex from matching
114114
if (replaceString === null) {
115115
const fullMatchTextWithoutCR = fullMatchTextWithTrailingContent.replace(/\r\n/g, '\n');
116-
replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithoutCR);
116+
replaceString = searchModel.replacePattern.getReplaceString(fullMatchTextWithoutCR, searchModel.preserveCase);
117117
}
118118
}
119119

@@ -895,6 +895,7 @@ export class SearchModel extends Disposable {
895895
private _replaceActive: boolean = false;
896896
private _replaceString: string | null = null;
897897
private _replacePattern: ReplacePattern | null = null;
898+
private _preserveCase: boolean = false;
898899

899900
private readonly _onReplaceTermChanged: Emitter<void> = this._register(new Emitter<void>());
900901
readonly onReplaceTermChanged: Event<void> = this._onReplaceTermChanged.event;
@@ -926,6 +927,14 @@ export class SearchModel extends Disposable {
926927
return this._replaceString || '';
927928
}
928929

930+
set preserveCase(value: boolean) {
931+
this._preserveCase = value;
932+
}
933+
934+
get preserveCase(): boolean {
935+
return this._preserveCase;
936+
}
937+
929938
set replaceString(replaceString: string) {
930939
this._replaceString = replaceString;
931940
if (this._searchQuery) {

src/vs/workbench/services/search/common/replace.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import * as strings from 'vs/base/common/strings';
77
import { IPatternInfo } from 'vs/workbench/services/search/common/search';
88
import { CharCode } from 'vs/base/common/charCode';
9+
import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search';
910

1011
export class ReplacePattern {
1112

@@ -54,7 +55,7 @@ export class ReplacePattern {
5455
* Returns the replace string for the first match in the given text.
5556
* If text has no matches then returns null.
5657
*/
57-
getReplaceString(text: string): string | null {
58+
getReplaceString(text: string, preserveCase?: boolean): string | null {
5859
this._regExp.lastIndex = 0;
5960
let match = this._regExp.exec(text);
6061
if (match) {
@@ -65,12 +66,20 @@ export class ReplacePattern {
6566
let replaceString = text.replace(this._regExp, this.pattern);
6667
return replaceString.substr(match.index, match[0].length - (text.length - replaceString.length));
6768
}
68-
return this.pattern;
69+
return this.buildReplaceString(match, preserveCase);
6970
}
7071

7172
return null;
7273
}
7374

75+
public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string {
76+
if (preserveCase) {
77+
return buildReplaceStringWithCasePreserved(matches, this._replacePattern);
78+
} else {
79+
return this._replacePattern;
80+
}
81+
}
82+
7483
/**
7584
* \n => LF
7685
* \t => TAB

0 commit comments

Comments
 (0)