mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-14 20:34:30 +01:00
280 lines
7.3 KiB
TypeScript
280 lines
7.3 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { CharCode } from 'vs/base/common/charCode';
|
|
|
|
const enum ReplacePatternKind {
|
|
StaticValue = 0,
|
|
DynamicPieces = 1
|
|
}
|
|
|
|
/**
|
|
* Assigned when the replace pattern is entirely static.
|
|
*/
|
|
class StaticValueReplacePattern {
|
|
public readonly kind = ReplacePatternKind.StaticValue;
|
|
constructor(public readonly staticValue: string) { }
|
|
}
|
|
|
|
/**
|
|
* Assigned when the replace pattern has replacemend patterns.
|
|
*/
|
|
class DynamicPiecesReplacePattern {
|
|
public readonly kind = ReplacePatternKind.DynamicPieces;
|
|
constructor(public readonly pieces: ReplacePiece[]) { }
|
|
}
|
|
|
|
export class ReplacePattern {
|
|
|
|
public static fromStaticValue(value: string): ReplacePattern {
|
|
return new ReplacePattern([ReplacePiece.staticValue(value)]);
|
|
}
|
|
|
|
private readonly _state: StaticValueReplacePattern | DynamicPiecesReplacePattern;
|
|
|
|
public get hasReplacementPatterns(): boolean {
|
|
return (this._state.kind === ReplacePatternKind.DynamicPieces);
|
|
}
|
|
|
|
constructor(pieces: ReplacePiece[] | null) {
|
|
if (!pieces || pieces.length === 0) {
|
|
this._state = new StaticValueReplacePattern('');
|
|
} else if (pieces.length === 1 && pieces[0].staticValue !== null) {
|
|
this._state = new StaticValueReplacePattern(pieces[0].staticValue);
|
|
} else {
|
|
this._state = new DynamicPiecesReplacePattern(pieces);
|
|
}
|
|
}
|
|
|
|
public buildReplaceString(matches: string[] | null): string {
|
|
if (this._state.kind === ReplacePatternKind.StaticValue) {
|
|
return this._state.staticValue;
|
|
}
|
|
|
|
let result = '';
|
|
for (let i = 0, len = this._state.pieces.length; i < len; i++) {
|
|
let piece = this._state.pieces[i];
|
|
if (piece.staticValue !== null) {
|
|
// static value ReplacePiece
|
|
result += piece.staticValue;
|
|
continue;
|
|
}
|
|
|
|
// match index ReplacePiece
|
|
result += ReplacePattern._substitute(piece.matchIndex, matches);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static _substitute(matchIndex: number, matches: string[] | null): string {
|
|
if (matches === null) {
|
|
return '';
|
|
}
|
|
if (matchIndex === 0) {
|
|
return matches[0];
|
|
}
|
|
|
|
let remainder = '';
|
|
while (matchIndex > 0) {
|
|
if (matchIndex < matches.length) {
|
|
// A match can be undefined
|
|
let match = (matches[matchIndex] || '');
|
|
return match + remainder;
|
|
}
|
|
remainder = String(matchIndex % 10) + remainder;
|
|
matchIndex = Math.floor(matchIndex / 10);
|
|
}
|
|
return '$' + remainder;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A replace piece can either be a static string or an index to a specific match.
|
|
*/
|
|
export class ReplacePiece {
|
|
|
|
public static staticValue(value: string): ReplacePiece {
|
|
return new ReplacePiece(value, -1);
|
|
}
|
|
|
|
public static matchIndex(index: number): ReplacePiece {
|
|
return new ReplacePiece(null, index);
|
|
}
|
|
|
|
public readonly staticValue: string | null;
|
|
public readonly matchIndex: number;
|
|
|
|
private constructor(staticValue: string | null, matchIndex: number) {
|
|
this.staticValue = staticValue;
|
|
this.matchIndex = matchIndex;
|
|
}
|
|
}
|
|
|
|
class ReplacePieceBuilder {
|
|
|
|
private readonly _source: string;
|
|
private _lastCharIndex: number;
|
|
private readonly _result: ReplacePiece[];
|
|
private _resultLen: number;
|
|
private _currentStaticPiece: string;
|
|
|
|
constructor(source: string) {
|
|
this._source = source;
|
|
this._lastCharIndex = 0;
|
|
this._result = [];
|
|
this._resultLen = 0;
|
|
this._currentStaticPiece = '';
|
|
}
|
|
|
|
public emitUnchanged(toCharIndex: number): void {
|
|
this._emitStatic(this._source.substring(this._lastCharIndex, toCharIndex));
|
|
this._lastCharIndex = toCharIndex;
|
|
}
|
|
|
|
public emitStatic(value: string, toCharIndex: number): void {
|
|
this._emitStatic(value);
|
|
this._lastCharIndex = toCharIndex;
|
|
}
|
|
|
|
private _emitStatic(value: string): void {
|
|
if (value.length === 0) {
|
|
return;
|
|
}
|
|
this._currentStaticPiece += value;
|
|
}
|
|
|
|
public emitMatchIndex(index: number, toCharIndex: number): void {
|
|
if (this._currentStaticPiece.length !== 0) {
|
|
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
|
|
this._currentStaticPiece = '';
|
|
}
|
|
this._result[this._resultLen++] = ReplacePiece.matchIndex(index);
|
|
this._lastCharIndex = toCharIndex;
|
|
}
|
|
|
|
|
|
public finalize(): ReplacePattern {
|
|
this.emitUnchanged(this._source.length);
|
|
if (this._currentStaticPiece.length !== 0) {
|
|
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
|
|
this._currentStaticPiece = '';
|
|
}
|
|
return new ReplacePattern(this._result);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* \n => inserts a LF
|
|
* \t => inserts a TAB
|
|
* \\ => inserts a "\".
|
|
* $$ => inserts a "$".
|
|
* $& and $0 => inserts the matched substring.
|
|
* $n => Where n is a non-negative integer lesser than 100, inserts the nth parenthesized submatch string
|
|
* everything else stays untouched
|
|
*
|
|
* Also see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
|
|
*/
|
|
export function parseReplaceString(replaceString: string): ReplacePattern {
|
|
if (!replaceString || replaceString.length === 0) {
|
|
return new ReplacePattern(null);
|
|
}
|
|
|
|
let result = new ReplacePieceBuilder(replaceString);
|
|
|
|
for (let i = 0, len = replaceString.length; i < len; i++) {
|
|
let chCode = replaceString.charCodeAt(i);
|
|
|
|
if (chCode === CharCode.Backslash) {
|
|
|
|
// move to next char
|
|
i++;
|
|
|
|
if (i >= len) {
|
|
// string ends with a \
|
|
break;
|
|
}
|
|
|
|
let nextChCode = replaceString.charCodeAt(i);
|
|
// let replaceWithCharacter: string | null = null;
|
|
|
|
switch (nextChCode) {
|
|
case CharCode.Backslash:
|
|
// \\ => inserts a "\"
|
|
result.emitUnchanged(i - 1);
|
|
result.emitStatic('\\', i + 1);
|
|
break;
|
|
case CharCode.n:
|
|
// \n => inserts a LF
|
|
result.emitUnchanged(i - 1);
|
|
result.emitStatic('\n', i + 1);
|
|
break;
|
|
case CharCode.t:
|
|
// \t => inserts a TAB
|
|
result.emitUnchanged(i - 1);
|
|
result.emitStatic('\t', i + 1);
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (chCode === CharCode.DollarSign) {
|
|
|
|
// move to next char
|
|
i++;
|
|
|
|
if (i >= len) {
|
|
// string ends with a $
|
|
break;
|
|
}
|
|
|
|
let nextChCode = replaceString.charCodeAt(i);
|
|
|
|
if (nextChCode === CharCode.DollarSign) {
|
|
// $$ => inserts a "$"
|
|
result.emitUnchanged(i - 1);
|
|
result.emitStatic('$', i + 1);
|
|
continue;
|
|
}
|
|
|
|
if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) {
|
|
// $& and $0 => inserts the matched substring.
|
|
result.emitUnchanged(i - 1);
|
|
result.emitMatchIndex(0, i + 1);
|
|
continue;
|
|
}
|
|
|
|
if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) {
|
|
// $n
|
|
|
|
let matchIndex = nextChCode - CharCode.Digit0;
|
|
|
|
// peek next char to probe for $nn
|
|
if (i + 1 < len) {
|
|
let nextNextChCode = replaceString.charCodeAt(i + 1);
|
|
if (CharCode.Digit0 <= nextNextChCode && nextNextChCode <= CharCode.Digit9) {
|
|
// $nn
|
|
|
|
// move to next char
|
|
i++;
|
|
matchIndex = matchIndex * 10 + (nextNextChCode - CharCode.Digit0);
|
|
|
|
result.emitUnchanged(i - 2);
|
|
result.emitMatchIndex(matchIndex, i + 1);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
result.emitUnchanged(i - 1);
|
|
result.emitMatchIndex(matchIndex, i + 1);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result.finalize();
|
|
}
|