Skip to content

Commit

Permalink
Implement #9010
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 committed Jul 13, 2016
1 parent d926a2f commit 758a61e
Show file tree
Hide file tree
Showing 12 changed files with 402 additions and 240 deletions.
103 changes: 4 additions & 99 deletions src/vs/editor/contrib/find/common/findModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import {RunOnceScheduler} from 'vs/base/common/async';
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings';
import {ReplacePattern} from 'vs/platform/search/common/replace';
import {ReplaceCommand} from 'vs/editor/common/commands/replaceCommand';
import {Position} from 'vs/editor/common/core/position';
import {Range} from 'vs/editor/common/core/range';
Expand Down Expand Up @@ -290,13 +291,8 @@ export class FindModelBoundToEditorModel {
}

private getReplaceString(matchedString:string): string {
if (!this._state.isRegex) {
return this._state.replaceString;
}
let regexp = strings.createRegExp(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord, true);
// Parse the replace string to support that \t or \n mean the right thing
let parsedReplaceString = parseReplaceString(this._state.replaceString);
return matchedString.replace(regexp, parsedReplaceString);
let replacePattern= new ReplacePattern(this._state.replaceString, {pattern: this._state.searchString, isRegExp: this._state.isRegex, isCaseSensitive: this._state.matchCase, isWordMatch: this._state.wholeWord});
return replacePattern.getReplaceString(matchedString);
}

private _rangeIsMatch(range:Range): boolean {
Expand Down Expand Up @@ -378,95 +374,4 @@ export class FindModelBoundToEditorModel {
this._ignoreModelContentChanged = false;
}
}
}

const BACKSLASH_CHAR_CODE = '\\'.charCodeAt(0);
const DOLLAR_CHAR_CODE = '$'.charCodeAt(0);
const ZERO_CHAR_CODE = '0'.charCodeAt(0);
const n_CHAR_CODE = 'n'.charCodeAt(0);
const t_CHAR_CODE = 't'.charCodeAt(0);

/**
* \n => LF
* \t => TAB
* \\ => \
* $0 => $& (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter)
* everything else stays untouched
*/
export function parseReplaceString(input:string): string {
if (!input || input.length === 0) {
return input;
}

let substrFrom = 0, result = '';
for (let i = 0, len = input.length; i < len; i++) {
let chCode = input.charCodeAt(i);

if (chCode === BACKSLASH_CHAR_CODE) {

// move to next char
i++;

if (i >= len) {
// string ends with a \
break;
}

let nextChCode = input.charCodeAt(i);
let replaceWithCharacter: string = null;

switch (nextChCode) {
case BACKSLASH_CHAR_CODE:
// \\ => \
replaceWithCharacter = '\\';
break;
case n_CHAR_CODE:
// \n => LF
replaceWithCharacter = '\n';
break;
case t_CHAR_CODE:
// \t => TAB
replaceWithCharacter = '\t';
break;
}

if (replaceWithCharacter) {
result += input.substring(substrFrom, i - 1) + replaceWithCharacter;
substrFrom = i + 1;
}
}

if (chCode === DOLLAR_CHAR_CODE) {

// move to next char
i++;

if (i >= len) {
// string ends with a $
break;
}

let nextChCode = input.charCodeAt(i);
let replaceWithCharacter: string = null;

switch (nextChCode) {
case ZERO_CHAR_CODE:
// $0 => $&
replaceWithCharacter = '$&';
break;
}

if (replaceWithCharacter) {
result += input.substring(substrFrom, i - 1) + replaceWithCharacter;
substrFrom = i + 1;
}
}
}

if (substrFrom === 0) {
// no replacement occured
return input;
}

return result + input.substring(substrFrom);
}
}
56 changes: 1 addition & 55 deletions src/vs/editor/contrib/find/test/common/findModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,66 +10,12 @@ import {Position} from 'vs/editor/common/core/position';
import {Selection} from 'vs/editor/common/core/selection';
import {Range} from 'vs/editor/common/core/range';
import {Handler, ICommonCodeEditor, IRange} from 'vs/editor/common/editorCommon';
import {FindModelBoundToEditorModel, parseReplaceString} from 'vs/editor/contrib/find/common/findModel';
import {FindModelBoundToEditorModel} from 'vs/editor/contrib/find/common/findModel';
import {FindReplaceState} from 'vs/editor/contrib/find/common/findState';
import {withMockCodeEditor} from 'vs/editor/test/common/mocks/mockCodeEditor';

