Skip to content

Commit

Permalink
Diff editor for search and replace #7602
Browse files Browse the repository at this point in the history
  • Loading branch information
sandy081 committed Jun 23, 2016
1 parent e4a183d commit 0fd128a
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 23 deletions.
11 changes: 11 additions & 0 deletions src/vs/workbench/parts/search/browser/replaceContributions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
import { ReplaceService } from 'vs/workbench/parts/search/browser/replaceService';

export function registerContributions(): void {
registerSingleton(IReplaceService, ReplaceService);
}
132 changes: 124 additions & 8 deletions src/vs/workbench/parts/search/browser/replaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,164 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import nls = require('vs/nls');
import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import * as Map from 'vs/base/common/map';
import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
import { IEditorService } from 'vs/platform/editor/common/editor';
import { EditorInput } from 'vs/workbench/common/editor';
import { IEditorService, IEditorInput, ITextEditorModel } from 'vs/platform/editor/common/editor';
import { IModel } from 'vs/editor/common/editorCommon';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IEventService } from 'vs/platform/event/common/event';
import { Match, FileMatch } from 'vs/workbench/parts/search/common/searchModel';
import { Match, FileMatch, FileMatchOrMatch } from 'vs/workbench/parts/search/common/searchModel';
import { BulkEdit, IResourceEdit, createBulkEdit } from 'vs/editor/common/services/bulkEdit';
import { IProgressRunner } from 'vs/platform/progress/common/progress';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';

class EditorInputCache {

private cache: Map.SimpleMap<URI, TPromise<DiffEditorInput>>;

constructor(private replaceService: ReplaceService, private editorService, private modelService: IModelService) {
this.cache= new Map.SimpleMap<URI, TPromise<DiffEditorInput>>();
}

public getInput(fileMatch: FileMatch, text: string): TPromise<DiffEditorInput> {
let editorInputPromise= this.cache.get(fileMatch.resource());
if (!editorInputPromise) {
editorInputPromise= this.createInput(fileMatch);
this.cache.set(fileMatch.resource(), editorInputPromise);
this.refreshInput(fileMatch, text, true);
}

fileMatch.addListener2('disposed', fileMatch => this.disposeInput(fileMatch));

return editorInputPromise;
}

public refreshInput(fileMatch: FileMatch, text: string, reloadFromSource: boolean= false): void {
let editorInputPromise= this.cache.get(fileMatch.resource());
if (editorInputPromise) {
editorInputPromise.done(() => {
if (reloadFromSource) {
this.editorService.resolveEditorModel({resource: fileMatch.resource()}).then((value: ITextEditorModel) => {
let replaceResource= this.getReplaceResource(fileMatch.resource());
this.modelService.getModel(replaceResource).setValue((<IModel> value.textEditorModel).getValue());
this.replaceService.replace(fileMatch, text, null, replaceResource);
});
} else {
let replaceResource= this.getReplaceResource(fileMatch.resource());
this.modelService.getModel(replaceResource).undo();
this.replaceService.replace(fileMatch, text, null, replaceResource);
}
});
}
}

public disposeInput(fileMatch: FileMatch): void
public disposeInput(resource: URI): void
public disposeInput(arg: any): void {
let resourceUri= arg instanceof URI ? arg : arg instanceof FileMatch ? arg.resource() : null;
if (resourceUri) {
let editorInputPromise= this.cache.get(resourceUri);
if (editorInputPromise) {
editorInputPromise.done((diffInput) => {
diffInput.dispose();
this.modelService.destroyModel(this.getReplaceResource(resourceUri));
this.cache.delete(resourceUri);
});
}
}
}

public disposeAll(): void {
this.cache.keys().forEach(resource => this.disposeInput(resource));
}

private createInput(fileMatch: FileMatch): TPromise<DiffEditorInput> {
return TPromise.join([this.createLeftInput(fileMatch),
this.createRightInput(fileMatch)]).then(inputs => {
const [left, right] = inputs;
return new DiffEditorInput(nls.localize('fileReplaceChanges', "{0} ↔ {1} (after)", fileMatch.name(), fileMatch.name()), undefined, <EditorInput>left, <EditorInput>right);
});
}

private createLeftInput(element: FileMatch): TPromise<IEditorInput> {
return this.editorService.createInput({ resource: element.resource() });
}

private createRightInput(element: FileMatch): TPromise<IEditorInput> {
return new TPromise((c, e, p) => {
this.editorService.resolveEditorModel({resource: element.resource()}).then((value: ITextEditorModel) => {
let model= <IModel> value.textEditorModel;
let replaceResource= this.getReplaceResource(element.resource());
this.modelService.createModel(model.getValue(), model.getMode(), replaceResource);
c(this.editorService.createInput({ resource: replaceResource }));
});
});
}

private getReplaceResource(resource: URI): URI {
return resource.with({scheme: 'private'});
}

}

