bulkEditPane.ts 14.5 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import 'vs/css!./bulkEdit';
7
import { WorkbenchAsyncDataTree, TreeResourceNavigator, IOpenEvent } from 'vs/platform/list/browser/listService';
8
import { WorkspaceEdit } from 'vs/editor/common/modes';
J
Johannes Rieken 已提交
9
import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, BulkEditAriaProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditTree';
10 11
import { FuzzyScore } from 'vs/base/common/filters';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
J
Johannes Rieken 已提交
12
import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
13
import { diffInserted, diffRemoved } from 'vs/platform/theme/common/colorRegistry';
J
Johannes Rieken 已提交
14
import { localize } from 'vs/nls';
J
Johannes Rieken 已提交
15 16
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
J
Johannes Rieken 已提交
17
import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview';
J
Johannes Rieken 已提交
18
import { ILabelService } from 'vs/platform/label/common/label';
19 20
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { URI } from 'vs/base/common/uri';
J
Johannes Rieken 已提交
21 22 23 24
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
25
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
J
Johannes Rieken 已提交
26
import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
27
import { ResourceLabels, IResourceLabelsContainer } from 'vs/workbench/browser/labels';
28 29
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import Severity from 'vs/base/common/severity';
J
Johannes Rieken 已提交
30
import { basename } from 'vs/base/common/resources';
31
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
32
import { IAction } from 'vs/base/common/actions';
33
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
34
import { ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
35
import { CancellationToken } from 'vs/base/common/cancellation';
36
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
37 38
import type { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
39
import { IViewDescriptorService } from 'vs/workbench/common/views';
J
Johannes Rieken 已提交
40 41 42 43 44

const enum State {
	Data = 'data',
	Message = 'message'
}
45

J
Johannes Rieken 已提交
46
export class BulkEditPane extends ViewPane {
47

48
	static readonly ID = 'refactorPreview';
49

50
	static readonly ctxHasCategories = new RawContextKey('refactorPreview.hasCategories', false);
51
	static readonly ctxGroupByFile = new RawContextKey('refactorPreview.groupByFile', true);
J
Johannes Rieken 已提交
52
	static readonly ctxHasCheckedChanges = new RawContextKey('refactorPreview.hasCheckedChanges', true);
53

54 55
	private static readonly _memGroupByFile = `${BulkEditPane.ID}.groupByFile`;

56
	private _tree!: WorkbenchAsyncDataTree<BulkFileOperations, BulkEditElement, FuzzyScore>;
57 58
	private _treeDataSource!: BulkEditDataSource;
	private _treeViewStates = new Map<boolean, IAsyncDataTreeViewState>();
J
Johannes Rieken 已提交
59 60
	private _message!: HTMLSpanElement;

61 62
	private readonly _ctxHasCategories: IContextKey<boolean>;
	private readonly _ctxGroupByFile: IContextKey<boolean>;
J
Johannes Rieken 已提交
63
	private readonly _ctxHasCheckedChanges: IContextKey<boolean>;
64

J
Johannes Rieken 已提交
65 66
	private readonly _disposables = new DisposableStore();
	private readonly _sessionDisposables = new DisposableStore();
67 68
	private _currentResolve?: (edit?: WorkspaceEdit) => void;
	private _currentInput?: BulkFileOperations;
69

70

71
	constructor(
J
Johannes Rieken 已提交
72
		options: IViewletViewOptions,
73
		@IInstantiationService private readonly _instaService: IInstantiationService,
J
Johannes Rieken 已提交
74 75
		@IEditorService private readonly _editorService: IEditorService,
		@ILabelService private readonly _labelService: ILabelService,
76
		@ITextModelService private readonly _textModelService: ITextModelService,
77
		@IDialogService private readonly _dialogService: IDialogService,
78 79 80
		@IMenuService private readonly _menuService: IMenuService,
		@IContextMenuService private readonly _contextMenuService: IContextMenuService,
		@IContextKeyService private readonly _contextKeyService: IContextKeyService,
81
		@IStorageService private readonly _storageService: IStorageService,
82
		@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
J
Johannes Rieken 已提交
83 84 85
		@IKeybindingService keybindingService: IKeybindingService,
		@IContextMenuService contextMenuService: IContextMenuService,
		@IConfigurationService configurationService: IConfigurationService,
86
	) {
J
Johannes Rieken 已提交
87
		super(
88
			{ ...options, titleMenuId: MenuId.BulkEditTitle },
89
			keybindingService, contextMenuService, configurationService, _contextKeyService, viewDescriptorService, _instaService
J
Johannes Rieken 已提交
90
		);
J
Johannes Rieken 已提交
91 92

		this.element.classList.add('bulk-edit-panel', 'show-file-icons');
93
		this._ctxHasCategories = BulkEditPane.ctxHasCategories.bindTo(_contextKeyService);
94
		this._ctxGroupByFile = BulkEditPane.ctxGroupByFile.bindTo(_contextKeyService);
J
Johannes Rieken 已提交
95
		this._ctxHasCheckedChanges = BulkEditPane.ctxHasCheckedChanges.bindTo(_contextKeyService);
J
Johannes Rieken 已提交
96
	}
97

J
Johannes Rieken 已提交
98 99 100
	dispose(): void {
		this._tree.dispose();
		this._disposables.dispose();
101 102
	}

J
Johannes Rieken 已提交
103
	protected renderBody(parent: HTMLElement): void {
104

105 106 107 108 109 110
		const resourceLabels = this._instaService.createInstance(
			ResourceLabels,
			<IResourceLabelsContainer>{ onDidChangeVisibility: this.onDidChangeBodyVisibility }
		);
		this._disposables.add(resourceLabels);

J
Johannes Rieken 已提交
111
		// tree
112
		const treeContainer = document.createElement('div');
J
Johannes Rieken 已提交
113
		treeContainer.className = 'tree';
114 115 116 117
		treeContainer.style.width = '100%';
		treeContainer.style.height = '100%';
		parent.appendChild(treeContainer);

118 119 120 121
		this._treeDataSource = this._instaService.createInstance(BulkEditDataSource);
		this._treeDataSource.groupByFile = this._storageService.getBoolean(BulkEditPane._memGroupByFile, StorageScope.GLOBAL, true);
		this._ctxGroupByFile.set(this._treeDataSource.groupByFile);

122
		this._tree = this._instaService.createInstance(
J
Johannes Rieken 已提交
123
			WorkbenchAsyncDataTree, this.id, treeContainer,
124
			new BulkEditDelegate(),
125 126
			[new TextEditElementRenderer(), this._instaService.createInstance(FileElementRenderer, resourceLabels), new CategoryElementRenderer()],
			this._treeDataSource,
127
			{
128
				accessibilityProvider: this._instaService.createInstance(BulkEditAccessibilityProvider),
J
Johannes Rieken 已提交
129
				ariaProvider: new BulkEditAriaProvider(),
130
				identityProvider: new BulkEditIdentityProvider(),
J
Johannes Rieken 已提交
131
				expandOnlyOnTwistieClick: true,
132 133
				multipleSelectionSupport: false,
				keyboardNavigationLabelProvider: new BulkEditNaviLabelProvider(),
134 135
			}
		);
J
Johannes Rieken 已提交
136

137 138
		this._disposables.add(this._tree.onContextMenu(this._onContextMenu, this));

139
		const navigator = new TreeResourceNavigator(this._tree, { openOnFocus: true });
140 141 142
		this._disposables.add(navigator);
		this._disposables.add(navigator.onDidOpenResource(e => this._openElementAsEditor(e)));

J
Johannes Rieken 已提交
143
		// message
J
Johannes Rieken 已提交
144 145 146 147 148 149 150
		this._message = document.createElement('span');
		this._message.className = 'message';
		this._message.innerText = localize('empty.msg', "Invoke a code action, like rename, to see a preview of its changes here.");
		parent.appendChild(this._message);

		//
		this._setState(State.Message);
151 152
	}

J
Johannes Rieken 已提交
153 154
	protected layoutBody(height: number, width: number): void {
		this._tree.layout(height, width);
155 156
	}

J
Johannes Rieken 已提交
157
	private _setState(state: State): void {
J
Johannes Rieken 已提交
158
		this.element.dataset['state'] = state;
J
Johannes Rieken 已提交
159 160
	}

161
	async setInput(edit: WorkspaceEdit, token: CancellationToken): Promise<WorkspaceEdit | undefined> {
J
Johannes Rieken 已提交
162
		this._setState(State.Data);
J
Johannes Rieken 已提交
163
		this._sessionDisposables.clear();
164
		this._treeViewStates.clear();
J
Johannes Rieken 已提交
165

166
		if (this._currentResolve) {
167
			this._currentResolve(undefined);
168 169 170
			this._currentResolve = undefined;
		}

171
		const input = await this._instaService.invokeFunction(BulkFileOperations.create, edit);
172 173
		const provider = this._instaService.createInstance(BulkEditPreviewProvider, input);
		this._sessionDisposables.add(provider);
174 175
		this._sessionDisposables.add(input);

176 177 178 179
		//
		const hasCategories = input.categories.length > 1;
		this._ctxHasCategories.set(hasCategories);
		this._treeDataSource.groupByFile = !hasCategories || this._treeDataSource.groupByFile;
J
Johannes Rieken 已提交
180
		this._ctxHasCheckedChanges.set(input.checked.checkedCount > 0);
181

182
		this._currentInput = input;
183

184
		return new Promise(async resolve => {
J
Johannes Rieken 已提交
185

186 187
			token.onCancellationRequested(() => resolve());

188
			this._currentResolve = resolve;
189
			this._setTreeInput(input);
190 191

			// refresh when check state changes
192
			this._sessionDisposables.add(input.checked.onDidChange(() => {
193
				this._tree.updateChildren();
J
Johannes Rieken 已提交
194
				this._ctxHasCheckedChanges.set(input.checked.checkedCount > 0);
195
			}));
196 197 198
		});
	}

199 200 201 202 203 204 205 206 207 208
	private async _setTreeInput(input: BulkFileOperations) {

		const viewState = this._treeViewStates.get(this._treeDataSource.groupByFile);
		await this._tree.setInput(input, viewState);
		this._tree.domFocus();

		if (viewState) {
			return;
		}

J
Johannes Rieken 已提交
209 210
		// async expandAll (max=10) is the default when no view state is given
		const expand = [...this._tree.getNode(input).children].slice(0, 10);
211
		while (expand.length > 0) {
J
Johannes Rieken 已提交
212
			const { element } = expand.shift()!;
213 214 215
			if (element instanceof FileElement) {
				await this._tree.expand(element, true);
			}
J
Johannes Rieken 已提交
216
			if (element instanceof CategoryElement) {
217 218 219 220 221 222
				await this._tree.expand(element, true);
				expand.push(...this._tree.getNode(element).children);
			}
		}
	}

223
	accept(): void {
224 225 226

		const conflicts = this._currentInput?.conflicts.list();

J
Johannes Rieken 已提交
227
		if (!conflicts || conflicts.length === 0) {
228
			this._done(true);
J
Johannes Rieken 已提交
229
			return;
230 231 232
		}

		let message: string;
J
Johannes Rieken 已提交
233 234
		if (conflicts.length === 1) {
			message = localize('conflict.1', "Cannot apply refactoring because '{0}' has changed in the meantime.", this._labelService.getUriLabel(conflicts[0], { relative: true }));
235
		} else {
J
Johannes Rieken 已提交
236
			message = localize('conflict.N', "Cannot apply refactoring because {0} other files have changed in the meantime.", conflicts.length);
237 238 239
		}

		this._dialogService.show(Severity.Warning, message, []).finally(() => this._done(false));
240 241 242 243 244 245 246 247
	}

	discard() {
		this._done(false);
	}

	toggleChecked() {
		const [first] = this._tree.getFocus();
248 249
		if ((first instanceof FileElement || first instanceof TextEditElement) && !first.isDisabled()) {
			first.setChecked(!first.isChecked());
250 251 252
		}
	}

253 254 255 256 257 258 259 260 261 262 263 264
	groupByFile(): void {
		if (!this._treeDataSource.groupByFile) {
			this.toggleGrouping();
		}
	}

	groupByType(): void {
		if (this._treeDataSource.groupByFile) {
			this.toggleGrouping();
		}
	}

265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
	toggleGrouping() {
		const input = this._tree.getInput();
		if (input) {

			// (1) capture view state
			let oldViewState = this._tree.getViewState();
			this._treeViewStates.set(this._treeDataSource.groupByFile, oldViewState);

			// (2) toggle and update
			this._treeDataSource.groupByFile = !this._treeDataSource.groupByFile;
			this._setTreeInput(input);

			// (3) remember preference
			this._storageService.store(BulkEditPane._memGroupByFile, this._treeDataSource.groupByFile, StorageScope.GLOBAL);
			this._ctxGroupByFile.set(this._treeDataSource.groupByFile);
		}
	}

283 284
	private _done(accept: boolean): void {
		if (this._currentResolve) {
285
			this._currentResolve(accept ? this._currentInput?.getWorkspaceEdit() : undefined);
286
			this._currentInput = undefined;
287
		}
288 289
		this._setState(State.Message);
		this._sessionDisposables.clear();
290 291
	}

292
	private async _openElementAsEditor(e: IOpenEvent<BulkEditElement | null>): Promise<void> {
293 294 295 296 297 298 299 300
		type Mutable<T> = {
			-readonly [P in keyof T]: T[P]
		};

		let options: Mutable<ITextEditorOptions> = { ...e.editorOptions };
		let fileElement: FileElement;
		if (e.element instanceof TextEditElement) {
			fileElement = e.element.parent;
301
			options.selection = e.element.edit.textEdit.edit.range;
J
Johannes Rieken 已提交
302

303 304
		} else if (e.element instanceof FileElement) {
			fileElement = e.element;
305
			options.selection = e.element.edit.textEdits[0]?.textEdit.edit.range;
306 307 308 309

		} else {
			// invalid event
			return;
310 311
		}

312 313 314 315 316 317 318 319 320 321 322 323 324
		const previewUri = BulkEditPreviewProvider.asPreviewUri(fileElement.edit.uri);

		if (fileElement.edit.type & BulkFileOperationType.Delete) {
			// delete -> show single editor
			this._editorService.openEditor({
				label: localize('edt.title.del', "{0} (delete, refactor preview)", basename(fileElement.edit.uri)),
				resource: previewUri,
				options
			});

		} else {
			// rename, create, edits -> show diff editr
			let leftResource: URI | undefined;
J
Johannes Rieken 已提交
325
			try {
J
Johannes Rieken 已提交
326 327
				(await this._textModelService.createModelReference(fileElement.edit.uri)).dispose();
				leftResource = fileElement.edit.uri;
J
Johannes Rieken 已提交
328 329
			} catch {
				leftResource = BulkEditPreviewProvider.emptyPreview;
J
Johannes Rieken 已提交
330
			}
J
Johannes Rieken 已提交
331 332 333 334 335 336

			let typeLabel: string | undefined;
			if (fileElement.edit.type & BulkFileOperationType.Rename) {
				typeLabel = localize('rename', "rename");
			} else if (fileElement.edit.type & BulkFileOperationType.Create) {
				typeLabel = localize('create', "create");
337 338 339 340 341 342 343
			}

			let label: string;
			if (typeLabel) {
				label = localize('edt.title.2', "{0} ({1}, refactor preview)", basename(fileElement.edit.uri), typeLabel);
			} else {
				label = localize('edt.title.1', "{0} (refactor preview)", basename(fileElement.edit.uri));
J
Johannes Rieken 已提交
344
			}
J
Johannes Rieken 已提交
345

J
Johannes Rieken 已提交
346
			this._editorService.openEditor({
347 348 349
				leftResource,
				rightResource: previewUri,
				label,
350
				options
J
Johannes Rieken 已提交
351 352
			});
		}
353
	}
354 355

	private _onContextMenu(e: ITreeContextMenuEvent<any>): void {
356
		const menu = this._menuService.createMenu(MenuId.BulkEditContext, this._contextKeyService);
357 358 359 360 361
		const actions: IAction[] = [];
		const disposable = createAndFillInContextMenuActions(menu, undefined, actions, this._contextMenuService);

		this._contextMenuService.showContextMenu({
			getActions: () => actions,
J
Johannes Rieken 已提交
362
			getAnchor: () => e.anchor,
363 364 365 366 367 368
			onHide: () => {
				disposable.dispose();
				menu.dispose();
			}
		});
	}
369 370 371 372 373 374 375 376 377 378 379 380 381
}

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {

	const diffInsertedColor = theme.getColor(diffInserted);
	if (diffInsertedColor) {
		collector.addRule(`.monaco-workbench .bulk-edit-panel .highlight.insert { background-color: ${diffInsertedColor}; }`);
	}
	const diffRemovedColor = theme.getColor(diffRemoved);
	if (diffRemovedColor) {
		collector.addRule(`.monaco-workbench .bulk-edit-panel .highlight.remove { background-color: ${diffRemovedColor}; }`);
	}
});