editorService.ts 46.9 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 * as nls from 'vs/nls';
7
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
8
import { IResourceEditorInput, ITextEditorOptions, IEditorOptions, EditorActivation } from 'vs/platform/editor/common/editor';
9
import { SideBySideEditor, IEditorInput, IEditorPane, GroupIdentifier, IFileEditorInput, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditorPane, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, isTextEditorPane, IWorkbenchEditorConfiguration, toResource, IVisibleEditorPane } from 'vs/workbench/common/editor';
10 11 12
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { Registry } from 'vs/platform/registry/common/platform';
import { ResourceMap } from 'vs/base/common/map';
13
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
14
import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, FileChangeType, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
15
import { Schemas } from 'vs/base/common/network';
J
Joao Moreno 已提交
16
import { Event, Emitter } from 'vs/base/common/event';
17
import { URI } from 'vs/base/common/uri';
18
import { basename, isEqualOrParent, joinPath } from 'vs/base/common/resources';
19
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
B
Benjamin Pasero 已提交
20
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService';
21
import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorOverrideEntry, ICustomEditorViewTypesHandler, ICustomEditorInfo } from 'vs/workbench/services/editor/common/editorService';
22
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
23
import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
B
Benjamin Pasero 已提交
24
import { coalesce, distinct, insert } from 'vs/base/common/arrays';
25
import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
26
import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
I
isidor 已提交
27
import { ILabelService } from 'vs/platform/label/common/label';
28
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
29
import { withNullAsUndefined } from 'vs/base/common/types';
30
import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver';
31
import { IEditorViewState } from 'vs/editor/common/editorCommon';
32 33
import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput';
34
import { timeout } from 'vs/base/common/async';
35
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
36
import { indexOfPath } from 'vs/base/common/extpath';
37
import { DEFAULT_CUSTOM_EDITOR, updateViewTypeSchema, editorAssociationsConfigurationNode } from 'vs/workbench/services/editor/common/editorAssociationsSetting';
38
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
39

40
type CachedEditorInput = ResourceEditorInput | IFileEditorInput | UntitledTextEditorInput;
B
Benjamin Pasero 已提交
41
type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE;
42

B
Benjamin Pasero 已提交
43
export class EditorService extends Disposable implements EditorServiceImpl {
44

45
	_serviceBrand: undefined;
46

47 48
	//#region events

49 50
	private readonly _onDidActiveEditorChange = this._register(new Emitter<void>());
	readonly onDidActiveEditorChange = this._onDidActiveEditorChange.event;
51

52 53
	private readonly _onDidVisibleEditorsChange = this._register(new Emitter<void>());
	readonly onDidVisibleEditorsChange = this._onDidVisibleEditorsChange.event;
54

55 56
	private readonly _onDidCloseEditor = this._register(new Emitter<IEditorCloseEvent>());
	readonly onDidCloseEditor = this._onDidCloseEditor.event;
57

58 59
	private readonly _onDidOpenEditorFail = this._register(new Emitter<IEditorIdentifier>());
	readonly onDidOpenEditorFail = this._onDidOpenEditorFail.event;
60

61 62 63
	private readonly _onDidMostRecentlyActiveEditorsChange = this._register(new Emitter<void>());
	readonly onDidMostRecentlyActiveEditorsChange = this._onDidMostRecentlyActiveEditorsChange.event;

64 65
	//#endregion

66
	constructor(
67
		@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
68
		@IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService,
69 70 71
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@ILabelService private readonly labelService: ILabelService,
		@IFileService private readonly fileService: IFileService,
72
		@IConfigurationService private readonly configurationService: IConfigurationService,
73
		@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
74
	) {
75 76
		super();

77 78
		this.onConfigurationUpdated(configurationService.getValue<IWorkbenchEditorConfiguration>());

79 80 81 82
		this.registerListeners();
	}

	private registerListeners(): void {
83 84

		// Editor & group changes
85
		this.editorGroupService.whenRestored.then(() => this.onEditorsRestored());
86
		this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group));
87
		this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
88
		this.editorsObserver.onDidMostRecentlyActiveEditorsChange(() => this._onDidMostRecentlyActiveEditorsChange.fire());
89

90 91 92
		// Out of workspace file watchers
		this._register(this.onDidVisibleEditorsChange(() => this.handleVisibleEditorsChange()));

93
		// File changes & operations
94 95 96 97
		// Note: there is some duplication with the two file event handlers- Since we cannot always rely on the disk events
		// carrying all necessary data in all environments, we also use the file operation events to make sure operations are handled.
		// In any case there is no guarantee if the local event is fired first or the disk one. Thus, code must handle the case
		// that the event ordering is random as well as might not carry all information needed.
98 99 100 101 102
		this._register(this.fileService.onDidRunOperation(e => this.onDidRunFileOperation(e)));
		this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e)));

		// Configuration
		this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue<IWorkbenchEditorConfiguration>())));
103 104
	}

105 106 107 108
	//#region Editor & group event handlers

	private lastActiveEditor: IEditorInput | undefined = undefined;

109 110 111 112 113 114 115
	private onEditorsRestored(): void {

		// Register listeners to each opened group
		this.editorGroupService.groups.forEach(group => this.registerGroupListeners(group as IEditorGroupView));

		// Fire initial set of editor events if there is an active editor
		if (this.activeEditor) {
116
			this.doHandleActiveEditorChangeEvent();
117 118 119 120
			this._onDidVisibleEditorsChange.fire();
		}
	}

121
	private handleActiveEditorChange(group: IEditorGroup): void {
122
		if (group !== this.editorGroupService.activeGroup) {
123
			return; // ignore if not the active group
124 125 126 127 128 129
		}

		if (!this.lastActiveEditor && !group.activeEditor) {
			return; // ignore if we still have no active editor
		}

130
		this.doHandleActiveEditorChangeEvent();
131 132
	}

133
	private doHandleActiveEditorChangeEvent(): void {
134

135 136
		// Remember as last active
		const activeGroup = this.editorGroupService.activeGroup;
B
Benjamin Pasero 已提交
137
		this.lastActiveEditor = withNullAsUndefined(activeGroup.activeEditor);
138

139
		// Fire event to outside parties
140 141 142
		this._onDidActiveEditorChange.fire();
	}

