Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts
5297 views
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 { ButtonBar } from '../../../../../base/browser/ui/button/button.js';
7
import type { IAsyncDataTreeViewState } from '../../../../../base/browser/ui/tree/asyncDataTree.js';
8
import { ITreeContextMenuEvent } from '../../../../../base/browser/ui/tree/tree.js';
9
import { CachedFunction, LRUCachedFunction } from '../../../../../base/common/cache.js';
10
import { CancellationToken } from '../../../../../base/common/cancellation.js';
11
import { FuzzyScore } from '../../../../../base/common/filters.js';
12
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
13
import { Mutable } from '../../../../../base/common/types.js';
14
import { URI } from '../../../../../base/common/uri.js';
15
import './bulkEdit.css';
16
import { ResourceEdit } from '../../../../../editor/browser/services/bulkEditService.js';
17
import { IMultiDiffEditorOptions, IMultiDiffResourceId } from '../../../../../editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.js';
18
import { IRange } from '../../../../../editor/common/core/range.js';
19
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
20
import { localize } from '../../../../../nls.js';
21
import { MenuId } from '../../../../../platform/actions/common/actions.js';
22
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
23
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
24
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
25
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
26
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
27
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
28
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
29
import { ILabelService } from '../../../../../platform/label/common/label.js';
30
import { IOpenEvent, WorkbenchAsyncDataTree } from '../../../../../platform/list/browser/listService.js';
31
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
32
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
33
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
34
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
35
import { ResourceLabels } from '../../../../browser/labels.js';
36
import { ViewPane } from '../../../../browser/parts/views/viewPane.js';
37
import { IViewletViewOptions } from '../../../../browser/parts/views/viewsViewlet.js';
38
import { IMultiDiffEditorResource, IResourceDiffEditorInput } from '../../../../common/editor.js';
39
import { IViewDescriptorService } from '../../../../common/views.js';
40
import { BulkEditPreviewProvider, BulkFileOperation, BulkFileOperations, BulkFileOperationType } from './bulkEditPreview.js';
41
import { BulkEditAccessibilityProvider, BulkEditDataSource, BulkEditDelegate, BulkEditElement, BulkEditIdentityProvider, BulkEditNaviLabelProvider, BulkEditSorter, CategoryElement, CategoryElementRenderer, compareBulkFileOperations, FileElement, FileElementRenderer, TextEditElement, TextEditElementRenderer } from './bulkEditTree.js';
42
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
43
44
const enum State {
45
Data = 'data',
46
Message = 'message'
47
}
48
49
export class BulkEditPane extends ViewPane {
50
51
static readonly ID = 'refactorPreview';
52
static readonly Schema = 'vscode-bulkeditpreview-multieditor';
53
54
static readonly ctxHasCategories = new RawContextKey('refactorPreview.hasCategories', false);
55
static readonly ctxGroupByFile = new RawContextKey('refactorPreview.groupByFile', true);
56
static readonly ctxHasCheckedChanges = new RawContextKey('refactorPreview.hasCheckedChanges', true);
57
58
private static readonly _memGroupByFile = `${this.ID}.groupByFile`;
59
60
private _tree!: WorkbenchAsyncDataTree<BulkFileOperations, BulkEditElement, FuzzyScore>;
61
private _treeDataSource!: BulkEditDataSource;
62
private _treeViewStates = new Map<boolean, IAsyncDataTreeViewState>();
63
private _message!: HTMLSpanElement;
64
65
private readonly _ctxHasCategories: IContextKey<boolean>;
66
private readonly _ctxGroupByFile: IContextKey<boolean>;
67
private readonly _ctxHasCheckedChanges: IContextKey<boolean>;
68
69
private readonly _disposables = new DisposableStore();
70
private readonly _sessionDisposables = new DisposableStore();
71
private _currentResolve?: (edit?: ResourceEdit[]) => void;
72
private _currentInput?: BulkFileOperations;
73
private _currentProvider?: BulkEditPreviewProvider;
74
75
constructor(
76
options: IViewletViewOptions,
77
@IInstantiationService private readonly _instaService: IInstantiationService,
78
@IEditorService private readonly _editorService: IEditorService,
79
@ILabelService private readonly _labelService: ILabelService,
80
@ITextModelService private readonly _textModelService: ITextModelService,
81
@IDialogService private readonly _dialogService: IDialogService,
82
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
83
@IStorageService private readonly _storageService: IStorageService,
84
@IContextKeyService contextKeyService: IContextKeyService,
85
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
86
@IKeybindingService keybindingService: IKeybindingService,
87
@IContextMenuService contextMenuService: IContextMenuService,
88
@IConfigurationService configurationService: IConfigurationService,
89
@IOpenerService openerService: IOpenerService,
90
@IThemeService themeService: IThemeService,
91
@IHoverService hoverService: IHoverService,
92
) {
93
super(
94
{ ...options, titleMenuId: MenuId.BulkEditTitle },
95
keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instaService, openerService, themeService, hoverService
96
);
97
98
this.element.classList.add('bulk-edit-panel', 'show-file-icons');
99
this._ctxHasCategories = BulkEditPane.ctxHasCategories.bindTo(contextKeyService);
100
this._ctxGroupByFile = BulkEditPane.ctxGroupByFile.bindTo(contextKeyService);
101
this._ctxHasCheckedChanges = BulkEditPane.ctxHasCheckedChanges.bindTo(contextKeyService);
102
}
103
104
override dispose(): void {
105
this._tree.dispose();
106
this._disposables.dispose();
107
this._sessionDisposables.dispose();
108
super.dispose();
109
}
110
111
protected override renderBody(parent: HTMLElement): void {
112
super.renderBody(parent);
113
114
const resourceLabels = this._instaService.createInstance(
115
ResourceLabels,
116
{ onDidChangeVisibility: this.onDidChangeBodyVisibility }
117
);
118
this._disposables.add(resourceLabels);
119
120
const contentContainer = document.createElement('div');
121
contentContainer.className = 'content';
122
parent.appendChild(contentContainer);
123
124
// tree
125
const treeContainer = document.createElement('div');
126
contentContainer.appendChild(treeContainer);
127
128
this._treeDataSource = this._instaService.createInstance(BulkEditDataSource);
129
this._treeDataSource.groupByFile = this._storageService.getBoolean(BulkEditPane._memGroupByFile, StorageScope.PROFILE, true);
130
this._ctxGroupByFile.set(this._treeDataSource.groupByFile);
131
132
this._tree = this._instaService.createInstance(
133
WorkbenchAsyncDataTree<BulkFileOperations, BulkEditElement, FuzzyScore>, this.id, treeContainer,
134
new BulkEditDelegate(),
135
[this._instaService.createInstance(TextEditElementRenderer), this._instaService.createInstance(FileElementRenderer, resourceLabels), this._instaService.createInstance(CategoryElementRenderer)],
136
this._treeDataSource,
137
{
138
accessibilityProvider: this._instaService.createInstance(BulkEditAccessibilityProvider),
139
identityProvider: new BulkEditIdentityProvider(),
140
expandOnlyOnTwistieClick: true,
141
multipleSelectionSupport: false,
142
keyboardNavigationLabelProvider: new BulkEditNaviLabelProvider(),
143
sorter: new BulkEditSorter(),
144
selectionNavigation: true
145
}
146
);
147
148
this._disposables.add(this._tree.onContextMenu(this._onContextMenu, this));
149
this._disposables.add(this._tree.onDidOpen(e => this._openElementInMultiDiffEditor(e)));
150
151
// buttons
152
const buttonsContainer = document.createElement('div');
153
buttonsContainer.className = 'buttons';
154
contentContainer.appendChild(buttonsContainer);
155
const buttonBar = new ButtonBar(buttonsContainer);
156
this._disposables.add(buttonBar);
157
158
const btnConfirm = buttonBar.addButton({ supportIcons: true, ...defaultButtonStyles });
159
btnConfirm.label = localize('ok', 'Apply');
160
btnConfirm.onDidClick(() => this.accept(), this, this._disposables);
161
162
const btnCancel = buttonBar.addButton({ ...defaultButtonStyles, secondary: true });
163
btnCancel.label = localize('cancel', 'Discard');
164
btnCancel.onDidClick(() => this.discard(), this, this._disposables);
165
166
// message
167
this._message = document.createElement('span');
168
this._message.className = 'message';
169
this._message.innerText = localize('empty.msg', "Invoke a code action, like rename, to see a preview of its changes here.");
170
parent.appendChild(this._message);
171
172
//
173
this._setState(State.Message);
174
}
175
176
protected override layoutBody(height: number, width: number): void {
177
super.layoutBody(height, width);
178
const treeHeight = height - 50;
179
this._tree.getHTMLElement().parentElement!.style.height = `${treeHeight}px`;
180
this._tree.layout(treeHeight, width);
181
}
182
183
private _setState(state: State): void {
184
this.element.dataset['state'] = state;
185
}
186
187
async setInput(edit: ResourceEdit[], token: CancellationToken): Promise<ResourceEdit[] | undefined> {
188
this._setState(State.Data);
189
this._sessionDisposables.clear();
190
this._treeViewStates.clear();
191
192
if (this._currentResolve) {
193
this._currentResolve(undefined);
194
this._currentResolve = undefined;
195
}
196
197
const input = await this._instaService.invokeFunction(BulkFileOperations.create, edit);
198
this._currentProvider = this._instaService.createInstance(BulkEditPreviewProvider, input);
199
this._sessionDisposables.add(this._currentProvider);
200
this._sessionDisposables.add(input);
201
202
//
203
const hasCategories = input.categories.length > 1;
204
this._ctxHasCategories.set(hasCategories);
205
this._treeDataSource.groupByFile = !hasCategories || this._treeDataSource.groupByFile;
206
this._ctxHasCheckedChanges.set(input.checked.checkedCount > 0);
207
208
this._currentInput = input;
209
210
return new Promise<ResourceEdit[] | undefined>(resolve => {
211
212
token.onCancellationRequested(() => resolve(undefined));
213
214
this._currentResolve = resolve;
215
this._setTreeInput(input);
216
217
// refresh when check state changes
218
this._sessionDisposables.add(input.checked.onDidChange(() => {
219
this._tree.updateChildren();
220
this._ctxHasCheckedChanges.set(input.checked.checkedCount > 0);
221
}));
222
});
223
}
224
225
hasInput(): boolean {
226
return Boolean(this._currentInput);
227
}
228
229
private async _setTreeInput(input: BulkFileOperations) {
230
231
const viewState = this._treeViewStates.get(this._treeDataSource.groupByFile);
232
await this._tree.setInput(input, viewState);
233
this._tree.domFocus();
234
235
if (viewState) {
236
return;
237
}
238
239
// async expandAll (max=10) is the default when no view state is given
240
const expand = [...this._tree.getNode(input).children].slice(0, 10);
241
while (expand.length > 0) {
242
const { element } = expand.shift()!;
243
if (element instanceof FileElement) {
244
await this._tree.expand(element, true);
245
}
246
if (element instanceof CategoryElement) {
247
await this._tree.expand(element, true);
248
expand.push(...this._tree.getNode(element).children);
249
}
250
}
251
}
252
253
accept(): void {
254
255
const conflicts = this._currentInput?.conflicts.list();
256
257
if (!conflicts || conflicts.length === 0) {
258
this._done(true);
259
return;
260
}
261
262
let message: string;
263
if (conflicts.length === 1) {
264
message = localize('conflict.1', "Cannot apply refactoring because '{0}' has changed in the meantime.", this._labelService.getUriLabel(conflicts[0], { relative: true }));
265
} else {
266
message = localize('conflict.N', "Cannot apply refactoring because {0} other files have changed in the meantime.", conflicts.length);
267
}
268
269
this._dialogService.warn(message).finally(() => this._done(false));
270
}
271
272
discard() {
273
this._done(false);
274
}
275
276
private _done(accept: boolean): void {
277
this._currentResolve?.(accept ? this._currentInput?.getWorkspaceEdit() : undefined);
278
this._currentInput = undefined;
279
this._setState(State.Message);
280
this._sessionDisposables.clear();
281
}
282
283
toggleChecked() {
284
const [first] = this._tree.getFocus();
285
if ((first instanceof FileElement || first instanceof TextEditElement) && !first.isDisabled()) {
286
first.setChecked(!first.isChecked());
287
} else if (first instanceof CategoryElement) {
288
first.setChecked(!first.isChecked());
289
}
290
}
291
292
groupByFile(): void {
293
if (!this._treeDataSource.groupByFile) {
294
this.toggleGrouping();
295
}
296
}
297
298
groupByType(): void {
299
if (this._treeDataSource.groupByFile) {
300
this.toggleGrouping();
301
}
302
}
303
304
toggleGrouping() {
305
const input = this._tree.getInput();
306
if (input) {
307
308
// (1) capture view state
309
const oldViewState = this._tree.getViewState();
310
this._treeViewStates.set(this._treeDataSource.groupByFile, oldViewState);
311
312
// (2) toggle and update
313
this._treeDataSource.groupByFile = !this._treeDataSource.groupByFile;
314
this._setTreeInput(input);
315
316
// (3) remember preference
317
this._storageService.store(BulkEditPane._memGroupByFile, this._treeDataSource.groupByFile, StorageScope.PROFILE, StorageTarget.USER);
318
this._ctxGroupByFile.set(this._treeDataSource.groupByFile);
319
}
320
}
321
322
private async _openElementInMultiDiffEditor(e: IOpenEvent<BulkEditElement | undefined>): Promise<void> {
323
324
const fileOperations = this._currentInput?.fileOperations;
325
if (!fileOperations) {
326
return;
327
}
328
329
let selection: IRange | undefined = undefined;
330
let fileElement: FileElement;
331
if (e.element instanceof TextEditElement) {
332
fileElement = e.element.parent;
333
selection = e.element.edit.textEdit.textEdit.range;
334
} else if (e.element instanceof FileElement) {
335
fileElement = e.element;
336
selection = e.element.edit.textEdits[0]?.textEdit.textEdit.range;
337
} else {
338
// invalid event
339
return;
340
}
341
342
const result = await this._computeResourceDiffEditorInputs.get(fileOperations);
343
const resourceId = await result.getResourceDiffEditorInputIdOfOperation(fileElement.edit);
344
const options: Mutable<IMultiDiffEditorOptions> = {
345
...e.editorOptions,
346
viewState: {
347
revealData: {
348
resource: resourceId,
349
range: selection,
350
}
351
}
352
};
353
const multiDiffSource = URI.from({ scheme: BulkEditPane.Schema });
354
const label = 'Refactor Preview';
355
this._editorService.openEditor({
356
multiDiffSource,
357
label,
358
options,
359
isTransient: true,
360
description: label,
361
resources: result.resources
362
}, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
363
}
364
365
private readonly _computeResourceDiffEditorInputs = new LRUCachedFunction<
366
BulkFileOperation[],
367
Promise<{ resources: IMultiDiffEditorResource[]; getResourceDiffEditorInputIdOfOperation: (operation: BulkFileOperation) => Promise<IMultiDiffResourceId> }>
368
>(async (fileOperations) => {
369
const computeDiffEditorInput = new CachedFunction<BulkFileOperation, Promise<IMultiDiffEditorResource>>(async (fileOperation) => {
370
const fileOperationUri = fileOperation.uri;
371
const previewUri = this._currentProvider!.asPreviewUri(fileOperationUri);
372
// delete
373
if (fileOperation.type & BulkFileOperationType.Delete) {
374
return {
375
original: { resource: URI.revive(previewUri) },
376
modified: { resource: undefined },
377
goToFileResource: fileOperation.uri,
378
} satisfies IMultiDiffEditorResource;
379
380
}
381
// rename, create, edits
382
else {
383
let leftResource: URI | undefined;
384
try {
385
(await this._textModelService.createModelReference(fileOperationUri)).dispose();
386
leftResource = fileOperationUri;
387
} catch {
388
leftResource = BulkEditPreviewProvider.emptyPreview;
389
}
390
return {
391
original: { resource: URI.revive(leftResource) },
392
modified: { resource: URI.revive(previewUri) },
393
goToFileResource: leftResource,
394
} satisfies IMultiDiffEditorResource;
395
}
396
});
397
398
const sortedFileOperations = fileOperations.slice().sort(compareBulkFileOperations);
399
const resources: IResourceDiffEditorInput[] = [];
400
for (const operation of sortedFileOperations) {
401
resources.push(await computeDiffEditorInput.get(operation));
402
}
403
const getResourceDiffEditorInputIdOfOperation = async (operation: BulkFileOperation): Promise<IMultiDiffResourceId> => {
404
const resource = await computeDiffEditorInput.get(operation);
405
return { original: resource.original.resource, modified: resource.modified.resource };
406
};
407
return {
408
resources,
409
getResourceDiffEditorInputIdOfOperation
410
};
411
});
412
413
private _onContextMenu(e: ITreeContextMenuEvent<any>): void {
414
415
this._contextMenuService.showContextMenu({
416
menuId: MenuId.BulkEditContext,
417
contextKeyService: this.contextKeyService,
418
getAnchor: () => e.anchor
419
});
420
}
421
}
422
423