Skip to content

Commit a1de2a7

Browse files
authored
Merge pull request microsoft#96128 from irridia/kb-filereplace-case
Initial implementation: Support \U\u\L\l replace modifiers
2 parents d1220da + 71d5db7 commit a1de2a7

2 files changed

Lines changed: 105 additions & 10 deletions

File tree

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

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class StaticValueReplacePattern {
2020
}
2121

2222
/**
23-
* Assigned when the replace pattern has replacemend patterns.
23+
* Assigned when the replace pattern has replacement patterns.
2424
*/
2525
class DynamicPiecesReplacePattern {
2626
public readonly kind = ReplacePatternKind.DynamicPieces;
@@ -68,7 +68,38 @@ export class ReplacePattern {
6868
}
6969

7070
// match index ReplacePiece
71-
result += ReplacePattern._substitute(piece.matchIndex, matches);
71+
let match: string = ReplacePattern._substitute(piece.matchIndex, matches);
72+
if (piece.caseOps !== null && piece.caseOps.length > 0) {
73+
let repl: string[] = [];
74+
let lenOps: number = piece.caseOps.length;
75+
let opIdx: number = 0;
76+
for (let idx: number = 0, len: number = match.length; idx < len; idx++) {
77+
if (opIdx >= lenOps) {
78+
repl.push(match.slice(idx));
79+
break;
80+
}
81+
switch (piece.caseOps[opIdx]) {
82+
case 'U':
83+
repl.push(match[idx].toUpperCase());
84+
break;
85+
case 'u':
86+
repl.push(match[idx].toUpperCase());
87+
opIdx++;
88+
break;
89+
case 'L':
90+
repl.push(match[idx].toLowerCase());
91+
break;
92+
case 'l':
93+
repl.push(match[idx].toLowerCase());
94+
opIdx++;
95+
break;
96+
default:
97+
repl.push(match[idx]);
98+
}
99+
}
100+
match = repl.join('');
101+
}
102+
result += match;
72103
}
73104