143
	private registerGroupListeners(group: IEditorGroupView): void {
B
Benjamin Pasero 已提交
144
		const groupDisposables = new DisposableStore();
145

B
Benjamin Pasero 已提交
146
		groupDisposables.add(group.onDidGroupChange(e => {
147 148 149 150
			if (e.kind === GroupChangeKind.EDITOR_ACTIVE) {
				this.handleActiveEditorChange(group);
				this._onDidVisibleEditorsChange.fire();
			}
151 152
		}));

B
Benjamin Pasero 已提交
153
		groupDisposables.add(group.onDidCloseEditor(event => {
B
Benjamin Pasero 已提交
154
			this._onDidCloseEditor.fire(event);
155 156
		}));

B
Benjamin Pasero 已提交
157
		groupDisposables.add(group.onWillOpenEditor(event => {
158
			this.onGroupWillOpenEditor(group, event);
159 160
		}));

B
Benjamin Pasero 已提交
161
		groupDisposables.add(group.onDidOpenEditorFail(editor => {
I
isidor 已提交
162
			this._onDidOpenEditorFail.fire({ editor, groupId: group.id });
163 164
		}));

J
Joao Moreno 已提交
165
		Event.once(group.onWillDispose)(() => {
B
Benjamin Pasero 已提交
166
			dispose(groupDisposables);
167 168 169
		});
	}

170
	//#endregion
171

172 173 174 175 176 177 178 179 180
	//#region Visible Editors Change: Install file watchers for out of workspace resources that became visible

	private readonly activeOutOfWorkspaceWatchers = new ResourceMap<IDisposable>();

	private handleVisibleEditorsChange(): void {
		const visibleOutOfWorkspaceResources = new ResourceMap<URI>();

		for (const editor of this.visibleEditors) {
			const resources = distinct(coalesce([
181 182
				toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }),
				toResource(editor, { supportSideBySide: SideBySideEditor.DETAILS })
183 184 185 186 187 188
			]), resource => resource.toString());

			for (const resource of resources) {
				if (this.fileService.canHandleResource(resource) && !this.contextService.isInsideWorkspace(resource)) {
					visibleOutOfWorkspaceResources.set(resource, resource);
				}
189 190
			}
		}
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206

		// Handle no longer visible out of workspace resources
		this.activeOutOfWorkspaceWatchers.keys().forEach(resource => {
			if (!visibleOutOfWorkspaceResources.get(resource)) {
				dispose(this.activeOutOfWorkspaceWatchers.get(resource));
				this.activeOutOfWorkspaceWatchers.delete(resource);
			}
		});

		// Handle newly visible out of workspace resources
		visibleOutOfWorkspaceResources.forEach(resource => {
			if (!this.activeOutOfWorkspaceWatchers.get(resource)) {
				const disposable = this.fileService.watch(resource);
				this.activeOutOfWorkspaceWatchers.set(resource, disposable);
			}
		});
207 208
	}

209 210
	//#endregion

211
	//#region File Changes: Move & Deletes to move or close opend editors
212 213 214 215 216 217 218 219 220 221 222 223 224 225

	private onDidRunFileOperation(e: FileOperationEvent): void {

		// Handle moves specially when file is opened
		if (e.isOperation(FileOperation.MOVE)) {
			this.handleMovedFile(e.resource, e.target.resource);
		}

		// Handle deletes
		if (e.isOperation(FileOperation.DELETE) || e.isOperation(FileOperation.MOVE)) {
			this.handleDeletedFile(e.resource, false, e.target ? e.target.resource : undefined);
		}
	}

226 227 228 229 230 231 232
	private onDidFilesChange(e: FileChangesEvent): void {
		if (e.gotDeleted()) {
			this.handleDeletedFile(e, true);
		}
	}

	private handleMovedFile(source: URI, target: URI): void {
233 234 235 236 237
		for (const group of this.editorGroupService.groups) {
			let replacements: (IResourceEditorReplacement | IEditorReplacement)[] = [];

			for (const editor of group.editors) {
				const resource = editor.resource;
238
				if (!resource || !isEqualOrParent(resource, source)) {
239 240 241 242 243
					continue; // not matching our resource
				}

				// Determine new resulting target resource
				let targetResource: URI;
244 245
				if (source.toString() === resource.toString()) {
					targetResource = target; // file got moved
246 247
				} else {
					const ignoreCase = !this.fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive);
248 249
					const index = indexOfPath(resource.path, source.path, ignoreCase);
					targetResource = joinPath(target, resource.path.substr(index + source.path.length + 1)); // parent folder got moved
250 251 252 253 254 255 256 257
				}

				// Delegate move() to editor instance
				const moveResult = editor.move(group.id, targetResource);
				if (!moveResult) {
					return; // not target - ignore
				}

258
				const optionOverrides = {
259 260 261
					preserveFocus: true,
					pinned: group.isPinned(editor),
					index: group.getIndexOfEditor(editor),
262
					inactive: !group.isActive(editor)
263 264 265 266 267 268 269 270 271
				};

				// Construct a replacement with our extra options mixed in
				if (moveResult.editor instanceof EditorInput) {
					replacements.push({
						editor,
						replacement: moveResult.editor,
						options: {
							...moveResult.options,
272
							...optionOverrides
273 274 275 276 277 278 279 280
						}
					});
				} else {
					replacements.push({
						editor: { resource: editor.resource },
						replacement: {
							...moveResult.editor,
							options: {
B
Benjamin Pasero 已提交
281
								...moveResult.editor.options,
282
								...optionOverrides
283 284 285 286 287 288 289 290 291 292 293 294 295
							}
						}
					});
				}
			}

			// Apply replacements
			if (replacements.length) {
				this.replaceEditors(replacements, group);
			}
		}
	}

296
	private closeOnFileDelete: boolean = false;
297
	private fileEditorInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileEditorInputFactory();
298 299 300 301 302 303 304 305
	private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void {
		if (typeof configuration.workbench?.editor?.closeOnFileDelete === 'boolean') {
			this.closeOnFileDelete = configuration.workbench.editor.closeOnFileDelete;
		} else {
			this.closeOnFileDelete = false; // default
		}
	}

