diff --git a/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts index bf9247c8c81..6e34de0bdbe 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts @@ -250,7 +250,7 @@ export class MainThreadDocumentsAndEditors { const mainThreadDocuments = new MainThreadDocuments(this, extHostContext, this._modelService, modeService, this._textFileService, fileService, textModelResolverService, untitledEditorService); extHostContext.set(MainContext.MainThreadDocuments, mainThreadDocuments); - const mainThreadEditors = new MainThreadEditors(this, extHostContext, codeEditorService, this._workbenchEditorService, editorGroupService, telemetryService); + const mainThreadEditors = new MainThreadEditors(this, extHostContext, codeEditorService, this._workbenchEditorService, editorGroupService, telemetryService, textModelResolverService, fileService, this._modelService); extHostContext.set(MainContext.MainThreadEditors, mainThreadEditors); // It is expected that the ctor of the state computer calls our `_onDelta`. diff --git a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts index 8dc711aa435..7d615125aee 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts @@ -8,7 +8,7 @@ import URI from 'vs/base/common/uri'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { disposed } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ISingleEditOperation, IDecorationRenderOptions, IDecorationOptions, ILineChange } from 'vs/editor/common/editorCommon'; +import { ISingleEditOperation, IDecorationRenderOptions, IDecorationOptions, ILineChange, ICommonCodeEditor, isCommonCodeEditor } from 'vs/editor/common/editorCommon'; import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; @@ -18,9 +18,13 @@ import { ITextEditorConfigurationUpdate, TextEditorRevealType, IApplyEditsOption import { MainThreadDocumentsAndEditors } from './mainThreadDocumentsAndEditors'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { equals as objectEquals } from 'vs/base/common/objects'; -import { ExtHostContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IExtHostContext } from '../node/extHost.protocol'; +import { ExtHostContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IExtHostContext, IWorkspaceResourceEdit } from '../node/extHost.protocol'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { bulkEdit, IResourceEdit } from 'vs/editor/common/services/bulkEdit'; +import { IModelService } from 'vs/editor/common/services/modelService'; export class MainThreadEditors implements MainThreadEditorsShape { @@ -38,7 +42,10 @@ export class MainThreadEditors implements MainThreadEditorsShape { @ICodeEditorService private _codeEditorService: ICodeEditorService, @IWorkbenchEditorService workbenchEditorService: IWorkbenchEditorService, @IEditorGroupService editorGroupService: IEditorGroupService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService telemetryService: ITelemetryService, + @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IFileService private readonly _fileService: IFileService, + @IModelService private readonly _modelService: IModelService, ) { this._proxy = extHostContext.get(ExtHostContext.ExtHostEditors); this._documentsAndEditors = documentsAndEditors; @@ -194,6 +201,52 @@ export class MainThreadEditors implements MainThreadEditorsShape { return TPromise.as(this._documentsAndEditors.getEditor(id).applyEdits(modelVersionId, edits, opts)); } + $tryApplyWorkspaceEdit(workspaceResourceEdits: IWorkspaceResourceEdit[]): TPromise { + + // First check if loaded models were not changed in the meantime + for (let i = 0, len = workspaceResourceEdits.length; i < len; i++) { + const workspaceResourceEdit = workspaceResourceEdits[i]; + if (workspaceResourceEdit.modelVersionId) { + let model = this._modelService.getModel(workspaceResourceEdit.resource); + if (model && model.getVersionId() !== workspaceResourceEdit.modelVersionId) { + // model changed in the meantime + return TPromise.as(false); + } + } + } + + // Convert to shape expected by bulkEdit below + let resourceEdits: IResourceEdit[] = []; + for (let i = 0, len = workspaceResourceEdits.length; i < len; i++) { + const workspaceResourceEdit = workspaceResourceEdits[i]; + const uri = workspaceResourceEdit.resource; + const edits = workspaceResourceEdit.edits; + + for (let j = 0, lenJ = edits.length; j < lenJ; j++) { + const edit = edits[j]; + + resourceEdits.push({ + resource: uri, + newText: edit.newText, + newEol: edit.newEol, + range: edit.range + }); + } + } + + let codeEditor: ICommonCodeEditor; + let editor = this._workbenchEditorService.getActiveEditor(); + if (editor) { + let candidate = editor.getControl(); + if (isCommonCodeEditor(candidate)) { + codeEditor = candidate; + } + } + + return bulkEdit(this._textModelResolverService, codeEditor, resourceEdits, this._fileService) + .then(() => true); + } + $tryInsertSnippet(id: string, template: string, ranges: IRange[], opts: IUndoStopOptions): TPromise { if (!this._documentsAndEditors.getEditor(id)) { return TPromise.wrapError(disposed(`TextEditor(${id})`)); diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index 6138e4961b9..f72611dd369 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -8,13 +8,9 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import URI from 'vs/base/common/uri'; import { ISearchService, QueryType, ISearchQuery, ISearchProgressItem, ISearchComplete } from 'vs/platform/search/common/search'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ICommonCodeEditor, isCommonCodeEditor } from 'vs/editor/common/editorCommon'; -import { bulkEdit, IResourceEdit } from 'vs/editor/common/services/bulkEdit'; import { TPromise, PPromise } from 'vs/base/common/winjs.base'; import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainContext, IExtHostContext } from '../node/extHost.protocol'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IFileService } from 'vs/platform/files/common/files'; import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { RemoteFileService } from 'vs/workbench/services/files/electron-browser/remoteFileService'; @@ -34,8 +30,6 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @ISearchService private readonly _searchService: ISearchService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @ITextFileService private readonly _textFileService: ITextFileService, - @IWorkbenchEditorService private readonly _editorService: IWorkbenchEditorService, - @ITextModelService private readonly _textModelResolverService: ITextModelService, @IExperimentService private experimentService: IExperimentService, @IFileService private readonly _fileService: IFileService ) { @@ -109,21 +103,6 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { }); } - $applyWorkspaceEdit(edits: IResourceEdit[]): TPromise { - - let codeEditor: ICommonCodeEditor; - let editor = this._editorService.getActiveEditor(); - if (editor) { - let candidate = editor.getControl(); - if (isCommonCodeEditor(candidate)) { - codeEditor = candidate; - } - } - - return bulkEdit(this._textModelResolverService, codeEditor, edits, this._fileService) - .then(() => true); - } - // --- EXPERIMENT: workspace provider private _idPool: number = 0; diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 4c9ed5a511f..b73c2c21849 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -84,7 +84,7 @@ export function createApiFactory( const extHostDocumentsAndEditors = threadService.set(ExtHostContext.ExtHostDocumentsAndEditors, new ExtHostDocumentsAndEditors(threadService, extensionService)); const extHostDocuments = threadService.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(threadService, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = threadService.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(threadService, extHostDocumentsAndEditors)); - const extHostDocumentSaveParticipant = threadService.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostDocuments, threadService.get(MainContext.MainThreadWorkspace))); + const extHostDocumentSaveParticipant = threadService.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostDocuments, threadService.get(MainContext.MainThreadEditors))); const extHostEditors = threadService.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(threadService, extHostDocumentsAndEditors)); const extHostCommands = threadService.set(ExtHostContext.ExtHostCommands, new ExtHostCommands(threadService, extHostHeapService)); const extHostTreeViews = threadService.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(threadService.get(MainContext.MainThreadTreeViews), extHostCommands)); @@ -417,7 +417,7 @@ export function createApiFactory( return extHostWorkspace.saveAll(includeUntitled); }, applyEdit(edit: vscode.WorkspaceEdit): TPromise { - return extHostWorkspace.appyEdit(edit); + return extHostEditors.applyWorkspaceEdit(edit); }, createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): vscode.FileSystemWatcher => { return extHostFileSystemEvent.createFileSystemWatcher(pattern, ignoreCreate, ignoreChange, ignoreDelete); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 2a77558ba23..06b6ce6c0de 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -26,7 +26,6 @@ import { IProgressOptions, IProgressStep } from 'vs/platform/progress/common/pro import * as editorCommon from 'vs/editor/common/editorCommon'; import * as modes from 'vs/editor/common/modes'; -import { IResourceEdit } from 'vs/editor/common/services/bulkEdit'; import { ITextSource } from 'vs/editor/common/model/textSource'; import { ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; @@ -189,6 +188,16 @@ export interface ITextDocumentShowOptions { selection?: IRange; } +export interface IWorkspaceResourceEdit { + resource: URI; + modelVersionId?: number; + edits: { + range?: IRange; + newText: string; + newEol?: editorCommon.EndOfLineSequence; + }[]; +} + export interface MainThreadEditorsShape extends IDisposable { $tryShowTextDocument(resource: URI, options: ITextDocumentShowOptions): TPromise; $registerTextEditorDecorationType(key: string, options: editorCommon.IDecorationRenderOptions): void; @@ -200,6 +209,7 @@ export interface MainThreadEditorsShape extends IDisposable { $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise; $trySetSelections(id: string, selections: ISelection[]): TPromise; $tryApplyEdits(id: string, modelVersionId: number, edits: editorCommon.ISingleEditOperation[], opts: IApplyEditsOptions): TPromise; + $tryApplyWorkspaceEdit(workspaceResourceEdits: IWorkspaceResourceEdit[]): TPromise; $tryInsertSnippet(id: string, template: string, selections: IRange[], opts: IUndoStopOptions): TPromise; $getDiffInformation(id: string): TPromise; } @@ -301,7 +311,6 @@ export interface MainThreadWorkspaceShape extends IDisposable { $startSearch(include: string, exclude: string, maxResults: number, requestId: number): Thenable; $cancelSearch(requestId: number): Thenable; $saveAll(includeUntitled?: boolean): Thenable; - $applyWorkspaceEdit(edits: IResourceEdit[]): TPromise; $registerFileSystemProvider(handle: number, authority: string): void; $unregisterFileSystemProvider(handle: number): void; diff --git a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts index 90e5e6e0d81..f1adf5fdf56 100644 --- a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts @@ -10,10 +10,9 @@ import URI from 'vs/base/common/uri'; import { sequence, always } from 'vs/base/common/async'; import { illegalState } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; -import { MainThreadWorkspaceShape, ExtHostDocumentSaveParticipantShape } from 'vs/workbench/api/node/extHost.protocol'; +import { ExtHostDocumentSaveParticipantShape, MainThreadEditorsShape, IWorkspaceResourceEdit } from 'vs/workbench/api/node/extHost.protocol'; import { TextEdit } from 'vs/workbench/api/node/extHostTypes'; import { fromRange, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/node/extHostTypeConverters'; -import { IResourceEdit } from 'vs/editor/common/services/bulkEdit'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import * as vscode from 'vscode'; @@ -21,14 +20,14 @@ import * as vscode from 'vscode'; export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSaveParticipantShape { private _documents: ExtHostDocuments; - private _workspace: MainThreadWorkspaceShape; + private _mainThreadEditors: MainThreadEditorsShape; private _callbacks = new CallbackList(); private _badListeners = new WeakMap(); private _thresholds: { timeout: number; errors: number; }; - constructor(documents: ExtHostDocuments, workspace: MainThreadWorkspaceShape, thresholds: { timeout: number; errors: number; } = { timeout: 1500, errors: 3 }) { + constructor(documents: ExtHostDocuments, mainThreadEditors: MainThreadEditorsShape, thresholds: { timeout: number; errors: number; } = { timeout: 1500, errors: 3 }) { this._documents = documents; - this._workspace = workspace; + this._mainThreadEditors = mainThreadEditors; this._thresholds = thresholds; } @@ -133,13 +132,15 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic }).then(values => { - let edits: IResourceEdit[] = []; + let workspaceResourceEdit: IWorkspaceResourceEdit = { + resource: document.uri, + edits: [] + }; for (const value of values) { if (Array.isArray(value) && (value).every(e => e instanceof TextEdit)) { for (const { newText, newEol, range } of value) { - edits.push({ - resource: document.uri, + workspaceResourceEdit.edits.push({ range: range && fromRange(range), newText, newEol: EndOfLine.from(newEol) @@ -150,12 +151,12 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic // apply edits if any and if document // didn't change somehow in the meantime - if (edits.length === 0) { + if (workspaceResourceEdit.edits.length === 0) { return undefined; } if (version === document.version) { - return this._workspace.$applyWorkspaceEdit(edits); + return this._mainThreadEditors.$tryApplyWorkspaceEdit([workspaceResourceEdit]); } // TODO@joh bubble this to listener? diff --git a/src/vs/workbench/api/node/extHostTextEditors.ts b/src/vs/workbench/api/node/extHostTextEditors.ts index e820774046d..d1f70271a48 100644 --- a/src/vs/workbench/api/node/extHostTextEditors.ts +++ b/src/vs/workbench/api/node/extHostTextEditors.ts @@ -13,7 +13,7 @@ import * as TypeConverters from './extHostTypeConverters'; import { TextEditorDecorationType, ExtHostTextEditor } from './extHostTextEditor'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors'; import { Position as EditorPosition } from 'vs/platform/editor/common/editor'; -import { MainContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IResolvedTextEditorConfiguration, ISelectionChangeEvent, IMainContext } from './extHost.protocol'; +import { MainContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IResolvedTextEditorConfiguration, ISelectionChangeEvent, IMainContext, IWorkspaceResourceEdit } from './extHost.protocol'; import * as vscode from 'vscode'; export class ExtHostEditors implements ExtHostEditorsShape { @@ -91,6 +91,40 @@ export class ExtHostEditors implements ExtHostEditorsShape { return new TextEditorDecorationType(this._proxy, options); } + applyWorkspaceEdit(edit: vscode.WorkspaceEdit): TPromise { + + let workspaceResourceEdits: IWorkspaceResourceEdit[] = []; + + let entries = edit.entries(); + for (let entry of entries) { + let [uri, edits] = entry; + + let doc = this._extHostDocumentsAndEditors.getDocument(uri.toString()); + let docVersion: number = undefined; + if (doc) { + docVersion = doc.version; + } + + let workspaceResourceEdit: IWorkspaceResourceEdit = { + resource: uri, + modelVersionId: docVersion, + edits: [] + }; + + for (let edit of edits) { + workspaceResourceEdit.edits.push({ + newText: edit.newText, + newEol: TypeConverters.EndOfLine.from(edit.newEol), + range: edit.range && TypeConverters.fromRange(edit.range) + }); + } + + workspaceResourceEdits.push(workspaceResourceEdit); + } + + return this._proxy.$tryApplyWorkspaceEdit(workspaceResourceEdits); + } + // --- called from main thread $acceptOptionsChanged(id: string, opts: IResolvedTextEditorConfiguration): void { diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 43323d5c00c..629947ee2a5 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -10,9 +10,7 @@ import { normalize } from 'vs/base/common/paths'; import { delta } from 'vs/base/common/arrays'; import { relative } from 'path'; import { Workspace } from 'vs/platform/workspace/common/workspace'; -import { IResourceEdit } from 'vs/editor/common/services/bulkEdit'; import { TPromise } from 'vs/base/common/winjs.base'; -import { fromRange, EndOfLine } from 'vs/workbench/api/node/extHostTypeConverters'; import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape, IMainContext } from './extHost.protocol'; import * as vscode from 'vscode'; import { compare } from 'vs/base/common/strings'; @@ -182,27 +180,6 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { return this._proxy.$saveAll(includeUntitled); } - appyEdit(edit: vscode.WorkspaceEdit): TPromise { - - let resourceEdits: IResourceEdit[] = []; - - let entries = edit.entries(); - for (let entry of entries) { - let [uri, edits] = entry; - - for (let edit of edits) { - resourceEdits.push({ - resource: uri, - newText: edit.newText, - newEol: EndOfLine.from(edit.newEol), - range: edit.range && fromRange(edit.range) - }); - } - } - - return this._proxy.$applyWorkspaceEdit(resourceEdits); - } - // --- EXPERIMENT: workspace resolver diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts index f521f635930..06167349726 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts @@ -10,10 +10,9 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbench/api/node/extHostTypes'; -import { MainThreadWorkspaceShape } from 'vs/workbench/api/node/extHost.protocol'; +import { MainThreadEditorsShape, IWorkspaceResourceEdit } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; import { OneGetThreadService } from './testThreadService'; -import { IResourceEdit } from 'vs/editor/common/services/bulkEdit'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import * as vscode from 'vscode'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; @@ -21,7 +20,7 @@ import { mock } from 'vs/workbench/test/electron-browser/api/mock'; suite('ExtHostDocumentSaveParticipant', () => { let resource = URI.parse('foo:bar'); - let workspace = new class extends mock() { }; + let mainThreadEditors = new class extends mock() { }; let documents: ExtHostDocuments; setup(() => { @@ -40,12 +39,12 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('no listeners, no problem', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors); return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => assert.ok(true)); }); test('event delivery', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors); let event: vscode.TextDocumentWillSaveEvent; let sub = participant.onWillSaveTextDocumentEvent(function (e) { @@ -62,7 +61,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, immutable', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors); let event: vscode.TextDocumentWillSaveEvent; let sub = participant.onWillSaveTextDocumentEvent(function (e) { @@ -78,7 +77,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, bad listener', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors); let sub = participant.onWillSaveTextDocumentEvent(function (e) { throw new Error('💀'); @@ -93,7 +92,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, bad listener doesn\'t prevent more events', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors); let sub1 = participant.onWillSaveTextDocumentEvent(function (e) { throw new Error('💀'); @@ -112,7 +111,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, in subscriber order', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors); let counter = 0; let sub1 = participant.onWillSaveTextDocumentEvent(function (event) { @@ -130,7 +129,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, ignore bad listeners', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace, { timeout: 5, errors: 1 }); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors, { timeout: 5, errors: 1 }); let callCount = 0; let sub = participant.onWillSaveTextDocumentEvent(function (event) { @@ -151,7 +150,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, overall timeout', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace, { timeout: 20, errors: 5 }); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors, { timeout: 20, errors: 5 }); let callCount = 0; let sub1 = participant.onWillSaveTextDocumentEvent(function (event) { @@ -179,7 +178,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, waitUntil', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors); let sub = participant.onWillSaveTextDocumentEvent(function (event) { @@ -195,7 +194,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, waitUntil must be called sync', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors); let sub = participant.onWillSaveTextDocumentEvent(function (event) { @@ -218,7 +217,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, waitUntil will timeout', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace, { timeout: 5, errors: 3 }); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors, { timeout: 5, errors: 3 }); let sub = participant.onWillSaveTextDocumentEvent(function (event) { event.waitUntil(TPromise.timeout(15)); @@ -233,7 +232,7 @@ suite('ExtHostDocumentSaveParticipant', () => { }); test('event delivery, waitUntil failure handling', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, workspace); + const participant = new ExtHostDocumentSaveParticipant(documents, mainThreadEditors); let sub1 = participant.onWillSaveTextDocumentEvent(function (e) { e.waitUntil(TPromise.wrapError(new Error('dddd'))); @@ -253,9 +252,9 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, pushEdits sync', () => { - let edits: IResourceEdit[]; - const participant = new ExtHostDocumentSaveParticipant(documents, new class extends mock() { - $applyWorkspaceEdit(_edits) { + let edits: IWorkspaceResourceEdit[]; + const participant = new ExtHostDocumentSaveParticipant(documents, new class extends mock() { + $tryApplyWorkspaceEdit(_edits: IWorkspaceResourceEdit[]) { edits = _edits; return TPromise.as(true); } @@ -269,15 +268,16 @@ suite('ExtHostDocumentSaveParticipant', () => { return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); - assert.equal(edits.length, 2); + assert.equal(edits.length, 1); + assert.equal(edits[0].edits.length, 2); }); }); test('event delivery, concurrent change', () => { - let edits: IResourceEdit[]; - const participant = new ExtHostDocumentSaveParticipant(documents, new class extends mock() { - $applyWorkspaceEdit(_edits) { + let edits: IWorkspaceResourceEdit[]; + const participant = new ExtHostDocumentSaveParticipant(documents, new class extends mock() { + $tryApplyWorkspaceEdit(_edits: IWorkspaceResourceEdit[]) { edits = _edits; return TPromise.as(true); } @@ -310,20 +310,23 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, two listeners -> two document states', () => { - const participant = new ExtHostDocumentSaveParticipant(documents, new class extends mock() { - $applyWorkspaceEdit(_edits: IResourceEdit[]) { + const participant = new ExtHostDocumentSaveParticipant(documents, new class extends mock() { + $tryApplyWorkspaceEdit(_edits: IWorkspaceResourceEdit[]) { - for (const { resource, newText, range } of _edits) { - documents.$acceptModelChanged(resource.toString(), { - changes: [{ - range, - rangeLength: undefined, - text: newText - }], - eol: undefined, - versionId: documents.getDocumentData(resource).version + 1 - }, true); + for (const { resource, edits } of _edits) { + for (const { newText, range } of edits) { + documents.$acceptModelChanged(resource.toString(), { + changes: [{ + range, + rangeLength: undefined, + text: newText + }], + eol: undefined, + versionId: documents.getDocumentData(resource).version + 1 + }, true); + } } + return TPromise.as(true); } }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts index 59c4972297b..402466a620e 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditor.test.ts @@ -64,6 +64,7 @@ suite('ExtHostTextEditorOptions', () => { $tryRevealRange: undefined, $trySetSelections: undefined, $tryApplyEdits: undefined, + $tryApplyWorkspaceEdit: undefined, $tryInsertSnippet: undefined, $getDiffInformation: undefined }; diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts new file mode 100644 index 00000000000..4c012a03b62 --- /dev/null +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TPromise } from 'vs/base/common/winjs.base'; +import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; +import { MainContext, MainThreadEditorsShape, IWorkspaceResourceEdit } from 'vs/workbench/api/node/extHost.protocol'; +import URI from 'vs/base/common/uri'; +import { mock } from 'vs/workbench/test/electron-browser/api/mock'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; +import { OneGetThreadService, TestThreadService } from 'vs/workbench/test/electron-browser/api/testThreadService'; +import { ExtHostEditors } from 'vs/workbench/api/node/extHostTextEditors'; + +suite('ExtHostTextEditors.applyWorkspaceEdit', () => { + + const resource = URI.parse('foo:bar'); + let editors: ExtHostEditors; + let workspaceResourceEdits: IWorkspaceResourceEdit[]; + + setup(() => { + workspaceResourceEdits = null; + + let threadService = new TestThreadService(); + threadService.setTestInstance(MainContext.MainThreadEditors, new class extends mock() { + $tryApplyWorkspaceEdit(_workspaceResourceEdits: IWorkspaceResourceEdit[]): TPromise { + workspaceResourceEdits = _workspaceResourceEdits; + return TPromise.as(true); + } + }); + const documentsAndEditors = new ExtHostDocumentsAndEditors(OneGetThreadService(null)); + documentsAndEditors.$acceptDocumentsAndEditorsDelta({ + addedDocuments: [{ + isDirty: false, + modeId: 'foo', + url: resource, + versionId: 1337, + lines: ['foo'], + EOL: '\n', + }] + }); + editors = new ExtHostEditors(threadService, documentsAndEditors); + }); + + test('uses version id if document available', () => { + let edit = new extHostTypes.WorkspaceEdit(); + edit.replace(resource, new extHostTypes.Range(0, 0, 0, 0), 'hello'); + return editors.applyWorkspaceEdit(edit).then((result) => { + assert.equal(workspaceResourceEdits.length, 1); + assert.equal(workspaceResourceEdits[0].modelVersionId, 1337); + }); + }); + + test('does not use version id if document is not available', () => { + let edit = new extHostTypes.WorkspaceEdit(); + edit.replace(URI.parse('foo:bar2'), new extHostTypes.Range(0, 0, 0, 0), 'hello'); + return editors.applyWorkspaceEdit(edit).then((result) => { + assert.equal(workspaceResourceEdits.length, 1); + assert.ok(typeof workspaceResourceEdits[0].modelVersionId === 'undefined'); + }); + }); + +}); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts new file mode 100644 index 00000000000..cef45043b6b --- /dev/null +++ b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { MainThreadDocumentsAndEditors } from 'vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors'; +import { OneGetThreadService, TestThreadService } from './testThreadService'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; +import { MockCodeEditorService } from 'vs/editor/test/common/mocks/mockCodeEditorService'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ExtHostDocumentsAndEditorsShape, IWorkspaceResourceEdit, ExtHostContext, ExtHostDocumentsShape } from 'vs/workbench/api/node/extHost.protocol'; +import { mock } from 'vs/workbench/test/electron-browser/api/mock'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; +import Event from 'vs/base/common/event'; +import { MainThreadEditors } from 'vs/workbench/api/electron-browser/mainThreadEditors'; +import URI from 'vs/base/common/uri'; +import { Range } from 'vs/editor/common/core/range'; +import { Position } from 'vs/editor/common/core/position'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; + +suite('MainThreadEditors', () => { + + const resource = URI.parse('foo:bar'); + + let modelService: IModelService; + let editors: MainThreadEditors; + + setup(() => { + const configService = new TestConfigurationService(); + modelService = new ModelServiceImpl(null, configService); + const codeEditorService = new MockCodeEditorService(); + const textFileService = new class extends mock() { + isDirty() { return false; }; + models = { + onModelSaved: Event.None, + onModelReverted: Event.None, + onModelDirty: Event.None, + }; + }; + const workbenchEditorService = { + getVisibleEditors() { return []; }, + getActiveEditor() { return undefined; } + }; + const editorGroupService = new class extends mock() { + onEditorsChanged = Event.None; + onEditorsMoved = Event.None; + }; + + const testThreadService = new TestThreadService(true); + testThreadService.setTestInstance(ExtHostContext.ExtHostDocuments, new class extends mock() { + $acceptModelChanged(): void { + } + }); + testThreadService.setTestInstance(ExtHostContext.ExtHostDocumentsAndEditors, new class extends mock() { + $acceptDocumentsAndEditorsDelta(): void { + } + }); + + const documentAndEditor = new MainThreadDocumentsAndEditors( + testThreadService, + modelService, + textFileService, + workbenchEditorService, + codeEditorService, + null, + null, + null, + null, + editorGroupService, + null + ); + + editors = new MainThreadEditors( + documentAndEditor, + OneGetThreadService(null), + codeEditorService, + workbenchEditorService, + editorGroupService, + null, + null, + null, + modelService + ); + }); + + test(`applyWorkspaceEdit returns false if model is changed by user`, () => { + + let model = modelService.createModel('something', null, resource); + + let workspaceResourceEdit: IWorkspaceResourceEdit = { + resource: resource, + modelVersionId: model.getVersionId(), + edits: [{ + newText: 'asdfg', + range: new Range(1, 1, 1, 1) + }] + }; + + // Act as if the user edited the model + model.applyEdits([EditOperation.insert(new Position(0, 0), 'something')]); + + return editors.$tryApplyWorkspaceEdit([workspaceResourceEdit]).then((result) => { + assert.equal(result, false); + }); + }); +}); diff --git a/src/vs/workbench/test/electron-browser/api/testThreadService.ts b/src/vs/workbench/test/electron-browser/api/testThreadService.ts index 5e988c1bd38..901935ee7a9 100644 --- a/src/vs/workbench/test/electron-browser/api/testThreadService.ts +++ b/src/vs/workbench/test/electron-browser/api/testThreadService.ts @@ -76,8 +76,8 @@ export abstract class AbstractTestThreadService { } export class TestThreadService extends AbstractTestThreadService implements IThreadService { - constructor() { - super(false); + constructor(isMainProcess: boolean = false) { + super(isMainProcess); } private _callCountValue: number = 0;