From 00688bf051948d9ce5000328493fd828dece4f48 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 17 Nov 2019 17:44:57 +0100 Subject: [PATCH] working copies - properly implement save, saveAs, saveAll (#84672) --- .../api/browser/mainThreadWorkspace.ts | 8 +- src/vs/workbench/browser/contextkeys.ts | 36 +- .../browser/parts/editor/editorActions.ts | 22 +- .../browser/parts/editor/textEditor.ts | 11 +- src/vs/workbench/common/editor.ts | 129 ++++++- .../common/editor/untitledTextEditorInput.ts | 52 ++- .../customEditor/browser/customEditorInput.ts | 11 +- .../contrib/debug/browser/debugService.ts | 4 +- .../files/browser/fileActions.contribution.ts | 60 +-- .../contrib/files/browser/fileActions.ts | 8 +- .../contrib/files/browser/fileCommands.ts | 343 +++++------------- .../files/browser/files.contribution.ts | 6 +- ...Handler.ts => textFileSaveErrorHandler.ts} | 4 +- .../files/browser/views/openEditorsView.ts | 20 +- .../files/common/editors/fileEditorInput.ts | 35 +- .../tasks/browser/abstractTaskService.ts | 4 +- .../browser/testCustomEditors.ts | 12 +- .../dialogs/browser/simpleFileDialog.ts | 11 +- .../services/editor/browser/editorService.ts | 92 ++++- .../services/editor/common/editorService.ts | 34 +- .../common/filesConfigurationService.ts | 9 +- .../services/history/browser/history.ts | 16 +- .../services/textfile/common/textfiles.ts | 1 - .../common/untitledTextEditorService.ts | 1 - .../workingCopy/common/workingCopyService.ts | 5 + .../workbench/test/workbenchTestServices.ts | 16 +- 26 files changed, 551 insertions(+), 399 deletions(-) rename src/vs/workbench/contrib/files/browser/{saveErrorHandler.ts => textFileSaveErrorHandler.ts} (98%) diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index b41e21a9cada7..1ad44491aa17e 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -15,7 +15,7 @@ import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'v import { IWorkspaceContextService, WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape, IWorkspaceData, ITextSearchComplete } from '../common/extHost.protocol'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -37,7 +37,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { extHostContext: IExtHostContext, @ISearchService private readonly _searchService: ISearchService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, - @ITextFileService private readonly _textFileService: ITextFileService, + @IEditorService private readonly _editorService: IEditorService, @IWorkspaceEditingService private readonly _workspaceEditingService: IWorkspaceEditingService, @INotificationService private readonly _notificationService: INotificationService, @IRequestService private readonly _requestService: IRequestService, @@ -212,9 +212,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- save & edit resources --- $saveAll(includeUntitled?: boolean): Promise { - return this._textFileService.saveAll(includeUntitled).then(result => { - return result.results.every(each => each.success === true); - }); + return this._editorService.saveAll({ includeUntitled }); } $resolveProxy(url: string): Promise { diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index d45fe4e74e77d..66d6d8138a68e 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; import { IWindowsConfiguration } from 'vs/platform/windows/common/windows'; -import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorIsSaveableContext, toResource, SideBySideEditor, EditorAreaVisibleContext } from 'vs/workbench/common/editor'; +import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorIsSaveableContext, EditorAreaVisibleContext, DirtyWorkingCopiesContext } from 'vs/workbench/common/editor'; import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -21,8 +21,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; import { PanelPositionContext } from 'vs/workbench/common/panel'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; -import { IFileService } from 'vs/platform/files/common/files'; -import { Schemas } from 'vs/base/common/network'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export const IsMacContext = new RawContextKey('isMac', isMacintosh); export const IsLinuxContext = new RawContextKey('isLinux', isLinux); @@ -51,6 +50,8 @@ export const IsFullscreenContext = new RawContextKey('isFullscreen', fa export class WorkbenchContextKeysHandler extends Disposable { private inputFocusedContext: IContextKey; + private dirtyWorkingCopiesContext: IContextKey; + private activeEditorContext: IContextKey; private activeEditorIsSaveable: IContextKey; @@ -75,19 +76,18 @@ export class WorkbenchContextKeysHandler extends Disposable { private panelPositionContext: IContextKey; constructor( - @IContextKeyService private contextKeyService: IContextKeyService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IConfigurationService private configurationService: IConfigurationService, - @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, - @IEditorService private editorService: IEditorService, - @IEditorGroupsService private editorGroupService: IEditorGroupsService, - @IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService, - @IViewletService private viewletService: IViewletService, - @IFileService private fileService: IFileService + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IEditorService private readonly editorService: IEditorService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IViewletService private readonly viewletService: IViewletService, + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService ) { super(); - // Platform IsMacContext.bindTo(this.contextKeyService); IsLinuxContext.bindTo(this.contextKeyService); @@ -116,6 +116,9 @@ export class WorkbenchContextKeysHandler extends Disposable { this.activeEditorGroupLast = ActiveEditorGroupLastContext.bindTo(this.contextKeyService); this.multipleEditorGroupsContext = MultipleEditorGroupsContext.bindTo(this.contextKeyService); + // Working Copies + this.dirtyWorkingCopiesContext = DirtyWorkingCopiesContext.bindTo(this.contextKeyService); + // Inputs this.inputFocusedContext = InputFocusedContext.bindTo(this.contextKeyService); @@ -183,6 +186,8 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.viewletService.onDidViewletOpen(() => this.updateSideBarContextKeys())); this._register(this.layoutService.onPartVisibilityChange(() => this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART)))); + + this._register(this.workingCopyService.onDidChangeDirty(w => this.dirtyWorkingCopiesContext.set(w.isDirty() || this.workingCopyService.hasDirty))); } private updateEditorContextKeys(): void { @@ -217,10 +222,7 @@ export class WorkbenchContextKeysHandler extends Disposable { if (activeControl) { this.activeEditorContext.set(activeControl.getId()); - - const resource = toResource(activeControl.input, { supportSideBySide: SideBySideEditor.MASTER }); - const canSave = resource ? this.fileService.canHandleResource(resource) || resource.scheme === Schemas.untitled : false; - this.activeEditorIsSaveable.set(canSave); + this.activeEditorIsSaveable.set(!activeControl.input.isReadonly()); } else { this.activeEditorContext.reset(); this.activeEditorIsSaveable.reset(); diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 92fd5dd5f94c4..24e6f4bc58cdf 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -15,7 +15,6 @@ import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { CLOSE_EDITOR_COMMAND_ID, NAVIGATE_ALL_EDITORS_GROUP_PREFIX, MOVE_ACTIVE_EDITOR_COMMAND_ID, NAVIGATE_IN_ACTIVE_GROUP_PREFIX, ActiveEditorMoveArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, mergeAllGroups } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IEditorGroupsService, IEditorGroup, GroupsArrangement, EditorsOrder, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; @@ -598,10 +597,10 @@ export abstract class BaseCloseAllAction extends Action { id: string, label: string, clazz: string | undefined, - private textFileService: ITextFileService, private workingCopyService: IWorkingCopyService, private fileDialogService: IFileDialogService, - protected editorGroupService: IEditorGroupsService + protected editorGroupService: IEditorGroupsService, + private editorService: IEditorService ) { super(id, label, clazz); } @@ -647,11 +646,10 @@ export abstract class BaseCloseAllAction extends Action { let saveOrRevert: boolean; if (confirm === ConfirmResult.DONT_SAVE) { - await this.textFileService.revertAll(undefined, { soft: true }); + await this.editorService.revertAll({ soft: true }); saveOrRevert = true; } else { - const res = await this.textFileService.saveAll(true); - saveOrRevert = res.results.every(r => !!r.success); + saveOrRevert = await this.editorService.saveAll({ includeUntitled: true }); } if (saveOrRevert) { @@ -670,12 +668,12 @@ export class CloseAllEditorsAction extends BaseCloseAllAction { constructor( id: string, label: string, - @ITextFileService textFileService: ITextFileService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IFileDialogService fileDialogService: IFileDialogService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService ) { - super(id, label, 'codicon-close-all', textFileService, workingCopyService, fileDialogService, editorGroupService); + super(id, label, 'codicon-close-all', workingCopyService, fileDialogService, editorGroupService, editorService); } protected doCloseAll(): Promise { @@ -691,12 +689,12 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction { constructor( id: string, label: string, - @ITextFileService textFileService: ITextFileService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IFileDialogService fileDialogService: IFileDialogService, - @IEditorGroupsService editorGroupService: IEditorGroupsService + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IEditorService editorService: IEditorService ) { - super(id, label, undefined, textFileService, workingCopyService, fileDialogService, editorGroupService); + super(id, label, undefined, workingCopyService, fileDialogService, editorGroupService, editorService); } protected async doCloseAll(): Promise { diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index b9e4dcc4696da..8ba1dcd86e295 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { distinct, deepClone, assign } from 'vs/base/common/objects'; -import { isObject, assertIsDefined } from 'vs/base/common/types'; +import { isObject, assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { Dimension } from 'vs/base/browser/dom'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { EditorInput, EditorOptions, IEditorMemento, ITextEditor } from 'vs/workbench/common/editor'; @@ -249,6 +249,15 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { this.editorMemento.saveEditorState(this.group, resource, editorViewState); } + getViewState(): IEditorViewState | undefined { + const resource = this.input?.getResource(); + if (resource) { + return withNullAsUndefined(this.retrieveTextEditorViewState(resource)); + } + + return undefined; + } + protected retrieveTextEditorViewState(resource: URI): IEditorViewState | null { const control = this.getControl(); if (!isCodeEditor(control)) { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index aff98eee1b05c..938d57f515e96 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -14,14 +14,18 @@ import { IInstantiationService, IConstructorSignature0, ServicesAccessor, Brande import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITextModel } from 'vs/editor/common/model'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ICompositeControl } from 'vs/workbench/common/composite'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { IPathData } from 'vs/platform/windows/common/windows'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { isEqual } from 'vs/base/common/resources'; +export const DirtyWorkingCopiesContext = new RawContextKey('dirtyWorkingCopies', false); export const ActiveEditorContext = new RawContextKey('activeEditor', null); export const ActiveEditorIsSaveableContext = new RawContextKey('activeEditorIsSaveable', false); export const EditorsVisibleContext = new RawContextKey('editorIsOpen', false); @@ -120,6 +124,17 @@ export interface ITextEditor extends IEditor { * Returns the underlying text editor widget of this editor. */ getControl(): ICodeEditor | undefined; + + /** + * Returns the current view state of the text editor if any. + */ + getViewState(): IEditorViewState | undefined; +} + +export function isTextEditor(thing: IEditor | undefined): thing is ITextEditor { + const candidate = thing as ITextEditor | undefined; + + return typeof candidate?.getViewState === 'function'; } export interface ITextDiffEditor extends IEditor { @@ -299,16 +314,33 @@ export interface IEditorInput extends IDisposable { */ resolve(): Promise; + /** + * Returns if this input is readonly or not. + */ + isReadonly(): boolean; + + /** + * Returns if the input is an untitled editor or not. + */ + isUntitled(): boolean; + /** * Returns if this input is dirty or not. */ isDirty(): boolean; /** - * Saves the editor if it is dirty. + * Saves the editor. */ save(options?: ISaveOptions): Promise; + /** + * Saves the editor to a different location. The provided groupId + * helps implementors to e.g. preserve view state of the editor + * and re-open it in the correct group after saving. + */ + saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise; + /** * Reverts this input. */ @@ -318,6 +350,11 @@ export interface IEditorInput extends IDisposable { * Returns if the other object matches this input. */ matches(other: unknown): boolean; + + /** + * Returns if this editor is disposed. + */ + isDisposed(): boolean; } /** @@ -401,6 +438,22 @@ export abstract class EditorInput extends Disposable implements IEditorInput { */ abstract resolve(): Promise; + /** + * Returns if this input is readonly or not. + */ + isReadonly(): boolean { + // Subclasses need to explicitly opt-in to being editable. + return !this.isDirty(); + } + + /** + * Returns if the input is an untitled editor or not. + */ + isUntitled(): boolean { + // Subclasses need to explicitly opt-in to being untitled. + return false; + } + /** * An editor that is dirty will be asked to be saved once it closes. */ @@ -415,6 +468,13 @@ export abstract class EditorInput extends Disposable implements IEditorInput { return Promise.resolve(true); } + /** + * Saves the editor to a different location. + */ + saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + return Promise.resolve(true); + } + /** * Reverts the editor if it is dirty. Subclasses return a promise with a boolean indicating the success of the operation. */ @@ -455,6 +515,59 @@ export abstract class EditorInput extends Disposable implements IEditorInput { } } +export abstract class TextEditorInput extends EditorInput { + + constructor( + protected readonly resource: URI, + @IEditorService protected readonly editorService: IEditorService, + @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, + @ITextFileService protected readonly textFileService: ITextFileService + ) { + super(); + } + + getResource(): URI { + return this.resource; + } + + save(options?: ITextFileSaveOptions): Promise { + return this.textFileService.save(this.resource, options); + } + + saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + return this.doSaveAs(group, options); + } + + protected async doSaveAs(group: GroupIdentifier, options?: ITextFileSaveOptions, replaceAllEditors?: boolean): Promise { + + // Preserve view state by opening the editor first. In addition + // this allows the user to review the contents of the editor. + let viewState: IEditorViewState | undefined = undefined; + const editor = await this.editorService.openEditor(this, undefined, group); + if (isTextEditor(editor)) { + viewState = editor.getViewState(); + } + + // Save as + const target = await this.textFileService.saveAs(this.resource, undefined, options); + if (!target) { + return false; // save cancelled + } + + // Replace editor preserving viewstate (either across all groups or + // only selected group) if the target is different from the current resource + if (!isEqual(target, this.resource)) { + const replacement: IResourceInput = { resource: target, options: { pinned: true, viewState } }; + const targetGroups = replaceAllEditors ? this.editorGroupService.groups.map(group => group.id) : [group]; + for (const group of targetGroups) { + await this.editorService.replaceEditors([{ editor: { resource: this.resource }, replacement }], group); + } + } + + return true; + } +} + export const enum EncodingMode { /** @@ -542,6 +655,14 @@ export class SideBySideEditorInput extends EditorInput { return this._details; } + isReadonly(): boolean { + return this.master.isReadonly(); + } + + isUntitled(): boolean { + return this.master.isUntitled(); + } + isDirty(): boolean { return this.master.isDirty(); } @@ -550,6 +671,10 @@ export class SideBySideEditorInput extends EditorInput { return this.master.save(options); } + saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + return this.master.saveAs(groupId, options); + } + revert(options?: IRevertOptions): Promise { return this.master.revert(options); } diff --git a/src/vs/workbench/common/editor/untitledTextEditorInput.ts b/src/vs/workbench/common/editor/untitledTextEditorInput.ts index 14c409a73eb0c..9a7d62d909c4f 100644 --- a/src/vs/workbench/common/editor/untitledTextEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledTextEditorInput.ts @@ -8,43 +8,53 @@ import { suggestFilename } from 'vs/base/common/mime'; import { createMemoizer } from 'vs/base/common/decorators'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; -import { EditorInput, IEncodingSupport, EncodingMode, Verbosity, IModeSupport } from 'vs/workbench/common/editor'; +import { IEncodingSupport, EncodingMode, Verbosity, IModeSupport, TextEditorInput, GroupIdentifier } from 'vs/workbench/common/editor'; import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { Emitter } from 'vs/base/common/event'; +import { ITextFileService, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ILabelService } from 'vs/platform/label/common/label'; import { IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; -import { ISaveOptions, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; /** * An editor input to be used for untitled text buffers. */ -export class UntitledTextEditorInput extends EditorInput implements IEncodingSupport, IModeSupport { +export class UntitledTextEditorInput extends TextEditorInput implements IEncodingSupport, IModeSupport { static readonly ID: string = 'workbench.editors.untitledEditorInput'; + private static readonly MEMOIZER = createMemoizer(); private cachedModel: UntitledTextEditorModel | null = null; private modelResolve: Promise | null = null; - private readonly _onDidModelChangeContent: Emitter = this._register(new Emitter()); - readonly onDidModelChangeContent: Event = this._onDidModelChangeContent.event; + private readonly _onDidModelChangeContent = this._register(new Emitter()); + readonly onDidModelChangeContent = this._onDidModelChangeContent.event; - private readonly _onDidModelChangeEncoding: Emitter = this._register(new Emitter()); - readonly onDidModelChangeEncoding: Event = this._onDidModelChangeEncoding.event; + private readonly _onDidModelChangeEncoding = this._register(new Emitter()); + readonly onDidModelChangeEncoding = this._onDidModelChangeEncoding.event; constructor( - private readonly resource: URI, + resource: URI, private readonly _hasAssociatedFilePath: boolean, private preferredMode: string | undefined, private readonly initialValue: string | undefined, private preferredEncoding: string | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextFileService private readonly textFileService: ITextFileService, - @ILabelService private readonly labelService: ILabelService + @ITextFileService textFileService: ITextFileService, + @ILabelService private readonly labelService: ILabelService, + @IEditorService editorService: IEditorService, + @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(); + super(resource, editorService, editorGroupService, textFileService); + + this.registerListeners(); + } + + private registerListeners(): void { this._register(this.labelService.onDidChangeFormatters(() => UntitledTextEditorInput.MEMOIZER.clear())); } @@ -56,10 +66,6 @@ export class UntitledTextEditorInput extends EditorInput implements IEncodingSup return UntitledTextEditorInput.ID; } - getResource(): URI { - return this.resource; - } - getName(): string { return this.hasAssociatedFilePath ? basenameOrAuthority(this.resource) : this.resource.path; } @@ -125,6 +131,14 @@ export class UntitledTextEditorInput extends EditorInput implements IEncodingSup } } + isReadonly(): boolean { + return false; + } + + isUntitled(): boolean { + return true; + } + isDirty(): boolean { if (this.cachedModel) { return this.cachedModel.isDirty(); @@ -147,8 +161,8 @@ export class UntitledTextEditorInput extends EditorInput implements IEncodingSup return false; } - save(options?: ISaveOptions): Promise { - return this.textFileService.save(this.resource, options); + saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + return this.doSaveAs(group, options, true /* replace editor across all groups */); } revert(options?: IRevertOptions): Promise { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 376cfce5686aa..747f33c052c31 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -12,7 +12,7 @@ import { DataUri, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IEditorInput, Verbosity } from 'vs/workbench/common/editor'; +import { IEditorInput, Verbosity, GroupIdentifier } from 'vs/workbench/common/editor'; import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { CustomEditorModel } from '../common/customEditorModel'; @@ -117,6 +117,10 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { this._register(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); } + public isReadonly(): boolean { + return false; + } + public isDirty(): boolean { return this._model ? this._model.isDirty() : false; } @@ -125,6 +129,11 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return this._model ? this._model.save(options) : Promise.resolve(false); } + public saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + // TODO@matt implement properly (see TextEditorInput#saveAs()) + return this._model ? this._model.save(options) : Promise.resolve(false); + } + public revert(options?: IRevertOptions): Promise { return this._model ? this._model.revert(options) : Promise.resolve(false); } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 031da0c11ab81..94c07e70c9ccf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -258,7 +258,7 @@ export class DebugService implements IDebugService { try { // make sure to save all files and that the configuration is up to date await this.extensionService.activateByEvent('onDebug'); - await this.textFileService.saveAll(); + await this.editorService.saveAll(); await this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined); await this.extensionService.whenInstalledExtensionsRegistered(); @@ -568,7 +568,7 @@ export class DebugService implements IDebugService { } async restartSession(session: IDebugSession, restartData?: any): Promise { - await this.textFileService.saveAll(); + await this.editorService.saveAll(); const isAutoRestart = !!restartData; const runTasks: () => Promise = async () => { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 57456d500ea4d..032d8c5ddcd85 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -6,11 +6,11 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler, cutFileHandler, DOWNLOAD_COMMAND_ID, openFilePreserveFocusHandler, DOWNLOAD_LABEL } from 'vs/workbench/contrib/files/browser/fileActions'; -import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/saveErrorHandler'; +import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/contrib/files/browser/textFileSaveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry, ILocalizedString } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, DirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, DirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, SaveableEditorContext } from 'vs/workbench/contrib/files/browser/fileCommands'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -18,7 +18,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService, ExplorerResourceMoveableToTrash, ExplorerViewletVisibleContext } from 'vs/workbench/contrib/files/common/files'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL } from 'vs/workbench/browser/actions/workspaceCommands'; import { CLOSE_SAVED_EDITORS_COMMAND_ID, CLOSE_EDITORS_IN_GROUP_COMMAND_ID, CLOSE_EDITOR_COMMAND_ID, CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { AutoSaveContext } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { AutoSaveAfterShortDelayContext } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { URI } from 'vs/base/common/uri'; @@ -26,7 +26,7 @@ import { Schemas } from 'vs/base/common/network'; import { WorkspaceFolderCountContext, IsWebContext } from 'vs/workbench/browser/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { OpenFileFolderAction, OpenFileAction, OpenFolderAction, OpenWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; -import { ActiveEditorIsSaveableContext } from 'vs/workbench/common/editor'; +import { ActiveEditorIsSaveableContext, DirtyWorkingCopiesContext } from 'vs/workbench/common/editor'; import { SidebarFocusContext } from 'vs/workbench/common/viewlet'; import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; @@ -44,7 +44,6 @@ registry.registerWorkbenchAction(SyncActionDescriptor.create(GlobalNewUntitledFi registry.registerWorkbenchAction(SyncActionDescriptor.create(CompareWithClipboardAction, CompareWithClipboardAction.ID, CompareWithClipboardAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_C) }), 'File: Compare Active File with Clipboard', category.value); registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleAutoSaveAction, ToggleAutoSaveAction.ID, ToggleAutoSaveAction.LABEL), 'File: Toggle Auto Save', category.value); - const workspacesCategory = nls.localize('workspaces', "Workspaces"); registry.registerWorkbenchAction(SyncActionDescriptor.create(OpenWorkspaceAction, OpenWorkspaceAction.ID, OpenWorkspaceAction.LABEL), 'Workspaces: Open Workspace...', workspacesCategory); @@ -242,7 +241,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: 'navigation', order: 10, command: openToSideCommand, - when: ResourceContextKey.IsFileSystemResource + when: ContextKeyExpr.or(ResourceContextKey.IsFileSystemResource, ResourceContextKey.Scheme.isEqualTo(Schemas.untitled)) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -267,7 +266,19 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { title: SAVE_FILE_LABEL, precondition: DirtyEditorContext }, - when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) + when: ContextKeyExpr.or( + // Untitled Editors + ResourceContextKey.Scheme.isEqualTo(Schemas.untitled), + // Or: + ContextKeyExpr.and( + // Not: editor groups + OpenEditorsGroupContext.toNegated(), + // Not: readonly editors + SaveableEditorContext, + // Not: auto save after short delay + AutoSaveAfterShortDelayContext.toNegated() + ) + ) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -278,25 +289,28 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { title: nls.localize('revert', "Revert File"), precondition: DirtyEditorContext }, - when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) -}); - -MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { - group: '2_save', - command: { - id: SAVE_FILE_AS_COMMAND_ID, - title: SAVE_FILE_AS_LABEL - }, - when: ResourceContextKey.Scheme.isEqualTo(Schemas.untitled) + when: ContextKeyExpr.and( + // Not: editor groups + OpenEditorsGroupContext.toNegated(), + // Not: readonly editors + SaveableEditorContext, + // Not: untitled editors (revert closes them) + ResourceContextKey.Scheme.notEqualsTo(Schemas.untitled), + // Not: auto save after short delay + AutoSaveAfterShortDelayContext.toNegated() + ) }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { group: '2_save', + order: 30, command: { id: SAVE_ALL_IN_GROUP_COMMAND_ID, - title: nls.localize('saveAll', "Save All") + title: nls.localize('saveAll', "Save All"), + precondition: DirtyWorkingCopiesContext }, - when: ContextKeyExpr.and(OpenEditorsGroupContext, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo('')) + // Editor Group + when: OpenEditorsGroupContext }); MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { @@ -307,7 +321,7 @@ MenuRegistry.appendMenuItem(MenuId.OpenEditorsContext, { title: nls.localize('compareWithSaved', "Compare with Saved"), precondition: DirtyEditorContext }, - when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveContext.notEqualsTo('afterDelay') && AutoSaveContext.notEqualsTo(''), WorkbenchListDoubleSelection.toNegated()) + when: ContextKeyExpr.and(ResourceContextKey.IsFileSystemResource, AutoSaveAfterShortDelayContext.toNegated(), WorkbenchListDoubleSelection.toNegated()) }); const compareResourceCommand = { @@ -585,7 +599,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '4_save', command: { id: SaveAllAction.ID, - title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll") + title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll"), + precondition: DirtyWorkingCopiesContext }, order: 3 }); @@ -642,7 +657,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '6_close', command: { id: REVERT_FILE_COMMAND_ID, - title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File") + title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File"), + precondition: ContextKeyExpr.or(ActiveEditorIsSaveableContext, ContextKeyExpr.and(ExplorerViewletVisibleContext, SidebarFocusContext)) }, order: 1 }); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 3ccd49e88ffdd..72d5dc4669a17 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -44,7 +44,7 @@ import { onUnexpectedError, getErrorMessage } from 'vs/base/common/errors'; import { asDomUri, triggerDownload } from 'vs/base/browser/dom'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export const NEW_FILE_COMMAND_ID = 'explorer.newFile'; export const NEW_FILE_LABEL = nls.localize('newFile', "New File"); @@ -527,11 +527,11 @@ export abstract class BaseSaveAllAction extends Action { private registerListeners(): void { // update enablement based on working copy changes - this._register(this.workingCopyService.onDidChangeDirty(() => this.updateEnablement())); + this._register(this.workingCopyService.onDidChangeDirty(w => this.updateEnablement(w))); } - private updateEnablement(): void { - const hasDirty = this.workingCopyService.hasDirty; + private updateEnablement(workingCopy: IWorkingCopy): void { + const hasDirty = workingCopy.isDirty() || this.workingCopyService.hasDirty; if (this.lastIsDirty !== hasDirty) { this.enabled = hasDirty; this.lastIsDirty = this.enabled; diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 6e9e95d2866d4..a3c9bae7a01ba 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { toResource, IEditorCommandsContext, SideBySideEditor } from 'vs/workbench/common/editor'; +import { toResource, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier } from 'vs/workbench/common/editor'; import { IWindowOpenable, IOpenWindowOptions, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -14,17 +14,11 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, FilesExplorerFocusCondition } from 'vs/workbench/contrib/files/common/files'; import { ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ISaveOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IListService } from 'vs/platform/list/browser/listService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IFileService } from 'vs/platform/files/common/files'; -import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { IEditorViewState } from 'vs/editor/common/editorCommon'; -import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { isWindows } from 'vs/base/common/platform'; @@ -35,16 +29,13 @@ import { getMultiSelectedEditorContexts } from 'vs/workbench/browser/parts/edito import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService, SIDE_GROUP, ISaveEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService, GroupsOrder, EditorsOrder, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { basename, toLocalResource, joinPath, isEqual } from 'vs/base/common/resources'; +import { basename, joinPath, isEqual } from 'vs/base/common/resources'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { UNTITLED_WORKSPACE_NAME } from 'vs/platform/workspaces/common/workspaces'; -import { withUndefinedAsNull, withNullAsUndefined } from 'vs/base/common/types'; -import { assign } from 'vs/base/common/objects'; // Commands @@ -75,6 +66,7 @@ export const SAVE_FILES_COMMAND_ID = 'workbench.action.files.saveFiles'; export const OpenEditorsGroupContext = new RawContextKey('groupFocusedInOpenEditors', false); export const DirtyEditorContext = new RawContextKey('dirtyEditor', false); +export const SaveableEditorContext = new RawContextKey('saveableEditor', false); export const ResourceSelectedForCompareContext = new RawContextKey('resourceSelectedForCompare', false); export const REMOVE_ROOT_FOLDER_COMMAND_ID = 'removeRootFolder'; @@ -110,174 +102,8 @@ export const newWindowCommand = (accessor: ServicesAccessor, options?: IOpenEmpt hostService.openWindow(options); }; -async function save( - resource: URI | null, - isSaveAs: boolean, - options: ISaveOptions | undefined, - editorService: IEditorService, - fileService: IFileService, - untitledTextEditorService: IUntitledTextEditorService, - textFileService: ITextFileService, - editorGroupService: IEditorGroupsService, - environmentService: IWorkbenchEnvironmentService -): Promise { - if (!resource || (!fileService.canHandleResource(resource) && resource.scheme !== Schemas.untitled)) { - return; // save is not supported - } - - // Save As (or Save untitled with associated path) - if (isSaveAs || resource.scheme === Schemas.untitled) { - return doSaveAs(resource, isSaveAs, options, editorService, fileService, untitledTextEditorService, textFileService, editorGroupService, environmentService); - } - - // Pin the active editor if we are saving it - const activeControl = editorService.activeControl; - const activeEditorResource = activeControl?.input?.getResource(); - if (activeControl && activeEditorResource && isEqual(activeEditorResource, resource)) { - activeControl.group.pinEditor(activeControl.input); - } - - // Just save (force a change to the file to trigger external watchers if any) - options = assign({ force: true }, options || Object.create(null)); - - return textFileService.save(resource, options); -} - -async function doSaveAs( - resource: URI, - isSaveAs: boolean, - options: ISaveOptions | undefined, - editorService: IEditorService, - fileService: IFileService, - untitledTextEditorService: IUntitledTextEditorService, - textFileService: ITextFileService, - editorGroupService: IEditorGroupsService, - environmentService: IWorkbenchEnvironmentService -): Promise { - let viewStateOfSource: IEditorViewState | undefined = undefined; - const activeTextEditorWidget = getCodeEditor(editorService.activeTextEditorWidget); - if (activeTextEditorWidget) { - const activeResource = toResource(editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); - if (activeResource && (fileService.canHandleResource(activeResource) || resource.scheme === Schemas.untitled) && isEqual(activeResource, resource)) { - viewStateOfSource = withNullAsUndefined(activeTextEditorWidget.saveViewState()); - } - } - - // Special case: an untitled file with associated path gets saved directly unless "saveAs" is true - let target: URI | undefined; - if (!isSaveAs && resource.scheme === Schemas.untitled && untitledTextEditorService.hasAssociatedFilePath(resource)) { - const result = await textFileService.save(resource, options); - if (result) { - target = toLocalResource(resource, environmentService.configuration.remoteAuthority); - } - } - - // Otherwise, really "Save As..." - else { - - // Force a change to the file to trigger external watchers if any - // fixes https://github.com/Microsoft/vscode/issues/59655 - options = assign({ force: true }, options || Object.create(null)); - - target = await textFileService.saveAs(resource, undefined, options); - } - - if (!target || isEqual(target, resource)) { - return false; // save canceled or same resource used - } - - const replacement: IResourceInput = { - resource: target, - options: { - pinned: true, - viewState: viewStateOfSource - } - }; - - await Promise.all(editorGroupService.groups.map(group => - editorService.replaceEditors([{ - editor: { resource }, - replacement - }], group))); - - return true; -} - -async function saveAll(saveAllArguments: any, editorService: IEditorService, untitledTextEditorService: IUntitledTextEditorService, - textFileService: ITextFileService, editorGroupService: IEditorGroupsService): Promise { - - // Store some properties per untitled file to restore later after save is completed - const groupIdToUntitledResourceInput = new Map(); - - editorGroupService.groups.forEach(group => { - const activeEditorResource = group.activeEditor && group.activeEditor.getResource(); - group.editors.forEach(e => { - const resource = e.getResource(); - if (resource && untitledTextEditorService.isDirty(resource)) { - if (!groupIdToUntitledResourceInput.has(group.id)) { - groupIdToUntitledResourceInput.set(group.id, []); - } - - groupIdToUntitledResourceInput.get(group.id)!.push({ - encoding: untitledTextEditorService.getEncoding(resource), - resource, - options: { - inactive: activeEditorResource ? !isEqual(activeEditorResource, resource) : true, - pinned: true, - preserveFocus: true, - index: group.getIndexOfEditor(e) - } - }); - } - }); - }); - - // Save all - const result = await textFileService.saveAll(saveAllArguments); - - // Update untitled resources to the saved ones, so we open the proper files - groupIdToUntitledResourceInput.forEach((inputs, groupId) => { - inputs.forEach(i => { - const targetResult = result.results.filter(r => r.success && isEqual(r.source, i.resource)).pop(); - if (targetResult?.target) { - i.resource = targetResult.target; - } - }); - - editorService.openEditors(inputs, groupId); - }); -} - // Command registration -CommandsRegistry.registerCommand({ - id: REVERT_FILE_COMMAND_ID, - handler: async accessor => { - const notificationService = accessor.get(INotificationService); - const listService = accessor.get(IListService); - const editorGroupsService = accessor.get(IEditorGroupsService); - - const editors = getMultiSelectedEditors(listService, editorGroupsService); - if (editors.length) { - try { - await Promise.all(editors.map(async ({ groupId, editor }) => { - const resource = editor.getResource(); - if (resource && resource.scheme === Schemas.untitled) { - return; // we do not allow to revert untitled files - } - - // Use revert as a hint to pin the editor - editorGroupsService.getGroup(groupId)?.pinEditor(editor); - - return editor.revert({ force: true }); - })); - } catch (error) { - notificationService.error(nls.localize('genericRevertResourcesError', "Failed to revert '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); - } - } - } -}); - KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingWeight.WorkbenchContrib, when: ExplorerFocusCondition, @@ -293,10 +119,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // Set side input if (resources.length) { - const resolved = await fileService.resolveAll(resources.map(resource => ({ resource }))); + const untitledResources = resources.filter(resource => resource.scheme === Schemas.untitled); + const fileResources = resources.filter(resource => resource.scheme !== Schemas.untitled); + + const resolved = await fileService.resolveAll(fileResources.map(resource => ({ resource }))); const editors = resolved.filter(r => r.stat && r.success && !r.stat.isDirectory).map(r => ({ resource: r.stat!.resource - })); + })).concat(...untitledResources.map(untitledResource => ({ resource: untitledResource }))); await editorService.openEditors(editors, SIDE_GROUP); } @@ -478,59 +307,48 @@ CommandsRegistry.registerCommand({ } }); -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: SAVE_FILE_AS_COMMAND_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: undefined, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S, - handler: (accessor, resourceOrObject: URI | object | { from: string }) => { - const editorService = accessor.get(IEditorService); - let resource: URI | null = null; - if (resourceOrObject && 'from' in resourceOrObject && resourceOrObject.from === 'menu') { - resource = withUndefinedAsNull(toResource(editorService.activeEditor)); - } else { - resource = withUndefinedAsNull(getResourceForCommand(resourceOrObject, accessor.get(IListService), editorService)); +// Save / Save As / Save All / Revert + +function saveSelectedEditors(accessor: ServicesAccessor, options?: ISaveEditorsOptions): Promise { + const listService = accessor.get(IListService); + const editorGroupsService = accessor.get(IEditorGroupsService); + + const saveableEditors = getMultiSelectedEditors(listService, editorGroupsService).filter(({ editor }) => !editor.isReadonly()); + + return doSaveEditors(accessor, saveableEditors, options); +} + +function saveEditorsOfGroups(accessor: ServicesAccessor, groups = accessor.get(IEditorGroupsService).getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE), options?: ISaveEditorsOptions): Promise { + const saveableEditors: IEditorIdentifier[] = []; + for (const group of groups) { + for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + if (editor.isDirty()) { + saveableEditors.push({ groupId: group.id, editor }); + } } + } + + return doSaveEditors(accessor, saveableEditors, options); +} + +async function doSaveEditors(accessor: ServicesAccessor, editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { + const editorService = accessor.get(IEditorService); + const notificationService = accessor.get(INotificationService); - return save(resource, true, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IWorkbenchEnvironmentService)); + try { + await editorService.save(editors, options); + } catch (error) { + notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); } -}); +} KeybindingsRegistry.registerCommandAndKeybindingRule({ when: undefined, weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KEY_S, id: SAVE_FILE_COMMAND_ID, - handler: async (accessor, resource: URI | object) => { - const listService = accessor.get(IListService); - const editorGroupsService = accessor.get(IEditorGroupsService); - const notificationService = accessor.get(INotificationService); - - const editors = getMultiSelectedEditors(listService, editorGroupsService); - if (editors.length && !editors.some(({ editor }) => editor.getResource()?.scheme === Schemas.untitled)) { - try { - await Promise.all(editors.map(async ({ groupId, editor }) => { - - // Use save as a hint to pin the editor - editorGroupsService.getGroup(groupId)?.pinEditor(editor); - - return editor.save({ force: true }); - })); - } catch (error) { - notificationService.error(nls.localize('genericRevertResourcesError', "Failed to revert '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); - } - - return; - } - - const editorService = accessor.get(IEditorService); - const resources = getMultiSelectedResources(resource, listService, editorService); - - if (resources.length === 1) { - // If only one resource is selected explictly call save since the behavior is a bit different than save all #41841 - return save(resources[0], false, undefined, editorService, accessor.get(IFileService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IWorkbenchEnvironmentService)); - } - return saveAll(resources, editorService, accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService)); + handler: accessor => { + return saveSelectedEditors(accessor, { force: true }); } }); @@ -541,56 +359,87 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ win: { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S) }, id: SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, handler: accessor => { - const editorService = accessor.get(IEditorService); - - const resource = toResource(editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); - if (resource) { - return save(resource, false, { skipSaveParticipants: true }, editorService, accessor.get(IFileService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService), accessor.get(IWorkbenchEnvironmentService)); - } + return saveSelectedEditors(accessor, { force: true, skipSaveParticipants: true }); + } +}); - return undefined; +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: SAVE_FILE_AS_COMMAND_ID, + weight: KeybindingWeight.WorkbenchContrib, + when: undefined, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_S, + handler: accessor => { + return saveSelectedEditors(accessor, { saveAs: true }); } }); CommandsRegistry.registerCommand({ id: SAVE_ALL_COMMAND_ID, handler: (accessor) => { - return saveAll(true, accessor.get(IEditorService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService)); + return saveEditorsOfGroups(accessor); } }); CommandsRegistry.registerCommand({ id: SAVE_ALL_IN_GROUP_COMMAND_ID, handler: (accessor, _: URI | object, editorContext: IEditorCommandsContext) => { - const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService), accessor.get(IEditorGroupsService)); const editorGroupService = accessor.get(IEditorGroupsService); - let saveAllArg: any; + + const contexts = getMultiSelectedEditorContexts(editorContext, accessor.get(IListService), accessor.get(IEditorGroupsService)); + + let groups: IEditorGroup[] | undefined = undefined; if (!contexts.length) { - saveAllArg = true; + groups = [...editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)]; } else { - const fileService = accessor.get(IFileService); - saveAllArg = []; contexts.forEach(context => { const editorGroup = editorGroupService.getGroup(context.groupId); if (editorGroup) { - editorGroup.editors.forEach(editor => { - const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); - if (resource && (resource.scheme === Schemas.untitled || fileService.canHandleResource(resource))) { - saveAllArg.push(resource); - } - }); + if (!groups) { + groups = []; + } + + groups.push(editorGroup); } }); } - return saveAll(saveAllArg, accessor.get(IEditorService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService)); + return saveEditorsOfGroups(accessor, groups); } }); CommandsRegistry.registerCommand({ id: SAVE_FILES_COMMAND_ID, - handler: (accessor) => { - return saveAll(false, accessor.get(IEditorService), accessor.get(IUntitledTextEditorService), accessor.get(ITextFileService), accessor.get(IEditorGroupsService)); + handler: accessor => { + const editorService = accessor.get(IEditorService); + + return editorService.saveAll({ includeUntitled: false }); + } +}); + +CommandsRegistry.registerCommand({ + id: REVERT_FILE_COMMAND_ID, + handler: async accessor => { + const notificationService = accessor.get(INotificationService); + const listService = accessor.get(IListService); + const editorGroupsService = accessor.get(IEditorGroupsService); + + const editors = getMultiSelectedEditors(listService, editorGroupsService); + if (editors.length) { + try { + await Promise.all(editors.map(async ({ groupId, editor }) => { + if (editor.isUntitled()) { + return; // we do not allow to revert untitled editors + } + + // Use revert as a hint to pin the editor + editorGroupsService.getGroup(groupId)?.pinEditor(editor); + + return editor.revert({ force: true }); + })); + } catch (error) { + notificationService.error(nls.localize('genericRevertError', "Failed to revert '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false))); + } + } } }); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 59fba1cfeaf45..ee0dfa24cd331 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -16,7 +16,7 @@ import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactory import { AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; import { VIEWLET_ID, SortOrderConfiguration, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { FileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/fileEditorTracker'; -import { SaveErrorHandler } from 'vs/workbench/contrib/files/browser/saveErrorHandler'; +import { TextFileSaveErrorHandler } from 'vs/workbench/contrib/files/browser/textFileSaveErrorHandler'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { BinaryFileEditor } from 'vs/workbench/contrib/files/browser/editors/binaryFileEditor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -166,8 +166,8 @@ Registry.as(WorkbenchExtensions.Workbench).regi // Register File Editor Tracker Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileEditorTracker, LifecyclePhase.Starting); -// Register Save Error Handler -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SaveErrorHandler, LifecyclePhase.Starting); +// Register Text File Save Error Handler +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TextFileSaveErrorHandler, LifecyclePhase.Starting); // Register uri display for file uris Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileUriLabelContribution, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts similarity index 98% rename from src/vs/workbench/contrib/files/browser/saveErrorHandler.ts rename to src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts index 635033681e1eb..f4fdf7261cf96 100644 --- a/src/vs/workbench/contrib/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/textFileSaveErrorHandler.ts @@ -41,8 +41,8 @@ const LEARN_MORE_DIRTY_WRITE_IGNORE_KEY = 'learnMoreDirtyWriteError'; const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the editor tool bar to either undo your changes or overwrite the content of the file with your changes."); -// A handler for save error happening with conflict resolution actions -export class SaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution { +// A handler for text file save error happening with conflict resolution actions +export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution { private messages: ResourceMap; private conflictResolutionContext: IContextKey; private activeConflictResolutionResource?: URI; diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 8b93f82c51a1f..f0786144ffe07 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -30,7 +30,7 @@ import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; -import { DirtyEditorContext, OpenEditorsGroupContext } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { DirtyEditorContext, OpenEditorsGroupContext, SaveableEditorContext } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { ResourcesDropHandler, fillResourceDataTransfers, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; @@ -61,6 +61,7 @@ export class OpenEditorsView extends ViewletPanel { private resourceContext!: ResourceContextKey; private groupFocusedContext!: IContextKey; private dirtyEditorFocusedContext!: IContextKey; + private saveableEditorFocusedContext!: IContextKey; constructor( options: IViewletViewOptions, @@ -231,16 +232,19 @@ export class OpenEditorsView extends ViewletPanel { this._register(this.resourceContext); this.groupFocusedContext = OpenEditorsGroupContext.bindTo(this.contextKeyService); this.dirtyEditorFocusedContext = DirtyEditorContext.bindTo(this.contextKeyService); + this.saveableEditorFocusedContext = SaveableEditorContext.bindTo(this.contextKeyService); this._register(this.list.onContextMenu(e => this.onListContextMenu(e))); this.list.onFocusChange(e => { this.resourceContext.reset(); this.groupFocusedContext.reset(); this.dirtyEditorFocusedContext.reset(); + this.saveableEditorFocusedContext.reset(); const element = e.elements.length ? e.elements[0] : undefined; if (element instanceof OpenEditor) { const resource = element.getResource(); this.dirtyEditorFocusedContext.set(element.editor.isDirty()); + this.saveableEditorFocusedContext.set(!element.editor.isReadonly()); this.resourceContext.set(withUndefinedAsNull(resource)); } else if (!!element) { this.groupFocusedContext.set(true); @@ -407,7 +411,7 @@ export class OpenEditorsView extends ViewletPanel { } private updateDirtyIndicator(): void { - let dirty = this.dirtyCount; + let dirty = this.workingCopyService.dirtyCount; if (dirty === 0) { dom.addClass(this.dirtyCountElement, 'hidden'); } else { @@ -416,18 +420,6 @@ export class OpenEditorsView extends ViewletPanel { } } - private get dirtyCount(): number { - let dirtyCount = 0; - - for (const element of this.elements) { - if (element instanceof OpenEditor && element.editor.isDirty()) { - dirtyCount++; - } - } - - return dirtyCount; - } - private get elementCount(): number { return this.editorGroupService.groups.map(g => g.count) .reduce((first, second) => first + second, this.showGroups ? this.editorGroupService.groups.length : 0); diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index b6aa3d45ac279..83ba201f267c7 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -7,18 +7,20 @@ import { localize } from 'vs/nls'; import { createMemoizer } from 'vs/base/common/decorators'; import { dirname } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { EncodingMode, EditorInput, IFileEditorInput, ITextEditorModel, Verbosity } from 'vs/workbench/common/editor'; +import { EncodingMode, IFileEditorInput, ITextEditorModel, Verbosity, TextEditorInput } from 'vs/workbench/common/editor'; import { IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; -import { ITextFileService, ModelState, TextFileModelChangeEvent, LoadReason, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { FileOperationError, FileOperationResult, IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { ITextFileService, ModelState, TextFileModelChangeEvent, LoadReason, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IReference } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { FILE_EDITOR_INPUT_ID, TEXT_FILE_EDITOR_ID, BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const enum ForceOpenAs { None, @@ -29,7 +31,7 @@ const enum ForceOpenAs { /** * A file editor input is the input type for the file editor of file system resources. */ -export class FileEditorInput extends EditorInput implements IFileEditorInput { +export class FileEditorInput extends TextEditorInput implements IFileEditorInput { private static readonly MEMOIZER = createMemoizer(); @@ -40,21 +42,20 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { private textModelReference: Promise> | null = null; - /** - * An editor input who's contents are retrieved from file services. - */ constructor( - private resource: URI, + resource: URI, preferredEncoding: string | undefined, preferredMode: string | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextFileService private readonly textFileService: ITextFileService, + @ITextFileService textFileService: ITextFileService, @ITextModelService private readonly textModelResolverService: ITextModelService, @ILabelService private readonly labelService: ILabelService, @IFileService private readonly fileService: IFileService, - @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService + @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, + @IEditorService editorService: IEditorService, + @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(); + super(resource, editorService, editorGroupService, textFileService); if (preferredEncoding) { this.setPreferredEncoding(preferredEncoding); @@ -92,10 +93,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } } - getResource(): URI { - return this.resource; - } - getEncoding(): string | undefined { const textModel = this.textFileService.models.get(this.resource); if (textModel) { @@ -227,6 +224,10 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return label; } + isReadonly(): boolean { + return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + } + isDirty(): boolean { const model = this.textFileService.models.get(this.resource); if (!model) { @@ -244,10 +245,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { return model.isDirty(); } - save(options?: ITextFileSaveOptions): Promise { - return this.textFileService.save(this.resource, options); - } - revert(options?: IRevertOptions): Promise { return this.textFileService.revert(this.resource, options); } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index d64d2c4ce38b0..987d0ad21a680 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -1234,7 +1234,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private executeTask(task: Task, resolver: ITaskResolver): Promise { return ProblemMatcherRegistry.onReady().then(() => { - return this.textFileService.saveAll().then((value) => { // make sure all dirty files are saved + return this.editorService.saveAll().then((value) => { // make sure all dirty editors are saved let executeResult = this.getTaskSystem().run(task, resolver); return this.handleExecuteResult(executeResult); }); @@ -2164,7 +2164,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } ProblemMatcherRegistry.onReady().then(() => { - return this.textFileService.saveAll().then((value) => { // make sure all dirty files are saved + return this.editorService.saveAll().then((value) => { // make sure all dirty editors are saved let executeResult = this.getTaskSystem().rerun(); if (executeResult) { return this.handleExecuteResult(executeResult); diff --git a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts b/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts index e2f0bfcc1f7d7..b3602d62760a2 100644 --- a/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts +++ b/src/vs/workbench/contrib/testCustomEditors/browser/testCustomEditors.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, EditorOptions } from 'vs/workbench/common/editor'; +import { IEditorInputFactory, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, EditorModel, EditorOptions, GroupIdentifier } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { Dimension, addDisposableListener, EventType } from 'vs/base/browser/dom'; @@ -160,6 +160,10 @@ class TestCustomEditorInput extends EditorInput implements IWorkingCopy { } } + isReadonly(): boolean { + return false; + } + isDirty(): boolean { return this.dirty; } @@ -170,6 +174,12 @@ class TestCustomEditorInput extends EditorInput implements IWorkingCopy { return true; } + async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + this.setDirty(false); + + return true; + } + async revert(options?: IRevertOptions): Promise { this.setDirty(false); diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 3afa7519ab93e..5e123f78eaab0 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -32,9 +32,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ICommandHandler } from 'vs/platform/commands/common/commands'; -import { ITextFileService, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { toResource } from 'vs/workbench/common/editor'; import { normalizeDriveLetter } from 'vs/base/common/labels'; export namespace OpenLocalFileCommand { @@ -53,13 +51,12 @@ export namespace SaveLocalFileCommand { export const LABEL = nls.localize('saveLocalFile', "Save Local File..."); export function handler(): ICommandHandler { return accessor => { - const textFileService = accessor.get(ITextFileService); const editorService = accessor.get(IEditorService); - let resource: URI | undefined = toResource(editorService.activeEditor); - const options: ITextFileSaveOptions = { force: true, availableFileSystems: [Schemas.file] }; - if (resource) { - return textFileService.saveAs(resource, undefined, options); + const activeControl = editorService.activeControl; + if (activeControl) { + return editorService.save({ groupId: activeControl.group.id, editor: activeControl.input }, { saveAs: true, availableFileSystems: [Schemas.file] }); } + return Promise.resolve(undefined); }; } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 7f48cf25e5732..3140d640aba32 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -18,8 +18,8 @@ import { URI } from 'vs/base/common/uri'; import { basename, isEqual } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { localize } from 'vs/nls'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection, EditorsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { coalesce } from 'vs/base/common/arrays'; @@ -28,6 +28,7 @@ import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/wor import { ILabelService } from 'vs/platform/label/common/label'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; type CachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput; type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE; @@ -654,6 +655,93 @@ export class EditorService extends Disposable implements EditorServiceImpl { } //#endregion + + //#region save + + async save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { + + // Convert to array + if (!Array.isArray(editors)) { + editors = [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 editorsToSaveAsSequentially: IEditorIdentifier[] = []; + if (options?.saveAs) { + editorsToSaveAsSequentially.push(...editors); + } else { + for (const { groupId, editor } of editors) { + if (editor.isUntitled()) { + editorsToSaveAsSequentially.push({ groupId, editor }); + } else { + editorsToSaveParallel.push({ groupId, editor }); + } + } + } + + // Editors to save in parallel + await Promise.all(editorsToSaveParallel.map(({ groupId, editor }) => { + + // Use save as a hint to pin the editor + this.editorGroupService.getGroup(groupId)?.pinEditor(editor); + + // Save + return editor.save(options); + })); + + // Editors to save sequentially + for (const { groupId, editor } of editorsToSaveAsSequentially) { + if (editor.isDisposed()) { + continue; // might have been disposed from from the save already + } + + const result = await editor.saveAs(groupId, options); + if (!result) { + return false; // failed or cancelled, abort + } + } + + return true; + } + + saveAll(options?: ISaveAllEditorsOptions): Promise { + const editors: IEditorIdentifier[] = []; + + // Collect all editors in MRU order that are dirty + this.forEachDirtyEditor(({ groupId, editor }) => { + if (!editor.isUntitled() || options?.includeUntitled) { + editors.push({ groupId, editor }); + } + }); + + return this.save(editors, options); + } + + async revertAll(options?: IRevertOptions): Promise { + + // Revert each editor in MRU order + const reverts: Promise[] = []; + this.forEachDirtyEditor(({ editor }) => reverts.push(editor.revert(options))); + + await Promise.all(reverts); + } + + private forEachDirtyEditor(callback: (editor: IEditorIdentifier) => void): void { + for (const group of this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { + for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + if (editor.isDirty()) { + callback({ groupId: group.id, editor }); + } + } + } + } + + //#endregion } export interface IEditorOpenHandler { diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index e6411b1adb8f3..2fb95f9f8ec82 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -5,11 +5,12 @@ import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IEditorIdentifier } from 'vs/workbench/common/editor'; import { Event } from 'vs/base/common/event'; import { IEditor as ICodeEditor } from 'vs/editor/common/editorCommon'; import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ISaveOptions, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; export const IEditorService = createDecorator('editorService'); @@ -44,6 +45,22 @@ export interface IVisibleEditor extends IEditor { group: IEditorGroup; } +export interface ISaveEditorsOptions extends ISaveOptions { + + /** + * If true, will ask for a location of the editor to save to. + */ + saveAs?: boolean; +} + +export interface ISaveAllEditorsOptions extends ISaveEditorsOptions { + + /** + * Wether to include untitled editors as well. + */ + includeUntitled?: boolean; +} + export interface IEditorService { _serviceBrand: undefined; @@ -185,4 +202,19 @@ export interface IEditorService { * Converts a lightweight input to a workbench editor input. */ createInput(input: IResourceEditor): IEditorInput | null; + + /** + * Save the provided list of editors. + */ + save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise; + + /** + * Save all editors. + */ + saveAll(options?: ISaveAllEditorsOptions): Promise; + + /** + * Reverts all editors. + */ + revertAll(options?: IRevertOptions): Promise; } diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index a982e0161fc3d..6595d5b8ed002 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -14,7 +14,7 @@ import { isUndefinedOrNull } from 'vs/base/common/types'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { equals } from 'vs/base/common/objects'; -export const AutoSaveContext = new RawContextKey('config.files.autoSave', undefined); +export const AutoSaveAfterShortDelayContext = new RawContextKey('autoSaveAfterShortDelayContext', false); export interface IAutoSaveConfiguration { autoSaveDelay?: number; @@ -69,7 +69,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi private configuredAutoSaveOnFocusChange: boolean | undefined; private configuredAutoSaveOnWindowChange: boolean | undefined; - private autoSaveContext: IContextKey; + private autoSaveAfterShortDelayContext: IContextKey; private currentFilesAssociationConfig: { [key: string]: string; }; @@ -82,7 +82,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi ) { super(); - this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService); + this.autoSaveAfterShortDelayContext = AutoSaveAfterShortDelayContext.bindTo(contextKeyService); const configuration = configurationService.getValue(); @@ -108,7 +108,6 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi // Auto Save const autoSaveMode = configuration?.files?.autoSave || AutoSaveConfiguration.OFF; - this.autoSaveContext.set(autoSaveMode); switch (autoSaveMode) { case AutoSaveConfiguration.AFTER_DELAY: this.configuredAutoSaveDelay = configuration?.files?.autoSaveDelay; @@ -135,6 +134,8 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi break; } + this.autoSaveAfterShortDelayContext.set(this.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY); + // Emit as event this._onAutoSaveConfigurationChange.fire(this.getAutoSaveConfiguration()); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 429b7460fa6f5..5c3bb1e5f2e7b 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -482,10 +482,10 @@ export class HistoryService extends Disposable implements IHistoryService { } private handleEditorEventInHistory(editor?: IBaseEditor): void { - const input = editor?.input; - // Ensure we have not configured to exclude input - if (!input || !this.include(input)) { + // Ensure we have not configured to exclude input and don't track invalid inputs + const input = editor?.input; + if (!input || input.isDisposed() || !this.include(input)) { return; } @@ -592,10 +592,10 @@ export class HistoryService extends Disposable implements IHistoryService { // stack but we need to keep our currentTextEditorState up to date with // the navigtion that occurs. if (this.navigatingInStack) { - if (codeEditor && control?.input) { + if (codeEditor && control?.input && !control.input.isDisposed()) { this.currentTextEditorState = new TextEditorState(control.input, codeEditor.getSelection()); } else { - this.currentTextEditorState = null; // we navigated to a non text editor + this.currentTextEditorState = null; // we navigated to a non text or disposed editor } } @@ -603,15 +603,15 @@ export class HistoryService extends Disposable implements IHistoryService { else { // navigation inside text editor - if (codeEditor && control?.input) { + if (codeEditor && control?.input && !control.input.isDisposed()) { this.handleTextEditorEvent(control, codeEditor, event); } - // navigation to non-text editor + // navigation to non-text disposed editor else { this.currentTextEditorState = null; // at this time we have no active text editor view state - if (control?.input) { + if (control?.input && !control.input.isDisposed()) { this.handleNonTextEditorEvent(control); } } diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 78c9bc6dcb887..b29d7dc560db8 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -418,7 +418,6 @@ export interface ITextFileSaveOptions extends ISaveOptions { overwriteReadonly?: boolean; overwriteEncoding?: boolean; writeElevated?: boolean; - availableFileSystems?: readonly string[]; } export interface ILoadOptions { diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts index fcb13e5c16cb4..32af9851cea27 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorService.ts @@ -160,7 +160,6 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe untitledInputs.forEach(input => { if (input) { input.revert(); - input.dispose(); reverted.push(input.getResource()); } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index d01206271bb3a..ecac6c3a87742 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -59,6 +59,11 @@ export interface ISaveOptions { * Instructs the save operation to skip any save participants. */ skipSaveParticipants?: boolean; + + /** + * A hint as to which file systems should be available for saving. + */ + availableFileSystems?: string[]; } export interface IRevertOptions { diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 75fbbbc3a3a47..6268b95feeea1 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -57,7 +57,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, EditorsOrder, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService, IOpenEditorOverrideHandler, IVisibleEditor } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, IOpenEditorOverrideHandler, IVisibleEditor, ISaveEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; @@ -92,7 +92,7 @@ import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/ele import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { find } from 'vs/base/common/arrays'; -import { WorkingCopyService, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { WorkingCopyService, IWorkingCopyService, IRevertOptions } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { @@ -921,6 +921,18 @@ export class TestEditorService implements EditorServiceImpl { createInput(_input: IResourceInput | IUntitledTextResourceInput | IResourceDiffInput | IResourceSideBySideInput): IEditorInput { throw new Error('not implemented'); } + + save(editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { + throw new Error('Method not implemented.'); + } + + saveAll(options?: ISaveEditorsOptions): Promise { + throw new Error('Method not implemented.'); + } + + revertAll(options?: IRevertOptions): Promise { + throw new Error('Method not implemented.'); + } } export class TestFileService implements IFileService {