306
	private handleDeletedFile(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void {
307 308 309 310 311 312 313 314 315 316 317
		for (const editor of this.getAllNonDirtyEditors({ includeUntitled: false, supportSideBySide: true })) {
			(async () => {
				const resource = editor.resource;
				if (!resource) {
					return;
				}

				// Handle deletes in opened editors depending on:
				// - the user has not disabled the setting closeOnFileDelete
				// - the file change is local
				// - the input is  a file that is not resolved (we need to dispose because we cannot restore otherwise since we do not have the contents)
318
				if (this.closeOnFileDelete || !isExternal || (this.fileEditorInputFactory.isFileEditorInput(editor) && !editor.isResolved())) {
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386

					// Do NOT close any opened editor that matches the resource path (either equal or being parent) of the
					// resource we move to (movedTo). Otherwise we would close a resource that has been renamed to the same
					// path but different casing.
					if (movedTo && isEqualOrParent(resource, movedTo)) {
						return;
					}

					let matches = false;
					if (arg1 instanceof FileChangesEvent) {
						matches = arg1.contains(resource, FileChangeType.DELETED);
					} else {
						matches = isEqualOrParent(resource, arg1);
					}

					if (!matches) {
						return;
					}

					// We have received reports of users seeing delete events even though the file still
					// exists (network shares issue: https://github.com/Microsoft/vscode/issues/13665).
					// Since we do not want to close an editor without reason, we have to check if the
					// file is really gone and not just a faulty file event.
					// This only applies to external file events, so we need to check for the isExternal
					// flag.
					let exists = false;
					if (isExternal && this.fileService.canHandleResource(resource)) {
						await timeout(100);
						exists = await this.fileService.exists(resource);
					}

					if (!exists && !editor.isDisposed()) {
						editor.dispose();
					}
				}
			})();
		}
	}

	private getAllNonDirtyEditors(options: { includeUntitled: boolean, supportSideBySide: boolean }): IEditorInput[] {
		const editors: IEditorInput[] = [];

		function conditionallyAddEditor(editor: IEditorInput): void {
			if (editor.isUntitled() && !options.includeUntitled) {
				return;
			}

			if (editor.isDirty()) {
				return;
			}

			editors.push(editor);
		}

		for (const editor of this.editors) {
			if (options.supportSideBySide && editor instanceof SideBySideEditorInput) {
				conditionallyAddEditor(editor.master);
				conditionallyAddEditor(editor.details);
			} else {
				conditionallyAddEditor(editor);
			}
		}

		return editors;
	}

	//#endregion

387 388 389 390
	//#region Editor accessors

	private readonly editorsObserver = this._register(this.instantiationService.createInstance(EditorsObserver));

391 392
	get activeEditorPane(): IVisibleEditorPane | undefined {
		return this.editorGroupService.activeGroup?.activeEditorPane;
393 394
	}

395 396 397 398 399 400
	get activeTextEditorControl(): ICodeEditor | IDiffEditor | undefined {
		const activeEditorPane = this.activeEditorPane;
		if (activeEditorPane) {
			const activeControl = activeEditorPane.getControl();
			if (isCodeEditor(activeControl) || isDiffEditor(activeControl)) {
				return activeControl;
401
			}
402
			if (isCompositeEditor(activeControl) && isCodeEditor(activeControl.activeCodeEditor)) {
403 404
				return activeControl.activeCodeEditor;
			}
405 406
		}

R
Rob Lourens 已提交
407
		return undefined;
408 409
	}

410 411 412
	get activeTextEditorMode(): string | undefined {
		let activeCodeEditor: ICodeEditor | undefined = undefined;

413 414 415
		const activeTextEditorControl = this.activeTextEditorControl;
		if (isDiffEditor(activeTextEditorControl)) {
			activeCodeEditor = activeTextEditorControl.getModifiedEditor();
416
		} else {
417
			activeCodeEditor = activeTextEditorControl;
418 419 420 421 422
		}

		return activeCodeEditor?.getModel()?.getLanguageIdentifier().language;
	}

B
Benjamin Pasero 已提交
423 424 425 426
	get count(): number {
		return this.editorsObserver.count;
	}

427
	get editors(): IEditorInput[] {
B
Benjamin Pasero 已提交
428 429 430 431 432 433 434 435 436 437 438 439
		return this.getEditors(EditorsOrder.SEQUENTIAL).map(({ editor }) => editor);
	}

	getEditors(order: EditorsOrder): ReadonlyArray<IEditorIdentifier> {
		if (order === EditorsOrder.MOST_RECENTLY_ACTIVE) {
			return this.editorsObserver.editors;
		}

		const editors: IEditorIdentifier[] = [];

		this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(group => {
			editors.push(...group.getEditors(EditorsOrder.SEQUENTIAL).map(editor => ({ editor, groupId: group.id })));
440 441 442 443 444
		});

		return editors;
	}

445
	get activeEditor(): IEditorInput | undefined {
446
		const activeGroup = this.editorGroupService.activeGroup;
447

M
Matt Bierner 已提交
448
		return activeGroup ? withNullAsUndefined(activeGroup.activeEditor) : undefined;
449 450
	}

451 452
	get visibleEditorPanes(): IVisibleEditorPane[] {
		return coalesce(this.editorGroupService.groups.map(group => group.activeEditorPane));
453 454
	}

455
	get visibleTextEditorControls(): Array<ICodeEditor | IDiffEditor> {
456 457 458 459 460 461 462 463 464
		const visibleTextEditorControls: Array<ICodeEditor | IDiffEditor> = [];
		for (const visibleEditorPane of this.visibleEditorPanes) {
			const control = visibleEditorPane.getControl();
			if (isCodeEditor(control) || isDiffEditor(control)) {
				visibleTextEditorControls.push(control);
			}
		}

		return visibleTextEditorControls;
465 466
	}

467
	get visibleEditors(): IEditorInput[] {
468
		return coalesce(this.editorGroupService.groups.map(group => group.activeEditor));
469 470
	}

471 472
	//#endregion

473 474
	//#region preventOpenEditor()

475 476
	private readonly openEditorHandlers: IOpenEditorOverrideHandler[] = [];

477
	overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable {
B
Benjamin Pasero 已提交
478
		const remove = insert(this.openEditorHandlers, handler);
479

B
Benjamin Pasero 已提交
480
		return toDisposable(() => remove());
481 482
	}

R
rebornix 已提交
483 484 485 486 487 488 489 490 491 492
	getEditorOverrides(editorInput: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup | undefined): [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry][] {
		const ret = [];
		for (const handler of this.openEditorHandlers) {
			const handlers = handler.getEditorOverrides ? handler.getEditorOverrides(editorInput, options, group).map(val => { return [handler, val] as [IOpenEditorOverrideHandler, IOpenEditorOverrideEntry]; }) : [];
			ret.push(...handlers);
		}

		return ret;
	}

493 494 495 496 497 498
	private onGroupWillOpenEditor(group: IEditorGroup, event: IEditorOpeningEvent): void {
		if (event.options && event.options.ignoreOverrides) {
			return;
		}

		for (const handler of this.openEditorHandlers) {
R
rebornix 已提交
499
			const result = handler.open(event.editor, event.options, group);
500 501 502 503 504 505 506 507
			const override = result?.override;
			if (override) {
				event.prevent((() => override.then(editor => withNullAsUndefined(editor))));
				break;
			}
		}
	}

508 509
	//#endregion

510 511
	//#region openEditor()

512
	openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise<IEditorPane | undefined>;
513 514
	openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: OpenInEditorGroup): Promise<ITextEditorPane | undefined>;
	openEditor(editor: IResourceDiffEditorInput, group?: OpenInEditorGroup): Promise<ITextDiffEditorPane | undefined>;
