Path: blob/main/src/vs/editor/contrib/find/browser/replacePattern.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CharCode } from '../../../../base/common/charCode.js';6import { buildReplaceStringWithCasePreserved } from '../../../../base/common/search.js';78const enum ReplacePatternKind {9StaticValue = 0,10DynamicPieces = 111}1213/**14* Assigned when the replace pattern is entirely static.15*/16class StaticValueReplacePattern {17public readonly kind = ReplacePatternKind.StaticValue;18constructor(public readonly staticValue: string) { }19}2021/**22* Assigned when the replace pattern has replacement patterns.23*/24class DynamicPiecesReplacePattern {25public readonly kind = ReplacePatternKind.DynamicPieces;26constructor(public readonly pieces: ReplacePiece[]) { }27}2829export class ReplacePattern {3031public static fromStaticValue(value: string): ReplacePattern {32return new ReplacePattern([ReplacePiece.staticValue(value)]);33}3435private readonly _state: StaticValueReplacePattern | DynamicPiecesReplacePattern;3637public get hasReplacementPatterns(): boolean {38return (this._state.kind === ReplacePatternKind.DynamicPieces);39}4041constructor(pieces: ReplacePiece[] | null) {42if (!pieces || pieces.length === 0) {43this._state = new StaticValueReplacePattern('');44} else if (pieces.length === 1 && pieces[0].staticValue !== null) {45this._state = new StaticValueReplacePattern(pieces[0].staticValue);46} else {47this._state = new DynamicPiecesReplacePattern(pieces);48}49}5051public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string {52if (this._state.kind === ReplacePatternKind.StaticValue) {53if (preserveCase) {54return buildReplaceStringWithCasePreserved(matches, this._state.staticValue);55} else {56return this._state.staticValue;57}58}5960let result = '';61for (let i = 0, len = this._state.pieces.length; i < len; i++) {62const piece = this._state.pieces[i];63if (piece.staticValue !== null) {64// static value ReplacePiece65result += piece.staticValue;66continue;67}6869// match index ReplacePiece70let match: string = ReplacePattern._substitute(piece.matchIndex, matches);71if (piece.caseOps !== null && piece.caseOps.length > 0) {72const repl: string[] = [];73const lenOps: number = piece.caseOps.length;74let opIdx: number = 0;75for (let idx: number = 0, len: number = match.length; idx < len; idx++) {76if (opIdx >= lenOps) {77repl.push(match.slice(idx));78break;79}80switch (piece.caseOps[opIdx]) {81case 'U':82repl.push(match[idx].toUpperCase());83break;84case 'u':85repl.push(match[idx].toUpperCase());86opIdx++;87break;88case 'L':89repl.push(match[idx].toLowerCase());90break;91case 'l':92repl.push(match[idx].toLowerCase());93opIdx++;94break;95default:96repl.push(match[idx]);97}98}99match = repl.join('');100}101result += match;102}103104return result;105}106107private static _substitute(matchIndex: number, matches: string[] | null): string {108if (matches === null) {109return '';110}111if (matchIndex === 0) {112return matches[0];113}114115let remainder = '';116while (matchIndex > 0) {117if (matchIndex < matches.length) {118// A match can be undefined119const match = (matches[matchIndex] || '');120return match + remainder;121}122remainder = String(matchIndex % 10) + remainder;123matchIndex = Math.floor(matchIndex / 10);124}125return '$' + remainder;126}127}128129/**130* A replace piece can either be a static string or an index to a specific match.131*/132export class ReplacePiece {133134public static staticValue(value: string): ReplacePiece {135return new ReplacePiece(value, -1, null);136}137138public static matchIndex(index: number): ReplacePiece {139return new ReplacePiece(null, index, null);140}141142public static caseOps(index: number, caseOps: string[]): ReplacePiece {143return new ReplacePiece(null, index, caseOps);144}145146public readonly staticValue: string | null;147public readonly matchIndex: number;148public readonly caseOps: string[] | null;149150private constructor(staticValue: string | null, matchIndex: number, caseOps: string[] | null) {151this.staticValue = staticValue;152this.matchIndex = matchIndex;153if (!caseOps || caseOps.length === 0) {154this.caseOps = null;155} else {156this.caseOps = caseOps.slice(0);157}158}159}160161class ReplacePieceBuilder {162163private readonly _source: string;164private _lastCharIndex: number;165private readonly _result: ReplacePiece[];166private _resultLen: number;167private _currentStaticPiece: string;168169constructor(source: string) {170this._source = source;171this._lastCharIndex = 0;172this._result = [];173this._resultLen = 0;174this._currentStaticPiece = '';175}176177public emitUnchanged(toCharIndex: number): void {178this._emitStatic(this._source.substring(this._lastCharIndex, toCharIndex));179this._lastCharIndex = toCharIndex;180}181182public emitStatic(value: string, toCharIndex: number): void {183this._emitStatic(value);184this._lastCharIndex = toCharIndex;185}186187private _emitStatic(value: string): void {188if (value.length === 0) {189return;190}191this._currentStaticPiece += value;192}193194public emitMatchIndex(index: number, toCharIndex: number, caseOps: string[]): void {195if (this._currentStaticPiece.length !== 0) {196this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);197this._currentStaticPiece = '';198}199this._result[this._resultLen++] = ReplacePiece.caseOps(index, caseOps);200this._lastCharIndex = toCharIndex;201}202203204public finalize(): ReplacePattern {205this.emitUnchanged(this._source.length);206if (this._currentStaticPiece.length !== 0) {207this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);208this._currentStaticPiece = '';209}210return new ReplacePattern(this._result);211}212}213214/**215* \n => inserts a LF216* \t => inserts a TAB217* \\ => inserts a "\".218* \u => upper-cases one character in a match.219* \U => upper-cases ALL remaining characters in a match.220* \l => lower-cases one character in a match.221* \L => lower-cases ALL remaining characters in a match.222* $$ => inserts a "$".223* $& and $0 => inserts the matched substring.224* $n => Where n is a non-negative integer lesser than 100, inserts the nth parenthesized submatch string225* everything else stays untouched226*227* Also see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter228*/229export function parseReplaceString(replaceString: string): ReplacePattern {230if (!replaceString || replaceString.length === 0) {231return new ReplacePattern(null);232}233234const caseOps: string[] = [];235const result = new ReplacePieceBuilder(replaceString);236237for (let i = 0, len = replaceString.length; i < len; i++) {238const chCode = replaceString.charCodeAt(i);239240if (chCode === CharCode.Backslash) {241242// move to next char243i++;244245if (i >= len) {246// string ends with a \247break;248}249250const nextChCode = replaceString.charCodeAt(i);251// let replaceWithCharacter: string | null = null;252253switch (nextChCode) {254case CharCode.Backslash:255// \\ => inserts a "\"256result.emitUnchanged(i - 1);257result.emitStatic('\\', i + 1);258break;259case CharCode.n:260// \n => inserts a LF261result.emitUnchanged(i - 1);262result.emitStatic('\n', i + 1);263break;264case CharCode.t:265// \t => inserts a TAB266result.emitUnchanged(i - 1);267result.emitStatic('\t', i + 1);268break;269// Case modification of string replacements, patterned after Boost, but only applied270// to the replacement text, not subsequent content.271case CharCode.u:272// \u => upper-cases one character.273case CharCode.U:274// \U => upper-cases ALL following characters.275case CharCode.l:276// \l => lower-cases one character.277case CharCode.L:278// \L => lower-cases ALL following characters.279result.emitUnchanged(i - 1);280result.emitStatic('', i + 1);281caseOps.push(String.fromCharCode(nextChCode));282break;283}284285continue;286}287288if (chCode === CharCode.DollarSign) {289290// move to next char291i++;292293if (i >= len) {294// string ends with a $295break;296}297298const nextChCode = replaceString.charCodeAt(i);299300if (nextChCode === CharCode.DollarSign) {301// $$ => inserts a "$"302result.emitUnchanged(i - 1);303result.emitStatic('$', i + 1);304continue;305}306307if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) {308// $& and $0 => inserts the matched substring.309result.emitUnchanged(i - 1);310result.emitMatchIndex(0, i + 1, caseOps);311caseOps.length = 0;312continue;313}314315if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) {316// $n317318let matchIndex = nextChCode - CharCode.Digit0;319320// peek next char to probe for $nn321if (i + 1 < len) {322const nextNextChCode = replaceString.charCodeAt(i + 1);323if (CharCode.Digit0 <= nextNextChCode && nextNextChCode <= CharCode.Digit9) {324// $nn325326// move to next char327i++;328matchIndex = matchIndex * 10 + (nextNextChCode - CharCode.Digit0);329330result.emitUnchanged(i - 2);331result.emitMatchIndex(matchIndex, i + 1, caseOps);332caseOps.length = 0;333continue;334}335}336337result.emitUnchanged(i - 1);338result.emitMatchIndex(matchIndex, i + 1, caseOps);339caseOps.length = 0;340continue;341}342}343}344345return result.finalize();346}347348349