diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index 78fc3423b5c8fdb10c96f493e04be10e6b5c8253..1363276ddb161ff314e02bc9ab88c31e722aecb2 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -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'; @@ -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 { @@ -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); -} +} \ No newline at end of file diff --git a/src/vs/editor/contrib/find/test/common/findModel.test.ts b/src/vs/editor/contrib/find/test/common/findModel.test.ts index fb7a769f0a2b659611f725e151429fad3891280f..54274a9c02dae4c27e7ebd39b2957b83b1385f5d 100644 --- a/src/vs/editor/contrib/find/test/common/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/common/findModel.test.ts @@ -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([ diff --git a/src/vs/platform/search/common/replace.ts b/src/vs/platform/search/common/replace.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ca69b60ef884b50435fc2f6ba9f641d4c7e9539 --- /dev/null +++ b/src/vs/platform/search/common/replace.ts @@ -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; + } +} + diff --git a/src/vs/platform/search/test/common/replace.test.ts b/src/vs/platform/search/test/common/replace.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e2b574e91e3530a7fad62d2830c2323902a19c0a --- /dev/null +++ b/src/vs/platform/search/test/common/replace.test.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * 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 assert from 'assert'; +import { ReplacePattern } from 'vs/platform/search/common/replace'; + +suite('Replace Pattern test', () => { + + test('parse replace string', () => { + let testParse = (input:string, expected:string, expectedHasParameters:boolean) => { + let actual = new ReplacePattern(input, {pattern: 'somepattern', isRegExp: true}); + assert.equal(expected, actual.pattern); + assert.equal(expectedHasParameters, actual.hasParameters); + + actual= new ReplacePattern('hello' + input + 'hi', {pattern: 'sonepattern', isRegExp: true}); + assert.equal('hello' + expected + 'hi', actual.pattern); + assert.equal(expectedHasParameters, actual.hasParameters); + }; + + // no backslash => no treatment + testParse('hello', 'hello', false); + + // \t => TAB + testParse('\\thello', '\thello', false); + + // \n => LF + testParse('\\nhello', '\nhello', false); + + // \\t => \t + testParse('\\\\thello', '\\thello', false); + + // \\\t => \TAB + testParse('\\\\\\thello', '\\\thello', false); + + // \\\\t => \\t + testParse('\\\\\\\\thello', '\\\\thello', false); + + // \ at the end => no treatment + testParse('hello\\', 'hello\\', false); + + // \ with unknown char => no treatment + testParse('hello\\x', 'hello\\x', false); + + // \ with back reference => no treatment + testParse('hello\\0', 'hello\\0', false); + + + + // $1 => no treatment + testParse('hello$1', 'hello$1', true); + // $2 => no treatment + testParse('hello$2', 'hello$2', true); + // $12 => no treatment + testParse('hello$12', 'hello$12', true); + // $99 => no treatment + testParse('hello$99', 'hello$99', true); + // $99a => no treatment + testParse('hello$99a', 'hello$99a', true); + // $100 => no treatment + testParse('hello$100', 'hello$100', false); + // $100a => no treatment + testParse('hello$100a', 'hello$100a', false); + // $10a0 => no treatment + testParse('hello$10a0', 'hello$10a0', true); + // $$ => no treatment + testParse('hello$$', 'hello$$', false); + // $$0 => no treatment + testParse('hello$$0', 'hello$$0', false); + + // $0 => $& + testParse('hello$0', 'hello$&', true); + testParse('hello$02', 'hello$&2', true); + }); + + test('get replace string for a matched string', () => { + let testObject= new ReplacePattern('hello', {pattern: 'bla', isRegExp: true}); + let actual= testObject.getReplaceString('bla'); + assert.equal('hello', actual); + + testObject= new ReplacePattern('hello', {pattern: 'bla', isRegExp: false}); + actual= testObject.getReplaceString('bla'); + assert.equal('hello', actual); + + testObject= new ReplacePattern('hello', {pattern: '(bla)', isRegExp: true}); + actual= testObject.getReplaceString('bla'); + assert.equal('hello', actual); + + testObject= new ReplacePattern('hello$0', {pattern: '(bla)', isRegExp: true}); + actual= testObject.getReplaceString('bla'); + assert.equal('hellobla', actual); + + testObject= new ReplacePattern('import * as $1 from \'$2\';', {pattern: 'let\\s+(\\w+)\\s*=\\s*require\\s*\\(\\s*[\'\"]([\\w\.\\-/]+)\\s*[\'\"]\\s*\\)\\s*', isRegExp: true}); + actual= testObject.getReplaceString('let fs = require(\'fs\')'); + assert.equal('import * as fs from \'fs\';', actual); + + actual= testObject.getReplaceString('let something = require(\'fs\')'); + assert.equal('import * as something from \'fs\';', actual); + + actual= testObject.getReplaceString('let require(\'fs\')'); + assert.equal('let require(\'fs\')', actual); + + testObject= new ReplacePattern('import * as $1 from \'$1\';', {pattern: 'let\\s+(\\w+)\\s*=\\s*require\\s*\\(\\s*[\'\"]([\\w\.\\-/]+)\\s*[\'\"]\\s*\\)\\s*', isRegExp: true}); + actual= testObject.getReplaceString('let something = require(\'fs\')'); + assert.equal('import * as something from \'something\';', actual); + + testObject= new ReplacePattern('import * as $2 from \'$1\';', {pattern: 'let\\s+(\\w+)\\s*=\\s*require\\s*\\(\\s*[\'\"]([\\w\.\\-/]+)\\s*[\'\"]\\s*\\)\\s*', isRegExp: true}); + actual= testObject.getReplaceString('let something = require(\'fs\')'); + assert.equal('import * as fs from \'something\';', actual); + + testObject= new ReplacePattern('import * as $0 from \'$0\';', {pattern: 'let\\s+(\\w+)\\s*=\\s*require\\s*\\(\\s*[\'\"]([\\w\.\\-/]+)\\s*[\'\"]\\s*\\)\\s*', isRegExp: true}); + actual= testObject.getReplaceString('let something = require(\'fs\');'); + assert.equal('import * as let something = require(\'fs\') from \'let something = require(\'fs\')\';;', actual); + + testObject= new ReplacePattern('import * as $1 from \'$2\';', {pattern: 'let\\s+(\\w+)\\s*=\\s*require\\s*\\(\\s*[\'\"]([\\w\.\\-/]+)\\s*[\'\"]\\s*\\)\\s*', isRegExp: false}); + actual= testObject.getReplaceString('let fs = require(\'fs\');'); + assert.equal('import * as $1 from \'$2\';', actual); + + testObject= new ReplacePattern('cat$1', {pattern: 'for(.*)', isRegExp: true}); + actual= testObject.getReplaceString('for ()'); + assert.equal('cat ()', actual); + + // Not maching cases + testObject= new ReplacePattern('hello', {pattern: 'bla', isRegExp: true}); + actual= testObject.getReplaceString('foo'); + assert.equal('hello', actual); + + testObject= new ReplacePattern('hello', {pattern: 'bla', isRegExp: false}); + actual= testObject.getReplaceString('foo'); + assert.equal('hello', actual); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/parts/search/browser/replaceService.ts b/src/vs/workbench/parts/search/browser/replaceService.ts index bc4d5de445b344e1c4095e6bee959996ef00234a..f099fb6a2c7112d97a7c4937130a2e707d066ac1 100644 --- a/src/vs/workbench/parts/search/browser/replaceService.ts +++ b/src/vs/workbench/parts/search/browser/replaceService.ts @@ -23,27 +23,25 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; class EditorInputCache { private cache: Map.SimpleMap>; - private replaceTextcache: Map.SimpleMap; constructor(private replaceService: ReplaceService, private editorService: IWorkbenchEditorService, private modelService: IModelService) { this.cache= new Map.SimpleMap>(); - this.replaceTextcache= new Map.SimpleMap(); } - public getInput(fileMatch: FileMatch, text: string): TPromise { + public getInput(fileMatch: FileMatch): TPromise { let editorInputPromise= this.cache.get(fileMatch.resource()); if (!editorInputPromise) { editorInputPromise= this.createInput(fileMatch); this.cache.set(fileMatch.resource(), editorInputPromise); - this.refreshInput(fileMatch, text, true); + this.refreshInput(fileMatch, true); fileMatch.onDispose(() => this.disposeInput(fileMatch)); - fileMatch.onChange((modelChange) => this.refreshInput(fileMatch, this.replaceTextcache.get(fileMatch.resource()), modelChange)); + fileMatch.onChange((modelChange) => this.refreshInput(fileMatch, modelChange)); } return editorInputPromise; } - public refreshInput(fileMatch: FileMatch, text: string, reloadFromSource: boolean= false): void { + public refreshInput(fileMatch: FileMatch, reloadFromSource: boolean= false): void { let editorInputPromise= this.cache.get(fileMatch.resource()); if (editorInputPromise) { editorInputPromise.done(() => { @@ -51,14 +49,13 @@ class EditorInputCache { this.editorService.resolveEditorModel({resource: fileMatch.resource()}).then((value: ITextEditorModel) => { let replaceResource= this.getReplaceResource(fileMatch.resource()); this.modelService.getModel(replaceResource).setValue(( value.textEditorModel).getValue()); - this.replaceService.replace(fileMatch, text, null, replaceResource); + this.replaceService.replace(fileMatch, null, replaceResource); }); } else { let replaceResource= this.getReplaceResource(fileMatch.resource()); this.modelService.getModel(replaceResource).undo(); - this.replaceService.replace(fileMatch, text, null, replaceResource); + this.replaceService.replace(fileMatch, null, replaceResource); } - this.replaceTextcache.set(fileMatch.resource(), text); }); } } @@ -73,7 +70,6 @@ class EditorInputCache { editorInputPromise.done((diffInput) => { this.disposeReplaceInput(this.getReplaceResource(resourceUri), diffInput); this.cache.delete(resourceUri); - this.replaceTextcache.delete(resourceUri); }); } } @@ -127,16 +123,17 @@ export class ReplaceService implements IReplaceService { this.cache= new EditorInputCache(this, editorService, modelService); } - public replace(match: Match, text: string): TPromise - public replace(files: FileMatch[], text: string, progress?: IProgressRunner): TPromise - public replace(match: FileMatchOrMatch, text: string, progress?: IProgressRunner, resource?: URI): TPromise - public replace(arg: any, text: string, progress: IProgressRunner= null, resource: URI= null): TPromise { + public replace(match: Match): TPromise + public replace(files: FileMatch[], progress?: IProgressRunner): TPromise + public replace(match: FileMatchOrMatch, progress?: IProgressRunner, resource?: URI): TPromise + public replace(arg: any, progress: IProgressRunner= null, resource: URI= null): TPromise { let bulkEdit: BulkEdit = createBulkEdit(this.eventService, this.editorService, null); bulkEdit.progress(progress); if (arg instanceof Match) { - bulkEdit.add([this.createEdit(arg, text, resource)]); + let match= arg; + bulkEdit.add([this.createEdit(match, match.replaceString, resource)]); } if (arg instanceof FileMatch) { @@ -148,7 +145,7 @@ export class ReplaceService implements IReplaceService { let fileMatch = element; if (fileMatch.count() > 0) { fileMatch.matches().forEach(match => { - bulkEdit.add([this.createEdit(match, text, resource)]); + bulkEdit.add([this.createEdit(match, match.replaceString, resource)]); }); } }); @@ -157,12 +154,12 @@ export class ReplaceService implements IReplaceService { return bulkEdit.finish(); } - public getInput(element: FileMatch, text: string): TPromise { - return this.cache.getInput(element, text); + public getInput(element: FileMatch): TPromise { + return this.cache.getInput(element); } - public refreshInput(element: FileMatch, text: string, reload: boolean= false): void { - this.cache.refreshInput(element, text, reload); + public refreshInput(element: FileMatch, reload: boolean= false): void { + this.cache.refreshInput(element, reload); } public disposeAllInputs(): void { diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index 72907f038e66cde2e4e754dd5a998fdce3ca3e12..638cc097f6f5bbd3a8c547b0267513788c3fe930 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -185,7 +185,7 @@ export class ReplaceAllAction extends Action { public run(): TPromise { this.telemetryService.publicLog('replaceAll.action.selected'); - return this.fileMatch.parent().replace(this.fileMatch, this.fileMatch.parent().searchModel.replaceText).then(() => { + return this.fileMatch.parent().replace(this.fileMatch).then(() => { this.viewlet.open(this.fileMatch); }); } @@ -206,7 +206,7 @@ export class ReplaceAction extends Action { public run(): TPromise { this.telemetryService.publicLog('replace.action.selected'); - return this.element.parent().replace(this.element, this.element.parent().parent().searchModel.replaceText).then(() => { + return this.element.parent().replace(this.element).then(() => { this.viewlet.open(this.element); }); } diff --git a/src/vs/workbench/parts/search/browser/searchResultsView.ts b/src/vs/workbench/parts/search/browser/searchResultsView.ts index 5342b1ed6b2a561023e0f7f145fc996a2d0b77fa..c1b82d7d7134c6c333d843aadaadc2ffea8307a9 100644 --- a/src/vs/workbench/parts/search/browser/searchResultsView.ts +++ b/src/vs/workbench/parts/search/browser/searchResultsView.ts @@ -161,20 +161,19 @@ export class SearchRenderer extends ActionsRenderer { // Match else if (element instanceof Match) { dom.addClass(domElement, 'linematch'); - + let match= element; let elements: string[] = []; - let preview = element.preview(); + let preview = match.preview(); elements.push(''); elements.push(strings.escape(preview.before)); - let input= tree.getInput(); - let showReplaceText= input.searchModel.hasReplaceText(); + let showReplaceText= (tree.getInput()).searchModel.hasReplaceString(); elements.push(''); elements.push(strings.escape(preview.inside)); if (showReplaceText) { elements.push(''); - elements.push(strings.escape(input.searchModel.replaceText)); + elements.push(strings.escape(match.replaceString)); } elements.push(''); elements.push(strings.escape(preview.after)); @@ -182,7 +181,7 @@ export class SearchRenderer extends ActionsRenderer { $('a.plain') .innerHtml(elements.join(strings.empty)) - .title((preview.before + (input.searchModel.isReplaceActive() ? input.searchModel.replaceText : preview.inside) + preview.after).trim().substr(0, 999)) + .title((preview.before + (showReplaceText ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999)) .appendTo(domElement); } @@ -203,12 +202,13 @@ export class SearchAccessibilityProvider implements IAccessibilityProvider { } if (element instanceof Match) { + let match= element; let input= tree.getInput(); if (input.searchModel.isReplaceActive()) { - let preview = element.preview(); - return nls.localize('replacePreviewResultAria', "Replace preview result, {0}", preview.before + input.searchModel.replaceText + preview.after); + let preview = match.preview(); + return nls.localize('replacePreviewResultAria', "Replace preview result, {0}", preview.before + match.replaceString + preview.after); } - return nls.localize('searchResultAria', "{0}, Search result", element.text()); + return nls.localize('searchResultAria', "{0}, Search result", match.text()); } } } diff --git a/src/vs/workbench/parts/search/browser/searchViewlet.ts b/src/vs/workbench/parts/search/browser/searchViewlet.ts index c0636073f32778b49dd8c8132e1fd0c9d0a6bfd7..dd487bcdddb9a2b2765852b968924cd071066169 100644 --- a/src/vs/workbench/parts/search/browser/searchViewlet.ts +++ b/src/vs/workbench/parts/search/browser/searchViewlet.ts @@ -290,11 +290,11 @@ export class SearchViewlet extends Viewlet { this.toUnbind.push(this.searchWidget.onReplaceToggled(() => this.onReplaceToggled())); this.toUnbind.push(this.searchWidget.onReplaceStateChange((state) => { - this.viewModel.replaceText= this.searchWidget.getReplaceValue(); + this.viewModel.replaceString= this.searchWidget.getReplaceValue(); this.tree.refresh(); })); this.toUnbind.push(this.searchWidget.onReplaceValueChanged((value) => { - this.viewModel.replaceText= this.searchWidget.getReplaceValue(); + this.viewModel.replaceString= this.searchWidget.getReplaceValue(); this.refreshInputs(); this.tree.refresh(); })); @@ -344,7 +344,7 @@ export class SearchViewlet extends Viewlet { private refreshInputs(): void { this.viewModel.searchResult.matches().forEach((fileMatch) => { - this.replaceService.refreshInput(fileMatch, this.viewModel.replaceText); + this.replaceService.refreshInput(fileMatch); }); } @@ -370,7 +370,7 @@ export class SearchViewlet extends Viewlet { if (this.messageService.confirm(confirmation)) { this.searchWidget.setReplaceAllActionState(false); - this.viewModel.searchResult.replaceAll(replaceValue, progressRunner).then(() => { + this.viewModel.searchResult.replaceAll(progressRunner).then(() => { progressRunner.done(); this.showMessage(afterReplaceAllMessage); }, (error) => { @@ -731,7 +731,7 @@ export class SearchViewlet extends Viewlet { } this.onSearchResultsChanged().then(() => autoExpand(true)); - this.viewModel.replaceText= this.searchWidget.getReplaceValue(); + this.viewModel.replaceString= this.searchWidget.getReplaceValue(); let hasResults = !this.viewModel.searchResult.isEmpty(); this.loading = false; @@ -902,7 +902,7 @@ export class SearchViewlet extends Viewlet { this.telemetryService.publicLog('searchResultChosen'); - return this.viewModel.hasReplaceText() ? this.openReplacePreviewEditor(lineMatch, preserveFocus, sideBySide, pinned) : this.open(lineMatch, preserveFocus, sideBySide, pinned); + return this.viewModel.hasReplaceString() ? this.openReplacePreviewEditor(lineMatch, preserveFocus, sideBySide, pinned) : this.open(lineMatch, preserveFocus, sideBySide, pinned); } public open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { @@ -920,7 +920,7 @@ export class SearchViewlet extends Viewlet { private openReplacePreviewEditor(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { this.telemetryService.publicLog('replace.open.previewEditor'); - return this.replaceService.getInput(element instanceof Match ? element.parent() : element, this.viewModel.replaceText).then((editorInput) => { + return this.replaceService.getInput(element instanceof Match ? element.parent() : element).then((editorInput) => { this.editorService.openEditor(editorInput, {preserveFocus: preserveFocus, pinned}).then((editor) => { let editorControl= (editor.getControl()); if (element instanceof Match) { @@ -941,12 +941,12 @@ export class SearchViewlet extends Viewlet { if (match) { let range= match.range(); if (this.viewModel.isReplaceActive()) { - let replaceText= this.viewModel.replaceText; + let replaceString= match.replaceString; return { startLineNumber: range.startLineNumber, - startColumn: range.startColumn + replaceText.length, + startColumn: range.startColumn + replaceString.length, endLineNumber: range.startLineNumber, - endColumn: range.startColumn + replaceText.length + endColumn: range.startColumn + replaceString.length }; } return range; diff --git a/src/vs/workbench/parts/search/common/replace.ts b/src/vs/workbench/parts/search/common/replace.ts index 678e186bf7eeb7c1839187b0cbbe877d2b4ca928..847896322a88f2f5a966a9b7f39fbf900e114498 100644 --- a/src/vs/workbench/parts/search/common/replace.ts +++ b/src/vs/workbench/parts/search/common/replace.ts @@ -16,26 +16,26 @@ export interface IReplaceService { _serviceBrand: any; /** - * Replace the match with the given text. + * Replaces the given match in the file that match belongs to */ - replace(match: Match, text: string): TPromise; + replace(match: Match): TPromise; /** - * Replace all the matches in the given file matches with provided text. + * Replace all the matches from the given file matches in the files * You can also pass the progress runner to update the progress of replacing. */ - replace(files: FileMatch[], text: string, progress?: IProgressRunner): TPromise; + replace(files: FileMatch[], progress?: IProgressRunner): TPromise; /** - * Gets the input for the file match with given text + * Gets the input for the file match */ - getInput(element: FileMatch, text: string): TPromise; + getInput(element: FileMatch): TPromise; /** - * Refresh the input for the fiel match with given text. If reload, content of repalced editor is reloaded completely + * Refresh the input for the file match. If reload, content of repalced editor is reloaded completely * Otherwise undo the last changes and refreshes with new text. */ - refreshInput(element: FileMatch, text: string, reload?: boolean): void; + refreshInput(element: FileMatch, reload?: boolean): void; /** * Disposes all Inputs diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index 044d2d83a664998c6624add03f6e1bde3c738a81..e670cb6b0a18e46bf1e3376e5f3d31f2efe59786 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -16,6 +16,7 @@ import { ArraySet } from 'vs/base/common/set'; import Event, { Emitter } from 'vs/base/common/event'; import * as Search from 'vs/platform/search/common/search'; import { ISearchProgressItem, ISearchComplete, ISearchQuery } from 'vs/platform/search/common/search'; +import { ReplacePattern } from 'vs/platform/search/common/replace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Range } from 'vs/editor/common/core/range'; import { IModel, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness, IModelDecorationOptions } from 'vs/editor/common/editorCommon'; @@ -66,6 +67,10 @@ export class Match { after, }; } + + public get replaceString(): string { + return this.parent().parent().searchModel.replacePattern.getReplaceString(this.preview().inside); + } } export class FileMatch extends Disposable { @@ -208,8 +213,8 @@ export class FileMatch extends Disposable { this._onChange.fire(false); } - public replace(match: Match, replaceText: string): TPromise { - return this.replaceService.replace(match, replaceText).then(() => { + public replace(match: Match): TPromise { + return this.replaceService.replace(match).then(() => { this._matches.delete(match.id()); this._onChange.fire(false); }); @@ -299,16 +304,16 @@ export class SearchResult extends Disposable { this.doRemove(match); } - public replace(match: FileMatch, replaceText: string): TPromise { - return this.replaceService.replace([match], replaceText).then(() => { + public replace(match: FileMatch): TPromise { + return this.replaceService.replace([match]).then(() => { this.doRemove(match, false, true); }); } - public replaceAll(replaceText: string, progressRunner: IProgressRunner): TPromise { + public replaceAll(progressRunner: IProgressRunner): TPromise { this._replacingAll= true; let replaceAllTimer = this.telemetryService.timedPublicLog('replaceAll.started'); - return this.replaceService.replace(this.matches(), replaceText, progressRunner).then(() => { + return this.replaceService.replace(this.matches(), progressRunner).then(() => { replaceAllTimer.stop(); this._replacingAll= false; this.clear(); @@ -402,7 +407,8 @@ export class SearchModel extends Disposable { private _searchResult: SearchResult; private _searchQuery: ISearchQuery= null; - private _replaceText: string= null; + private _replaceString: string= null; + private _replacePattern: ReplacePattern= null; private currentRequest: PPromise; private progressTimer: timer.ITimerEvent; @@ -418,7 +424,15 @@ export class SearchModel extends Disposable { * Return true if replace is enabled otherwise false */ public isReplaceActive():boolean { - return this.replaceText !== null && this.replaceText !== void 0; + return this._replaceString !== null && this._replaceString !== void 0; + } + + /** + * Return true if replace is enabled and replace text is not empty, otherwise false. + * This is necessary in cases handling empty replace text when replace is active. + */ + public hasReplaceString():boolean { + return this.isReplaceActive() && !!this._replaceString; } /** @@ -426,37 +440,34 @@ export class SearchModel extends Disposable { * Can be null if replace is not enabled. Use replace() before. * Can be empty. */ - public get replaceText(): string { - return this._replaceText; + public get replacePattern(): ReplacePattern { + return this._replacePattern; } - public set replaceText(replace: string) { - this._replaceText= replace; + public set replaceString(replaceString: string) { + this._replaceString= replaceString; + if (this._searchQuery) { + this._replacePattern= new ReplacePattern(replaceString, this._searchQuery.contentPattern); + } } public get searchResult():SearchResult { return this._searchResult; } - /** - * Return true if replace is enabled and replace text is not empty, otherwise false. - * This is necessary in cases handling empty replace text when replace is active. - */ - public hasReplaceText():boolean { - return this.isReplaceActive() && !!this.replaceText; - } - public search(query: ISearchQuery): PPromise { this.cancelSearch(); this.searchResult.clear(); this._searchQuery= query; this._searchResult.query= this._searchQuery.contentPattern; + this._replacePattern= new ReplacePattern(this._replaceString, this._searchQuery.contentPattern); + this.progressTimer = this.telemetryService.timedPublicLog('searchResultsFirstRender'); this.doneTimer = this.telemetryService.timedPublicLog('searchResultsFinished'); this.timerEvent = timer.start(timer.Topic.WORKBENCH, 'Search'); - this.currentRequest = this.searchService.search(this._searchQuery); + this.currentRequest = this.searchService.search(this._searchQuery); this.currentRequest.then(value => this.onSearchCompleted(value), e => this.onSearchError(e), p => this.onSearchProgress(p)); diff --git a/src/vs/workbench/parts/search/test/common/searchModel.test.ts b/src/vs/workbench/parts/search/test/common/searchModel.test.ts index 618d02afe56a65b54833defdbe3be365f15bef62..16fb5fd2b78feb46a89ef156f3e5159c18726698 100644 --- a/src/vs/workbench/parts/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchModel.test.ts @@ -230,28 +230,28 @@ suite('SearchModel', () => { test('Search Model: isReplaceActive return false if replace text is set to null', function () { let testObject:SearchModel= instantiationService.createInstance(SearchModel); - testObject.replaceText= null; + testObject.replaceString= null; assert.ok(!testObject.isReplaceActive()); }); test('Search Model: isReplaceActive return false if replace text is set to undefined', function () { let testObject:SearchModel= instantiationService.createInstance(SearchModel); - testObject.replaceText= void 0; + testObject.replaceString= void 0; assert.ok(!testObject.isReplaceActive()); }); test('Search Model: isReplaceActive return true if replace text is set to empty string', function () { let testObject:SearchModel= instantiationService.createInstance(SearchModel); - testObject.replaceText= ''; + testObject.replaceString= ''; assert.ok(testObject.isReplaceActive()); }); test('Search Model: isReplaceActive return true if replace text is set to non empty string', function () { let testObject:SearchModel= instantiationService.createInstance(SearchModel); - testObject.replaceText= 'some value'; + testObject.replaceString= 'some value'; assert.ok(testObject.isReplaceActive()); }); @@ -259,35 +259,35 @@ suite('SearchModel', () => { test('Search Model: hasReplaceText return false if no replace text is set', function () { let testObject:SearchModel= instantiationService.createInstance(SearchModel); - assert.ok(!testObject.hasReplaceText()); + assert.ok(!testObject.hasReplaceString()); }); test('Search Model: hasReplaceText return false if replace text is set to null', function () { let testObject:SearchModel= instantiationService.createInstance(SearchModel); - testObject.replaceText= null; + testObject.replaceString= null; - assert.ok(!testObject.hasReplaceText()); + assert.ok(!testObject.hasReplaceString()); }); test('Search Model: hasReplaceText return false if replace text is set to undefined', function () { let testObject:SearchModel= instantiationService.createInstance(SearchModel); - testObject.replaceText= void 0; + testObject.replaceString= void 0; - assert.ok(!testObject.hasReplaceText()); + assert.ok(!testObject.hasReplaceString()); }); test('Search Model: hasReplaceText return false if replace text is set to empty string', function () { let testObject:SearchModel= instantiationService.createInstance(SearchModel); - testObject.replaceText= ''; + testObject.replaceString= ''; - assert.ok(!testObject.hasReplaceText()); + assert.ok(!testObject.hasReplaceString()); }); test('Search Model: hasReplaceText return true if replace text is set to non empty string', function () { let testObject:SearchModel= instantiationService.createInstance(SearchModel); - testObject.replaceText= 'some value'; + testObject.replaceString= 'some value'; - assert.ok(testObject.hasReplaceText()); + assert.ok(testObject.hasReplaceString()); }); function aRawMatch(resource: string, ...lineMatches: ILineMatch[]): IFileMatch { diff --git a/src/vs/workbench/parts/search/test/common/searchResult.test.ts b/src/vs/workbench/parts/search/test/common/searchResult.test.ts index 450cc89c72abcfa5ac2d6b4ac8740ea6b00fd784..918185da6916ecd52aa0231ec96b0fb7f6e0a809 100644 --- a/src/vs/workbench/parts/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchResult.test.ts @@ -180,7 +180,7 @@ suite('SearchResult', () => { let testObject = aSearchResult(); testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); - testObject.replace(testObject.matches()[0], ''); + testObject.replace(testObject.matches()[0]); assert.ok(testObject.isEmpty()); }); @@ -193,7 +193,7 @@ suite('SearchResult', () => { testObject.onChange(target); let objectRoRemove= testObject.matches()[0]; - testObject.replace(objectRoRemove, ''); + testObject.replace(objectRoRemove); assert.ok(target.calledOnce); assert.deepEqual([{elements: [objectRoRemove], removed: true}], target.args[0]); @@ -204,7 +204,7 @@ suite('SearchResult', () => { let testObject = aSearchResult(); testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1')), aRawMatch('file://c:/2', aLineMatch('preview 2'))]); - testObject.replaceAll('', null); + testObject.replaceAll(null); assert.ok(testObject.isEmpty()); });