515
	async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise<IEditorPane | undefined> {
516 517 518 519 520 521 522 523 524 525
		const result = this.doResolveEditorOpenRequest(editor, optionsOrGroup, group);
		if (result) {
			const [resolvedGroup, resolvedEditor, resolvedOptions] = result;

			return withNullAsUndefined(await resolvedGroup.openEditor(resolvedEditor, resolvedOptions));
		}

		return undefined;
	}

526
	doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): [IEditorGroup, EditorInput, EditorOptions | undefined] | undefined {
B
Benjamin Pasero 已提交
527 528 529
		let resolvedGroup: IEditorGroup | undefined;
		let candidateGroup: OpenInEditorGroup | undefined;

B
Benjamin Pasero 已提交
530 531
		let typedEditor: EditorInput | undefined;
		let typedOptions: EditorOptions | undefined;
532 533 534

		// Typed Editor Support
		if (editor instanceof EditorInput) {
B
Benjamin Pasero 已提交
535 536
			typedEditor = editor;
			typedOptions = this.toOptions(optionsOrGroup as IEditorOptions);
537

B
Benjamin Pasero 已提交
538 539
			candidateGroup = group;
			resolvedGroup = this.findTargetGroup(typedEditor, typedOptions, candidateGroup);
540 541 542
		}

		// Untyped Text Editor Support
B
Benjamin Pasero 已提交
543
		else {
544
			const textInput = <IResourceEditorInputType>editor;
545
			typedEditor = this.createEditorInput(textInput);
B
Benjamin Pasero 已提交
546 547
			if (typedEditor) {
				typedOptions = TextEditorOptions.from(textInput);
548

B
Benjamin Pasero 已提交
549 550 551 552 553 554
				candidateGroup = optionsOrGroup as OpenInEditorGroup;
				resolvedGroup = this.findTargetGroup(typedEditor, typedOptions, candidateGroup);
			}
		}

		if (typedEditor && resolvedGroup) {
B
Benjamin Pasero 已提交
555
			if (
556 557 558
				this.editorGroupService.activeGroup !== resolvedGroup && 	// only if target group is not already active
				typedOptions && !typedOptions.inactive &&					// never for inactive editors
				typedOptions.preserveFocus &&								// only if preserveFocus
559
				typeof typedOptions.activation !== 'number' &&				// only if activation is not already defined (either true or false)
560
				candidateGroup !== SIDE_GROUP								// never for the SIDE_GROUP
B
Benjamin Pasero 已提交
561
			) {
562 563 564
				// If the resolved group is not the active one, we typically
				// want the group to become active. There are a few cases
				// where we stay away from encorcing this, e.g. if the caller
565
				// is already providing `activation`.
566 567 568 569 570
				//
				// Specifically for historic reasons we do not activate a
				// group is it is opened as `SIDE_GROUP` with `preserveFocus:true`.
				// repeated Alt-clicking of files in the explorer always open
				// into the same side group and not cause a group to be created each time.
571
				typedOptions.overwrite({ activation: EditorActivation.ACTIVATE });
B
Benjamin Pasero 已提交
572
			}
B
Benjamin Pasero 已提交
573

574
			return [resolvedGroup, typedEditor, typedOptions];
575 576
		}

B
Benjamin Pasero 已提交
577
		return undefined;
578
	}
579

B
Benjamin Pasero 已提交
580
	private findTargetGroup(input: IEditorInput, options?: IEditorOptions, group?: OpenInEditorGroup): IEditorGroup {
581
		let targetGroup: IEditorGroup | undefined;
582

B
Benjamin Pasero 已提交
583 584
		// Group: Instance of Group
		if (group && typeof group !== 'number') {
B
Benjamin Pasero 已提交
585
			targetGroup = group;
B
Benjamin Pasero 已提交
586 587
		}

588
		// Group: Side by Side
B
Benjamin Pasero 已提交
589
		else if (group === SIDE_GROUP) {
590
			targetGroup = this.findSideBySideGroup();
591 592
		}

593
		// Group: Specific Group
B
Benjamin Pasero 已提交
594
		else if (typeof group === 'number' && group >= 0) {
595
			targetGroup = this.editorGroupService.getGroup(group);
596 597
		}

598 599
		// Group: Unspecified without a specific index to open
		else if (!options || typeof options.index !== 'number') {
600
			const groupsByLastActive = this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
601 602

			// Respect option to reveal an editor if it is already visible in any group
B
Benjamin Pasero 已提交
603
			if (options?.revealIfVisible) {
604
				for (const group of groupsByLastActive) {
B
Benjamin Pasero 已提交
605
					if (group.isActive(input)) {
606 607 608 609 610 611 612
						targetGroup = group;
						break;
					}
				}
			}

			// Respect option to reveal an editor if it is open (not necessarily visible)
B
Benjamin Pasero 已提交
613
			// Still prefer to reveal an editor in a group where the editor is active though.
B
Benjamin Pasero 已提交
614
			if (!targetGroup) {
B
Benjamin Pasero 已提交
615
				if (options?.revealIfOpened || this.configurationService.getValue<boolean>('workbench.editor.revealIfOpen')) {
B
Benjamin Pasero 已提交
616 617 618
					let groupWithInputActive: IEditorGroup | undefined = undefined;
					let groupWithInputOpened: IEditorGroup | undefined = undefined;

B
Benjamin Pasero 已提交
619
					for (const group of groupsByLastActive) {
B
Benjamin Pasero 已提交
620 621 622 623 624 625 626 627
						if (group.isOpened(input)) {
							if (!groupWithInputOpened) {
								groupWithInputOpened = group;
							}

							if (!groupWithInputActive && group.isActive(input)) {
								groupWithInputActive = group;
							}
B
Benjamin Pasero 已提交
628
						}
B
Benjamin Pasero 已提交
629 630 631

						if (groupWithInputOpened && groupWithInputActive) {
							break; // we found all groups we wanted
B
Benjamin Pasero 已提交
632
						}
633
					}
B
Benjamin Pasero 已提交
634 635 636

					// Prefer a target group where the input is visible
					targetGroup = groupWithInputActive || groupWithInputOpened;
637 638 639 640 641
				}
			}
		}

		// Fallback to active group if target not valid
642
		if (!targetGroup) {
643
			targetGroup = this.editorGroupService.activeGroup;
644 645
		}

646 647 648
		return targetGroup;
	}