suite('FindModel', () => {

test('parseFindWidgetString', () => {
let testParse = (input:string, expected:string) => {
let actual = parseReplaceString(input);
assert.equal(actual, expected);

let actual2 = parseReplaceString('hello' + input + 'hi');
assert.equal(actual2, 'hello' + expected + 'hi');
};

// no backslash => no treatment
testParse('hello', 'hello');

// \t => TAB
testParse('\\thello', '\thello');

// \n => LF
testParse('\\nhello', '\nhello');

// \\t => \t
testParse('\\\\thello', '\\thello');

// \\\t => \TAB
testParse('\\\\\\thello', '\\\thello');

// \\\\t => \\t
testParse('\\\\\\\\thello', '\\\\thello');

// \ at the end => no treatment
testParse('hello\\', 'hello\\');

// \ with unknown char => no treatment
testParse('hello\\x', 'hello\\x');

// \ with back reference => no treatment
testParse('hello\\0', 'hello\\0');



// $1 => no treatment
testParse('hello$1', 'hello$1');
// $2 => no treatment
testParse('hello$2', 'hello$2');
// $12 => no treatment
testParse('hello$12', 'hello$12');
// $$ => no treatment
testParse('hello$$', 'hello$$');
// $$0 => no treatment
testParse('hello$$0', 'hello$$0');

// $0 => $&
testParse('hello$0', 'hello$&');
testParse('hello$02', 'hello$&2');
});

function findTest(testName:string, callback:(editor:ICommonCodeEditor, cursor:Cursor)=>void): void {
test(testName, () => {
withMockCodeEditor([
Expand Down
169 changes: 169 additions & 0 deletions src/vs/platform/search/common/replace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';

import * as strings from 'vs/base/common/strings';
import {IPatternInfo} from 'vs/platform/search/common/search';

const BACKSLASH_CHAR_CODE = '\\'.charCodeAt(0);
const DOLLAR_CHAR_CODE = '$'.charCodeAt(0);
const ZERO_CHAR_CODE = '0'.charCodeAt(0);
const ONE_CHAR_CODE = '1'.charCodeAt(0);
const NINE_CHAR_CODE = '9'.charCodeAt(0);
const BACK_TICK_CHAR_CODE = '`'.charCodeAt(0);
const SINGLE_QUOTE_CHAR_CODE = '`'.charCodeAt(0);
const n_CHAR_CODE = 'n'.charCodeAt(0);
const t_CHAR_CODE = 't'.charCodeAt(0);

export class ReplacePattern {

private _replacePattern: string;
private _searchRegExp: RegExp;
private _hasParameters: boolean= false;

constructor(private replaceString: string, private searchPatternInfo: IPatternInfo) {
this._replacePattern= replaceString;
if (searchPatternInfo.isRegExp) {
this._searchRegExp= strings.createRegExp(searchPatternInfo.pattern, searchPatternInfo.isRegExp, searchPatternInfo.isCaseSensitive, searchPatternInfo.isWordMatch, true);
this.parseReplaceString(replaceString);
}
}

public get hasParameters(): boolean {
return this._hasParameters;
}

public get pattern(): string {
return this._replacePattern;
}

public getReplaceString(matchedString: string): string {
if (this.hasParameters) {
return matchedString.replace(this._searchRegExp, this.pattern);
}
return this.pattern;
}

/**
* \n => LF
* \t => TAB
* \\ => \
* $0 => $& (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter)
* everything else stays untouched
*/
private parseReplaceString(replaceString: string): void {
if (!replaceString || replaceString.length === 0) {
return;
}

let substrFrom = 0, result = '';
for (let i = 0, len = replaceString.length; i < len; i++) {
let chCode = replaceString.charCodeAt(i);

if (chCode === BACKSLASH_CHAR_CODE) {

// move to next char
i++;

if (i >= len) {
// string ends with a \
break;
}

let nextChCode = replaceString.charCodeAt(i);
let replaceWithCharacter: string = null;

switch (nextChCode) {
case BACKSLASH_CHAR_CODE:
// \\ => \
replaceWithCharacter = '\\';
break;
case n_CHAR_CODE:
// \n => LF
replaceWithCharacter = '\n';
break;
case t_CHAR_CODE:
// \t => TAB
replaceWithCharacter = '\t';
break;
}

if (replaceWithCharacter) {
result += replaceString.substring(substrFrom, i - 1) + replaceWithCharacter;
substrFrom = i + 1;
}
}

if (chCode === DOLLAR_CHAR_CODE) {

// move to next char
i++;

if (i >= len) {
// string ends with a $
break;
}

let nextChCode = replaceString.charCodeAt(i);
let replaceWithCharacter: string = null;

switch (nextChCode) {
case ZERO_CHAR_CODE:
// $0 => $&
replaceWithCharacter = '$&';
this._hasParameters = true;
break;
case BACK_TICK_CHAR_CODE:
case SINGLE_QUOTE_CHAR_CODE:
this._hasParameters = true;
break;
default:
// check if it is a valid string parameter $n (0 <= n <= 99). $0 is already handled by now.
if (!this.between(nextChCode, ONE_CHAR_CODE, NINE_CHAR_CODE)) {
break;
}
if (i === replaceString.length - 1) {
this._hasParameters = true;
break;
}
let charCode= replaceString.charCodeAt(++i);
if (!this.between(charCode, ZERO_CHAR_CODE, NINE_CHAR_CODE)) {
this._hasParameters = true;
--i;
break;
}
if (i === replaceString.length - 1) {
this._hasParameters = true;
break;
}
charCode= replaceString.charCodeAt(++i);
if (!this.between(charCode, ZERO_CHAR_CODE, NINE_CHAR_CODE)) {
this._hasParameters = true;
--i;
break;
}
break;
}

if (replaceWithCharacter) {
result += replaceString.substring(substrFrom, i - 1) + replaceWithCharacter;
substrFrom = i + 1;
}
}
}

if (substrFrom === 0) {
// no replacement occured
return;
}

this._replacePattern= result + replaceString.substring(substrFrom);
}

private between(value: number, from: number, to: number): boolean {
return from <= value && value <= to;
}
}

Loading

0 comments on commit 758a61e

Please sign in to comment.