Skip to content

Commit f82e3c0

Browse files
committed
don't apply code action when things have changed in the meantime
1 parent 68fadbd commit f82e3c0

5 files changed

Lines changed: 156 additions & 26 deletions

File tree

src/vs/base/common/map.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,8 +454,8 @@ export class ResourceMap<T> {
454454
return this.map.delete(this.toKey(resource));
455455
}
456456

457-
forEach(clb: (value: T) => void): void {
458-
this.map.forEach(clb);
457+
forEach(clb: (value: T, key: URI) => void): void {
458+
this.map.forEach((value, index) => clb(value, URI.parse(index)));
459459
}
460460

461461
values(): T[] {

src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
2626
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
2727
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
2828
import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
29+
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
30+
import Severity from 'vs/base/common/severity';
31+
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
2932

3033
const enum State {
3134
Data = 'data',
@@ -54,6 +57,7 @@ export class BulkEditPane extends ViewPane {
5457
@IEditorService private readonly _editorService: IEditorService,
5558
@ILabelService private readonly _labelService: ILabelService,
5659
@ITextModelService private readonly _textModelService: ITextModelService,
60+
@IDialogService private readonly _dialogService: IDialogService,
5761
@IKeybindingService keybindingService: IKeybindingService,
5862
@IContextMenuService contextMenuService: IContextMenuService,
5963
@IConfigurationService configurationService: IConfigurationService,
@@ -140,8 +144,9 @@ export class BulkEditPane extends ViewPane {
140144

141145
const input = await this._instaService.invokeFunction(BulkFileOperations.create, edit);
142146
const provider = this._instaService.createInstance(BulkEditPreviewProvider, input);
143-
144147
this._sessionDisposables.add(provider);
148+
this._sessionDisposables.add(input);
149+
145150
this._currentInput = input;
146151

147152
this._acceptAction.enabled = true;
@@ -167,7 +172,21 @@ export class BulkEditPane extends ViewPane {
167172
}
168173

169174
accept(): void {
170-
this._done(true);
175+
176+
const conflicts = this._currentInput?.conflicts.list();
177+
178+
if (isFalsyOrEmpty(conflicts)) {
179+
this._done(true);
180+
}
181+
182+
let message: string;
183+
if (conflicts!.length === 1) {
184+
message = localize('conflict.1', "Cannot apply refactoring because '{0}' has changed in the meantime.", this._labelService.getUriLabel(conflicts![0], { relative: true }));
185+
} else {
186+
message = localize('conflict.N', "Cannot apply refactoring because {0} other files have changed in the meantime.", conflicts!.length);
187+
}
188+
189+
this._dialogService.show(Severity.Warning, message, []).finally(() => this._done(false));
171190
}
172191

173192
discard() {
@@ -182,19 +201,19 @@ export class BulkEditPane extends ViewPane {
182201
}
183202

184203
private _done(accept: boolean): void {
185-
this._setState(State.Message);
186-
this._sessionDisposables.clear();
187204
if (this._currentResolve) {
188205
this._currentResolve(accept ? this._currentInput?.asWorkspaceEdit() : undefined);
189206
this._acceptAction.enabled = false;
190207
this._discardAction.enabled = false;
208+
this._currentInput = undefined;
191209
}
210+
this._setState(State.Message);
211+
this._sessionDisposables.clear();
192212
}
193213

194214
private async _previewTextEditElement(element: TextEditElement): Promise<void> {
195215

196216
let leftResource: URI;
197-
198217
try {
199218
(await this._textModelService.createModelReference(element.parent.uri)).dispose();
200219
leftResource = element.parent.uri;

src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati
1717
import { IFileService } from 'vs/platform/files/common/files';
1818
import { Emitter, Event } from 'vs/base/common/event';
1919
import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
20+
import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflicts';
2021

2122
class CheckedObject {
2223

@@ -88,10 +89,19 @@ export class BulkFileOperations {
8889

8990
readonly fileOperations: BulkFileOperation[] = [];
9091

92+
readonly conflicts: ConflictDetector;
93+
9194
constructor(
9295
private readonly _bulkEdit: WorkspaceEdit,
9396
@IFileService private readonly _fileService: IFileService,
94-
) { }
97+
@IInstantiationService instaService: IInstantiationService,
98+
) {
99+
this.conflicts = instaService.createInstance(ConflictDetector, _bulkEdit);
100+
}
101+
102+
dispose(): void {
103+
this.conflicts.dispose();
104+
}
95105

96106
async _init() {
97107
const operationByResource = new Map<string, BulkFileOperation>();

src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,8 @@ import { ILabelService } from 'vs/platform/label/common/label';
2525
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
2626
import { EditorOption } from 'vs/editor/common/config/editorOptions';
2727
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
28+
import { Recording } from 'vs/workbench/services/bulkEdit/browser/conflicts';
2829

29-
abstract class Recording {
30-
31-
static start(fileService: IFileService): Recording {
32-
33-
let _changes = new Set<string>();
34-
let subscription = fileService.onAfterOperation(e => {
35-
_changes.add(e.resource.toString());
36-
});
37-
38-
return {
39-
stop() { return subscription.dispose(); },
40-
hasChanged(resource) { return _changes.has(resource.toString()); }
41-
};
42-
}
43-
44-
abstract stop(): void;
45-
abstract hasChanged(resource: URI): boolean;
46-
}
4730

4831
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };
4932

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { IFileService } from 'vs/platform/files/common/files';
7+
import { URI } from 'vs/base/common/uri';
8+
import { WorkspaceEdit, isResourceTextEdit } from 'vs/editor/common/modes';
9+
import { IModelService } from 'vs/editor/common/services/modelService';
10+
import { ResourceMap } from 'vs/base/common/map';
11+
import { DisposableStore } from 'vs/base/common/lifecycle';
12+
import { Emitter, Event } from 'vs/base/common/event';
13+
import type { ITextModel } from 'vs/editor/common/model';
14+
15+
export abstract class Recording {
16+
17+
static start(fileService: IFileService): Recording {
18+
19+
let _changes = new Set<string>();
20+
let subscription = fileService.onAfterOperation(e => {
21+
_changes.add(e.resource.toString());
22+
});
23+
24+
return {
25+
stop() { return subscription.dispose(); },
26+
hasChanged(resource) { return _changes.has(resource.toString()); }
27+
};
28+
}
29+
30+
abstract stop(): void;
31+
abstract hasChanged(resource: URI): boolean;
32+
}
33+
34+
export class ConflictDetector {
35+
36+
private readonly _conflicts = new ResourceMap<boolean>();
37+
private readonly _changes = new ResourceMap<boolean>();
38+
private readonly _disposables = new DisposableStore();
39+
40+
private readonly _onDidConflict = new Emitter<this>();
41+
readonly onDidConflict: Event<this> = this._onDidConflict.event;
42+
43+
constructor(
44+
workspaceEdit: WorkspaceEdit,
45+
@IFileService fileService: IFileService,
46+
@IModelService modelService: IModelService,
47+
) {
48+
49+
const _workspaceEditResources = new ResourceMap<boolean>();
50+
51+
for (let edit of workspaceEdit.edits) {
52+
if (isResourceTextEdit(edit)) {
53+
54+
_workspaceEditResources.set(edit.resource, true);
55+
56+
if (typeof edit.modelVersionId === 'number') {
57+
const model = modelService.getModel(edit.resource);
58+
if (model && model.getVersionId() !== edit.modelVersionId) {
59+
this._conflicts.set(edit.resource, true);
60+
this._onDidConflict.fire(this);
61+
}
62+
}
63+
64+
} else if (edit.newUri) {
65+
_workspaceEditResources.set(edit.newUri, true);
66+
67+
} else if (edit.oldUri) {
68+
_workspaceEditResources.set(edit.oldUri, true);
69+
}
70+
}
71+
72+
// listen to file changes
73+
this._disposables.add(fileService.onFileChanges(e => {
74+
for (let change of e.changes) {
75+
76+
// change
77+
this._changes.set(change.resource, true);
78+
79+
// conflict
80+
if (_workspaceEditResources.has(change.resource)) {
81+
this._conflicts.set(change.resource, true);
82+
this._onDidConflict.fire(this);
83+
}
84+
}
85+
}));
86+
87+
88+
// listen to model changes...?
89+
const onDidChangeModel = (model: ITextModel) => {
90+
// change
91+
this._changes.set(model.uri, true);
92+
93+
// conflict
94+
if (_workspaceEditResources.has(model.uri)) {
95+
this._conflicts.set(model.uri, true);
96+
this._onDidConflict.fire(this);
97+
}
98+
};
99+
for (let model of modelService.getModels()) {
100+
this._disposables.add(model.onDidChangeContent(() => onDidChangeModel(model)));
101+
}
102+
}
103+
104+
dispose(): void {
105+
this._disposables.dispose();
106+
this._onDidConflict.dispose();
107+
}
108+
109+
list(): URI[] {
110+
const result: URI[] = this._conflicts.keys();
111+
this._changes.forEach((_value, key) => {
112+
if (!this._conflicts.has(key)) {
113+
result.push(key);
114+
}
115+
});
116+
return result;
117+
}
118+
}

0 commit comments

Comments
 (0)