649
	private findSideBySideGroup(): IEditorGroup {
650
		const direction = preferredSideBySideGroupDirection(this.configurationService);
651

652
		let neighbourGroup = this.editorGroupService.findGroup({ direction });
653
		if (!neighbourGroup) {
654
			neighbourGroup = this.editorGroupService.addGroup(this.editorGroupService.activeGroup, direction);
655 656 657
		}

		return neighbourGroup;
658 659
	}

B
Benjamin Pasero 已提交
660
	private toOptions(options?: IEditorOptions | ITextEditorOptions | EditorOptions): EditorOptions {
661 662 663 664 665
		if (!options || options instanceof EditorOptions) {
			return options as EditorOptions;
		}

		const textOptions: ITextEditorOptions = options;
B
Benjamin Pasero 已提交
666
		if (textOptions.selection || textOptions.viewState) {
667 668 669 670 671 672
			return TextEditorOptions.create(options);
		}

		return EditorOptions.create(options);
	}

673 674
	//#endregion

675 676
	//#region openEditors()

677
	openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise<IEditorPane[]>;
678 679
	openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise<IEditorPane[]>;
	async openEditors(editors: Array<IEditorInputWithOptions | IResourceEditorInputType>, group?: OpenInEditorGroup): Promise<IEditorPane[]> {
680 681 682 683 684 685 686

		// Convert to typed editors and options
		const typedEditors: IEditorInputWithOptions[] = [];
		editors.forEach(editor => {
			if (isEditorInputWithOptions(editor)) {
				typedEditors.push(editor);
			} else {
687
				typedEditors.push({ editor: this.createEditorInput(editor), options: TextEditorOptions.from(editor) });
688 689 690 691
			}
		});

		// Find target groups to open
692
		const mapGroupToEditors = new Map<IEditorGroup, IEditorInputWithOptions[]>();
693
		if (group === SIDE_GROUP) {
694
			mapGroupToEditors.set(this.findSideBySideGroup(), typedEditors);
695 696 697 698 699 700 701 702 703 704 705 706 707 708
		} else {
			typedEditors.forEach(typedEditor => {
				const targetGroup = this.findTargetGroup(typedEditor.editor, typedEditor.options, group);

				let targetGroupEditors = mapGroupToEditors.get(targetGroup);
				if (!targetGroupEditors) {
					targetGroupEditors = [];
					mapGroupToEditors.set(targetGroup, targetGroupEditors);
				}

				targetGroupEditors.push(typedEditor);
			});
		}

B
Benjamin Pasero 已提交
709
		// Open in target groups
710
		const result: Promise<IEditorPane | null>[] = [];
711
		mapGroupToEditors.forEach((editorsWithOptions, group) => {
712
			result.push(group.openEditors(editorsWithOptions));
713
		});
714

B
Benjamin Pasero 已提交
715
		return coalesce(await Promise.all(result));
716 717 718 719
	}

	//#endregion

720 721
	//#region isOpen()

722
	isOpen(editor: IEditorInput): boolean;
723 724
	isOpen(editor: IResourceEditorInput): boolean;
	isOpen(editor: IEditorInput | IResourceEditorInput): boolean {
725 726 727 728 729 730 731 732 733
		if (editor instanceof EditorInput) {
			return this.editorGroupService.groups.some(group => group.isOpened(editor));
		}

		if (editor.resource) {
			return this.editorsObserver.hasEditor(editor.resource);
		}

		return false;
734 735 736 737
	}

	//#endregion

738 739
	//#region replaceEditors()

J
Johannes Rieken 已提交
740 741
	replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
	replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
742
	replaceEditors(editors: Array<IEditorReplacement | IResourceEditorReplacement>, group: IEditorGroup | GroupIdentifier): Promise<void> {
743 744 745 746
		const typedEditors: IEditorReplacement[] = [];

		editors.forEach(replaceEditorArg => {
			if (replaceEditorArg.editor instanceof EditorInput) {
B
Benjamin Pasero 已提交
747 748 749 750 751 752 753
				const replacementArg = replaceEditorArg as IEditorReplacement;

				typedEditors.push({
					editor: replacementArg.editor,
					replacement: replacementArg.replacement,
					options: this.toOptions(replacementArg.options)
				});
754
			} else {
B
Benjamin Pasero 已提交
755
				const replacementArg = replaceEditorArg as IResourceEditorReplacement;
756 757

				typedEditors.push({
758 759
					editor: this.createEditorInput(replacementArg.editor),
					replacement: this.createEditorInput(replacementArg.replacement),
B
Benjamin Pasero 已提交
760
					options: this.toOptions(replacementArg.replacement.options)
761 762 763 764
				});
			}
		});

765
		const targetGroup = typeof group === 'number' ? this.editorGroupService.getGroup(group) : group;
B
Benjamin Pasero 已提交
766 767 768 769 770
		if (targetGroup) {
			return targetGroup.replaceEditors(typedEditors);
		}

		return Promise.resolve();
771 772 773 774
	}

	//#endregion

775 776 777
	//#region invokeWithinEditorContext()

	invokeWithinEditorContext<T>(fn: (accessor: ServicesAccessor) => T): T {
778 779 780
		const activeTextEditorControl = this.activeTextEditorControl;
		if (isCodeEditor(activeTextEditorControl)) {
			return activeTextEditorControl.invokeWithinContext(fn);
781 782
		}

783
		const activeGroup = this.editorGroupService.activeGroup;
784 785 786 787 788
		if (activeGroup) {
			return activeGroup.invokeWithinContext(fn);
		}

		return this.instantiationService.invokeFunction(fn);
789 790 791 792
	}

	//#endregion

793
	//#region createEditorInput()
794

795 796
	private readonly editorInputCache = new ResourceMap<CachedEditorInput>();

