This commit is contained in:
Johannes Rieken 2020-02-03 12:30:04 +01:00
parent 920559ace1
commit 30b939c18f
3 changed files with 70 additions and 105 deletions

View file

@ -25,7 +25,8 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { Recording } from 'vs/workbench/services/bulkEdit/browser/conflicts';
import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflicts';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };
@ -234,10 +235,9 @@ class BulkEdit {
editor: ICodeEditor | undefined,
progress: IProgress<IProgressStep> | undefined,
edits: Edit[],
@IInstantiationService private readonly _instaService: IInstantiationService,
@ILogService private readonly _logService: ILogService,
@ITextModelService private readonly _textModelService: ITextModelService,
@IFileService private readonly _fileService: IFileService,
@IEditorWorkerService private readonly _workerService: IEditorWorkerService,
@ITextFileService private readonly _textFileService: ITextFileService,
@ILabelService private readonly _uriLabelServie: ILabelService,
@IConfigurationService private readonly _configurationService: IConfigurationService
@ -288,14 +288,15 @@ class BulkEdit {
// for child operations
this._progress.report({ total });
let progress: IProgress<void> = { report: _ => this._progress.report({ increment: 1 }) };
const conflicts = this._instaService.createInstance(ConflictDetector, { edits: this._edits });
const progress: IProgress<void> = { report: _ => this._progress.report({ increment: 1 }) };
// do it.
for (const group of groups) {
if (WorkspaceFileEdit.is(group[0])) {
await this._performFileEdits(<WorkspaceFileEdit[]>group, progress);
} else {
await this._performTextEdits(<WorkspaceTextEdit[]>group, progress);
await this._performTextEdits(<WorkspaceTextEdit[]>group, conflicts, progress);
}
}
}
@ -335,25 +336,14 @@ class BulkEdit {
}
}
private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress<void>): Promise<void> {
private async _performTextEdits(edits: WorkspaceTextEdit[], conflicts: ConflictDetector, progress: IProgress<void>): Promise<void> {
this._logService.debug('_performTextEdits', JSON.stringify(edits));
const recording = Recording.start(this._fileService);
const model = new BulkEditModel(this._editor, progress, edits, this._workerService, this._textModelService);
const model = this._instaService.createInstance(BulkEditModel, this._editor, progress, edits);
await model.prepare();
const conflicts = edits
.filter(edit => recording.hasChanged(edit.resource))
.map(edit => this._uriLabelServie.getUriLabel(edit.resource, { relative: true }));
recording.stop();
if (conflicts.length > 0) {
model.dispose();
throw new Error(localize('conflict', "These files have changed in the meantime: {0}", conflicts.join(', ')));
}
this._throwIfConflicts(conflicts);
const validationResult = model.validate();
if (validationResult.canApply === false) {
model.dispose();
@ -363,6 +353,13 @@ class BulkEdit {
model.apply();
model.dispose();
}
private _throwIfConflicts(conflicts: ConflictDetector) {
if (conflicts.hasConflicts()) {
const paths = conflicts.list().map(uri => this._uriLabelServie.getUriLabel(uri, { relative: true }));
throw new Error(localize('conflict', "These files have changed in the meantime: {0}", paths.join(', ')));
}
}
}
export class BulkEditService implements IBulkEditService {
@ -372,15 +369,10 @@ export class BulkEditService implements IBulkEditService {
private _previewHandler?: IBulkEditPreviewHandler;
constructor(
@IInstantiationService private readonly _instaService: IInstantiationService,
@ILogService private readonly _logService: ILogService,
@IModelService private readonly _modelService: IModelService,
@IEditorService private readonly _editorService: IEditorService,
@IEditorWorkerService private readonly _workerService: IEditorWorkerService,
@ITextModelService private readonly _textModelService: ITextModelService,
@IFileService private readonly _fileService: IFileService,
@ITextFileService private readonly _textFileService: ITextFileService,
@ILabelService private readonly _labelService: ILabelService,
@IConfigurationService private readonly _configurationService: IConfigurationService
) { }
setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable {
@ -433,10 +425,7 @@ export class BulkEditService implements IBulkEditService {
// If the code editor is readonly still allow bulk edits to be applied #68549
codeEditor = undefined;
}
const bulkEdit = new BulkEdit(
codeEditor, options?.progress, edits,
this._logService, this._textModelService, this._fileService, this._workerService, this._textFileService, this._labelService, this._configurationService
);
const bulkEdit = this._instaService.createInstance(BulkEdit, codeEditor, options?.progress, edits);
return bulkEdit.perform().then(() => {
return { ariaSummary: bulkEdit.ariaMessage() };
}).catch(err => {

View file

@ -10,31 +10,11 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { ResourceMap } from 'vs/base/common/map';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import type { ITextModel } from 'vs/editor/common/model';
export abstract class Recording {
static start(fileService: IFileService): Recording {
let _changes = new Set<string>();
let subscription = fileService.onAfterOperation(e => {
_changes.add(e.resource.toString());
});
return {
stop() { return subscription.dispose(); },
hasChanged(resource) { return _changes.has(resource.toString()); }
};
}
abstract stop(): void;
abstract hasChanged(resource: URI): boolean;
}
import { ITextModel } from 'vs/editor/common/model';
export class ConflictDetector {
private readonly _conflicts = new ResourceMap<boolean>();
private readonly _changes = new ResourceMap<boolean>();
private readonly _disposables = new DisposableStore();
private readonly _onDidConflict = new Emitter<this>();
@ -79,9 +59,6 @@ export class ConflictDetector {
continue;
}
// change
this._changes.set(change.resource, true);
// conflict
if (_workspaceEditResources.has(change.resource)) {
this._conflicts.set(change.resource, true);
@ -92,8 +69,6 @@ export class ConflictDetector {
// listen to model changes...?
const onDidChangeModel = (model: ITextModel) => {
// change
this._changes.set(model.uri, true);
// conflict
if (_workspaceEditResources.has(model.uri)) {
@ -112,12 +87,10 @@ export class ConflictDetector {
}
list(): URI[] {
const result: URI[] = this._conflicts.keys();
// this._changes.forEach((_value, key) => {
// if (!this._conflicts.has(key)) {
// result.push(key);
// }
// });
return result;
return this._conflicts.keys();
}
hasConflicts(): boolean {
return this._conflicts.size > 0;
}
}

View file

@ -21,13 +21,25 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { TestFileService, TestEditorService, TestEditorGroupsService, TestEnvironmentService, TestContextService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices';
import { BulkEditService } from 'vs/workbench/services/bulkEdit/browser/bulkEditService';
import { NullLogService } from 'vs/platform/log/common/log';
import { NullLogService, ILogService } from 'vs/platform/log/common/log';
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
import { IReference, ImmortalReference } from 'vs/base/common/lifecycle';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { LabelService } from 'vs/workbench/services/label/common/labelService';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { IFileService } from 'vs/platform/files/common/files';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { ILabelService } from 'vs/platform/label/common/label';
suite('MainThreadEditors', () => {
@ -42,18 +54,30 @@ suite('MainThreadEditors', () => {
const deletedResources = new Set<URI>();
setup(() => {
const configService = new TestConfigurationService();
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService());
const codeEditorService = new TestCodeEditorService();
movedResources.clear();
copiedResources.clear();
createdResources.clear();
deletedResources.clear();
const fileService = new TestFileService();
const textFileService = new class extends mock<ITextFileService>() {
const configService = new TestConfigurationService();
modelService = new ModelServiceImpl(configService, new TestTextResourcePropertiesService(configService), new TestThemeService(), new NullLogService());
const services = new ServiceCollection();
services.set(IBulkEditService, new SyncDescriptor(BulkEditService));
services.set(ILabelService, new SyncDescriptor(LabelService));
services.set(ILogService, new NullLogService());
services.set(IWorkspaceContextService, new TestContextService());
services.set(IWorkbenchEnvironmentService, TestEnvironmentService);
services.set(IConfigurationService, configService);
services.set(IModelService, modelService);
services.set(ICodeEditorService, new TestCodeEditorService());
services.set(IFileService, new TestFileService());
services.set(IEditorService, new TestEditorService());
services.set(IEditorGroupsService, new TestEditorGroupsService());
services.set(ITextFileService, new class extends mock<ITextFileService>() {
isDirty() { return false; }
create(uri: URI, contents?: string, options?: any) {
createdResources.add(uri);
@ -76,10 +100,8 @@ suite('MainThreadEditors', () => {
onDidRevert: Event.None,
onDidChangeDirty: Event.None
};
};
const workbenchEditorService = new TestEditorService();
const editorGroupService = new TestEditorGroupsService();
const textModelService = new class extends mock<ITextModelService>() {
});
services.set(ITextModelService, new class extends mock<ITextModelService>() {
createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
const textEditorModel = new class extends mock<IResolvedTextEditorModel>() {
textEditorModel = modelService.getModel(resource)!;
@ -87,13 +109,20 @@ suite('MainThreadEditors', () => {
textEditorModel.isReadonly = () => false;
return Promise.resolve(new ImmortalReference(textEditorModel));
}
};
});
services.set(IEditorWorkerService, new class extends mock<IEditorWorkerService>() {
const editorWorkerService = new class extends mock<IEditorWorkerService>() {
});
services.set(IPanelService, new class extends mock<IPanelService>() implements IPanelService {
_serviceBrand: undefined;
onDidPanelOpen = Event.None;
onDidPanelClose = Event.None;
getActivePanel() {
return undefined;
}
});
};
const bulkEditService = new BulkEditService(new NullLogService(), modelService, new TestEditorService(), editorWorkerService, textModelService, new TestFileService(), textFileService, new LabelService(TestEnvironmentService, new TestContextService()), configService);
const instaService = new InstantiationService(services);
const rpcProtocol = new TestRPCProtocol();
rpcProtocol.set(ExtHostContext.ExtHostDocuments, new class extends mock<ExtHostDocumentsShape>() {
@ -105,35 +134,9 @@ suite('MainThreadEditors', () => {
}
});
const documentAndEditor = new MainThreadDocumentsAndEditors(
rpcProtocol,
modelService,
textFileService,
workbenchEditorService,
codeEditorService,
fileService,
null!,
editorGroupService,
bulkEditService,
new class extends mock<IPanelService>() implements IPanelService {
_serviceBrand: undefined;
onDidPanelOpen = Event.None;
onDidPanelClose = Event.None;
getActivePanel() {
return undefined;
}
},
TestEnvironmentService
);
const documentAndEditor = instaService.createInstance(MainThreadDocumentsAndEditors, rpcProtocol);
editors = new MainThreadTextEditors(
documentAndEditor,
SingleProxyRPCProtocol(null),
codeEditorService,
bulkEditService,
workbenchEditorService,
editorGroupService,
);
editors = instaService.createInstance(MainThreadTextEditors, documentAndEditor, SingleProxyRPCProtocol(null));
});
test(`applyWorkspaceEdit returns false if model is changed by user`, () => {