export class ReplaceService implements IReplaceService {

public serviceId= IReplaceService;

constructor(@IEventService private eventService: IEventService, @IEditorService private editorService) {
private cache: EditorInputCache;

constructor(@IEventService private eventService: IEventService, @IEditorService private editorService, @IModelService private modelService: IModelService) {
this.cache= new EditorInputCache(this, editorService, modelService);
}

public replace(match: Match, text: string): TPromise<any>
public replace(files: FileMatch[], text: string, progress?: IProgressRunner): TPromise<any>
public replace(arg: any, text: string, progress: IProgressRunner= null): TPromise<any> {
public replace(match: FileMatchOrMatch, text: string, progress?: IProgressRunner, resource?: URI): TPromise<any>
public replace(arg: any, text: string, progress: IProgressRunner= null, resource: URI= null): TPromise<any> {

let bulkEdit: BulkEdit = createBulkEdit(this.eventService, this.editorService, null);
bulkEdit.progress(progress);

if (arg instanceof Match) {
bulkEdit.add([this.createEdit(arg, text)]);
bulkEdit.add([this.createEdit(arg, text, resource)]);
}

if (arg instanceof FileMatch) {
arg= [arg];
}

if (arg instanceof Array) {
arg.forEach(element => {
let fileMatch = <FileMatch>element;
fileMatch.matches().forEach(match => {
bulkEdit.add([this.createEdit(match, text)]);
bulkEdit.add([this.createEdit(match, text, resource)]);
});
});
}

return bulkEdit.finish();
}

private createEdit(match: Match, text: string): IResourceEdit {
public getInput(element: FileMatch, text: string): TPromise<EditorInput> {
return this.cache.getInput(element, text);
}

public refreshInput(element: FileMatch, text: string, reload: boolean= false): void {
this.cache.refreshInput(element, text, reload);
}

public disposeAllInputs(): void {
this.cache.disposeAll();
}

private createEdit(match: Match, text: string, resource: URI= null): IResourceEdit {
let fileMatch: FileMatch= match.parent();
let resourceEdit: IResourceEdit= {
resource: fileMatch.resource(),
resource: resource !== null ? resource: fileMatch.resource(),
range: match.range(),
newText: text
};
Expand Down
6 changes: 2 additions & 4 deletions src/vs/workbench/parts/search/browser/search.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ import {IViewletService} from 'vs/workbench/services/viewlet/common/viewletServi
import {KeyMod, KeyCode} from 'vs/base/common/keyCodes';
import {OpenSearchViewletAction} from 'vs/workbench/parts/search/browser/searchActions';
import {VIEWLET_ID} from 'vs/workbench/parts/search/common/constants';
import {registerSingleton} from 'vs/platform/instantiation/common/extensions';
import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
import { ReplaceService } from 'vs/workbench/parts/search/browser/replaceService';
import { registerContributions } from 'vs/workbench/parts/search/browser/replaceContributions';

registerSingleton(IReplaceService, ReplaceService);
registerContributions();

KeybindingsRegistry.registerCommandDesc({
id: 'workbench.action.search.toggleQueryDetails',
Expand Down
40 changes: 31 additions & 9 deletions src/vs/workbench/parts/search/browser/searchViewlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import 'vs/css!./media/searchviewlet';
import nls = require('vs/nls');
import {TPromise, PPromise} from 'vs/base/common/winjs.base';
import {Delayer} from 'vs/base/common/async';
import {EditorType} from 'vs/editor/common/editorCommon';
import {EditorType, IEditor} from 'vs/editor/common/editorCommon';
import lifecycle = require('vs/base/common/lifecycle');
import errors = require('vs/base/common/errors');
import aria = require('vs/base/browser/ui/aria/aria');
Expand Down Expand Up @@ -62,7 +61,6 @@ export class SearchViewlet extends Viewlet {
private toDispose: lifecycle.IDisposable[];

private currentRequest: PPromise<ISearchComplete, ISearchProgressItem>;
private delayedRefresh: Delayer<void>;
private loading: boolean;
private queryBuilder: QueryBuilder;
private viewModel: SearchResult;
Expand Down Expand Up @@ -104,7 +102,6 @@ export class SearchViewlet extends Viewlet {
super(VIEWLET_ID, telemetryService);

this.toDispose = [];
this.delayedRefresh = new Delayer<void>(200);
this.viewletVisible = keybindingService.createKey<boolean>('searchViewletVisible', true);
this.callOnModelChange = [];

Expand Down Expand Up @@ -287,6 +284,7 @@ export class SearchViewlet extends Viewlet {
if (this.viewModel) {
this.viewModel.replaceText= this.searchWidget.getReplaceValue();
}
this.refreshInputs();
this.tree.refresh();
});

Expand All @@ -300,6 +298,14 @@ export class SearchViewlet extends Viewlet {
this.searchWidget.onReplaceAll(() => this.replaceAll());
}

private refreshInputs(): void {
if (this.viewModel) {
this.viewModel.matches().forEach((fileMatch) => {
this.replaceService.refreshInput(fileMatch, this.viewModel.replaceText);
});
}
}

private replaceAll(): void {
let progressRunner= this.progressService.show(100);

Expand Down Expand Up @@ -829,8 +835,11 @@ export class SearchViewlet extends Viewlet {
this.tree.setInput(this.viewModel).then(() => {
autoExpand(false);
this.callOnModelChange.push(this.viewModel.addListener2('changed', (e: any) => {
if (this.replacingAll) {
this.delayedRefresh.trigger(() => this.tree.refresh(e, true));
if (!this.replacingAll) {
this.tree.refresh(e, true);
if (e instanceof FileMatch) {
this.replaceService.refreshInput(e, this.viewModel.replaceText, true);
}
}
}));
}).done(null, errors.onUnexpectedError);
Expand Down Expand Up @@ -880,6 +889,7 @@ export class SearchViewlet extends Viewlet {
}
}, 200);

this.replaceService.disposeAllInputs();
this.currentRequest = this.searchService.search(query);
this.currentRequest.then(onComplete, onError, onProgress);
}
Expand All @@ -892,6 +902,7 @@ export class SearchViewlet extends Viewlet {
this.actionRegistry['clearSearchResults'].enabled = false;

// clean up ui
this.replaceService.disposeAllInputs();
this.messages.hide();
this.tree.setInput(this.instantiationService.createInstance(SearchResult, null)).done(null, errors.onUnexpectedError);
this.results.show();
Expand All @@ -905,7 +916,7 @@ export class SearchViewlet extends Viewlet {

this.telemetryService.publicLog('searchResultChosen');

return this.open(lineMatch, preserveFocus, sideBySide, pinned);
return this.viewModel.isReplaceActive() ? this.openReplaceEditor(lineMatch, preserveFocus, sideBySide, pinned) : this.open(lineMatch, preserveFocus, sideBySide, pinned);
}

public open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise<any> {
Expand All @@ -914,13 +925,24 @@ export class SearchViewlet extends Viewlet {
return this.editorService.openEditor({
resource: resource,
options: {
preserveFocus,
pinned,
preserveFocus: preserveFocus,
pinned: pinned,
selection: selection
}
}, sideBySide);
}

private openReplaceEditor(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise<any> {
return this.replaceService.getInput(element instanceof Match ? element.parent() : element, this.viewModel.replaceText).then((editorInput) => {
this.editorService.openEditor(editorInput, {preserveFocus: preserveFocus, pinned: pinned}).then((editor) => {
let editorControl= (<IEditor>editor.getControl());
if (element instanceof Match) {
editorControl.revealLineInCenter(element.range().startLineNumber);
}
});
});
}

private getSelectionFrom(element: FileMatchOrMatch): any {
if (element instanceof EmptyMatch) {
return void 0;
Expand Down
17 changes: 17 additions & 0 deletions src/vs/workbench/parts/search/common/replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { Match, FileMatch } from 'vs/workbench/parts/search/common/searchModel';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { IProgressRunner } from 'vs/platform/progress/common/progress';
import { EditorInput } from 'vs/workbench/common/editor';

export var IReplaceService = createDecorator<IReplaceService>('replaceService');

Expand All @@ -24,4 +25,20 @@ export interface IReplaceService {
* You can also pass the progress runner to update the progress of replacing.
*/
replace(files: FileMatch[], text: string, progress?: IProgressRunner): TPromise<any>;

/**
* Gets the input for the file match with given text
*/
getInput(element: FileMatch, text: string): TPromise<EditorInput>;

/**
* Refresh the input for the fiel match with given text. 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;

/**
* Disposes all Inputs
*/
disposeAllInputs(): void;
}
6 changes: 4 additions & 2 deletions src/vs/workbench/parts/search/common/searchModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,21 @@ export class EmptyMatch extends Match {
}
}

export class FileMatch implements lifecycle.IDisposable {
export class FileMatch extends EventEmitter implements lifecycle.IDisposable {

private _parent: SearchResult;
private _resource: URI;
_matches: { [key: string]: Match };

constructor(parent: SearchResult, resource: URI) {
super();
this._resource = resource;
this._parent = parent;
this._matches = Object.create(null);
}

public dispose(): void {
// nothing
this.emit('disposed', this);
}

public id(): string {
Expand Down Expand Up @@ -160,6 +161,7 @@ export class LiveFileMatch extends FileMatch implements lifecycle.IDisposable {
if (!this._isTextModelDisposed()) {
this._model.deltaDecorations(this._modelDecorations, []);
}
super.dispose();
}

private _updateMatches(): void {
Expand Down

0 comments on commit 0fd128a

Please sign in to comment.