797
	createEditorInput(input: IEditorInputWithOptions | IEditorInput | IResourceEditorInputType): EditorInput {
798

799
		// Typed Editor Input Support (EditorInput)
800 801 802 803
		if (input instanceof EditorInput) {
			return input;
		}

804 805 806 807 808 809
		// Typed Editor Input Support (IEditorInputWithOptions)
		const editorInputWithOptions = input as IEditorInputWithOptions;
		if (editorInputWithOptions.editor instanceof EditorInput) {
			return editorInputWithOptions.editor;
		}

810
		// Diff Editor Support
811
		const resourceDiffInput = input as IResourceDiffEditorInput;
812
		if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) {
813 814
			const leftInput = this.createEditorInput({ resource: resourceDiffInput.leftResource, forceFile: resourceDiffInput.forceFile });
			const rightInput = this.createEditorInput({ resource: resourceDiffInput.rightResource, forceFile: resourceDiffInput.forceFile });
815

816 817 818 819 820 821
			return new DiffEditorInput(
				resourceDiffInput.label || this.toSideBySideLabel(leftInput, rightInput, ''),
				resourceDiffInput.description,
				leftInput,
				rightInput
			);
822 823 824
		}

		// Untitled file support
825
		const untitledInput = input as IUntitledTextResourceEditorInput;
826
		if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) {
B
Benjamin Pasero 已提交
827
			const untitledOptions = {
B
Benjamin Pasero 已提交
828 829 830
				mode: untitledInput.mode,
				initialValue: untitledInput.contents,
				encoding: untitledInput.encoding
B
Benjamin Pasero 已提交
831 832 833
			};

			// Untitled resource: use as hint for an existing untitled editor
834
			let untitledModel: IUntitledTextEditorModel;
B
Benjamin Pasero 已提交
835
			if (untitledInput.resource?.scheme === Schemas.untitled) {
836
				untitledModel = this.untitledTextEditorService.create({ untitledResource: untitledInput.resource, ...untitledOptions });
B
Benjamin Pasero 已提交
837 838 839 840
			}

			// Other resource: use as hint for associated filepath
			else {
841
				untitledModel = this.untitledTextEditorService.create({ associatedResource: untitledInput.resource, ...untitledOptions });
B
Benjamin Pasero 已提交
842
			}
843 844 845 846 847 848 849

			return this.createOrGetCached(untitledModel.resource, () => {

				// Factory function for new untitled editor
				const input = this.instantiationService.createInstance(UntitledTextEditorInput, untitledModel);

				// We dispose the untitled model once the editor
850 851 852 853
				// is being disposed. Even though we may have not
				// created the model initially, the lifecycle for
				// untitled is tightly coupled with the editor
				// lifecycle for now.
854 855 856 857
				Event.once(input.onDispose)(() => untitledModel.dispose());

				return input;
			}) as EditorInput;
858 859 860
		}

		// Resource Editor Support
861 862 863
		const resourceEditorInput = input as IResourceEditorInput;
		if (resourceEditorInput.resource instanceof URI) {
			let label = resourceEditorInput.label;
864
			if (!label) {
865
				label = basename(resourceEditorInput.resource); // derive the label from the path
866 867
			}

868
			return this.createOrGetCached(resourceEditorInput.resource, () => {
869

870
				// File
871 872
				if (resourceEditorInput.forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(resourceEditorInput.resource)) {
					return this.fileEditorInputFactory.createFileEditorInput(resourceEditorInput.resource, resourceEditorInput.encoding, resourceEditorInput.mode, this.instantiationService);
B
Benjamin Pasero 已提交
873 874
				}

875
				// Resource
876
				return this.instantiationService.createInstance(ResourceEditorInput, resourceEditorInput.label, resourceEditorInput.description, resourceEditorInput.resource, resourceEditorInput.mode);
877
			}, cachedInput => {
878

879 880 881
				// Untitled
				if (cachedInput instanceof UntitledTextEditorInput) {
					return;
B
Benjamin Pasero 已提交
882
				}
883

884 885
				// Files
				else if (!(cachedInput instanceof ResourceEditorInput)) {
886 887
					if (resourceEditorInput.encoding) {
						cachedInput.setPreferredEncoding(resourceEditorInput.encoding);
888 889
					}

890 891
					if (resourceEditorInput.mode) {
						cachedInput.setPreferredMode(resourceEditorInput.mode);
892
					}
893
				}
894

895 896 897 898 899
				// Resources
				else {
					if (label) {
						cachedInput.setName(label);
					}
900

901 902
					if (resourceEditorInput.description) {
						cachedInput.setDescription(resourceEditorInput.description);
903 904
					}

905 906
					if (resourceEditorInput.mode) {
						cachedInput.setPreferredMode(resourceEditorInput.mode);
907 908 909
					}
				}
			}) as EditorInput;
910 911
		}

912 913 914 915 916 917 918 919 920 921 922 923 924
		throw new Error('Unknown input type');
	}

	private createOrGetCached(resource: URI, factoryFn: () => CachedEditorInput, cachedFn?: (input: CachedEditorInput) => void): CachedEditorInput {

		// Return early if already cached
		let input = this.editorInputCache.get(resource);
		if (input) {
			if (cachedFn) {
				cachedFn(input);
			}

			return input;
925 926
		}

927 928 929 930
		// Otherwise create and add to cache
		input = factoryFn();
		this.editorInputCache.set(resource, input);
		Event.once(input.onDispose)(() => this.editorInputCache.delete(resource));
931 932 933 934

		return input;
	}

935
	private toSideBySideLabel(leftInput: EditorInput, rightInput: EditorInput, divider: string): string | undefined {
936 937
		const leftResource = leftInput.resource;
		const rightResource = rightInput.resource;
938 939 940

		// Without any resource, do not try to compute a label
		if (!leftResource || !rightResource) {
B
Benjamin Pasero 已提交
941
			return undefined;
B
Benjamin Pasero 已提交
942
		}
943

944 945 946
		// If both editors are file inputs, we produce an optimized label
		// by adding the relative path of both inputs to the label. This
		// makes it easier to understand a file-based comparison.
947
		if (this.fileEditorInputFactory.isFileEditorInput(leftInput) && this.fileEditorInputFactory.isFileEditorInput(rightInput)) {
948
			return `${this.labelService.getUriLabel(leftResource, { relative: true })} ${divider} ${this.labelService.getUriLabel(rightResource, { relative: true })}`;
949 950
		}

951 952
		// Signal back that the label should be computed from within the editor
		return undefined;
953
	}
954 955

	//#endregion
956

B
Benjamin Pasero 已提交
957
	//#region save/revert
