提交 00688bf0 编写于 作者: B Benjamin Pasero

working copies - properly implement save, saveAs, saveAll (#84672)

上级 40a67d11
......@@ -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<boolean> {
return this._textFileService.saveAll(includeUntitled).then(result => {
return result.results.every(each => each.success === true);
});
return this._editorService.saveAll({ includeUntitled });
}
$resolveProxy(url: string): Promise<string | undefined> {
......
......@@ -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<boolean>('isMac', isMacintosh);
export const IsLinuxContext = new RawContextKey<boolean>('isLinux', isLinux);
......@@ -51,6 +50,8 @@ export const IsFullscreenContext = new RawContextKey<boolean>('isFullscreen', fa
export class WorkbenchContextKeysHandler extends Disposable {
private inputFocusedContext: IContextKey<boolean>;
private dirtyWorkingCopiesContext: IContextKey<boolean>;
private activeEditorContext: IContextKey<string | null>;
private activeEditorIsSaveable: IContextKey<boolean>;
......@@ -75,19 +76,18 @@ export class WorkbenchContextKeysHandler extends Disposable {
private panelPositionContext: IContextKey<string>;
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();
......
......@@ -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<any> {
......@@ -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<any> {
......
......@@ -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)) {
......
......@@ -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<boolean>('dirtyWorkingCopies', false);
export const ActiveEditorContext = new RawContextKey<string | null>('activeEditor', null);
export const ActiveEditorIsSaveableContext = new RawContextKey<boolean>('activeEditorIsSaveable', false);
export const EditorsVisibleContext = new RawContextKey<boolean>('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<IEditorModel | null>;
/**
* 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<boolean>;
/**
* 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<boolean>;
/**
* 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<IEditorModel | null>;
/**
* 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<boolean> {
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<boolean> {
return this.textFileService.save(this.resource, options);
}
saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise<boolean> {
return this.doSaveAs(group, options);
}
protected async doSaveAs(group: GroupIdentifier, options?: ITextFileSaveOptions, replaceAllEditors?: boolean): Promise<boolean> {
// 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<boolean> {
return this.master.saveAs(groupId, options);
}
revert(options?: IRevertOptions): Promise<boolean> {
return this.master.revert(options);
}
......
......@@ -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<UntitledTextEditorModel & IResolvedTextEditorModel> | null = null;
private readonly _onDidModelChangeContent: Emitter<void> = this._register(new Emitter<void>());
readonly onDidModelChangeContent: Event<void> = this._onDidModelChangeContent.event;
private readonly _onDidModelChangeContent = this._register(new Emitter<void>());
readonly onDidModelChangeContent = this._onDidModelChangeContent.event;
private readonly _onDidModelChangeEncoding: Emitter<void> = this._register(new Emitter<void>());
readonly onDidModelChangeEncoding: Event<void> = this._onDidModelChangeEncoding.event;
private readonly _onDidModelChangeEncoding = this._register(new Emitter<void>());
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<boolean> {
return this.textFileService.save(this.resource, options);
saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise<boolean> {
return this.doSaveAs(group, options, true /* replace editor across all groups */);
}
revert(options?: IRevertOptions): Promise<boolean> {
......
......@@ -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<boolean> {
// TODO@matt implement properly (see TextEditorInput#saveAs())
return this._model ? this._model.save(options) : Promise.resolve(false);
}
public revert(options?: IRevertOptions): Promise<boolean> {
return this._model ? this._model.revert(options) : Promise.resolve(false);
}
......
......@@ -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<any> {
await this.textFileService.saveAll();
await this.editorService.saveAll();
const isAutoRestart = !!restartData;
const runTasks: () => Promise<TaskRunResult> = async () => {
......
......@@ -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
});
......
......@@ -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;
......
......@@ -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<boolean>('groupFocusedInOpenEditors', false);
export const DirtyEditorContext = new RawContextKey<boolean>('dirtyEditor', false);
export const SaveableEditorContext = new RawContextKey<boolean>('saveableEditor', false);
export const ResourceSelectedForCompareContext = new RawContextKey<boolean>('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<any> {
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<boolean> {
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<any> {
// Store some properties per untitled file to restore later after save is completed
const groupIdToUntitledResourceInput = new Map<number, IResourceInput[]>();
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<void> {
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<void> {
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<void> {
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)));
}
}
}
});
......
......@@ -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<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).regi
// Register File Editor Tracker
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileEditorTracker, LifecyclePhase.Starting);
// Register Save Error Handler
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SaveErrorHandler, LifecyclePhase.Starting);
// Register Text File Save Error Handler
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TextFileSaveErrorHandler, LifecyclePhase.Starting);
// Register uri display for file uris
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileUriLabelContribution, LifecyclePhase.Starting);
......
......@@ -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<INotificationHandle>;
private conflictResolutionContext: IContextKey<boolean>;
private activeConflictResolutionResource?: URI;
......
......@@ -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<boolean>;
private dirtyEditorFocusedContext!: IContextKey<boolean>;
private saveableEditorFocusedContext!: IContextKey<boolean>;
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);
......
......@@ -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<IReference<ITextEditorModel>> | 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<boolean> {
return this.textFileService.save(this.resource, options);
}
revert(options?: IRevertOptions): Promise<boolean> {
return this.textFileService.revert(this.resource, options);
}
......
......@@ -1234,7 +1234,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer
private executeTask(task: Task, resolver: ITaskResolver): Promise<ITaskSummary> {
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);
......
......@@ -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<boolean> {
this.setDirty(false);
return true;
}
async revert(options?: IRevertOptions): Promise<boolean> {
this.setDirty(false);
......
......@@ -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);
};
}
......
......@@ -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<boolean> {
// 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<boolean> {
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<void> {
// Revert each editor in MRU order
const reverts: Promise<boolean>[] = [];
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 {
......
......@@ -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<IEditorService>('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<boolean>;
/**
* Save all editors.
*/
saveAll(options?: ISaveAllEditorsOptions): Promise<boolean>;
/**
* Reverts all editors.
*/
revertAll(options?: IRevertOptions): Promise<void>;
}
......@@ -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<string>('config.files.autoSave', undefined);
export const AutoSaveAfterShortDelayContext = new RawContextKey<boolean>('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<string>;
private autoSaveAfterShortDelayContext: IContextKey<boolean>;
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<IFilesConfiguration>();
......@@ -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());
......
......@@ -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);
}
}
......
......@@ -418,7 +418,6 @@ export interface ITextFileSaveOptions extends ISaveOptions {
overwriteReadonly?: boolean;
overwriteEncoding?: boolean;
writeElevated?: boolean;
availableFileSystems?: readonly string[];
}
export interface ILoadOptions {
......
......@@ -160,7 +160,6 @@ export class UntitledTextEditorService extends Disposable implements IUntitledTe
untitledInputs.forEach(input => {
if (input) {
input.revert();
input.dispose();
reverted.push(input.getResource());
}
......
......@@ -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 {
......
......@@ -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<boolean> {
throw new Error('Method not implemented.');
}
saveAll(options?: ISaveEditorsOptions): Promise<boolean> {
throw new Error('Method not implemented.');
}
revertAll(options?: IRevertOptions): Promise<void> {
throw new Error('Method not implemented.');
}
}
export class TestFileService implements IFileService {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册