/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import * as errors from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import * as network from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; import { IEditorService } from 'vs/platform/editor/common/editor'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Match, FileMatch, FileMatchOrMatch, ISearchWorkbenchService } from 'vs/workbench/parts/search/common/searchModel'; import { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; import { IProgressRunner } from 'vs/platform/progress/common/progress'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IFileService } from 'vs/platform/files/common/files'; import { ResourceTextEdit } from 'vs/editor/common/modes'; const REPLACE_PREVIEW = 'replacePreview'; const toReplaceResource = (fileResource: URI): URI => { return fileResource.with({ scheme: network.Schemas.internal, fragment: REPLACE_PREVIEW, query: JSON.stringify({ scheme: fileResource.scheme }) }); }; const toFileResource = (replaceResource: URI): URI => { return replaceResource.with({ scheme: JSON.parse(replaceResource.query)['scheme'], fragment: '', query: '' }); }; export class ReplacePreviewContentProvider implements ITextModelContentProvider, IWorkbenchContribution { constructor( @IInstantiationService private instantiationService: IInstantiationService, @ITextModelService private textModelResolverService: ITextModelService ) { this.textModelResolverService.registerTextModelContentProvider(network.Schemas.internal, this); } public provideTextContent(uri: URI): TPromise { if (uri.fragment === REPLACE_PREVIEW) { return this.instantiationService.createInstance(ReplacePreviewModel).resolve(uri); } return null; } } class ReplacePreviewModel extends Disposable { constructor( @IModelService private modelService: IModelService, @IModeService private modeService: IModeService, @ITextModelService private textModelResolverService: ITextModelService, @IReplaceService private replaceService: IReplaceService, @ISearchWorkbenchService private searchWorkbenchService: ISearchWorkbenchService ) { super(); } resolve(replacePreviewUri: URI): TPromise { const fileResource = toFileResource(replacePreviewUri); const fileMatch = this.searchWorkbenchService.searchModel.searchResult.matches().filter(match => match.resource().toString() === fileResource.toString())[0]; return this.textModelResolverService.createModelReference(fileResource).then(ref => { ref = this._register(ref); const sourceModel = ref.object.textEditorModel; const sourceModelModeId = sourceModel.getLanguageIdentifier().language; const replacePreviewModel = this.modelService.createModel(sourceModel.getValue(), this.modeService.getOrCreateMode(sourceModelModeId), replacePreviewUri); this._register(fileMatch.onChange(modelChange => this.update(sourceModel, replacePreviewModel, fileMatch, modelChange))); this._register(this.searchWorkbenchService.searchModel.onReplaceTermChanged(() => this.update(sourceModel, replacePreviewModel, fileMatch))); this._register(fileMatch.onDispose(() => replacePreviewModel.dispose())); // TODO@Sandeep we should not dispose a model directly but rather the reference (depends on https://github.com/Microsoft/vscode/issues/17073) this._register(replacePreviewModel.onWillDispose(() => this.dispose())); this._register(sourceModel.onWillDispose(() => this.dispose())); return replacePreviewModel; }); } private update(sourceModel: ITextModel, replacePreviewModel: ITextModel, fileMatch: FileMatch, override: boolean = false): void { if (!sourceModel.isDisposed() && !replacePreviewModel.isDisposed()) { this.replaceService.updateReplacePreview(fileMatch, override); } } } export class ReplaceService implements IReplaceService { public _serviceBrand: any; constructor( @IFileService private fileService: IFileService, @IEditorService private editorService: IWorkbenchEditorService, @ITextModelService private textModelResolverService: ITextModelService ) { } 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 = new BulkEdit(null, progress, this.textModelResolverService, this.fileService); if (arg instanceof Match) { let match = arg; bulkEdit.add([this.createEdit(match, match.replaceString, resource)]); } if (arg instanceof FileMatch) { arg = [arg]; } if (arg instanceof Array) { arg.forEach(element => { let fileMatch = element; if (fileMatch.count() > 0) { fileMatch.matches().forEach(match => { bulkEdit.add([this.createEdit(match, match.replaceString, resource)]); }); } }); } return bulkEdit.perform(); } public openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { const fileMatch = element instanceof Match ? element.parent() : element; return this.editorService.openEditor({ leftResource: fileMatch.resource(), rightResource: toReplaceResource(fileMatch.resource()), label: nls.localize('fileReplaceChanges', "{0} ↔ {1} (Replace Preview)", fileMatch.name(), fileMatch.name()), options: { preserveFocus, pinned, revealIfVisible: true } }).then(editor => { this.updateReplacePreview(fileMatch).then(() => { let editorControl = (editor.getControl()); if (element instanceof Match) { editorControl.revealLineInCenter(element.range().startLineNumber, ScrollType.Immediate); } }); }, errors.onUnexpectedError); } public updateReplacePreview(fileMatch: FileMatch, override: boolean = false): TPromise { const replacePreviewUri = toReplaceResource(fileMatch.resource()); return TPromise.join([this.textModelResolverService.createModelReference(fileMatch.resource()), this.textModelResolverService.createModelReference(replacePreviewUri)]) .then(([sourceModelRef, replaceModelRef]) => { const sourceModel = sourceModelRef.object.textEditorModel; const replaceModel = replaceModelRef.object.textEditorModel; let returnValue = TPromise.wrap(null); // If model is disposed do not update if (sourceModel && replaceModel) { if (override) { replaceModel.setValue(sourceModel.getValue()); } else { replaceModel.undo(); } returnValue = this.replace(fileMatch, null, replacePreviewUri); } return returnValue.then(() => { sourceModelRef.dispose(); replaceModelRef.dispose(); }); }); } private createEdit(match: Match, text: string, resource: URI = null): ResourceTextEdit { let fileMatch: FileMatch = match.parent(); let resourceEdit: ResourceTextEdit = { resource: resource !== null ? resource : fileMatch.resource(), edits: [{ range: match.range(), text: text }] }; return resourceEdit; } }