Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/find/browser/replacePattern.ts
3296 views
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 { CharCode } from '../../../../base/common/charCode.js';
7
import { buildReplaceStringWithCasePreserved } from '../../../../base/common/search.js';
8
9
const enum ReplacePatternKind {
10
StaticValue = 0,
11
DynamicPieces = 1
12
}
13
14
/**
15
* Assigned when the replace pattern is entirely static.
16
*/
17
class StaticValueReplacePattern {
18
public readonly kind = ReplacePatternKind.StaticValue;
19
constructor(public readonly staticValue: string) { }
20
}
21
22
/**
23
* Assigned when the replace pattern has replacement patterns.
24
*/
25
class DynamicPiecesReplacePattern {
26
public readonly kind = ReplacePatternKind.DynamicPieces;
27
constructor(public readonly pieces: ReplacePiece[]) { }
28
}
29
30
export class ReplacePattern {
31
32
public static fromStaticValue(value: string): ReplacePattern {
33
return new ReplacePattern([ReplacePiece.staticValue(value)]);
34
}
35
36
private readonly _state: StaticValueReplacePattern | DynamicPiecesReplacePattern;
37
38
public get hasReplacementPatterns(): boolean {
39
return (this._state.kind === ReplacePatternKind.DynamicPieces);
40
}
41
42
constructor(pieces: ReplacePiece[] | null) {
43
if (!pieces || pieces.length === 0) {
44
this._state = new StaticValueReplacePattern('');
45
} else if (pieces.length === 1 && pieces[0].staticValue !== null) {
46
this._state = new StaticValueReplacePattern(pieces[0].staticValue);
47
} else {
48
this._state = new DynamicPiecesReplacePattern(pieces);
49
}
50
}
51
52
public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string {
53
if (this._state.kind === ReplacePatternKind.StaticValue) {
54
if (preserveCase) {
55
return buildReplaceStringWithCasePreserved(matches, this._state.staticValue);
56
} else {
57
return this._state.staticValue;
58
}
59
}
60
61
let result = '';
62
for (let i = 0, len = this._state.pieces.length; i < len; i++) {
63
const piece = this._state.pieces[i];
64
if (piece.staticValue !== null) {
65
// static value ReplacePiece
66
result += piece.staticValue;
67
continue;
68
}
69
70
// match index ReplacePiece
71
let match: string = ReplacePattern._substitute(piece.matchIndex, matches);
72
if (piece.caseOps !== null && piece.caseOps.length > 0) {
73
const repl: string[] = [];
74
const 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;
103
}
104
105
return result;
106
}
107
108
private static _substitute(matchIndex: number, matches: string[] | null): string {
109
if (matches === null) {
110
return '';
111
}
112
if (matchIndex === 0) {
113
return matches[0];
114
}
115
116
let remainder = '';
117
while (matchIndex > 0) {
118
if (matchIndex < matches.length) {
119
// A match can be undefined
120
const match = (matches[matchIndex] || '');
121
return match + remainder;
122
}
123
remainder = String(matchIndex % 10) + remainder;
124
matchIndex = Math.floor(matchIndex / 10);
125
}
126
return '$' + remainder;
127
}
128
}
129
130
/**
131
* A replace piece can either be a static string or an index to a specific match.
132
*/
133
export class ReplacePiece {
134
135
public static staticValue(value: string): ReplacePiece {
136
return new ReplacePiece(value, -1, null);
137
}
138
139
public static matchIndex(index: number): ReplacePiece {
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);
145
}
146
147
public readonly staticValue: string | null;
148
public readonly matchIndex: number;
149
public readonly caseOps: string[] | null;
150
151
private constructor(staticValue: string | null, matchIndex: number, caseOps: string[] | null) {
152
this.staticValue = staticValue;
153
this.matchIndex = matchIndex;
154
if (!caseOps || caseOps.length === 0) {
155
this.caseOps = null;
156
} else {
157
this.caseOps = caseOps.slice(0);
158
}
159
}
160
}
161
162
class ReplacePieceBuilder {
163
164
private readonly _source: string;
165
private _lastCharIndex: number;
166
private readonly _result: ReplacePiece[];
167
private _resultLen: number;
168
private _currentStaticPiece: string;
169
170
constructor(source: string) {
171
this._source = source;
172
this._lastCharIndex = 0;
173
this._result = [];
174
this._resultLen = 0;
175
this._currentStaticPiece = '';
176
}
177
178
public emitUnchanged(toCharIndex: number): void {
179
this._emitStatic(this._source.substring(this._lastCharIndex, toCharIndex));
180
this._lastCharIndex = toCharIndex;
181
}
182
183
public emitStatic(value: string, toCharIndex: number): void {
184
this._emitStatic(value);
185
this._lastCharIndex = toCharIndex;
186
}
187
188
private _emitStatic(value: string): void {
189
if (value.length === 0) {
190
return;
191
}
192
this._currentStaticPiece += value;
193
}
194
195
public emitMatchIndex(index: number, toCharIndex: number, caseOps: string[]): void {
196
if (this._currentStaticPiece.length !== 0) {
197
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
198
this._currentStaticPiece = '';
199
}
200
this._result[this._resultLen++] = ReplacePiece.caseOps(index, caseOps);
201
this._lastCharIndex = toCharIndex;
202
}
203
204
205
public finalize(): ReplacePattern {
206
this.emitUnchanged(this._source.length);
207
if (this._currentStaticPiece.length !== 0) {
208
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
209
this._currentStaticPiece = '';
210
}
211
return new ReplacePattern(this._result);
212
}
213
}
214
215
/**
216
* \n => inserts a LF
217
* \t => inserts a TAB
218
* \\ => 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.
223
* $$ => inserts a "$".
224
* $& and $0 => inserts the matched substring.
225
* $n => Where n is a non-negative integer lesser than 100, inserts the nth parenthesized submatch string
226
* everything else stays untouched
227
*
228
* Also see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
229
*/
230
export function parseReplaceString(replaceString: string): ReplacePattern {
231
if (!replaceString || replaceString.length === 0) {
232
return new ReplacePattern(null);
233
}
234
235
const caseOps: string[] = [];
236
const result = new ReplacePieceBuilder(replaceString);
237
238
for (let i = 0, len = replaceString.length; i < len; i++) {
239
const chCode = replaceString.charCodeAt(i);
240
241
if (chCode === CharCode.Backslash) {
242
243
// move to next char
244
i++;
245
246
if (i >= len) {
247
// string ends with a \
248
break;
249
}
250
251
const nextChCode = replaceString.charCodeAt(i);
252
// let replaceWithCharacter: string | null = null;
253
254
switch (nextChCode) {
255
case CharCode.Backslash:
256
// \\ => inserts a "\"
257
result.emitUnchanged(i - 1);
258
result.emitStatic('\\', i + 1);
259
break;
260
case CharCode.n:
261
// \n => inserts a LF
262
result.emitUnchanged(i - 1);
263
result.emitStatic('\n', i + 1);
264
break;
265
case CharCode.t:
266
// \t => inserts a TAB
267
result.emitUnchanged(i - 1);
268
result.emitStatic('\t', i + 1);
269
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;
284
}
285
286
continue;
287
}
288
289
if (chCode === CharCode.DollarSign) {
290
291
// move to next char
292
i++;
293
294
if (i >= len) {
295
// string ends with a $
296
break;
297
}
298
299
const nextChCode = replaceString.charCodeAt(i);
300
301
if (nextChCode === CharCode.DollarSign) {
302
// $$ => inserts a "$"
303
result.emitUnchanged(i - 1);
304
result.emitStatic('$', i + 1);
305
continue;
306
}
307
308
if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) {
309
// $& and $0 => inserts the matched substring.
310
result.emitUnchanged(i - 1);
311
result.emitMatchIndex(0, i + 1, caseOps);
312
caseOps.length = 0;
313
continue;
314
}
315
316
if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) {
317
// $n
318
319
let matchIndex = nextChCode - CharCode.Digit0;
320
321
// peek next char to probe for $nn
322
if (i + 1 < len) {
323
const nextNextChCode = replaceString.charCodeAt(i + 1);
324
if (CharCode.Digit0 <= nextNextChCode && nextNextChCode <= CharCode.Digit9) {
325
// $nn
326
327
// move to next char
328
i++;
329
matchIndex = matchIndex * 10 + (nextNextChCode - CharCode.Digit0);
330
331
result.emitUnchanged(i - 2);
332
result.emitMatchIndex(matchIndex, i + 1, caseOps);
333
caseOps.length = 0;
334
continue;
335
}
336
}
337
338
result.emitUnchanged(i - 1);
339
result.emitMatchIndex(matchIndex, i + 1, caseOps);
340
caseOps.length = 0;
341
continue;
342
}
343
}
344
}
345
346
return result.finalize();
347
}
348
349