958 959 960 961 962 963 964 965

	async save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean> {

		// Convert to array
		if (!Array.isArray(editors)) {
			editors = [editors];
		}

966 967 968 969
		// Make sure to not save the same editor multiple times
		// by using the `matches()` method to find duplicates
		const uniqueEditors = this.getUniqueEditors(editors);

970 971 972 973 974 975
		// Split editors up into a bucket that is saved in parallel
		// and sequentially. Unless "Save As", all non-untitled editors
		// can be saved in parallel to speed up the operation. Remaining
		// editors are potentially bringing up some UI and thus run
		// sequentially.
		const editorsToSaveParallel: IEditorIdentifier[] = [];
976
		const editorsToSaveSequentially: IEditorIdentifier[] = [];
977
		if (options?.saveAs) {
978
			editorsToSaveSequentially.push(...uniqueEditors);
979
		} else {
980
			for (const { groupId, editor } of uniqueEditors) {
981
				if (editor.isUntitled()) {
982
					editorsToSaveSequentially.push({ groupId, editor });
983 984 985 986 987 988 989
				} else {
					editorsToSaveParallel.push({ groupId, editor });
				}
			}
		}

		// Editors to save in parallel
990
		const saveResults = await Promise.all(editorsToSaveParallel.map(({ groupId, editor }) => {
991

992 993 994 995
			// Use save as a hint to pin the editor if used explicitly
			if (options?.reason === SaveReason.EXPLICIT) {
				this.editorGroupService.getGroup(groupId)?.pinEditor(editor);
			}
996 997

			// Save
998
			return editor.save(groupId, options);
999 1000 1001
		}));

		// Editors to save sequentially
1002
		for (const { groupId, editor } of editorsToSaveSequentially) {
1003
			if (editor.isDisposed()) {
1004
				continue; // might have been disposed from the save already
1005 1006
			}

1007 1008 1009 1010
			// Preserve view state by opening the editor first if the editor
			// is untitled or we "Save As". This also allows the user to review
			// the contents of the editor before making a decision.
			let viewState: IEditorViewState | undefined = undefined;
1011 1012 1013
			const editorPane = await this.openEditor(editor, undefined, groupId);
			if (isTextEditorPane(editorPane)) {
				viewState = editorPane.getViewState();
1014
			}
1015

1016
			const result = options?.saveAs ? await editor.saveAs(groupId, options) : await editor.save(groupId, options);
1017 1018
			saveResults.push(result);

1019
			if (!result) {
1020
				break; // failed or cancelled, abort
1021
			}
1022 1023 1024 1025 1026 1027 1028 1029 1030 1031

			// Replace editor preserving viewstate (either across all groups or
			// only selected group) if the resulting editor is different from the
			// current one.
			if (!result.matches(editor)) {
				const targetGroups = editor.isUntitled() ? this.editorGroupService.groups.map(group => group.id) /* untitled replaces across all groups */ : [groupId];
				for (const group of targetGroups) {
					await this.replaceEditors([{ editor, replacement: result, options: { pinned: true, viewState } }], group);
				}
			}
1032 1033
		}

1034
		return saveResults.every(result => !!result);
1035 1036 1037
	}

	saveAll(options?: ISaveAllEditorsOptions): Promise<boolean> {
B
Benjamin Pasero 已提交
1038
		return this.save(this.getAllDirtyEditors(options), options);
1039 1040
	}

1041
	async revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<void> {
B
Benjamin Pasero 已提交
1042 1043 1044 1045 1046 1047

		// Convert to array
		if (!Array.isArray(editors)) {
			editors = [editors];
		}

1048 1049 1050 1051 1052
		// Make sure to not revert the same editor multiple times
		// by using the `matches()` method to find duplicates
		const uniqueEditors = this.getUniqueEditors(editors);

		await Promise.all(uniqueEditors.map(async ({ groupId, editor }) => {
B
Benjamin Pasero 已提交
1053 1054 1055 1056

			// Use revert as a hint to pin the editor
			this.editorGroupService.getGroup(groupId)?.pinEditor(editor);

B
Benjamin Pasero 已提交
1057
			return editor.revert(groupId, options);
B
Benjamin Pasero 已提交
1058
		}));
1059 1060
	}

1061
	async revertAll(options?: IRevertAllEditorsOptions): Promise<void> {
B
Benjamin Pasero 已提交
1062
		return this.revert(this.getAllDirtyEditors(options), options);
B
Benjamin Pasero 已提交
1063 1064
	}

B
Benjamin Pasero 已提交
1065
	private getAllDirtyEditors(options?: IBaseSaveRevertAllEditorOptions): IEditorIdentifier[] {
B
Benjamin Pasero 已提交
1066 1067
		const editors: IEditorIdentifier[] = [];

1068 1069
		for (const group of this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
			for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) {
B
Benjamin Pasero 已提交
1070
				if (editor.isDirty() && (!editor.isUntitled() || !!options?.includeUntitled)) {
B
Benjamin Pasero 已提交
1071
					editors.push({ groupId: group.id, editor });
1072 1073 1074
				}
			}
		}
B
Benjamin Pasero 已提交
1075 1076

		return editors;
1077 1078
	}

1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091
	private getUniqueEditors(editors: IEditorIdentifier[]): IEditorIdentifier[] {
		const uniqueEditors: IEditorIdentifier[] = [];
		for (const { editor, groupId } of editors) {
			if (uniqueEditors.some(uniqueEditor => uniqueEditor.editor.matches(editor))) {
				continue;
			}

			uniqueEditors.push({ editor, groupId });
		}

		return uniqueEditors;
	}

1092
	//#endregion
1093

1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135
	//#region Custom View Type
	private customEditorViewTypesHandlers = new Map<string, ICustomEditorViewTypesHandler>();
	registerCustomEditorViewTypesHandler(source: string, handler: ICustomEditorViewTypesHandler): IDisposable {
		if (this.customEditorViewTypesHandlers.has(source)) {
			throw new Error(`Use a different name for the custom editor component, ${source} is already occupied.`);
		}

		this.customEditorViewTypesHandlers.set(source, handler);
		this.updateSchema();

		const viewTypeChangeEvent = handler.onDidChangeViewTypes(() => {
			this.updateSchema();
		});

		return {
			dispose: () => {
				viewTypeChangeEvent.dispose();
				this.customEditorViewTypesHandlers.delete(source);
				this.updateSchema();
			}
		};
	}

	private updateSchema() {
		const enumValues: string[] = [];
		const enumDescriptions: string[] = [];

		const infos: ICustomEditorInfo[] = [DEFAULT_CUSTOM_EDITOR];

		for (const [, handler] of this.customEditorViewTypesHandlers) {
			infos.push(...handler.getViewTypes());
		}

		infos.forEach(info => {
			enumValues.push(info.id);
			enumDescriptions.push(nls.localize('editorAssociations.viewType.sourceDescription', "Source: {0}", info.providerDisplayName));
		});

		updateViewTypeSchema(enumValues, enumDescriptions);
	}

	//#endregion