74105
return result;
@@ -102,19 +133,29 @@ export class ReplacePattern {
102133
export class ReplacePiece {
103134

104135
public static staticValue(value: string): ReplacePiece {
105-
return new ReplacePiece(value, -1);
136+
return new ReplacePiece(value, -1, null);
106137
}
107138

108139
public static matchIndex(index: number): ReplacePiece {
109-
return new ReplacePiece(null, index);
140+
return new ReplacePiece(null, index, null);
141+
}
142+
143+
public static caseOps(index: number, caseOps: string[]): ReplacePiece {
144+
return new ReplacePiece(null, index, caseOps);
110145
}
111146

112147
public readonly staticValue: string | null;
113148
public readonly matchIndex: number;
149+
public readonly caseOps: string[] | null;
114150

115-
private constructor(staticValue: string | null, matchIndex: number) {
151+
private constructor(staticValue: string | null, matchIndex: number, caseOps: string[] | null) {
116152
this.staticValue = staticValue;
117153
this.matchIndex = matchIndex;
154+
if (!caseOps || caseOps.length === 0) {
155+
this.caseOps = null;
156+
} else {
157+
this.caseOps = caseOps.slice(0);
158+
}
118159
}
119160
}
120161

@@ -151,12 +192,12 @@ class ReplacePieceBuilder {
151192
this._currentStaticPiece += value;
152193
}
153194

154-
public emitMatchIndex(index: number, toCharIndex: number): void {
195+
public emitMatchIndex(index: number, toCharIndex: number, caseOps: string[]): void {
155196
if (this._currentStaticPiece.length !== 0) {
156197
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
157198
this._currentStaticPiece = '';
158199
}
159-
this._result[this._resultLen++] = ReplacePiece.matchIndex(index);
200+
this._result[this._resultLen++] = ReplacePiece.caseOps(index, caseOps);
160201
this._lastCharIndex = toCharIndex;
161202
}
162203

@@ -175,6 +216,10 @@ class ReplacePieceBuilder {
175216
* \n => inserts a LF
176217
* \t => inserts a TAB
177218
* \\ => inserts a "\".
219+
* \u => upper-cases one character in a match.
220+
* \U => upper-cases ALL remaining characters in a match.
221+
* \l => lower-cases one character in a match.
222+
* \L => lower-cases ALL remaining characters in a match.
178223
* $$ => inserts a "$".
179224
* $& and $0 => inserts the matched substring.
180225
* $n => Where n is a non-negative integer lesser than 100, inserts the nth parenthesized submatch string
@@ -187,6 +232,7 @@ export function parseReplaceString(replaceString: string): ReplacePattern {
187232
return new ReplacePattern(null);
188233
}
189234

235+
let caseOps: string[] = [];
190236
let result = new ReplacePieceBuilder(replaceString);
191237

192238
for (let i = 0, len = replaceString.length; i < len; i++) {
@@ -221,6 +267,20 @@ export function parseReplaceString(replaceString: string): ReplacePattern {
221267
result.emitUnchanged(i - 1);
222268
result.emitStatic('\t', i + 1);
223269
break;
270+
// Case modification of string replacements, patterned after Boost, but only applied
271+
// to the replacement text, not subsequent content.
272+
case CharCode.u:
273+
// \u => upper-cases one character.
274+
case CharCode.U:
275+
// \U => upper-cases ALL following characters.
276+
case CharCode.l:
277+
// \l => lower-cases one character.
278+
case CharCode.L:
279+
// \L => lower-cases ALL following characters.
280+
result.emitUnchanged(i - 1);
281+
result.emitStatic('', i + 1);
282+
caseOps.push(String.fromCharCode(nextChCode));
283+
break;
224284
}
225285

226286
continue;
@@ -248,7 +308,8 @@ export function parseReplaceString(replaceString: string): ReplacePattern {
248308
if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) {
249309
// $& and $0 => inserts the matched substring.
250310
result.emitUnchanged(i - 1);
251-
result.emitMatchIndex(0, i + 1);
311+
result.emitMatchIndex(0, i + 1, caseOps);
312+
caseOps.length = 0;
252313
continue;
253314
}
254315

@@ -268,13 +329,15 @@ export function parseReplaceString(replaceString: string): ReplacePattern {
268329
matchIndex = matchIndex * 10 + (nextNextChCode - CharCode.Digit0);
269330

270331
result.emitUnchanged(i - 2);
271-
result.emitMatchIndex(matchIndex, i + 1);
332+
result.emitMatchIndex(matchIndex, i + 1, caseOps);
333+
caseOps.length = 0;
272334
continue;
273335
}
274336
}
275337

276338
result.emitUnchanged(i - 1);
277-
result.emitMatchIndex(matchIndex, i + 1);
339+
result.emitMatchIndex(matchIndex, i + 1, caseOps);
340+
caseOps.length = 0;
278341
continue;
279342
}
280343
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,38 @@ suite('Replace Pattern test', () => {
6969
testParse('hello$\'', [ReplacePiece.staticValue('hello$\'')]);
7070
});
7171

72+
test('parse replace string with case modifiers', () => {
73+
let testParse = (input: string, expectedPieces: ReplacePiece[]) => {
74+
let actual = parseReplaceString(input);
75+
let expected = new ReplacePattern(expectedPieces);
76+
assert.deepEqual(actual, expected, 'Parsing ' + input);
77+
};
78+
function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void {
79+
let replacePattern = parseReplaceString(replaceString);
80+
let m = search.exec(target);
81+
let actual = replacePattern.buildReplaceString(m);
82+
83+
assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`);
84+
}
85+
86+
// \U, \u => uppercase \L, \l => lowercase \E => cancel
87+
88+
testParse('hello\\U$1', [ReplacePiece.staticValue('hello'), ReplacePiece.caseOps(1, ['U'])]);
89+
assertReplace('func privateFunc(', /func (\w+)\(/, 'func \\U$1(', 'func PRIVATEFUNC(');
90+
91+
testParse('hello\\u$1', [ReplacePiece.staticValue('hello'), ReplacePiece.caseOps(1, ['u'])]);
92+
assertReplace('func privateFunc(', /func (\w+)\(/, 'func \\u$1(', 'func PrivateFunc(');
93+
94+
testParse('hello\\L$1', [ReplacePiece.staticValue('hello'), ReplacePiece.caseOps(1, ['L'])]);
95+
assertReplace('func privateFunc(', /func (\w+)\(/, 'func \\L$1(', 'func privatefunc(');
96+
97+
testParse('hello\\l$1', [ReplacePiece.staticValue('hello'), ReplacePiece.caseOps(1, ['l'])]);
98+
assertReplace('func PrivateFunc(', /func (\w+)\(/, 'func \\l$1(', 'func privateFunc(');
99+
100+
testParse('hello$1\\u\\u\\U$4goodbye', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(1), ReplacePiece.caseOps(4, ['u', 'u', 'U']), ReplacePiece.staticValue('goodbye')]);
101+
assertReplace('hellogooDbye', /hello(\w+)/, 'hello\\u\\u\\l\\l\\U$1', 'helloGOodBYE');
102+
});
103+
72104
test('replace has JavaScript semantics', () => {
73105
let testJSReplaceSemantics = (target: string, search: RegExp, replaceString: string, expected: string) => {
74106
let replacePattern = parseReplaceString(replaceString);

0 commit comments

Comments
 (0)