Files
vscode/src/vs/editor/contrib/find/replacePattern.ts
T
2018-10-15 10:30:49 +02:00

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();
}