vscode/src/vs/workbench/services/editor/browser/editorService.ts
2021-11-24 13:58:32 +01:00

1013 lines
37 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IResourceEditorInput, IEditorOptions, EditorActivation, EditorResolution, IResourceEditorInputIdentifier, ITextResourceEditorInput } from 'vs/platform/editor/common/editor';
import { SideBySideEditor, IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, EditorInputWithOptions, isEditorInputWithOptions, IEditorIdentifier, IEditorCloseEvent, ITextDiffEditorPane, IRevertOptions, SaveReason, EditorsOrder, IWorkbenchEditorConfiguration, EditorResourceAccessor, IVisibleEditorPane, EditorInputCapabilities, isResourceDiffEditorInput, IUntypedEditorInput, isResourceEditorInput, isEditorInput, isEditorInputWithOptionsAndGroup, GroupChangeKind } from 'vs/workbench/common/editor';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
import { ResourceMap } from 'vs/base/common/map';
import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { Event, Emitter, MicrotaskEmitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, isEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IUntypedEditorReplacement, IEditorService, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorsOptions, PreferredGroup, isPreferredGroup, IEditorsChangeEvent } from 'vs/workbench/services/editor/common/editorService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Disposable, IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
import { coalesce, distinct } from 'vs/base/common/arrays';
import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorGroupView, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { isUndefined, withNullAsUndefined } from 'vs/base/common/types';
import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver';
import { Promises, timeout } from 'vs/base/common/async';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { indexOfPath } from 'vs/base/common/extpath';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { IEditorResolverService, ResolvedStatus } from 'vs/workbench/services/editor/common/editorResolverService';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IWorkspaceTrustRequestService, WorkspaceTrustUriResponse } from 'vs/platform/workspace/common/workspaceTrust';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { findGroup } from 'vs/workbench/services/editor/common/editorGroupFinder';
import { ITextEditorService } from 'vs/workbench/services/textfile/common/textEditorService';
export class EditorService extends Disposable implements EditorServiceImpl {
declare readonly _serviceBrand: undefined;
//#region events
private readonly _onDidActiveEditorChange = this._register(new Emitter<void>());
readonly onDidActiveEditorChange = this._onDidActiveEditorChange.event;
private readonly _onDidVisibleEditorsChange = this._register(new Emitter<void>());
readonly onDidVisibleEditorsChange = this._onDidVisibleEditorsChange.event;
private readonly _onDidEditorsChange = this._register(new MicrotaskEmitter<IEditorsChangeEvent[]>({ merge: events => events.flat(1) }));
readonly onDidEditorsChange = this._onDidEditorsChange.event;
private readonly _onDidCloseEditor = this._register(new Emitter<IEditorCloseEvent>());
readonly onDidCloseEditor = this._onDidCloseEditor.event;
private readonly _onDidOpenEditorFail = this._register(new Emitter<IEditorIdentifier>());
readonly onDidOpenEditorFail = this._onDidOpenEditorFail.event;
private readonly _onDidMostRecentlyActiveEditorsChange = this._register(new Emitter<void>());
readonly onDidMostRecentlyActiveEditorsChange = this._onDidMostRecentlyActiveEditorsChange.event;
//#endregion
constructor(
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IFileService private readonly fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IEditorResolverService private readonly editorResolverService: IEditorResolverService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService,
@IHostService private readonly hostService: IHostService,
@ITextEditorService private readonly textEditorService: ITextEditorService
) {
super();
this.onConfigurationUpdated(configurationService.getValue<IWorkbenchEditorConfiguration>());
this.registerListeners();
}
private registerListeners(): void {
// Editor & group changes
this.editorGroupService.whenReady.then(() => this.onEditorGroupsReady());
this.editorGroupService.onDidChangeActiveGroup(group => this.handleActiveEditorChange(group));
this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
this.editorsObserver.onDidMostRecentlyActiveEditorsChange(() => this._onDidMostRecentlyActiveEditorsChange.fire());
// Out of workspace file watchers
this._register(this.onDidVisibleEditorsChange(() => this.handleVisibleEditorsChange()));
// File changes & operations
// 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.
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>())));
}
//#region Editor & group event handlers
private lastActiveEditor: EditorInput | undefined = undefined;
private onEditorGroupsReady(): void {
// Register listeners to each opened group
for (const group of this.editorGroupService.groups) {
this.registerGroupListeners(group as IEditorGroupView);
}
// Fire initial set of editor events if there is an active editor
if (this.activeEditor) {
this.doHandleActiveEditorChangeEvent();
this._onDidVisibleEditorsChange.fire();
}
}
private handleActiveEditorChange(group: IEditorGroup): void {
if (group !== this.editorGroupService.activeGroup) {
return; // ignore if not the active group
}
if (!this.lastActiveEditor && !group.activeEditor) {
return; // ignore if we still have no active editor
}
this.doHandleActiveEditorChangeEvent();
}
private doHandleActiveEditorChangeEvent(): void {
// Remember as last active
const activeGroup = this.editorGroupService.activeGroup;
this.lastActiveEditor = withNullAsUndefined(activeGroup.activeEditor);
// Fire event to outside parties
this._onDidActiveEditorChange.fire();
}
private registerGroupListeners(group: IEditorGroupView): void {
const groupDisposables = new DisposableStore();
groupDisposables.add(group.onDidModelChange(e => {
switch (e.kind) {
case GroupChangeKind.EDITOR_ACTIVE:
if (group.activeEditor) {
this._onDidEditorsChange.fire([{ groupId: group.id, editor: group.activeEditor, kind: GroupChangeKind.EDITOR_ACTIVE }]);
}
break;
default:
this._onDidEditorsChange.fire([{ groupId: group.id, ...e }]);
break;
}
}));
// Need to separatly listen to the group change for things like active editor changing
// as this doesn't always change the model (This could be a bug that needs more investigation)
groupDisposables.add(group.onDidGroupChange(e => {
if (e.kind === GroupChangeKind.EDITOR_ACTIVE) {
this.handleActiveEditorChange(group);
this._onDidVisibleEditorsChange.fire();
}
}));
groupDisposables.add(group.onDidCloseEditor(event => {
this._onDidCloseEditor.fire(event);
}));
groupDisposables.add(group.onDidOpenEditorFail(editor => {
this._onDidOpenEditorFail.fire({ editor, groupId: group.id });
}));
Event.once(group.onWillDispose)(() => {
dispose(groupDisposables);
});
}
//#endregion
//#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([
EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }),
EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.SECONDARY })
]), resource => resource.toString());
for (const resource of resources) {
if (this.fileService.hasProvider(resource) && !this.contextService.isInsideWorkspace(resource)) {
visibleOutOfWorkspaceResources.set(resource, resource);
}
}
}
// Handle no longer visible out of workspace resources
for (const resource of this.activeOutOfWorkspaceWatchers.keys()) {
if (!visibleOutOfWorkspaceResources.get(resource)) {
dispose(this.activeOutOfWorkspaceWatchers.get(resource));
this.activeOutOfWorkspaceWatchers.delete(resource);
}
}
// Handle newly visible out of workspace resources
for (const resource of visibleOutOfWorkspaceResources.keys()) {
if (!this.activeOutOfWorkspaceWatchers.get(resource)) {
const disposable = this.fileService.watch(resource);
this.activeOutOfWorkspaceWatchers.set(resource, disposable);
}
}
}
//#endregion
//#region File Changes: Move & Deletes to move or close opend editors
private async onDidRunFileOperation(e: FileOperationEvent): Promise<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);
}
}
private onDidFilesChange(e: FileChangesEvent): void {
if (e.gotDeleted()) {
this.handleDeletedFile(e, true);
}
}
private async handleMovedFile(source: URI, target: URI): Promise<void> {
for (const group of this.editorGroupService.groups) {
let replacements: (IUntypedEditorReplacement | IEditorReplacement)[] = [];
for (const editor of group.editors) {
const resource = editor.resource;
if (!resource || !this.uriIdentityService.extUri.isEqualOrParent(resource, source)) {
continue; // not matching our resource
}
// Determine new resulting target resource
let targetResource: URI;
if (this.uriIdentityService.extUri.isEqual(source, resource)) {
targetResource = target; // file got moved
} else {
const index = indexOfPath(resource.path, source.path, this.uriIdentityService.extUri.ignorePathCasing(resource));
targetResource = joinPath(target, resource.path.substr(index + source.path.length + 1)); // parent folder got moved
}
// Delegate rename() to editor instance
const moveResult = await editor.rename(group.id, targetResource);
if (!moveResult) {
return; // not target - ignore
}
const optionOverrides = {
preserveFocus: true,
pinned: group.isPinned(editor),
sticky: group.isSticky(editor),
index: group.getIndexOfEditor(editor),
inactive: !group.isActive(editor)
};
// Construct a replacement with our extra options mixed in
if (isEditorInput(moveResult.editor)) {
replacements.push({
editor,
replacement: moveResult.editor,
options: {
...moveResult.options,
...optionOverrides
}
});
} else {
replacements.push({
editor,
replacement: {
...moveResult.editor,
options: {
...moveResult.editor.options,
...optionOverrides
}
}
});
}
}
// Apply replacements
if (replacements.length) {
this.replaceEditors(replacements, group);
}
}
}
private closeOnFileDelete: boolean = false;
private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void {
if (typeof configuration.workbench?.editor?.closeOnFileDelete === 'boolean') {
this.closeOnFileDelete = configuration.workbench.editor.closeOnFileDelete;
} else {
this.closeOnFileDelete = false; // default
}
}
private handleDeletedFile(arg1: URI | FileChangesEvent, isExternal: boolean, movedTo?: URI): void {
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:
// - we close any editor when `closeOnFileDelete: true`
// - we close any editor when the delete occurred from within VSCode
// - we close any editor without resolved working copy assuming that
// this editor could not be opened after the file is gone
if (this.closeOnFileDelete || !isExternal || !this.workingCopyService.has(resource)) {
// 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 && this.uriIdentityService.extUri.isEqualOrParent(resource, movedTo)) {
return;
}
let matches = false;
if (arg1 instanceof FileChangesEvent) {
matches = arg1.contains(resource, FileChangeType.DELETED);
} else {
matches = this.uriIdentityService.extUri.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.hasProvider(resource)) {
await timeout(100);
exists = await this.fileService.exists(resource);
}
if (!exists && !editor.isDisposed()) {
editor.dispose();
}
}
})();
}
}
private getAllNonDirtyEditors(options: { includeUntitled: boolean, supportSideBySide: boolean }): EditorInput[] {
const editors: EditorInput[] = [];
function conditionallyAddEditor(editor: EditorInput): void {
if (editor.hasCapability(EditorInputCapabilities.Untitled) && !options.includeUntitled) {
return;
}
if (editor.isDirty()) {
return;
}
editors.push(editor);
}
for (const editor of this.editors) {
if (options.supportSideBySide && editor instanceof SideBySideEditorInput) {
conditionallyAddEditor(editor.primary);
conditionallyAddEditor(editor.secondary);
} else {
conditionallyAddEditor(editor);
}
}
return editors;
}
//#endregion
//#region Editor accessors
private readonly editorsObserver = this._register(this.instantiationService.createInstance(EditorsObserver));
get activeEditorPane(): IVisibleEditorPane | undefined {
return this.editorGroupService.activeGroup?.activeEditorPane;
}
get activeTextEditorControl(): ICodeEditor | IDiffEditor | undefined {
const activeEditorPane = this.activeEditorPane;
if (activeEditorPane) {
const activeControl = activeEditorPane.getControl();
if (isCodeEditor(activeControl) || isDiffEditor(activeControl)) {
return activeControl;
}
if (isCompositeEditor(activeControl) && isCodeEditor(activeControl.activeCodeEditor)) {
return activeControl.activeCodeEditor;
}
}
return undefined;
}
get activeTextEditorMode(): string | undefined {
let activeCodeEditor: ICodeEditor | undefined = undefined;
const activeTextEditorControl = this.activeTextEditorControl;
if (isDiffEditor(activeTextEditorControl)) {
activeCodeEditor = activeTextEditorControl.getModifiedEditor();
} else {
activeCodeEditor = activeTextEditorControl;
}
return activeCodeEditor?.getModel()?.getLanguageId();
}
get count(): number {
return this.editorsObserver.count;
}
get editors(): EditorInput[] {
return this.getEditors(EditorsOrder.SEQUENTIAL).map(({ editor }) => editor);
}
getEditors(order: EditorsOrder, options?: { excludeSticky?: boolean }): readonly IEditorIdentifier[] {
switch (order) {
// MRU
case EditorsOrder.MOST_RECENTLY_ACTIVE:
if (options?.excludeSticky) {
return this.editorsObserver.editors.filter(({ groupId, editor }) => !this.editorGroupService.getGroup(groupId)?.isSticky(editor));
}
return this.editorsObserver.editors;
// Sequential
case EditorsOrder.SEQUENTIAL:
const editors: IEditorIdentifier[] = [];
for (const group of this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)) {
editors.push(...group.getEditors(EditorsOrder.SEQUENTIAL, options).map(editor => ({ editor, groupId: group.id })));
}
return editors;
}
}
get activeEditor(): EditorInput | undefined {
const activeGroup = this.editorGroupService.activeGroup;
return activeGroup ? withNullAsUndefined(activeGroup.activeEditor) : undefined;
}
get visibleEditorPanes(): IVisibleEditorPane[] {
return coalesce(this.editorGroupService.groups.map(group => group.activeEditorPane));
}
get visibleTextEditorControls(): Array<ICodeEditor | IDiffEditor> {
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;
}
get visibleEditors(): EditorInput[] {
return coalesce(this.editorGroupService.groups.map(group => group.activeEditor));
}
//#endregion
//#region openEditor()
openEditor(editor: EditorInput, options?: IEditorOptions, group?: PreferredGroup): Promise<IEditorPane | undefined>;
openEditor(editor: IUntypedEditorInput, group?: PreferredGroup): Promise<IEditorPane | undefined>;
openEditor(editor: IResourceEditorInput, group?: PreferredGroup): Promise<IEditorPane | undefined>;
openEditor(editor: ITextResourceEditorInput | IUntitledTextResourceEditorInput, group?: PreferredGroup): Promise<IEditorPane | undefined>;
openEditor(editor: IResourceDiffEditorInput, group?: PreferredGroup): Promise<ITextDiffEditorPane | undefined>;
openEditor(editor: EditorInput | IUntypedEditorInput, optionsOrPreferredGroup?: IEditorOptions | PreferredGroup, preferredGroup?: PreferredGroup): Promise<IEditorPane | undefined>;
async openEditor(editor: EditorInput | IUntypedEditorInput, optionsOrPreferredGroup?: IEditorOptions | PreferredGroup, preferredGroup?: PreferredGroup): Promise<IEditorPane | undefined> {
let typedEditor: EditorInput | undefined = undefined;
let options = isEditorInput(editor) ? optionsOrPreferredGroup as IEditorOptions : editor.options;
let group: IEditorGroup | undefined = undefined;
if (isPreferredGroup(optionsOrPreferredGroup)) {
preferredGroup = optionsOrPreferredGroup;
}
// Resolve override unless disabled
if (options?.override !== EditorResolution.DISABLED) {
const resolvedEditor = await this.editorResolverService.resolveEditor(isEditorInput(editor) ? { editor, options } : editor, preferredGroup);
if (resolvedEditor === ResolvedStatus.ABORT) {
return; // skip editor if override is aborted
}
// We resolved an editor to use
if (isEditorInputWithOptionsAndGroup(resolvedEditor)) {
typedEditor = resolvedEditor.editor;
options = resolvedEditor.options;
group = resolvedEditor.group;
}
}
// Override is disabled or did not apply: fallback to default
if (!typedEditor) {
typedEditor = isEditorInput(editor) ? editor : this.textEditorService.createTextEditor(editor);
}
// If group still isn't defined because of a disabled override we resolve it
if (!group) {
let activation: EditorActivation | undefined = undefined;
([group, activation] = this.instantiationService.invokeFunction(findGroup, { editor: typedEditor, options }, preferredGroup));
// Mixin editor group activation if returned
if (activation) {
options = { ...options, activation };
}
}
return group.openEditor(typedEditor, options);
}
//#endregion
//#region openEditors()
openEditors(editors: EditorInputWithOptions[], group?: PreferredGroup, options?: IOpenEditorsOptions): Promise<IEditorPane[]>;
openEditors(editors: IUntypedEditorInput[], group?: PreferredGroup, options?: IOpenEditorsOptions): Promise<IEditorPane[]>;
openEditors(editors: Array<EditorInputWithOptions | IUntypedEditorInput>, group?: PreferredGroup, options?: IOpenEditorsOptions): Promise<IEditorPane[]>;
async openEditors(editors: Array<EditorInputWithOptions | IUntypedEditorInput>, preferredGroup?: PreferredGroup, options?: IOpenEditorsOptions): Promise<IEditorPane[]> {
// Pass all editors to trust service to determine if
// we should proceed with opening the editors if we
// are asked to validate trust.
if (options?.validateTrust) {
const editorsTrusted = await this.handleWorkspaceTrust(editors);
if (!editorsTrusted) {
return [];
}
}
// Find target groups for editors to open
const mapGroupToTypedEditors = new Map<IEditorGroup, Array<EditorInputWithOptions>>();
for (const editor of editors) {
let typedEditor: EditorInputWithOptions | undefined = undefined;
let group: IEditorGroup | undefined = undefined;
// Resolve override unless disabled
if (editor.options?.override !== EditorResolution.DISABLED) {
const resolvedEditor = await this.editorResolverService.resolveEditor(editor, preferredGroup);
if (resolvedEditor === ResolvedStatus.ABORT) {
continue; // skip editor if override is aborted
}
// We resolved an editor to use
if (isEditorInputWithOptionsAndGroup(resolvedEditor)) {
typedEditor = resolvedEditor;
group = resolvedEditor.group;
}
}
// Override is disabled or did not apply: fallback to default
if (!typedEditor) {
typedEditor = isEditorInputWithOptions(editor) ? editor : { editor: this.textEditorService.createTextEditor(editor), options: editor.options };
}
// If group still isn't defined because of a disabled override we resolve it
if (!group) {
[group] = this.instantiationService.invokeFunction(findGroup, typedEditor, preferredGroup);
}
// Update map of groups to editors
let targetGroupEditors = mapGroupToTypedEditors.get(group);
if (!targetGroupEditors) {
targetGroupEditors = [];
mapGroupToTypedEditors.set(group, targetGroupEditors);
}
targetGroupEditors.push(typedEditor);
}
// Open in target groups
const result: Promise<IEditorPane | undefined>[] = [];
for (const [group, editors] of mapGroupToTypedEditors) {
result.push(group.openEditors(editors));
}
return coalesce(await Promises.settled(result));
}
private async handleWorkspaceTrust(editors: Array<EditorInputWithOptions | IUntypedEditorInput>): Promise<boolean> {
const { resources, diffMode } = this.extractEditorResources(editors);
const trustResult = await this.workspaceTrustRequestService.requestOpenFilesTrust(resources);
switch (trustResult) {
case WorkspaceTrustUriResponse.Open:
return true;
case WorkspaceTrustUriResponse.OpenInNewWindow:
await this.hostService.openWindow(resources.map(resource => ({ fileUri: resource })), { forceNewWindow: true, diffMode });
return false;
case WorkspaceTrustUriResponse.Cancel:
return false;
}
}
private extractEditorResources(editors: Array<EditorInputWithOptions | IUntypedEditorInput>): { resources: URI[], diffMode?: boolean } {
const resources = new ResourceMap<boolean>();
let diffMode = false;
for (const editor of editors) {
// Typed Editor
if (isEditorInputWithOptions(editor)) {
const resource = EditorResourceAccessor.getOriginalUri(editor.editor, { supportSideBySide: SideBySideEditor.BOTH });
if (URI.isUri(resource)) {
resources.set(resource, true);
} else if (resource) {
if (resource.primary) {
resources.set(resource.primary, true);
}
if (resource.secondary) {
resources.set(resource.secondary, true);
}
diffMode = editor.editor instanceof DiffEditorInput;
}
}
// Untyped editor
else {
if (isResourceDiffEditorInput(editor)) {
const originalResourceEditor = editor.original;
if (URI.isUri(originalResourceEditor.resource)) {
resources.set(originalResourceEditor.resource, true);
}
const modifiedResourceEditor = editor.modified;
if (URI.isUri(modifiedResourceEditor.resource)) {
resources.set(modifiedResourceEditor.resource, true);
}
diffMode = true;
} else if (isResourceEditorInput(editor)) {
resources.set(editor.resource, true);
}
}
}
return {
resources: Array.from(resources.keys()),
diffMode
};
}
//#endregion
//#region isOpened()
isOpened(editor: IResourceEditorInputIdentifier): boolean {
return this.editorsObserver.hasEditor({
resource: this.uriIdentityService.asCanonicalUri(editor.resource),
typeId: editor.typeId,
editorId: editor.editorId
});
}
//#endregion
//#region isOpened()
isVisible(editor: EditorInput): boolean {
for (const group of this.editorGroupService.groups) {
if (group.activeEditor?.matches(editor)) {
return true;
}
}
return false;
}
//#endregion
//#region findEditors()
findEditors(resource: URI): readonly IEditorIdentifier[];
findEditors(editor: IResourceEditorInputIdentifier): readonly IEditorIdentifier[];
findEditors(resource: URI, group: IEditorGroup | GroupIdentifier): readonly EditorInput[];
findEditors(editor: IResourceEditorInputIdentifier, group: IEditorGroup | GroupIdentifier): EditorInput | undefined;
findEditors(arg1: URI | IResourceEditorInputIdentifier, arg2?: IEditorGroup | GroupIdentifier): readonly IEditorIdentifier[] | readonly EditorInput[] | EditorInput | undefined;
findEditors(arg1: URI | IResourceEditorInputIdentifier, arg2?: IEditorGroup | GroupIdentifier): readonly IEditorIdentifier[] | readonly EditorInput[] | EditorInput | undefined {
const resource = URI.isUri(arg1) ? arg1 : arg1.resource;
const typeId = URI.isUri(arg1) ? undefined : arg1.typeId;
// Do a quick check for the resource via the editor observer
// which is a very efficient way to find an editor by resource
if (!this.editorsObserver.hasEditors(resource)) {
if (URI.isUri(arg1) || isUndefined(arg2)) {
return [];
}
return undefined;
}
// Search only in specific group
if (!isUndefined(arg2)) {
const targetGroup = typeof arg2 === 'number' ? this.editorGroupService.getGroup(arg2) : arg2;
// Resource provided: result is an array
if (URI.isUri(arg1)) {
if (!targetGroup) {
return [];
}
return targetGroup.findEditors(resource);
}
// Editor identifier provided, result is single
else {
if (!targetGroup) {
return undefined;
}
const editors = targetGroup.findEditors(resource);
for (const editor of editors) {
if (editor.typeId === typeId) {
return editor;
}
}
return undefined;
}
}
// Search across all groups in MRU order
else {
const result: IEditorIdentifier[] = [];
for (const group of this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
const editors: EditorInput[] = [];
// Resource provided: result is an array
if (URI.isUri(arg1)) {
editors.push(...this.findEditors(arg1, group));
}
// Editor identifier provided, result is single
else {
const editor = this.findEditors(arg1, group);
if (editor) {
editors.push(editor);
}
}
result.push(...editors.map(editor => ({ editor, groupId: group.id })));
}
return result;
}
}
//#endregion
//#region replaceEditors()
async replaceEditors(replacements: IUntypedEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
async replaceEditors(replacements: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
async replaceEditors(replacements: Array<IEditorReplacement | IUntypedEditorReplacement>, group: IEditorGroup | GroupIdentifier): Promise<void> {
const targetGroup = typeof group === 'number' ? this.editorGroupService.getGroup(group) : group;
// Convert all replacements to typed editors unless already
// typed and handle overrides properly.
const typedReplacements: IEditorReplacement[] = [];
for (const replacement of replacements) {
let typedReplacement: IEditorReplacement | undefined = undefined;
// Figure out the override rule based on options
let override: string | EditorResolution | undefined;
if (isEditorReplacement(replacement)) {
override = replacement.options?.override;
} else {
override = replacement.replacement.options?.override;
}
// Resolve override unless disabled
if (override !== EditorResolution.DISABLED) {
const resolvedEditor = await this.editorResolverService.resolveEditor(
isEditorReplacement(replacement) ? { editor: replacement.replacement, options: replacement.options } : replacement.replacement,
targetGroup
);
if (resolvedEditor === ResolvedStatus.ABORT) {
continue; // skip editor if override is aborted
}
// We resolved an editor to use
if (isEditorInputWithOptionsAndGroup(resolvedEditor)) {
typedReplacement = {
editor: replacement.editor,
replacement: resolvedEditor.editor,
options: resolvedEditor.options,
forceReplaceDirty: replacement.forceReplaceDirty
};
}
}
// Override is disabled or did not apply: fallback to default
if (!typedReplacement) {
typedReplacement = {
editor: replacement.editor,
replacement: isEditorReplacement(replacement) ? replacement.replacement : this.textEditorService.createTextEditor(replacement.replacement),
options: isEditorReplacement(replacement) ? replacement.options : replacement.replacement.options,
forceReplaceDirty: replacement.forceReplaceDirty
};
}
typedReplacements.push(typedReplacement);
}
return targetGroup?.replaceEditors(typedReplacements);
}
//#endregion
//#region save/revert
async save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean> {
// Convert to array
if (!Array.isArray(editors)) {
editors = [editors];
}
// Make sure to not save the same editor multiple times
// by using the `matches()` method to find duplicates
const uniqueEditors = this.getUniqueEditors(editors);
// 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[] = [];
const editorsToSaveSequentially: IEditorIdentifier[] = [];
if (options?.saveAs) {
editorsToSaveSequentially.push(...uniqueEditors);
} else {
for (const { groupId, editor } of uniqueEditors) {
if (editor.hasCapability(EditorInputCapabilities.Untitled)) {
editorsToSaveSequentially.push({ groupId, editor });
} else {
editorsToSaveParallel.push({ groupId, editor });
}
}
}
// Editors to save in parallel
const saveResults = await Promises.settled(editorsToSaveParallel.map(({ groupId, editor }) => {
// Use save as a hint to pin the editor if used explicitly
if (options?.reason === SaveReason.EXPLICIT) {
this.editorGroupService.getGroup(groupId)?.pinEditor(editor);
}
// Save
return editor.save(groupId, options);
}));
// Editors to save sequentially
for (const { groupId, editor } of editorsToSaveSequentially) {
if (editor.isDisposed()) {
continue; // might have been disposed from the save already
}
// 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.
const editorPane = await this.openEditor(editor, groupId);
const editorOptions: IEditorOptions = {
pinned: true,
viewState: editorPane?.getViewState()
};
const result = options?.saveAs ? await editor.saveAs(groupId, options) : await editor.save(groupId, options);
saveResults.push(result);
if (!result) {
break; // failed or cancelled, abort
}
// 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.hasCapability(EditorInputCapabilities.Untitled) ? this.editorGroupService.groups.map(group => group.id) /* untitled replaces across all groups */ : [groupId];
for (const targetGroup of targetGroups) {
const group = this.editorGroupService.getGroup(targetGroup);
await group?.replaceEditors([{ editor, replacement: result, options: editorOptions }]);
}
}
}
return saveResults.every(result => !!result);
}
saveAll(options?: ISaveAllEditorsOptions): Promise<boolean> {
return this.save(this.getAllDirtyEditors(options), options);
}
async revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<boolean> {
// Convert to array
if (!Array.isArray(editors)) {
editors = [editors];
}
// Make sure to not revert the same editor multiple times
// by using the `matches()` method to find duplicates
const uniqueEditors = this.getUniqueEditors(editors);
await Promises.settled(uniqueEditors.map(async ({ groupId, editor }) => {
// Use revert as a hint to pin the editor
this.editorGroupService.getGroup(groupId)?.pinEditor(editor);
return editor.revert(groupId, options);
}));
return !uniqueEditors.some(({ editor }) => editor.isDirty());
}
async revertAll(options?: IRevertAllEditorsOptions): Promise<boolean> {
return this.revert(this.getAllDirtyEditors(options), options);
}
private getAllDirtyEditors(options?: IBaseSaveRevertAllEditorOptions): IEditorIdentifier[] {
const editors: IEditorIdentifier[] = [];
for (const group of this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) {
if (!editor.isDirty()) {
continue;
}
if (!options?.includeUntitled && editor.hasCapability(EditorInputCapabilities.Untitled)) {
continue;
}
if (options?.excludeSticky && group.isSticky(editor)) {
continue;
}
editors.push({ groupId: group.id, editor });
}
}
return editors;
}
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;
}
//#endregion
override dispose(): void {
super.dispose();
// Dispose remaining watchers if any
this.activeOutOfWorkspaceWatchers.forEach(disposable => dispose(disposable));
this.activeOutOfWorkspaceWatchers.clear();
}
}
registerSingleton(IEditorService, EditorService);