1136 1137 1138 1139 1140 1141 1142
	dispose(): void {
		super.dispose();

		// Dispose remaining watchers if any
		this.activeOutOfWorkspaceWatchers.forEach(disposable => dispose(disposable));
		this.activeOutOfWorkspaceWatchers.clear();
	}
1143 1144 1145
}

export interface IEditorOpenHandler {
B
Benjamin Pasero 已提交
1146
	(
1147
		delegate: (group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => Promise<IEditorPane | null>,
B
Benjamin Pasero 已提交
1148 1149 1150
		group: IEditorGroup,
		editor: IEditorInput,
		options?: IEditorOptions | ITextEditorOptions
1151
	): Promise<IEditorPane | null>;
1152 1153 1154 1155
}

/**
 * The delegating workbench editor service can be used to override the behaviour of the openEditor()
1156 1157
 * method by providing a IEditorOpenHandler. All calls are being delegated to the existing editor
 * service otherwise.
1158
 */
1159 1160 1161
export class DelegatingEditorService implements IEditorService {

	_serviceBrand: undefined;
1162 1163

	constructor(
1164 1165 1166
		private editorOpenHandler: IEditorOpenHandler,
		@IEditorService private editorService: EditorService
	) { }
1167

R
rebornix 已提交
1168
	getEditorOverrides(editorInput: IEditorInput, options: IEditorOptions | undefined, group: IEditorGroup | undefined) {
1169
		return this.editorService.getEditorOverrides(editorInput, options, group);
R
rebornix 已提交
1170 1171
	}

1172
	openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise<IEditorPane | undefined>;
1173 1174
	openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: OpenInEditorGroup): Promise<ITextEditorPane | undefined>;
	openEditor(editor: IResourceDiffEditorInput, group?: OpenInEditorGroup): Promise<ITextDiffEditorPane | undefined>;
1175
	async openEditor(editor: IEditorInput | IResourceEditorInputType, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise<IEditorPane | undefined> {
1176 1177 1178 1179 1180
		const result = this.editorService.doResolveEditorOpenRequest(editor, optionsOrGroup, group);
		if (result) {
			const [resolvedGroup, resolvedEditor, resolvedOptions] = result;

			// Pass on to editor open handler
1181
			const editorPane = await this.editorOpenHandler(
1182 1183 1184 1185 1186 1187
				(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => group.openEditor(editor, options),
				resolvedGroup,
				resolvedEditor,
				resolvedOptions
			);

1188 1189
			if (editorPane) {
				return editorPane; // the opening was handled, so return early
1190
			}
1191

1192
			return withNullAsUndefined(await resolvedGroup.openEditor(resolvedEditor, resolvedOptions));
B
Benjamin Pasero 已提交
1193
		}
1194

1195 1196
		return undefined;
	}
B
Benjamin Pasero 已提交
1197

1198
	//#region Delegate to IEditorService
1199

1200 1201 1202 1203
	get onDidActiveEditorChange(): Event<void> { return this.editorService.onDidActiveEditorChange; }
	get onDidVisibleEditorsChange(): Event<void> { return this.editorService.onDidVisibleEditorsChange; }

	get activeEditor(): IEditorInput | undefined { return this.editorService.activeEditor; }
1204 1205
	get activeEditorPane(): IVisibleEditorPane | undefined { return this.editorService.activeEditorPane; }
	get activeTextEditorControl(): ICodeEditor | IDiffEditor | undefined { return this.editorService.activeTextEditorControl; }
1206
	get activeTextEditorMode(): string | undefined { return this.editorService.activeTextEditorMode; }
1207
	get visibleEditors(): ReadonlyArray<IEditorInput> { return this.editorService.visibleEditors; }
1208 1209
	get visibleEditorPanes(): ReadonlyArray<IVisibleEditorPane> { return this.editorService.visibleEditorPanes; }
	get visibleTextEditorControls(): ReadonlyArray<ICodeEditor | IDiffEditor> { return this.editorService.visibleTextEditorControls; }
1210
	get editors(): ReadonlyArray<IEditorInput> { return this.editorService.editors; }
B
Benjamin Pasero 已提交
1211 1212 1213
	get count(): number { return this.editorService.count; }

	getEditors(order: EditorsOrder): ReadonlyArray<IEditorIdentifier> { return this.editorService.getEditors(order); }
1214

1215
	openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise<IEditorPane[]>;
1216 1217
	openEditors(editors: IResourceEditorInputType[], group?: OpenInEditorGroup): Promise<IEditorPane[]>;
	openEditors(editors: Array<IEditorInputWithOptions | IResourceEditorInputType>, group?: OpenInEditorGroup): Promise<IEditorPane[]> {
1218
		return this.editorService.openEditors(editors, group);
1219
	}
1220 1221 1222 1223 1224 1225 1226

	replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
	replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
	replaceEditors(editors: Array<IEditorReplacement | IResourceEditorReplacement>, group: IEditorGroup | GroupIdentifier): Promise<void> {
		return this.editorService.replaceEditors(editors as IResourceEditorReplacement[] /* TS fail */, group);
	}

1227
	isOpen(editor: IEditorInput): boolean;
1228 1229
	isOpen(editor: IResourceEditorInput): boolean;
	isOpen(editor: IEditorInput | IResourceEditorInput): boolean { return this.editorService.isOpen(editor as IResourceEditorInput /* TS fail */); }
1230 1231 1232 1233 1234

	overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { return this.editorService.overrideOpenEditor(handler); }

	invokeWithinEditorContext<T>(fn: (accessor: ServicesAccessor) => T): T { return this.editorService.invokeWithinEditorContext(fn); }

1235
	createEditorInput(input: IResourceEditorInputType): IEditorInput { return this.editorService.createEditorInput(input); }
1236 1237 1238 1239

	save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean> { return this.editorService.save(editors, options); }
	saveAll(options?: ISaveAllEditorsOptions): Promise<boolean> { return this.editorService.saveAll(options); }

1240 1241
	revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<void> { return this.editorService.revert(editors, options); }
	revertAll(options?: IRevertAllEditorsOptions): Promise<void> { return this.editorService.revertAll(options); }
1242

1243 1244 1245 1246
	registerCustomEditorViewTypesHandler(source: string, handler: ICustomEditorViewTypesHandler): IDisposable {
		throw new Error('Method not implemented.');
	}

1247
	//#endregion
I
isidor 已提交
1248
}
1249

B
Benjamin Pasero 已提交
1250
registerSingleton(IEditorService, EditorService);
1251 1252 1253

Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
	.registerConfiguration(editorAssociationsConfigurationNode);