Skip to content

Commit c332f2d

Browse files
author
Ken Brownfield
committed
Suppert \U\u\L\l replace modifiers in global search/replace (see PR#96128)
1 parent 4de754d commit c332f2d

2 files changed

Lines changed: 89 additions & 2 deletions

File tree

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

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class ReplacePattern {
1313
private _replacePattern: string;
1414
private _hasParameters: boolean = false;
1515
private _regExp: RegExp;
16+
private _caseOpsRegExp: RegExp;
1617

1718
constructor(replaceString: string, searchPatternInfo: IPatternInfo)
1819
constructor(replaceString: string, parseParameters: boolean, regEx: RegExp)
@@ -37,6 +38,8 @@ export class ReplacePattern {
3738
if (this._regExp.global) {
3839
this._regExp = strings.createRegExp(this._regExp.source, true, { matchCase: !this._regExp.ignoreCase, wholeWord: false, multiline: this._regExp.multiline, global: false });
3940
}
41+
42+
this._caseOpsRegExp = new RegExp(/([^\\]*?)((?:\\[uUlL])+?|)(\$[0-9]+)(.*?)/g);
4043
}
4144

4245
get hasParameters(): boolean {
@@ -60,10 +63,10 @@ export class ReplacePattern {
6063
const match = this._regExp.exec(text);
6164
if (match) {
6265
if (this.hasParameters) {
66+
const replaceString = this.replaceWithCaseOperations(text, this._regExp, this.buildReplaceString(match, preserveCase));
6367
if (match[0] === text) {
64-
return text.replace(this._regExp, this.buildReplaceString(match, preserveCase));
68+
return replaceString;
6569
}
66-
const replaceString = text.replace(this._regExp, this.buildReplaceString(match, preserveCase));
6770
return replaceString.substr(match.index, match[0].length - (text.length - replaceString.length));
6871
}
6972
return this.buildReplaceString(match, preserveCase);
@@ -72,6 +75,84 @@ export class ReplacePattern {
7275
return null;
7376
}
7477

78+
/**
79+
* replaceWithCaseOperations applies case operations to relevant replacement strings and applies
80+
* the affected $N arguments. It then passes unaffected $N arguments through to string.replace().
81+
*
82+
* \u => upper-cases one character in a match.
83+
* \U => upper-cases ALL remaining characters in a match.
84+
* \l => lower-cases one character in a match.
85+
* \L => lower-cases ALL remaining characters in a match.
86+
*/
87+
private replaceWithCaseOperations(text: string, regex: RegExp, replaceString: string): string {
88+
// Short-circuit the common path.
89+
if (!/\\[uUlL]/.test(replaceString)) {
90+
return text.replace(regex, replaceString);
91+
}
92+
// Store the values of the search parameters.
93+
const firstMatch = regex.exec(text);
94+
if (firstMatch === null) {
95+
return text.replace(regex, replaceString);
96+
}
97+
98+
let patMatch: RegExpExecArray | null;
99+
let newReplaceString = '';
100+
let lastIndex = 0;
101+
let lastMatch = '';
102+
// For each annotated $N, perform text processing on the parameters and perform the substitution.
103+
while ((patMatch = this._caseOpsRegExp.exec(replaceString)) !== null) {
104+
lastIndex = patMatch.index;
105+
const fullMatch = patMatch[0];
106+
lastMatch = fullMatch;
107+
let caseOps = patMatch[2]; // \u, \l\u, etc.
108+
const money = patMatch[3]; // $1, $2, etc.
109+
110+
if (!caseOps) {
111+
newReplaceString += fullMatch;
112+
continue;
113+
}
114+
const replacement = firstMatch[parseInt(money.slice(1))];
115+
if (!replacement) {
116+
newReplaceString += fullMatch;
117+
continue;
118+
}
119+
const replacementLen = replacement.length;
120+
121+
newReplaceString += patMatch[1]; // prefix
122+
caseOps = caseOps.replace(/\\/g, '');
123+
let i = 0;
124+
for (; i < caseOps.length; i++) {
125+
switch (caseOps[i]) {
126+
case 'U':
127+
newReplaceString += replacement.slice(i).toUpperCase();
128+
i = replacementLen;
129+
break;
130+
case 'u':
131+
newReplaceString += replacement[i].toUpperCase();
132+
break;
133+
case 'L':
134+
newReplaceString += replacement.slice(i).toLowerCase();
135+
i = replacementLen;
136+
break;
137+
case 'l':
138+
newReplaceString += replacement[i].toLowerCase();
139+
break;
140+
}
141+
}
142+
// Append any remaining replacement string content not covered by case operations.
143+
if (i < replacementLen) {
144+
newReplaceString += replacement.slice(i);
145+
}
146+
147+
newReplaceString += patMatch[4]; // suffix
148+
}
149+
150+
// Append any remaining trailing content after the final regex match.
151+
newReplaceString += replaceString.slice(lastIndex + lastMatch.length);
152+
153+
return text.replace(regex, newReplaceString);
154+
}
155+
75156
public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string {
76157
if (preserveCase) {
77158
return buildReplaceStringWithCasePreserved(matches, this._replacePattern);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ suite('Replace Pattern test', () => {
140140
assert.equal('cat ()', actual);
141141
});
142142

143+
test('case operations', () => {
144+
let testObject = new ReplacePattern('a\\u$1l\\u\\l\\U$2M$3n', { pattern: 'a(l)l(good)m(e)n', isRegExp: true });
145+
let actual = testObject.getReplaceString('allgoodmen');
146+
assert.equal('aLlGoODMen', actual);
147+
});
148+
143149
test('get replace string for no matches', () => {
144150
let testObject = new ReplacePattern('hello', { pattern: 'bla', isRegExp: true });
145151
let actual = testObject.getReplaceString('foo');

0 commit comments

Comments
 (0)