From 2ff10468b3dd6965058550b0df44bbeef08ba8d7 Mon Sep 17 00:00:00 2001
From: Colin Grant <colin.grant@ericsson.com>
Date: Wed, 9 Mar 2022 08:34:26 -0700
Subject: [PATCH 1/8] Make untitled resources available in Core

---
 .../browser/common-frontend-contribution.ts   |  41 +++++-
 .../core/src/browser/decorations-service.ts   |   4 +-
 .../browser/frontend-application-module.ts    |   2 +
 .../core/src/browser/save-resource-service.ts |  31 +++-
 packages/core/src/browser/saveable.ts         |   5 -
 .../src/browser/shell/application-shell.ts    |  16 +-
 .../untitled-file-location-provider.ts        |  55 +++++++
 packages/core/src/common/resource.ts          |  79 ++++++++++
 .../src/browser/editor-preview-widget.ts      |  10 +-
 packages/editor/src/browser/editor-widget.ts  |   6 +-
 .../file-dialog/file-dialog-service.ts        |  12 +-
 .../src/browser/filesystem-frontend-module.ts |   9 +-
 .../filesystem-save-resource-service.ts       | 130 ++++++++++++++++
 .../monaco/src/browser/monaco-editor-model.ts |   5 +-
 .../monaco/src/browser/monaco-languages.ts    |   4 +
 .../src/browser/navigator-contribution.ts     |   6 +-
 .../plugin-vscode-commands-contribution.ts    |   6 -
 .../plugin-ext/src/common/rpc-protocol.ts     |   2 +-
 .../src/main/browser/documents-main.ts        |   6 +-
 .../main/browser/editor/untitled-resource.ts  | 100 +------------
 .../src/main/browser/main-context.ts          |   4 +-
 .../src/browser/workspace-commands.ts         |  12 +-
 .../workspace-file-dialog-root-provider.ts    |  30 ++++
 .../workspace-frontend-contribution.ts        | 139 ++----------------
 .../src/browser/workspace-frontend-module.ts  |  13 +-
 .../workspace-save-resource-service.ts        |  50 -------
 ...rkspace-untitled-file-location-provider.ts |  49 ++++++
 27 files changed, 490 insertions(+), 336 deletions(-)
 create mode 100644 packages/core/src/browser/untitled-file-location-provider.ts
 create mode 100644 packages/filesystem/src/browser/filesystem-save-resource-service.ts
 create mode 100644 packages/workspace/src/browser/workspace-file-dialog-root-provider.ts
 delete mode 100644 packages/workspace/src/browser/workspace-save-resource-service.ts
 create mode 100644 packages/workspace/src/browser/workspace-untitled-file-location-provider.ts

diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts
index cdaa905e49ccf..d9e7e33b49c5b 100644
--- a/packages/core/src/browser/common-frontend-contribution.ts
+++ b/packages/core/src/browser/common-frontend-contribution.ts
@@ -61,6 +61,7 @@ import { FrontendApplicationConfigProvider } from './frontend-application-config
 import { DecorationStyle } from './decoration-style';
 import { isPinned, Title, togglePinned, Widget } from './widgets';
 import { SaveResourceService } from './save-resource-service';
+import { UntitledFileLocationProvider } from './untitled-file-location-provider';
 
 export namespace CommonMenus {
 
@@ -268,12 +269,21 @@ export namespace CommonCommands {
         category: VIEW_CATEGORY,
         label: 'Show Menu Bar'
     });
-
+    export const NEW_FILE = Command.toDefaultLocalizedCommand({
+        id: 'workbench.action.files.newUntitledFile',
+        category: FILE_CATEGORY,
+        label: 'New File'
+    });
     export const SAVE = Command.toDefaultLocalizedCommand({
         id: 'core.save',
         category: FILE_CATEGORY,
         label: 'Save',
     });
+    export const SAVE_AS = Command.toDefaultLocalizedCommand({
+        id: 'file.saveAs',
+        category: FILE_CATEGORY,
+        label: 'Save As...',
+    });
     export const SAVE_WITHOUT_FORMATTING = Command.toDefaultLocalizedCommand({
         id: 'core.saveWithoutFormatting',
         category: FILE_CATEGORY,
@@ -385,6 +395,9 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
     @inject(WindowService)
     protected readonly windowService: WindowService;
 
+    @inject(UntitledFileLocationProvider)
+    protected readonly untitledFileLocationProvider: UntitledFileLocationProvider;
+
     protected pinnedKey: ContextKey<boolean>;
 
     async configure(app: FrontendApplication): Promise<void> {
@@ -697,6 +710,11 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
         });
 
         registry.registerSubmenu(CommonMenus.VIEW_APPEARANCE_SUBMENU, nls.localizeByDefault('Appearance'));
+
+        registry.registerMenuAction(CommonMenus.FILE_NEW, {
+            commandId: CommonCommands.NEW_FILE.id,
+            order: 'a'
+        });
     }
 
     registerCommands(commandRegistry: CommandRegistry): void {
@@ -889,10 +907,22 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
                 }
             }
         });
-
         commandRegistry.registerCommand(CommonCommands.SAVE, {
             execute: () => this.save({ formatType: FormatType.ON })
         });
+        commandRegistry.registerCommand(CommonCommands.SAVE_AS, {
+            isEnabled: () => this.saveResourceService.canSaveAs(this.shell.currentWidget),
+            execute: () => {
+                const { currentWidget } = this.shell;
+                // No clue what could have happened between `isEnabled` and `execute`
+                // when fetching currentWidget, so better to double-check:
+                if (this.saveResourceService.canSaveAs(currentWidget)) {
+                    this.saveResourceService.saveAs(currentWidget);
+                } else {
+                    this.messageService.error(nls.localize('theia/workspace/failSaveAs', 'Cannot run "{0}" for the current widget.', CommonCommands.SAVE_AS.label!));
+                }
+            },
+        });
         commandRegistry.registerCommand(CommonCommands.SAVE_WITHOUT_FORMATTING, {
             execute: () => this.save({ formatType: FormatType.OFF })
         });
@@ -924,6 +954,9 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
         commandRegistry.registerCommand(CommonCommands.CONFIGURE_DISPLAY_LANGUAGE, {
             execute: () => this.configureDisplayLanguage()
         });
+        commandRegistry.registerCommand(CommonCommands.NEW_FILE, {
+            execute: async () => open(this.openerService, await this.untitledFileLocationProvider.getUntitledFileLocation())
+        });
     }
 
     protected isElectron(): boolean {
@@ -1056,6 +1089,10 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
                 command: CommonCommands.UNPIN_TAB.id,
                 keybinding: 'ctrlcmd+k shift+enter',
                 when: 'activeEditorIsPinned'
+            },
+            {
+                command: CommonCommands.NEW_FILE.id,
+                keybinding: this.isElectron() ? 'ctrlcmd+n' : 'alt+n',
             }
         );
     }
diff --git a/packages/core/src/browser/decorations-service.ts b/packages/core/src/browser/decorations-service.ts
index d73abeda00260..f0c4b6c7228c7 100644
--- a/packages/core/src/browser/decorations-service.ts
+++ b/packages/core/src/browser/decorations-service.ts
@@ -48,7 +48,7 @@ export interface DecorationsService {
 
     registerDecorationsProvider(provider: DecorationsProvider): Disposable;
 
-    getDecoration(uri: URI, includeChildren: boolean): Decoration [];
+    getDecoration(uri: URI, includeChildren: boolean): Decoration[];
 }
 
 class DecorationDataRequest {
@@ -198,7 +198,7 @@ export class DecorationsServiceImpl implements DecorationsService {
         });
     }
 
-    getDecoration(uri: URI, includeChildren: boolean): Decoration [] {
+    getDecoration(uri: URI, includeChildren: boolean): Decoration[] {
         const data: Decoration[] = [];
         let containsChildren: boolean = false;
         for (const wrapper of this.data) {
diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts
index 0953a71339ace..bcc4694871344 100644
--- a/packages/core/src/browser/frontend-application-module.ts
+++ b/packages/core/src/browser/frontend-application-module.ts
@@ -121,6 +121,7 @@ import { RendererHost } from './widgets';
 import { TooltipService, TooltipServiceImpl } from './tooltip-service';
 import { bindFrontendStopwatch, bindBackendStopwatch } from './performance';
 import { SaveResourceService } from './save-resource-service';
+import { UntitledFileLocationProvider } from './untitled-file-location-provider';
 
 export { bindResourceProvider, bindMessageService, bindPreferenceService };
 
@@ -398,4 +399,5 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
     bindBackendStopwatch(bind);
 
     bind(SaveResourceService).toSelf().inSingletonScope();
+    bind(UntitledFileLocationProvider).toSelf().inSingletonScope();
 });
diff --git a/packages/core/src/browser/save-resource-service.ts b/packages/core/src/browser/save-resource-service.ts
index 31d267aa8e960..3ec83f2387baa 100644
--- a/packages/core/src/browser/save-resource-service.ts
+++ b/packages/core/src/browser/save-resource-service.ts
@@ -14,19 +14,26 @@
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
 
-import { injectable } from 'inversify';
-import { Saveable, SaveOptions } from './saveable';
+import { inject, injectable } from 'inversify';
+import { MessageService, UNTITLED_SCHEME } from '../common';
+import { Navigatable, NavigatableWidget } from './navigatable-types';
+import { Saveable, SaveableSource, SaveOptions } from './saveable';
 import { Widget } from './widgets';
 
 @injectable()
 export class SaveResourceService {
+    @inject(MessageService) protected readonly messageService: MessageService;
 
     /**
      * Indicate if the document can be saved ('Save' command should be disable if not).
      */
-     canSave(saveable: Saveable): boolean {
+    canSave(widget?: Widget): widget is Widget & (Saveable | SaveableSource) {
+        return this.canSaveNotSaveAs(widget) || this.canSaveAs(widget);
+    }
+
+    protected canSaveNotSaveAs(widget?: Widget): widget is Widget & (Saveable | SaveableSource) {
         // By default, we never allow a document to be saved if it is untitled.
-        return Saveable.isDirty(saveable) && !Saveable.isUntitled(saveable);
+        return Boolean(widget && Saveable.isDirty(widget) && NavigatableWidget.getUri(widget)?.scheme !== UNTITLED_SCHEME);
     }
 
     /**
@@ -36,10 +43,20 @@ export class SaveResourceService {
      * and is thus saveable.
      */
     async save(widget: Widget | undefined, options?: SaveOptions): Promise<void> {
-        const saveable = Saveable.get(widget);
-        if (saveable && this.canSave(saveable)) {
-            await saveable.save(options);
+        if (this.canSaveNotSaveAs(widget)) {
+            await Saveable.save(widget, options);
+        } else if (this.canSaveAs(widget)) {
+            await this.saveAs(widget, options);
+        } else {
+            this.messageService.error(`Cannot save the current widget "${widget?.title.label}" .`);
         }
     }
 
+    canSaveAs(saveable?: Widget): saveable is Widget & SaveableSource & Navigatable {
+        return false;
+    }
+
+    saveAs(sourceWidget: Widget & SaveableSource & Navigatable, options?: SaveOptions): Promise<void> {
+        return Promise.reject('Unsupported: The base SaveResourceService does not support saveAs action.');
+    }
 }
diff --git a/packages/core/src/browser/saveable.ts b/packages/core/src/browser/saveable.ts
index e5f59d86d264e..c0291f1f5321b 100644
--- a/packages/core/src/browser/saveable.ts
+++ b/packages/core/src/browser/saveable.ts
@@ -21,7 +21,6 @@ import { MaybePromise } from '../common/types';
 import { Key } from './keyboard/keys';
 import { AbstractDialog } from './dialogs';
 import { waitForClosed } from './widgets';
-import { URI } from 'vscode-uri';
 
 export interface Saveable {
     readonly dirty: boolean;
@@ -66,10 +65,6 @@ export namespace Saveable {
         return !!arg && ('dirty' in arg) && ('onDirtyChanged' in arg);
     }
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    export function isUntitled(arg: any): boolean {
-        return !!arg && ('uri' in arg) && URI.parse((arg as { uri: string; }).uri).scheme === 'untitled';
-    }
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     export function get(arg: any): Saveable | undefined {
         if (is(arg)) {
             return arg;
diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts
index 92df24bf73bf7..527172b41f1e4 100644
--- a/packages/core/src/browser/shell/application-shell.ts
+++ b/packages/core/src/browser/shell/application-shell.ts
@@ -38,6 +38,7 @@ import { waitForRevealed, waitForClosed } from '../widgets';
 import { CorePreferences } from '../core-preferences';
 import { BreadcrumbsRendererFactory } from '../breadcrumbs/breadcrumbs-renderer';
 import { Deferred } from '../../common/promise-util';
+import { SaveResourceService } from '../save-resource-service';
 
 /** The class name added to ApplicationShell instances. */
 const APPLICATION_SHELL_CLASS = 'theia-ApplicationShell';
@@ -216,7 +217,8 @@ export class ApplicationShell extends Widget {
         @inject(SplitPositionHandler) protected splitPositionHandler: SplitPositionHandler,
         @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService,
         @inject(ApplicationShellOptions) @optional() options: RecursivePartial<ApplicationShell.Options> = {},
-        @inject(CorePreferences) protected readonly corePreferences: CorePreferences
+        @inject(CorePreferences) protected readonly corePreferences: CorePreferences,
+        @inject(SaveResourceService) protected readonly saveResourceService: SaveResourceService,
     ) {
         super(options as Widget.IOptions);
     }
@@ -1831,32 +1833,28 @@ export class ApplicationShell extends Widget {
      * Test whether the current widget is dirty.
      */
     canSave(): boolean {
-        return Saveable.isDirty(this.currentWidget);
+        return this.saveResourceService.canSave(this.currentWidget);
     }
 
     /**
      * Save the current widget if it is dirty.
      */
     async save(options?: SaveOptions): Promise<void> {
-        await Saveable.save(this.currentWidget, options);
+        await this.saveResourceService.save(this.currentWidget, options);
     }
 
     /**
      * Test whether there is a dirty widget.
      */
     canSaveAll(): boolean {
-        return this.tracker.widgets.some(Saveable.isDirty);
+        return this.tracker.widgets.some(widget => this.saveResourceService.canSave(widget));
     }
 
     /**
      * Save all dirty widgets.
      */
     async saveAll(options?: SaveOptions): Promise<void> {
-        await Promise.all(this.tracker.widgets.map(widget => {
-            if (Saveable.isDirty(widget)) {
-                Saveable.save(widget, options);
-            }
-        }));
+        await Promise.all(this.tracker.widgets.map(widget => this.saveResourceService.save(widget)));
     }
 
     /**
diff --git a/packages/core/src/browser/untitled-file-location-provider.ts b/packages/core/src/browser/untitled-file-location-provider.ts
new file mode 100644
index 0000000000000..874a19b13b26e
--- /dev/null
+++ b/packages/core/src/browser/untitled-file-location-provider.ts
@@ -0,0 +1,55 @@
+// *****************************************************************************
+// Copyright (C) 2022 Ericsson and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { inject, injectable } from 'inversify';
+import URI from '../common/uri';
+import { createUntitledURI, MaybePromise, SelectionService, UriSelection } from '../common';
+import { EnvVariablesServer } from '../common/env-variables';
+import { NavigatableWidget } from './navigatable-types';
+import { ApplicationShell } from './shell';
+
+@injectable()
+export class UntitledFileLocationProvider {
+    @inject(ApplicationShell) protected readonly shell: ApplicationShell;
+    @inject(SelectionService) protected readonly selectionService: SelectionService;
+    @inject(EnvVariablesServer) protected readonly envVariables: EnvVariablesServer;
+
+    async getUntitledFileLocation(extension?: string): Promise<URI> {
+        return createUntitledURI(extension, await this.getParent());
+    }
+
+    protected async getParent(): Promise<URI> {
+        return await this.getFromCurrentWidget()
+            ?? await this.getFromSelection()
+            ?? this.getFromUserHome();
+    }
+
+    protected getFromCurrentWidget(): MaybePromise<URI | undefined> {
+        return this.ensureIsDirectory(NavigatableWidget.getUri(this.shell.currentWidget));
+    }
+
+    protected getFromSelection(): MaybePromise<URI | undefined> {
+        return this.ensureIsDirectory(UriSelection.getUri(this.selectionService.selection));
+    }
+
+    protected getFromUserHome(): MaybePromise<URI> {
+        return this.envVariables.getHomeDirUri().then(home => new URI(home));
+    }
+
+    protected ensureIsDirectory(uri?: URI): MaybePromise<URI | undefined> {
+        return uri?.parent;
+    }
+}
diff --git a/packages/core/src/common/resource.ts b/packages/core/src/common/resource.ts
index f2e0be51bd707..5e86388dbf50e 100644
--- a/packages/core/src/common/resource.ts
+++ b/packages/core/src/common/resource.ts
@@ -317,3 +317,82 @@ export class InMemoryTextResourceResolver implements ResourceResolver {
         return new InMemoryTextResource(uri);
     }
 }
+
+export const UNTITLED_SCHEME = 'untitled';
+
+let untitledResourceSequenceIndex = 0;
+
+@injectable()
+export class UntitledResourceResolver implements ResourceResolver {
+
+    protected readonly resources = new Map<string, UntitledResource>();
+
+    async resolve(uri: URI): Promise<UntitledResource> {
+        if (uri.scheme !== UNTITLED_SCHEME) {
+            throw new Error('The given uri is not untitled file uri: ' + uri);
+        } else {
+            const untitledResource = this.resources.get(uri.toString());
+            if (!untitledResource) {
+                return this.createUntitledResource('', '', uri);
+            } else {
+                return untitledResource;
+            }
+        }
+    }
+
+    async createUntitledResource(content?: string, extension?: string, uri?: URI): Promise<UntitledResource> {
+        return new UntitledResource(this.resources, uri ? uri : new URI().withScheme(UNTITLED_SCHEME).withPath(`/Untitled-${untitledResourceSequenceIndex++}${extension ?? ''}`),
+            content);
+    }
+}
+
+export class UntitledResource implements Resource {
+
+    protected readonly onDidChangeContentsEmitter = new Emitter<void>();
+    get onDidChangeContents(): Event<void> {
+        return this.onDidChangeContentsEmitter.event;
+    }
+
+    constructor(private resources: Map<string, UntitledResource>, public uri: URI, private content?: string) {
+        this.resources.set(this.uri.toString(), this);
+    }
+
+    dispose(): void {
+        this.resources.delete(this.uri.toString());
+        this.onDidChangeContentsEmitter.dispose();
+    }
+
+    async readContents(options?: { encoding?: string | undefined; } | undefined): Promise<string> {
+        if (this.content) {
+            return this.content;
+        } else {
+            return '';
+        }
+    }
+
+    async saveContents(content: string, options?: { encoding?: string, overwriteEncoding?: boolean }): Promise<void> {
+        // This function must exist to ensure readOnly is false for the Monaco editor.
+        // However it should not be called because saving 'untitled' is always processed as 'Save As'.
+        throw Error('Untitled resources cannot be saved.');
+    }
+
+    protected fireDidChangeContents(): void {
+        this.onDidChangeContentsEmitter.fire(undefined);
+    }
+
+    get version(): ResourceVersion | undefined {
+        return undefined;
+    }
+
+    get encoding(): string | undefined {
+        return undefined;
+    }
+}
+
+export function createUntitledURI(extension?: string, parent?: URI): URI {
+    const name = `Untitled-${untitledResourceSequenceIndex++}${extension ?? ''}`;
+    if (parent) {
+        return parent.resolve(name).withScheme(UNTITLED_SCHEME);
+    }
+    return new URI().resolve(name).withScheme(UNTITLED_SCHEME);
+}
diff --git a/packages/editor-preview/src/browser/editor-preview-widget.ts b/packages/editor-preview/src/browser/editor-preview-widget.ts
index b35f7643a4d9e..19e9127f34ec0 100644
--- a/packages/editor-preview/src/browser/editor-preview-widget.ts
+++ b/packages/editor-preview/src/browser/editor-preview-widget.ts
@@ -17,7 +17,7 @@
 import { Message } from '@theia/core/shared/@phosphor/messaging';
 import { DockPanel, TabBar, Widget, PINNED_CLASS } from '@theia/core/lib/browser';
 import { EditorWidget, TextEditor } from '@theia/editor/lib/browser';
-import { Disposable, DisposableCollection, Emitter, SelectionService } from '@theia/core/lib/common';
+import { Disposable, DisposableCollection, Emitter, SelectionService, UNTITLED_SCHEME } from '@theia/core/lib/common';
 import { find } from '@theia/core/shared/@phosphor/algorithm';
 
 const PREVIEW_TITLE_CLASS = 'theia-editor-preview-title-unpinned';
@@ -97,9 +97,11 @@ export class EditorPreviewWidget extends EditorWidget {
         }
     }
 
-    override storeState(): { isPreview: boolean, editorState: object } {
-        const { _isPreview: isPreview } = this;
-        return { isPreview, editorState: this.editor.storeViewState() };
+    override storeState(): { isPreview: boolean, editorState: object } | undefined {
+        if (this.getResourceUri()?.scheme !== UNTITLED_SCHEME) {
+            const { _isPreview: isPreview } = this;
+            return { isPreview, editorState: this.editor.storeViewState() };
+        }
     }
 
     override restoreState(oldState: { isPreview: boolean, editorState: object }): void {
diff --git a/packages/editor/src/browser/editor-widget.ts b/packages/editor/src/browser/editor-widget.ts
index 3ea998f95c600..324e2728f59d0 100644
--- a/packages/editor/src/browser/editor-widget.ts
+++ b/packages/editor/src/browser/editor-widget.ts
@@ -14,7 +14,7 @@
 // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 // *****************************************************************************
 
-import { Disposable, SelectionService, Event } from '@theia/core/lib/common';
+import { Disposable, SelectionService, Event, UNTITLED_SCHEME } from '@theia/core/lib/common';
 import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget } from '@theia/core/lib/browser';
 import URI from '@theia/core/lib/common/uri';
 import { TextEditor } from './editor';
@@ -80,8 +80,8 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata
         }
     }
 
-    storeState(): object {
-        return this.editor.storeViewState();
+    storeState(): object | undefined {
+        return this.getResourceUri()?.scheme === UNTITLED_SCHEME ? undefined : this.editor.storeViewState();
     }
 
     restoreState(oldState: object): void {
diff --git a/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts b/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts
index 3148f54f303ad..ea9ac1249b727 100644
--- a/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts
+++ b/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts
@@ -35,6 +35,15 @@ export interface FileDialogService {
 
 }
 
+@injectable()
+export class FileDialogDefaultRootProvider {
+    @inject(EnvVariablesServer) protected readonly environments: EnvVariablesServer;
+
+    async getDefaultDialogRoot(): Promise<URI> {
+        return new URI(await this.environments.getHomeDirUri());
+    }
+}
+
 @injectable()
 export class DefaultFileDialogService implements FileDialogService {
 
@@ -47,6 +56,7 @@ export class DefaultFileDialogService implements FileDialogService {
     @inject(OpenFileDialogFactory) protected readonly openFileDialogFactory: OpenFileDialogFactory;
     @inject(LabelProvider) protected readonly labelProvider: LabelProvider;
     @inject(SaveFileDialogFactory) protected readonly saveFileDialogFactory: SaveFileDialogFactory;
+    @inject(FileDialogDefaultRootProvider) protected readonly rootProvider: FileDialogDefaultRootProvider;
 
     async showOpenDialog(props: OpenFileDialogProps & { canSelectMany: true }, folder?: FileStat): Promise<MaybeArray<URI> | undefined>;
     async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise<URI | undefined>;
@@ -81,7 +91,7 @@ export class DefaultFileDialogService implements FileDialogService {
     protected async getRootNode(folderToOpen?: FileStat): Promise<DirNode | undefined> {
         const folderExists = folderToOpen && await this.fileService.exists(folderToOpen.resource);
         const folder = folderToOpen && folderExists ? folderToOpen : {
-            resource: new URI(await this.environments.getHomeDirUri()),
+            resource: await this.rootProvider.getDefaultDialogRoot(),
             isDirectory: true
         };
         const folderUri = folder.resource;
diff --git a/packages/filesystem/src/browser/filesystem-frontend-module.ts b/packages/filesystem/src/browser/filesystem-frontend-module.ts
index 4744a824b0b0c..219aff925884a 100644
--- a/packages/filesystem/src/browser/filesystem-frontend-module.ts
+++ b/packages/filesystem/src/browser/filesystem-frontend-module.ts
@@ -38,8 +38,11 @@ import { FileSystemWatcherErrorHandler } from './filesystem-watcher-error-handle
 import { UTF8 } from '@theia/core/lib/common/encodings';
 import { FilepathBreadcrumbsContribution } from './breadcrumbs/filepath-breadcrumbs-contribution';
 import { BreadcrumbsFileTreeWidget, createFileTreeBreadcrumbsWidget } from './breadcrumbs/filepath-breadcrumbs-container';
+import { FilesystemSaveResourceService } from './filesystem-save-resource-service';
+import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service';
+import { FileDialogDefaultRootProvider } from './file-dialog';
 
-export default new ContainerModule(bind => {
+export default new ContainerModule((bind, unbind, isBound, rebind) => {
     bindFileSystemPreferences(bind);
 
     bindContributionProvider(bind, FileServiceContribution);
@@ -224,6 +227,10 @@ export default new ContainerModule(bind => {
     );
     bind(FilepathBreadcrumbsContribution).toSelf().inSingletonScope();
     bind(BreadcrumbsContribution).toService(FilepathBreadcrumbsContribution);
+
+    bind(FilesystemSaveResourceService).toSelf().inSingletonScope();
+    rebind(SaveResourceService).toService(FilesystemSaveResourceService);
+    bind(FileDialogDefaultRootProvider).toSelf().inSingletonScope();
 });
 
 export function bindFileResource(bind: interfaces.Bind): void {
diff --git a/packages/filesystem/src/browser/filesystem-save-resource-service.ts b/packages/filesystem/src/browser/filesystem-save-resource-service.ts
new file mode 100644
index 0000000000000..24b74d100db56
--- /dev/null
+++ b/packages/filesystem/src/browser/filesystem-save-resource-service.ts
@@ -0,0 +1,130 @@
+// *****************************************************************************
+// Copyright (C) 2022 Ericsson and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { environment, nls, UNTITLED_SCHEME } from '@theia/core';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { Navigatable, Saveable, SaveableSource, SaveOptions, Widget, open, OpenerService, ConfirmDialog, FormatType, CommonCommands } from '@theia/core/lib/browser';
+import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service';
+import URI from '@theia/core/lib/common/uri';
+import { FileService } from './file-service';
+import { FileDialogService } from './file-dialog';
+
+@injectable()
+export class FilesystemSaveResourceService extends SaveResourceService {
+
+    @inject(FileService) protected readonly fileService: FileService;
+    @inject(FileDialogService) protected readonly fileDialogService: FileDialogService;
+    @inject(OpenerService) protected readonly openerService: OpenerService;
+
+    /**
+     * This method ensures a few things about `widget`:
+     * - `widget.getResourceUri()` actually returns a URI.
+     * - `widget.saveable.createSnapshot` is defined.
+     * - `widget.saveable.revert` is defined.
+     */
+    override canSaveAs(widget: Widget | undefined): widget is Widget & SaveableSource & Navigatable {
+        return widget !== undefined
+            && Saveable.isSource(widget)
+            && typeof widget.saveable.createSnapshot === 'function'
+            && typeof widget.saveable.revert === 'function'
+            && Navigatable.is(widget)
+            && widget.getResourceUri() !== undefined;
+    }
+
+    /**
+     * Save `sourceWidget` to a new file picked by the user.
+     */
+    override async saveAs(sourceWidget: Widget & SaveableSource & Navigatable, options?: SaveOptions): Promise<void> {
+        let exist: boolean = false;
+        let overwrite: boolean = false;
+        let selected: URI | undefined;
+        const canSave = this.canSaveNotSaveAs(sourceWidget);
+        const uri: URI = sourceWidget.getResourceUri()!;
+        let stat;
+        if (uri.scheme === 'file') {
+            stat = await this.fileService.resolve(uri).catch(() => undefined);
+        } else if (uri.scheme === UNTITLED_SCHEME) {
+            stat = await this.fileService.resolve(uri.withScheme('file').parent).catch(() => undefined);
+        }
+        do {
+            selected = await this.fileDialogService.showSaveDialog(
+                {
+                    title: CommonCommands.SAVE_AS.label!,
+                    filters: {},
+                    inputValue: uri.path.base
+                }, stat);
+            if (selected) {
+                exist = await this.fileService.exists(selected);
+                if (exist) {
+                    overwrite = await this.confirmOverwrite(selected);
+                }
+            }
+        } while ((selected && exist && !overwrite) || (selected?.isEqual(uri) && !canSave));
+        if (selected && selected.isEqual(uri)) {
+            await this.save(sourceWidget, options);
+        } else if (selected) {
+            try {
+                await this.copyAndSave(sourceWidget, selected, overwrite);
+            } catch (e) {
+                console.warn(e);
+            }
+        }
+    }
+
+    /**
+     * @param sourceWidget widget to save as `target`.
+     * @param target The new URI for the widget.
+     * @param overwrite
+     */
+    private async copyAndSave(sourceWidget: Widget & SaveableSource & Navigatable, target: URI, overwrite: boolean): Promise<void> {
+        const snapshot = sourceWidget.saveable.createSnapshot!();
+        if (!await this.fileService.exists(target)) {
+            const sourceUri = sourceWidget.getResourceUri()!;
+            if (this.fileService.canHandleResource(sourceUri)) {
+                await this.fileService.copy(sourceUri, target, { overwrite });
+            } else {
+                await this.fileService.createFile(target);
+            }
+        }
+        const targetWidget = await open(this.openerService, target);
+        const targetSaveable = Saveable.get(targetWidget);
+        if (targetWidget && targetSaveable && targetSaveable.applySnapshot) {
+            targetSaveable.applySnapshot(snapshot);
+            await sourceWidget.saveable.revert!();
+            sourceWidget.close();
+            Saveable.save(targetWidget, { formatType: FormatType.ON });
+        } else {
+            this.messageService.error(nls.localize('theia/workspace/failApply', 'Could not apply changes to new file'));
+        }
+    }
+
+    async confirmOverwrite(uri: URI): Promise<boolean> {
+        // Electron already handles the confirmation so do not prompt again.
+        if (this.isElectron()) {
+            return true;
+        }
+        // Prompt users for confirmation before overwriting.
+        const confirmed = await new ConfirmDialog({
+            title: nls.localizeByDefault('Overwrite'),
+            msg: nls.localizeByDefault('{0} already exists. Are you sure you want to overwrite it?', uri.toString())
+        }).open();
+        return !!confirmed;
+    }
+
+    private isElectron(): boolean {
+        return environment.electron.is();
+    }
+}
diff --git a/packages/monaco/src/browser/monaco-editor-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts
index c4b70cf2a1c58..045c4c1b04a03 100644
--- a/packages/monaco/src/browser/monaco-editor-model.ts
+++ b/packages/monaco/src/browser/monaco-editor-model.ts
@@ -19,7 +19,7 @@ import { TextEditorDocument, EncodingMode, FindMatchesOptions, FindMatch, Editor
 import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
 import { Emitter, Event } from '@theia/core/lib/common/event';
 import { CancellationTokenSource, CancellationToken } from '@theia/core/lib/common/cancellation';
-import { Resource, ResourceError, ResourceVersion } from '@theia/core/lib/common/resource';
+import { Resource, ResourceError, ResourceVersion, UNTITLED_SCHEME } from '@theia/core/lib/common/resource';
 import { Saveable, SaveOptions } from '@theia/core/lib/browser/saveable';
 import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
 import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
@@ -163,6 +163,9 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument {
             const languageSelection = monaco.services.StaticServices.modeService.get().createByFilepathOrFirstLine(uri, firstLine);
             this.model = monaco.services.StaticServices.modelService.get().createModel(value, languageSelection, uri);
             this.resourceVersion = this.resource.version;
+            if (this.resource.uri.scheme === UNTITLED_SCHEME && this.model.getValueLength()) {
+                this.setDirty(true);
+            }
             this.updateSavedVersionId();
             this.toDispose.push(this.model);
             this.toDispose.push(this.model.onDidChangeContent(event => this.fireDidChangeContent(event)));
diff --git a/packages/monaco/src/browser/monaco-languages.ts b/packages/monaco/src/browser/monaco-languages.ts
index 2a62dce7add83..9a0596c309577 100644
--- a/packages/monaco/src/browser/monaco-languages.ts
+++ b/packages/monaco/src/browser/monaco-languages.ts
@@ -89,6 +89,10 @@ export class MonacoLanguages implements LanguageService {
         return this.mergeLanguages(monaco.languages.getLanguages().filter(language => language.id === languageId)).get(languageId);
     }
 
+    getExtension(languageId: string): string | undefined {
+        return this.getLanguage(languageId)?.extensions.values().next().value;
+    }
+
     protected mergeLanguages(registered: monaco.languages.ILanguageExtensionPoint[]): Map<string, Mutable<Language>> {
         const languages = new Map<string, Mutable<Language>>();
         for (const { id, aliases, extensions, filenames } of registered) {
diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts
index 8a8597a3aab87..ba9917f9c93ef 100644
--- a/packages/navigator/src/browser/navigator-contribution.ts
+++ b/packages/navigator/src/browser/navigator-contribution.ts
@@ -573,9 +573,9 @@ export class FileNavigatorContribution extends AbstractViewContribution<FileNavi
             priority: 1,
         });
         this.registerMoreToolbarItem({
-            id: WorkspaceCommands.NEW_FILE.id,
-            command: WorkspaceCommands.NEW_FILE.id,
-            tooltip: WorkspaceCommands.NEW_FILE.label,
+            id: CommonCommands.NEW_FILE.id,
+            command: CommonCommands.NEW_FILE.id,
+            tooltip: CommonCommands.NEW_FILE.label,
             group: NavigatorMoreToolbarGroups.NEW_OPEN,
         });
         this.registerMoreToolbarItem({
diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts
index b10f11d9853ef..44d44d01e5270 100755
--- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts
+++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts
@@ -19,7 +19,6 @@ import {
     ApplicationShell,
     CommonCommands,
     NavigatableWidget,
-    open,
     OpenerService, OpenHandler,
     QuickInputService,
     Saveable,
@@ -47,7 +46,6 @@ import {
     DocumentHighlight
 } from '@theia/plugin-ext/lib/common/plugin-api-rpc-model';
 import { DocumentsMainImpl } from '@theia/plugin-ext/lib/main/browser/documents-main';
-import { createUntitledURI } from '@theia/plugin-ext/lib/main/browser/editor/untitled-resource';
 import { isUriComponents, toDocumentSymbol, toPosition } from '@theia/plugin-ext/lib/plugin/type-converters';
 import { ViewColumn } from '@theia/plugin-ext/lib/plugin/types-impl';
 import { WorkspaceCommands } from '@theia/workspace/lib/browser';
@@ -273,10 +271,6 @@ export class PluginVscodeCommandsContribution implements CommandContribution {
          * because of it we filter out editors from views based on `NavigatableWidget.is`
          * and apply actions only to them
          */
-        commands.registerCommand({ id: 'workbench.action.files.newUntitledFile' }, {
-            execute: () => open(this.openerService, createUntitledURI())
-        });
-
         if (!environment.electron.is() || isOSX) {
             commands.registerCommand({ id: 'workbench.action.files.openFileFolder' }, {
                 execute: () => commands.executeCommand(WorkspaceCommands.OPEN.id)
diff --git a/packages/plugin-ext/src/common/rpc-protocol.ts b/packages/plugin-ext/src/common/rpc-protocol.ts
index b87adad494b92..e46c930a3c325 100644
--- a/packages/plugin-ext/src/common/rpc-protocol.ts
+++ b/packages/plugin-ext/src/common/rpc-protocol.ts
@@ -94,7 +94,7 @@ export class RPCProtocolImpl implements RPCProtocol {
 
     constructor(connection: MessageConnection, transformations?: {
         replacer?: (key: string | undefined, value: any) => any,
-        reviver?: (key: string | undefined, value: any) =>  any
+        reviver?: (key: string | undefined, value: any) => any
     }) {
         this.toDispose.push(
             this.multiplexer = new RPCMultiplexer(connection)
diff --git a/packages/plugin-ext/src/main/browser/documents-main.ts b/packages/plugin-ext/src/main/browser/documents-main.ts
index 8c83eac9da2a1..f469434f122e0 100644
--- a/packages/plugin-ext/src/main/browser/documents-main.ts
+++ b/packages/plugin-ext/src/main/browser/documents-main.ts
@@ -30,6 +30,7 @@ import { Range } from '@theia/core/shared/vscode-languageserver-protocol';
 import { OpenerService } from '@theia/core/lib/browser/opener-service';
 import { Reference } from '@theia/core/lib/common/reference';
 import { dispose } from '../../common/disposable-util';
+import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
 
 /*---------------------------------------------------------------------------------------------
  *  Copyright (c) Microsoft Corporation. All rights reserved.
@@ -94,6 +95,7 @@ export class DocumentsMainImpl implements DocumentsMain, Disposable {
         private openerService: OpenerService,
         private shell: ApplicationShell,
         private untitledResourceResolver: UntitledResourceResolver,
+        private languageService: MonacoLanguages,
     ) {
         this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DOCUMENTS_EXT);
 
@@ -177,8 +179,8 @@ export class DocumentsMainImpl implements DocumentsMain, Disposable {
     }
 
     async $tryCreateDocument(options?: { language?: string; content?: string; }): Promise<UriComponents> {
-        const language = options && options.language;
-        const content = options && options.content;
+        const language = options?.language && this.languageService.getExtension(options.language);
+        const content = options?.content;
         const resource = await this.untitledResourceResolver.createUntitledResource(content, language);
         return monaco.Uri.parse(resource.uri.toString());
     }
diff --git a/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts b/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts
index 69ffdbb294b0a..760c0af0ffc9f 100644
--- a/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts
+++ b/packages/plugin-ext/src/main/browser/editor/untitled-resource.ts
@@ -14,101 +14,5 @@
 // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 // *****************************************************************************
 
-import { Emitter, Event } from '@theia/core/lib/common/event';
-import { injectable } from '@theia/core/shared/inversify';
-import { Resource, ResourceResolver, ResourceVersion } from '@theia/core/lib/common/resource';
-import URI from '@theia/core/lib/common/uri';
-import { Schemes } from '../../../common/uri-components';
-
-let index = 0;
-
-@injectable()
-export class UntitledResourceResolver implements ResourceResolver {
-
-    protected readonly resources = new Map<string, UntitledResource>();
-
-    async resolve(uri: URI): Promise<UntitledResource> {
-        if (uri.scheme !== Schemes.untitled) {
-            throw new Error('The given uri is not untitled file uri: ' + uri);
-        } else {
-            const untitledResource = this.resources.get(uri.toString());
-            if (!untitledResource) {
-                return this.createUntitledResource('', '', uri);
-            } else {
-                return untitledResource;
-            }
-        }
-    }
-
-    async createUntitledResource(content?: string, language?: string, uri?: URI): Promise<UntitledResource> {
-        let extension;
-        if (language) {
-            for (const lang of monaco.languages.getLanguages()) {
-                if (lang.id === language) {
-                    if (lang.extensions) {
-                        extension = lang.extensions[0];
-                        break;
-                    }
-                }
-            }
-        }
-        return new UntitledResource(this.resources, uri ? uri : new URI().withScheme(Schemes.untitled).withPath(`/Untitled-${index++}${extension ? extension : ''}`),
-            content);
-    }
-}
-
-export class UntitledResource implements Resource {
-
-    protected readonly onDidChangeContentsEmitter = new Emitter<void>();
-    readonly onDidChangeContents: Event<void> = this.onDidChangeContentsEmitter.event;
-
-    constructor(private resources: Map<string, UntitledResource>, public uri: URI, private content?: string) {
-        this.resources.set(this.uri.toString(), this);
-    }
-
-    dispose(): void {
-        this.resources.delete(this.uri.toString());
-        this.onDidChangeContentsEmitter.dispose();
-    }
-
-    async readContents(options?: { encoding?: string | undefined; } | undefined): Promise<string> {
-        if (this.content) {
-            return this.content;
-        } else {
-            return '';
-        }
-    }
-
-    async saveContents(content: string, options?: { encoding?: string, overwriteEncoding?: boolean }): Promise<void> {
-        // This function must exist to ensure readOnly is false for the Monaco editor.
-        // However it should not be called because saving 'untitled' is always processed as 'Save As'.
-        throw Error('never');
-    }
-
-    protected fireDidChangeContents(): void {
-        this.onDidChangeContentsEmitter.fire(undefined);
-    }
-
-    get version(): ResourceVersion | undefined {
-        return undefined;
-    }
-
-    get encoding(): string | undefined {
-        return undefined;
-    }
-}
-
-export function createUntitledURI(language?: string): URI {
-    let extension;
-    if (language) {
-        for (const lang of monaco.languages.getLanguages()) {
-            if (lang.id === language) {
-                if (lang.extensions) {
-                    extension = lang.extensions[0];
-                    break;
-                }
-            }
-        }
-    }
-    return new URI().withScheme(Schemes.untitled).withPath(`/Untitled-${index++}${extension ? extension : ''}`);
-}
+/** @deprecated @since 1.24. Import from `core/lib/common/resource` instead. */
+export { UntitledResourceResolver, UntitledResource, createUntitledURI } from '@theia/core/lib/common/resource';
diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts
index cd23c95084d15..c5699790b864e 100644
--- a/packages/plugin-ext/src/main/browser/main-context.ts
+++ b/packages/plugin-ext/src/main/browser/main-context.ts
@@ -56,6 +56,7 @@ import { CommentsMainImp } from './comments/comments-main';
 import { CustomEditorsMainImpl } from './custom-editors/custom-editors-main';
 import { SecretsMainImpl } from './secrets-main';
 import { WebviewViewsMainImpl } from './webview-views/webview-views-main';
+import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
 
 export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void {
     const authenticationMain = new AuthenticationMainImpl(rpc, container);
@@ -86,7 +87,8 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container
     const openerService = container.get<OpenerService>(OpenerService);
     const shell = container.get(ApplicationShell);
     const untitledResourceResolver = container.get(UntitledResourceResolver);
-    const documentsMain = new DocumentsMainImpl(editorsAndDocuments, modelService, rpc, editorManager, openerService, shell, untitledResourceResolver);
+    const languageService = container.get(MonacoLanguages);
+    const documentsMain = new DocumentsMainImpl(editorsAndDocuments, modelService, rpc, editorManager, openerService, shell, untitledResourceResolver, languageService);
     rpc.set(PLUGIN_RPC_CONTEXT.DOCUMENTS_MAIN, documentsMain);
 
     const bulkEditService = container.get(MonacoBulkEditService);
diff --git a/packages/workspace/src/browser/workspace-commands.ts b/packages/workspace/src/browser/workspace-commands.ts
index 5936391450316..d9d3953ad45df 100644
--- a/packages/workspace/src/browser/workspace-commands.ts
+++ b/packages/workspace/src/browser/workspace-commands.ts
@@ -89,6 +89,7 @@ export namespace WorkspaceCommands {
         category: WORKSPACE_CATEGORY,
         label: 'Close Workspace'
     });
+    /** @deprecated @since 1.24 Use CommonCommands.NEW_FILE instead. */
     export const NEW_FILE = Command.toDefaultLocalizedCommand({
         id: 'file.newFile',
         category: FILE_CATEGORY,
@@ -142,11 +143,8 @@ export namespace WorkspaceCommands {
         category: WORKSPACE_CATEGORY,
         label: 'Open Workspace Configuration File'
     });
-    export const SAVE_AS = Command.toDefaultLocalizedCommand({
-        id: 'file.saveAs',
-        category: CommonCommands.FILE_CATEGORY,
-        label: 'Save As...',
-    });
+    /** @deprecated @since 1.24.0 Use `CommonCommands.SAVE_AS` instead */
+    export const SAVE_AS = CommonCommands.SAVE_AS;
     export const COPY_RELATIVE_FILE_PATH = Command.toDefaultLocalizedCommand({
         id: 'navigator.copyRelativeFilePath',
         label: 'Copy Relative Path'
@@ -157,10 +155,6 @@ export namespace WorkspaceCommands {
 export class FileMenuContribution implements MenuContribution {
 
     registerMenus(registry: MenuModelRegistry): void {
-        registry.registerMenuAction(CommonMenus.FILE_NEW, {
-            commandId: WorkspaceCommands.NEW_FILE.id,
-            order: 'a'
-        });
         registry.registerMenuAction(CommonMenus.FILE_NEW, {
             commandId: WorkspaceCommands.NEW_FOLDER.id,
             order: 'b'
diff --git a/packages/workspace/src/browser/workspace-file-dialog-root-provider.ts b/packages/workspace/src/browser/workspace-file-dialog-root-provider.ts
new file mode 100644
index 0000000000000..dfd2ecb9c0d16
--- /dev/null
+++ b/packages/workspace/src/browser/workspace-file-dialog-root-provider.ts
@@ -0,0 +1,30 @@
+// *****************************************************************************
+// Copyright (C) 2022 Ericsson and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { inject, injectable } from '@theia/core/shared/inversify';
+import URI from '@theia/core/lib/common/uri';
+import { FileDialogDefaultRootProvider } from '@theia/filesystem/lib/browser';
+import { WorkspaceService } from './workspace-service';
+
+@injectable()
+export class WorkspaceFileDialogDefaultRootProvider extends FileDialogDefaultRootProvider {
+    @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
+
+    override async getDefaultDialogRoot(): Promise<URI> {
+        const root = this.workspaceService.tryGetRoots()[0];
+        return root?.resource ?? super.getDefaultDialogRoot();
+    }
+}
diff --git a/packages/workspace/src/browser/workspace-frontend-contribution.ts b/packages/workspace/src/browser/workspace-frontend-contribution.ts
index 875fc46cabc56..2f0338386ee6b 100644
--- a/packages/workspace/src/browser/workspace-frontend-contribution.ts
+++ b/packages/workspace/src/browser/workspace-frontend-contribution.ts
@@ -15,17 +15,17 @@
 // *****************************************************************************
 
 import { injectable, inject } from '@theia/core/shared/inversify';
-import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService, MessageService, isWindows, MaybeArray } from '@theia/core/lib/common';
+import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, MessageService, isWindows, MaybeArray } from '@theia/core/lib/common';
 import { isOSX, environment, OS } from '@theia/core';
 import {
-    open, OpenerService, CommonMenus, StorageService, LabelProvider, ConfirmDialog, KeybindingRegistry, KeybindingContribution,
-    CommonCommands, FrontendApplicationContribution, ApplicationShell, Saveable, SaveableSource, Widget, Navigatable, SHELL_TABBAR_CONTEXT_COPY, OnWillStopAction, FormatType
+    open, OpenerService, CommonMenus, ConfirmDialog, KeybindingRegistry, KeybindingContribution,
+    FrontendApplicationContribution, SHELL_TABBAR_CONTEXT_COPY, OnWillStopAction, Navigatable, SaveableSource, Widget
 } from '@theia/core/lib/browser';
 import { FileDialogService, OpenFileDialogProps, FileDialogTreeFilters } from '@theia/filesystem/lib/browser';
 import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
 import { WorkspaceService } from './workspace-service';
 import { THEIA_EXT, VSCODE_EXT } from '../common';
-import { WorkspaceCommands, WorkspaceCommandContribution } from './workspace-commands';
+import { WorkspaceCommands } from './workspace-commands';
 import { QuickOpenWorkspace } from './quick-open-workspace';
 import { WorkspacePreferences } from './workspace-preferences';
 import URI from '@theia/core/lib/common/uri';
@@ -38,6 +38,7 @@ import { nls } from '@theia/core/lib/common/nls';
 import { BinaryBuffer } from '@theia/core/lib/common/buffer';
 import { FileStat } from '@theia/filesystem/lib/common/files';
 import { UntitledWorkspaceExitDialog } from './untitled-workspace-exit-dialog';
+import { FilesystemSaveResourceService } from '@theia/filesystem/lib/browser/filesystem-save-resource-service';
 
 export enum WorkspaceStates {
     /**
@@ -59,28 +60,17 @@ export type WorkbenchState = keyof typeof WorkspaceStates;
 @injectable()
 export class WorkspaceFrontendContribution implements CommandContribution, KeybindingContribution, MenuContribution, FrontendApplicationContribution {
 
-    @inject(ApplicationShell) protected readonly applicationShell: ApplicationShell;
     @inject(MessageService) protected readonly messageService: MessageService;
     @inject(FileService) protected readonly fileService: FileService;
     @inject(OpenerService) protected readonly openerService: OpenerService;
     @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
-    @inject(StorageService) protected readonly workspaceStorage: StorageService;
-    @inject(LabelProvider) protected readonly labelProvider: LabelProvider;
     @inject(QuickOpenWorkspace) protected readonly quickOpenWorkspace: QuickOpenWorkspace;
     @inject(FileDialogService) protected readonly fileDialogService: FileDialogService;
     @inject(WorkspacePreferences) protected preferences: WorkspacePreferences;
-    @inject(SelectionService) protected readonly selectionService: SelectionService;
-    @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
-    @inject(WorkspaceCommandContribution) protected readonly workspaceCommands: WorkspaceCommandContribution;
-
-    @inject(ContextKeyService)
-    protected readonly contextKeyService: ContextKeyService;
-
-    @inject(EncodingRegistry)
-    protected readonly encodingRegistry: EncodingRegistry;
-
-    @inject(PreferenceConfigurations)
-    protected readonly preferenceConfigurations: PreferenceConfigurations;
+    @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
+    @inject(EncodingRegistry) protected readonly encodingRegistry: EncodingRegistry;
+    @inject(PreferenceConfigurations) protected readonly preferenceConfigurations: PreferenceConfigurations;
+    @inject(FilesystemSaveResourceService) protected readonly saveService: FilesystemSaveResourceService;
 
     configure(): void {
         this.encodingRegistry.registerOverride({ encoding: UTF8, extension: THEIA_EXT });
@@ -161,19 +151,6 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
             isEnabled: () => this.workspaceService.isMultiRootWorkspaceEnabled,
             execute: () => this.saveWorkspaceAs()
         });
-        commands.registerCommand(WorkspaceCommands.SAVE_AS, {
-            isEnabled: () => this.canBeSavedAs(this.applicationShell.currentWidget),
-            execute: () => {
-                const { currentWidget } = this.applicationShell;
-                // No clue what could have happened between `isEnabled` and `execute`
-                // when fetching currentWidget, so better to double-check:
-                if (this.canBeSavedAs(currentWidget)) {
-                    this.saveAs(currentWidget);
-                } else {
-                    this.messageService.error(nls.localize('theia/workspace/failSaveAs', 'Cannot run "{0}" for the current widget.', WorkspaceCommands.SAVE_AS.label!));
-                }
-            },
-        });
         commands.registerCommand(WorkspaceCommands.OPEN_WORKSPACE_FILE, {
             isEnabled: () => this.workspaceService.saved,
             execute: () => {
@@ -232,10 +209,6 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
     }
 
     registerKeybindings(keybindings: KeybindingRegistry): void {
-        keybindings.registerKeybinding({
-            command: WorkspaceCommands.NEW_FILE.id,
-            keybinding: this.isElectron() ? 'ctrlcmd+n' : 'alt+n',
-        });
         keybindings.registerKeybinding({
             command: isOSX || !this.isElectron() ? WorkspaceCommands.OPEN.id : WorkspaceCommands.OPEN_FILE.id,
             keybinding: this.isElectron() ? 'ctrlcmd+o' : 'ctrlcmd+alt+o',
@@ -457,7 +430,7 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
                 }
                 exist = await this.fileService.exists(selected);
                 if (exist) {
-                    overwrite = await this.confirmOverwrite(selected);
+                    overwrite = await this.saveService.confirmOverwrite(selected);
                 }
             }
         } while (selected && exist && !overwrite);
@@ -473,85 +446,12 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
         return false;
     }
 
-    /**
-     * This method ensures a few things about `widget`:
-     * - `widget.getResourceUri()` actually returns a URI.
-     * - `widget.saveable.createSnapshot` is defined.
-     * - `widget.saveable.revert` is defined.
-     */
     canBeSavedAs(widget: Widget | undefined): widget is Widget & SaveableSource & Navigatable {
-        return widget !== undefined
-            && Saveable.isSource(widget)
-            && typeof widget.saveable.createSnapshot === 'function'
-            && typeof widget.saveable.revert === 'function'
-            && Navigatable.is(widget)
-            && widget.getResourceUri() !== undefined;
-    }
-
-    /**
-     * Save `sourceWidget` to a new file picked by the user.
-     */
-    async saveAs(sourceWidget: Widget & SaveableSource & Navigatable): Promise<void> {
-        let exist: boolean = false;
-        let overwrite: boolean = false;
-        let selected: URI | undefined;
-        const uri: URI = sourceWidget.getResourceUri()!;
-        let stat;
-        if (uri.scheme === 'file') {
-            stat = await this.fileService.resolve(uri);
-        } else {
-            stat = this.workspaceService.workspace;
-        }
-        do {
-            selected = await this.fileDialogService.showSaveDialog(
-                {
-                    title: WorkspaceCommands.SAVE_AS.label!,
-                    filters: {},
-                    inputValue: uri.path.base
-                }, stat);
-            if (selected) {
-                exist = await this.fileService.exists(selected);
-                if (exist) {
-                    overwrite = await this.confirmOverwrite(selected);
-                }
-            }
-        } while (selected && exist && !overwrite);
-        if (selected && selected.isEqual(uri)) {
-            await this.commandRegistry.executeCommand(CommonCommands.SAVE.id);
-        } else if (selected) {
-            try {
-                await this.copyAndSave(sourceWidget, selected, overwrite);
-            } catch (e) {
-                console.warn(e);
-            }
-        }
+        return this.saveService.canSaveAs(widget);
     }
 
-    /**
-     * @param sourceWidget widget to save as `target`.
-     * @param target The new URI for the widget.
-     * @param overwrite
-     */
-    private async copyAndSave(sourceWidget: Widget & SaveableSource & Navigatable, target: URI, overwrite: boolean): Promise<void> {
-        const snapshot = sourceWidget.saveable.createSnapshot!();
-        if (!await this.fileService.exists(target)) {
-            const sourceUri = sourceWidget.getResourceUri()!;
-            if (this.fileService.canHandleResource(sourceUri)) {
-                await this.fileService.copy(sourceUri, target, { overwrite });
-            } else {
-                await this.fileService.createFile(target);
-            }
-        }
-        const targetWidget = await open(this.openerService, target);
-        const targetSaveable = Saveable.get(targetWidget);
-        if (targetWidget && targetSaveable && targetSaveable.applySnapshot) {
-            targetSaveable.applySnapshot(snapshot);
-            await sourceWidget.saveable.revert!();
-            sourceWidget.close();
-            Saveable.save(targetWidget, { formatType: FormatType.ON });
-        } else {
-            this.messageService.error(nls.localize('theia/workspace/failApply', 'Could not apply changes to new file'));
-        }
+    async saveAs(widget: Widget & SaveableSource & Navigatable): Promise<void> {
+        return this.saveService.saveAs(widget);
     }
 
     protected updateWorkspaceStateKey(): WorkspaceState {
@@ -569,19 +469,6 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
         return 'empty';
     }
 
-    private async confirmOverwrite(uri: URI): Promise<boolean> {
-        // Electron already handles the confirmation so do not prompt again.
-        if (this.isElectron()) {
-            return true;
-        }
-        // Prompt users for confirmation before overwriting.
-        const confirmed = await new ConfirmDialog({
-            title: nls.localizeByDefault('Overwrite'),
-            msg: nls.localizeByDefault('{0} already exists. Are you sure you want to overwrite it?', uri.toString())
-        }).open();
-        return !!confirmed;
-    }
-
     private isElectron(): boolean {
         return environment.electron.is();
     }
diff --git a/packages/workspace/src/browser/workspace-frontend-module.ts b/packages/workspace/src/browser/workspace-frontend-module.ts
index 62e2f996ef550..1d191540e14f8 100644
--- a/packages/workspace/src/browser/workspace-frontend-module.ts
+++ b/packages/workspace/src/browser/workspace-frontend-module.ts
@@ -25,7 +25,8 @@ import {
     createOpenFileDialogContainer,
     createSaveFileDialogContainer,
     OpenFileDialog,
-    SaveFileDialog
+    SaveFileDialog,
+    FileDialogDefaultRootProvider
 } from '@theia/filesystem/lib/browser';
 import { StorageService } from '@theia/core/lib/browser/storage-service';
 import { LabelProviderContribution } from '@theia/core/lib/browser/label-provider';
@@ -50,8 +51,9 @@ import { WorkspaceBreadcrumbsContribution } from './workspace-breadcrumbs-contri
 import { FilepathBreadcrumbsContribution } from '@theia/filesystem/lib/browser/breadcrumbs/filepath-breadcrumbs-contribution';
 import { WorkspaceTrustService } from './workspace-trust-service';
 import { bindWorkspaceTrustPreferences } from './workspace-trust-preferences';
-import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service';
-import { WorkspaceSaveResourceService } from './workspace-save-resource-service';
+import { WorkspaceFileDialogDefaultRootProvider } from './workspace-file-dialog-root-provider';
+import { UntitledFileLocationProvider } from '@theia/core/lib/browser/untitled-file-location-provider';
+import { WorkspaceUntitledFileLocationProvider } from './workspace-untitled-file-location-provider';
 
 export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
     bindWorkspacePreferences(bind);
@@ -107,6 +109,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
     rebind(FilepathBreadcrumbsContribution).to(WorkspaceBreadcrumbsContribution).inSingletonScope();
 
     bind(WorkspaceTrustService).toSelf().inSingletonScope();
-
-    rebind(SaveResourceService).to(WorkspaceSaveResourceService).inSingletonScope();
+    bind(WorkspaceFileDialogDefaultRootProvider).toSelf().inSingletonScope();
+    rebind(FileDialogDefaultRootProvider).toSelf().inSingletonScope();
+    rebind(UntitledFileLocationProvider).to(WorkspaceUntitledFileLocationProvider).inSingletonScope();
 });
diff --git a/packages/workspace/src/browser/workspace-save-resource-service.ts b/packages/workspace/src/browser/workspace-save-resource-service.ts
deleted file mode 100644
index 218cfa6b8db1c..0000000000000
--- a/packages/workspace/src/browser/workspace-save-resource-service.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-/********************************************************************************
- * Copyright (C) 2022 Arm and others.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v. 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0.
- *
- * This Source Code may also be made available under the following Secondary
- * Licenses when the conditions for such availability set forth in the Eclipse
- * Public License v. 2.0 are satisfied: GNU General Public License, version 2
- * with the GNU Classpath Exception which is available at
- * https://www.gnu.org/software/classpath/license.html.
- *
- * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
- ********************************************************************************/
-
-import { inject, injectable } from '@theia/core/shared/inversify';
-import { WorkspaceFrontendContribution } from './workspace-frontend-contribution';
-import { Saveable, SaveOptions, Widget } from '@theia/core/lib/browser';
-import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service';
-import { MessageService } from '@theia/core/lib/common';
-
-@injectable()
-export class WorkspaceSaveResourceService extends SaveResourceService {
-
-    @inject(WorkspaceFrontendContribution) protected readonly workspaceFrontendContribution: WorkspaceFrontendContribution;
-
-    @inject(MessageService) protected readonly messageService: MessageService;
-
-    override canSave(saveable: Saveable): boolean {
-        // In addition to dirty documents, untitled documents can be saved because for these we treat 'Save' as 'Save As'.
-        return Saveable.isDirty(saveable) || Saveable.isUntitled(saveable);
-    }
-
-    override async save(widget: Widget | undefined, options?: SaveOptions): Promise<void> {
-        const saveable = Saveable.get(widget);
-        if (widget instanceof Widget && this.workspaceFrontendContribution.canBeSavedAs(widget) && saveable) {
-            if (Saveable.isUntitled(saveable)) {
-                this.workspaceFrontendContribution.saveAs(widget);
-            } else {
-                await saveable.save(options);
-            }
-        } else {
-            // This should not happen because the caller should check this.
-            this.messageService.error(`Cannot save the current widget "${widget?.title}" .`);
-        }
-
-    }
-
-}
diff --git a/packages/workspace/src/browser/workspace-untitled-file-location-provider.ts b/packages/workspace/src/browser/workspace-untitled-file-location-provider.ts
new file mode 100644
index 0000000000000..ad1e3c53f7847
--- /dev/null
+++ b/packages/workspace/src/browser/workspace-untitled-file-location-provider.ts
@@ -0,0 +1,49 @@
+// *****************************************************************************
+// Copyright (C) 2022 Ericsson and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { UntitledFileLocationProvider } from '@theia/core/lib/browser/untitled-file-location-provider';
+import URI from '@theia/core/lib/common/uri';
+import { WorkspaceService } from './workspace-service';
+import { MaybePromise } from '@theia/core';
+import { FileService } from '@theia/filesystem/lib/browser/file-service';
+
+@injectable()
+export class WorkspaceUntitledFileLocationProvider extends UntitledFileLocationProvider {
+    @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
+    @inject(FileService) protected readonly fileService: FileService;
+
+    protected override async getParent(): Promise<URI> {
+        return await this.getFromCurrentWidget()
+            ?? await this.getFromSelection()
+            ?? await this.getFromWorkspace()
+            ?? this.getFromUserHome();
+    }
+
+    protected getFromWorkspace(): MaybePromise<URI | undefined> {
+        return this.workspaceService.tryGetRoots()[0]?.resource;
+    }
+
+    protected override async ensureIsDirectory(uri?: URI): Promise<URI | undefined> {
+        if (uri) {
+            const asFile = uri.withScheme('file');
+            const stat = await this.fileService.resolve(asFile)
+                .catch(() => this.fileService.resolve(asFile.parent))
+                .catch(() => undefined);
+            return stat?.isDirectory ? stat.resource : stat?.resource.parent;
+        }
+    }
+}

From cb881a9207db15df4e1a0903eb531642241c9e2b Mon Sep 17 00:00:00 2001
From: Colin Grant <colin.grant@ericsson.com>
Date: Thu, 10 Mar 2022 08:22:32 -0700
Subject: [PATCH 2/8] Save on non-saveable is no-op, not error

---
 packages/core/src/browser/save-resource-service.ts | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/packages/core/src/browser/save-resource-service.ts b/packages/core/src/browser/save-resource-service.ts
index 3ec83f2387baa..afc8ae9cf2602 100644
--- a/packages/core/src/browser/save-resource-service.ts
+++ b/packages/core/src/browser/save-resource-service.ts
@@ -37,18 +37,15 @@ export class SaveResourceService {
     }
 
     /**
-     * Saves the document.
+     * Saves the document
      *
-     * This function is called only if `canSave` returns true, which means the document is not untitled
-     * and is thus saveable.
+     * No op if the widget is not saveable.
      */
     async save(widget: Widget | undefined, options?: SaveOptions): Promise<void> {
         if (this.canSaveNotSaveAs(widget)) {
             await Saveable.save(widget, options);
         } else if (this.canSaveAs(widget)) {
             await this.saveAs(widget, options);
-        } else {
-            this.messageService.error(`Cannot save the current widget "${widget?.title.label}" .`);
         }
     }
 

From 6dbef00ef0df9c1ba653d32dd290e94cc2cdf615 Mon Sep 17 00:00:00 2001
From: Colin Grant <colin.grant@ericsson.com>
Date: Mon, 14 Mar 2022 08:36:55 -0600
Subject: [PATCH 3/8] open new widget next to old widget

---
 .../filesystem/src/browser/filesystem-save-resource-service.ts  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/filesystem/src/browser/filesystem-save-resource-service.ts b/packages/filesystem/src/browser/filesystem-save-resource-service.ts
index 24b74d100db56..816601f25cf38 100644
--- a/packages/filesystem/src/browser/filesystem-save-resource-service.ts
+++ b/packages/filesystem/src/browser/filesystem-save-resource-service.ts
@@ -99,7 +99,7 @@ export class FilesystemSaveResourceService extends SaveResourceService {
                 await this.fileService.createFile(target);
             }
         }
-        const targetWidget = await open(this.openerService, target);
+        const targetWidget = await open(this.openerService, target, { widgetOptions: { ref: sourceWidget } });
         const targetSaveable = Saveable.get(targetWidget);
         if (targetWidget && targetSaveable && targetSaveable.applySnapshot) {
             targetSaveable.applySnapshot(snapshot);

From 9689bc4a9ed6c37346afe51335f66a623379987d Mon Sep 17 00:00:00 2001
From: Colin Grant <colin.grant@ericsson.com>
Date: Mon, 14 Mar 2022 09:07:59 -0600
Subject: [PATCH 4/8] Handle saves in sequence; use slice

---
 packages/core/src/browser/save-resource-service.ts   | 4 ++--
 packages/core/src/browser/shell/application-shell.ts | 4 +++-
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/packages/core/src/browser/save-resource-service.ts b/packages/core/src/browser/save-resource-service.ts
index afc8ae9cf2602..0c2f92d5c05a1 100644
--- a/packages/core/src/browser/save-resource-service.ts
+++ b/packages/core/src/browser/save-resource-service.ts
@@ -28,12 +28,12 @@ export class SaveResourceService {
      * Indicate if the document can be saved ('Save' command should be disable if not).
      */
     canSave(widget?: Widget): widget is Widget & (Saveable | SaveableSource) {
-        return this.canSaveNotSaveAs(widget) || this.canSaveAs(widget);
+        return Saveable.isDirty(widget) && (this.canSaveNotSaveAs(widget) || this.canSaveAs(widget));
     }
 
     protected canSaveNotSaveAs(widget?: Widget): widget is Widget & (Saveable | SaveableSource) {
         // By default, we never allow a document to be saved if it is untitled.
-        return Boolean(widget && Saveable.isDirty(widget) && NavigatableWidget.getUri(widget)?.scheme !== UNTITLED_SCHEME);
+        return Boolean(widget && NavigatableWidget.getUri(widget)?.scheme !== UNTITLED_SCHEME);
     }
 
     /**
diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts
index 527172b41f1e4..282cbecc26a25 100644
--- a/packages/core/src/browser/shell/application-shell.ts
+++ b/packages/core/src/browser/shell/application-shell.ts
@@ -1854,7 +1854,9 @@ export class ApplicationShell extends Widget {
      * Save all dirty widgets.
      */
     async saveAll(options?: SaveOptions): Promise<void> {
-        await Promise.all(this.tracker.widgets.map(widget => this.saveResourceService.save(widget)));
+        for (const widget of this.widgets) {
+            await this.saveResourceService.save(widget, options);
+        }
     }
 
     /**

From 15159df11b92fc3130494c9276665dc3cba2cb1e Mon Sep 17 00:00:00 2001
From: Colin Grant <colin.grant@ericsson.com>
Date: Tue, 15 Mar 2022 12:33:46 -0600
Subject: [PATCH 5/8] Use UserWorkingDirectoryProvider

---
 .../browser/common-frontend-contribution.ts   |  9 +++---
 .../browser/frontend-application-module.ts    |  4 +--
 ....ts => user-working-directory-provider.ts} | 25 ++++++----------
 .../file-dialog/file-dialog-service.ts        | 14 ++-------
 .../src/browser/filesystem-frontend-module.ts |  2 --
 .../monaco/src/browser/monaco-editor-model.ts |  4 +--
 .../workspace-file-dialog-root-provider.ts    | 30 -------------------
 .../src/browser/workspace-frontend-module.ts  | 10 ++-----
 ...kspace-user-working-directory-provider.ts} |  9 +++---
 9 files changed, 27 insertions(+), 80 deletions(-)
 rename packages/core/src/browser/{untitled-file-location-provider.ts => user-working-directory-provider.ts} (67%)
 delete mode 100644 packages/workspace/src/browser/workspace-file-dialog-root-provider.ts
 rename packages/workspace/src/browser/{workspace-untitled-file-location-provider.ts => workspace-user-working-directory-provider.ts} (84%)

diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts
index d9e7e33b49c5b..c2791b10ea611 100644
--- a/packages/core/src/browser/common-frontend-contribution.ts
+++ b/packages/core/src/browser/common-frontend-contribution.ts
@@ -61,7 +61,8 @@ import { FrontendApplicationConfigProvider } from './frontend-application-config
 import { DecorationStyle } from './decoration-style';
 import { isPinned, Title, togglePinned, Widget } from './widgets';
 import { SaveResourceService } from './save-resource-service';
-import { UntitledFileLocationProvider } from './untitled-file-location-provider';
+import { UserWorkingDirectoryProvider } from './user-working-directory-provider';
+import { createUntitledURI } from '../common';
 
 export namespace CommonMenus {
 
@@ -395,8 +396,8 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
     @inject(WindowService)
     protected readonly windowService: WindowService;
 
-    @inject(UntitledFileLocationProvider)
-    protected readonly untitledFileLocationProvider: UntitledFileLocationProvider;
+    @inject(UserWorkingDirectoryProvider)
+    protected readonly workingDirProvider: UserWorkingDirectoryProvider;
 
     protected pinnedKey: ContextKey<boolean>;
 
@@ -955,7 +956,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
             execute: () => this.configureDisplayLanguage()
         });
         commandRegistry.registerCommand(CommonCommands.NEW_FILE, {
-            execute: async () => open(this.openerService, await this.untitledFileLocationProvider.getUntitledFileLocation())
+            execute: async () => open(this.openerService, createUntitledURI('', await this.workingDirProvider.getUserWorkingDir()))
         });
     }
 
diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts
index bcc4694871344..c00b41e8df977 100644
--- a/packages/core/src/browser/frontend-application-module.ts
+++ b/packages/core/src/browser/frontend-application-module.ts
@@ -121,7 +121,7 @@ import { RendererHost } from './widgets';
 import { TooltipService, TooltipServiceImpl } from './tooltip-service';
 import { bindFrontendStopwatch, bindBackendStopwatch } from './performance';
 import { SaveResourceService } from './save-resource-service';
-import { UntitledFileLocationProvider } from './untitled-file-location-provider';
+import { UserWorkingDirectoryProvider } from './user-working-directory-provider';
 
 export { bindResourceProvider, bindMessageService, bindPreferenceService };
 
@@ -399,5 +399,5 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
     bindBackendStopwatch(bind);
 
     bind(SaveResourceService).toSelf().inSingletonScope();
-    bind(UntitledFileLocationProvider).toSelf().inSingletonScope();
+    bind(UserWorkingDirectoryProvider).toSelf().inSingletonScope();
 });
diff --git a/packages/core/src/browser/untitled-file-location-provider.ts b/packages/core/src/browser/user-working-directory-provider.ts
similarity index 67%
rename from packages/core/src/browser/untitled-file-location-provider.ts
rename to packages/core/src/browser/user-working-directory-provider.ts
index 874a19b13b26e..8f20b76c1d842 100644
--- a/packages/core/src/browser/untitled-file-location-provider.ts
+++ b/packages/core/src/browser/user-working-directory-provider.ts
@@ -16,31 +16,24 @@
 
 import { inject, injectable } from 'inversify';
 import URI from '../common/uri';
-import { createUntitledURI, MaybePromise, SelectionService, UriSelection } from '../common';
+import { MaybePromise, SelectionService, UriSelection } from '../common';
 import { EnvVariablesServer } from '../common/env-variables';
-import { NavigatableWidget } from './navigatable-types';
-import { ApplicationShell } from './shell';
 
 @injectable()
-export class UntitledFileLocationProvider {
-    @inject(ApplicationShell) protected readonly shell: ApplicationShell;
+export class UserWorkingDirectoryProvider {
     @inject(SelectionService) protected readonly selectionService: SelectionService;
     @inject(EnvVariablesServer) protected readonly envVariables: EnvVariablesServer;
 
-    async getUntitledFileLocation(extension?: string): Promise<URI> {
-        return createUntitledURI(extension, await this.getParent());
-    }
-
-    protected async getParent(): Promise<URI> {
-        return await this.getFromCurrentWidget()
-            ?? await this.getFromSelection()
+    /**
+     * @returns A {@link URI} that represents a good guess about the directory in which the user is currently operating.
+     *
+     * Factors considered may include the current widget, current selection, user home directory, or other application state.
+     */
+    async getUserWorkingDir(): Promise<URI> {
+        return await this.getFromSelection()
             ?? this.getFromUserHome();
     }
 
-    protected getFromCurrentWidget(): MaybePromise<URI | undefined> {
-        return this.ensureIsDirectory(NavigatableWidget.getUri(this.shell.currentWidget));
-    }
-
     protected getFromSelection(): MaybePromise<URI | undefined> {
         return this.ensureIsDirectory(UriSelection.getUri(this.selectionService.selection));
     }
diff --git a/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts b/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts
index ea9ac1249b727..686360348fbae 100644
--- a/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts
+++ b/packages/filesystem/src/browser/file-dialog/file-dialog-service.ts
@@ -23,6 +23,7 @@ import { DirNode } from '../file-tree';
 import { OpenFileDialogFactory, OpenFileDialogProps, SaveFileDialogFactory, SaveFileDialogProps } from './file-dialog';
 import { FileService } from '../file-service';
 import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
+import { UserWorkingDirectoryProvider } from '@theia/core/lib/browser/user-working-directory-provider';
 
 export const FileDialogService = Symbol('FileDialogService');
 export interface FileDialogService {
@@ -35,15 +36,6 @@ export interface FileDialogService {
 
 }
 
-@injectable()
-export class FileDialogDefaultRootProvider {
-    @inject(EnvVariablesServer) protected readonly environments: EnvVariablesServer;
-
-    async getDefaultDialogRoot(): Promise<URI> {
-        return new URI(await this.environments.getHomeDirUri());
-    }
-}
-
 @injectable()
 export class DefaultFileDialogService implements FileDialogService {
 
@@ -56,7 +48,7 @@ export class DefaultFileDialogService implements FileDialogService {
     @inject(OpenFileDialogFactory) protected readonly openFileDialogFactory: OpenFileDialogFactory;
     @inject(LabelProvider) protected readonly labelProvider: LabelProvider;
     @inject(SaveFileDialogFactory) protected readonly saveFileDialogFactory: SaveFileDialogFactory;
-    @inject(FileDialogDefaultRootProvider) protected readonly rootProvider: FileDialogDefaultRootProvider;
+    @inject(UserWorkingDirectoryProvider) protected readonly rootProvider: UserWorkingDirectoryProvider;
 
     async showOpenDialog(props: OpenFileDialogProps & { canSelectMany: true }, folder?: FileStat): Promise<MaybeArray<URI> | undefined>;
     async showOpenDialog(props: OpenFileDialogProps, folder?: FileStat): Promise<URI | undefined>;
@@ -91,7 +83,7 @@ export class DefaultFileDialogService implements FileDialogService {
     protected async getRootNode(folderToOpen?: FileStat): Promise<DirNode | undefined> {
         const folderExists = folderToOpen && await this.fileService.exists(folderToOpen.resource);
         const folder = folderToOpen && folderExists ? folderToOpen : {
-            resource: await this.rootProvider.getDefaultDialogRoot(),
+            resource: await this.rootProvider.getUserWorkingDir(),
             isDirectory: true
         };
         const folderUri = folder.resource;
diff --git a/packages/filesystem/src/browser/filesystem-frontend-module.ts b/packages/filesystem/src/browser/filesystem-frontend-module.ts
index 219aff925884a..0d70e4c096fda 100644
--- a/packages/filesystem/src/browser/filesystem-frontend-module.ts
+++ b/packages/filesystem/src/browser/filesystem-frontend-module.ts
@@ -40,7 +40,6 @@ import { FilepathBreadcrumbsContribution } from './breadcrumbs/filepath-breadcru
 import { BreadcrumbsFileTreeWidget, createFileTreeBreadcrumbsWidget } from './breadcrumbs/filepath-breadcrumbs-container';
 import { FilesystemSaveResourceService } from './filesystem-save-resource-service';
 import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service';
-import { FileDialogDefaultRootProvider } from './file-dialog';
 
 export default new ContainerModule((bind, unbind, isBound, rebind) => {
     bindFileSystemPreferences(bind);
@@ -230,7 +229,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
 
     bind(FilesystemSaveResourceService).toSelf().inSingletonScope();
     rebind(SaveResourceService).toService(FilesystemSaveResourceService);
-    bind(FileDialogDefaultRootProvider).toSelf().inSingletonScope();
 });
 
 export function bindFileResource(bind: interfaces.Bind): void {
diff --git a/packages/monaco/src/browser/monaco-editor-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts
index 045c4c1b04a03..8298ac6f1e24e 100644
--- a/packages/monaco/src/browser/monaco-editor-model.ts
+++ b/packages/monaco/src/browser/monaco-editor-model.ts
@@ -163,9 +163,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument {
             const languageSelection = monaco.services.StaticServices.modeService.get().createByFilepathOrFirstLine(uri, firstLine);
             this.model = monaco.services.StaticServices.modelService.get().createModel(value, languageSelection, uri);
             this.resourceVersion = this.resource.version;
-            if (this.resource.uri.scheme === UNTITLED_SCHEME && this.model.getValueLength()) {
-                this.setDirty(true);
-            }
+            this.setDirty(this._dirty || (this.resource.uri.scheme === UNTITLED_SCHEME && this.model.getValueLength() > 0));
             this.updateSavedVersionId();
             this.toDispose.push(this.model);
             this.toDispose.push(this.model.onDidChangeContent(event => this.fireDidChangeContent(event)));
diff --git a/packages/workspace/src/browser/workspace-file-dialog-root-provider.ts b/packages/workspace/src/browser/workspace-file-dialog-root-provider.ts
deleted file mode 100644
index dfd2ecb9c0d16..0000000000000
--- a/packages/workspace/src/browser/workspace-file-dialog-root-provider.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// *****************************************************************************
-// Copyright (C) 2022 Ericsson and others.
-//
-// This program and the accompanying materials are made available under the
-// terms of the Eclipse Public License v. 2.0 which is available at
-// http://www.eclipse.org/legal/epl-2.0.
-//
-// This Source Code may also be made available under the following Secondary
-// Licenses when the conditions for such availability set forth in the Eclipse
-// Public License v. 2.0 are satisfied: GNU General Public License, version 2
-// with the GNU Classpath Exception which is available at
-// https://www.gnu.org/software/classpath/license.html.
-//
-// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
-// *****************************************************************************
-
-import { inject, injectable } from '@theia/core/shared/inversify';
-import URI from '@theia/core/lib/common/uri';
-import { FileDialogDefaultRootProvider } from '@theia/filesystem/lib/browser';
-import { WorkspaceService } from './workspace-service';
-
-@injectable()
-export class WorkspaceFileDialogDefaultRootProvider extends FileDialogDefaultRootProvider {
-    @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
-
-    override async getDefaultDialogRoot(): Promise<URI> {
-        const root = this.workspaceService.tryGetRoots()[0];
-        return root?.resource ?? super.getDefaultDialogRoot();
-    }
-}
diff --git a/packages/workspace/src/browser/workspace-frontend-module.ts b/packages/workspace/src/browser/workspace-frontend-module.ts
index 1d191540e14f8..a9f38d99aadd3 100644
--- a/packages/workspace/src/browser/workspace-frontend-module.ts
+++ b/packages/workspace/src/browser/workspace-frontend-module.ts
@@ -26,7 +26,6 @@ import {
     createSaveFileDialogContainer,
     OpenFileDialog,
     SaveFileDialog,
-    FileDialogDefaultRootProvider
 } from '@theia/filesystem/lib/browser';
 import { StorageService } from '@theia/core/lib/browser/storage-service';
 import { LabelProviderContribution } from '@theia/core/lib/browser/label-provider';
@@ -51,9 +50,8 @@ import { WorkspaceBreadcrumbsContribution } from './workspace-breadcrumbs-contri
 import { FilepathBreadcrumbsContribution } from '@theia/filesystem/lib/browser/breadcrumbs/filepath-breadcrumbs-contribution';
 import { WorkspaceTrustService } from './workspace-trust-service';
 import { bindWorkspaceTrustPreferences } from './workspace-trust-preferences';
-import { WorkspaceFileDialogDefaultRootProvider } from './workspace-file-dialog-root-provider';
-import { UntitledFileLocationProvider } from '@theia/core/lib/browser/untitled-file-location-provider';
-import { WorkspaceUntitledFileLocationProvider } from './workspace-untitled-file-location-provider';
+import { UserWorkingDirectoryProvider } from '@theia/core/lib/browser/user-working-directory-provider';
+import { WorkspaceUserWorkingDirectoryProvider } from './workspace-user-working-directory-provider';
 
 export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
     bindWorkspacePreferences(bind);
@@ -109,7 +107,5 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
     rebind(FilepathBreadcrumbsContribution).to(WorkspaceBreadcrumbsContribution).inSingletonScope();
 
     bind(WorkspaceTrustService).toSelf().inSingletonScope();
-    bind(WorkspaceFileDialogDefaultRootProvider).toSelf().inSingletonScope();
-    rebind(FileDialogDefaultRootProvider).toSelf().inSingletonScope();
-    rebind(UntitledFileLocationProvider).to(WorkspaceUntitledFileLocationProvider).inSingletonScope();
+    rebind(UserWorkingDirectoryProvider).to(WorkspaceUserWorkingDirectoryProvider).inSingletonScope();
 });
diff --git a/packages/workspace/src/browser/workspace-untitled-file-location-provider.ts b/packages/workspace/src/browser/workspace-user-working-directory-provider.ts
similarity index 84%
rename from packages/workspace/src/browser/workspace-untitled-file-location-provider.ts
rename to packages/workspace/src/browser/workspace-user-working-directory-provider.ts
index ad1e3c53f7847..645e8812490d8 100644
--- a/packages/workspace/src/browser/workspace-untitled-file-location-provider.ts
+++ b/packages/workspace/src/browser/workspace-user-working-directory-provider.ts
@@ -15,20 +15,19 @@
 // *****************************************************************************
 
 import { inject, injectable } from '@theia/core/shared/inversify';
-import { UntitledFileLocationProvider } from '@theia/core/lib/browser/untitled-file-location-provider';
+import { UserWorkingDirectoryProvider } from '@theia/core/lib/browser/user-working-directory-provider';
 import URI from '@theia/core/lib/common/uri';
 import { WorkspaceService } from './workspace-service';
 import { MaybePromise } from '@theia/core';
 import { FileService } from '@theia/filesystem/lib/browser/file-service';
 
 @injectable()
-export class WorkspaceUntitledFileLocationProvider extends UntitledFileLocationProvider {
+export class WorkspaceUserWorkingDirectoryProvider extends UserWorkingDirectoryProvider {
     @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
     @inject(FileService) protected readonly fileService: FileService;
 
-    protected override async getParent(): Promise<URI> {
-        return await this.getFromCurrentWidget()
-            ?? await this.getFromSelection()
+    override async getUserWorkingDir(): Promise<URI> {
+        return await this.getFromSelection()
             ?? await this.getFromWorkspace()
             ?? this.getFromUserHome();
     }

From 68ad38f79cd88110a0998952630b360b081c0451 Mon Sep 17 00:00:00 2001
From: Colin Grant <colin.grant@ericsson.com>
Date: Wed, 23 Mar 2022 10:34:32 -0600
Subject: [PATCH 6/8] breaking

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a90d200b1e902..d5295c5091a75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
 
 - [plugin] added support for `DocumentSymbolProviderMetadata` [#10811](https://github.com/eclipse-theia/theia/pull/10811) - Contributed on behalf of STMicroelectronics
 - [playwright] fixed playwright tests for Windows and MacOS [#10826](https://github.com/eclipse-theia/theia/pull/10826) - Contributed on behalf of STMicroelectronics
+- [core] Move code for untitled resources into `core` from `plugin-ext` and allow users to open untitled editors with `New File` command. [#10868](https://github.com/eclipse-theia/theia/pull/10868)
 
 <a name="breaking_changes_1.24.0">[Breaking Changes:](#breaking_changes_1.24.0)</a>
 
@@ -17,6 +18,7 @@
 - [debug] the getter `model` was renamed to `getModel` and accepts an optional `URI` parameter [#10875](https://github.com/eclipse-theia/theia/pull/10875)
 - [filesystem] The `generateUniqueResourceURI` method from the `FileSystemUtils` class has an updated signature. Additionally, the method now returns a generated Uri that uses spaces as separators. The naming scheme was also changed to match VSCode. [10767](https://github.com/eclipse-theia/theia/pull/10767)
 - [markers] `ProblemDecorator` reimplemented to reduce redundancy and align more closely with VSCode. `collectMarkers` now returns `Map<string, TreeDecoration.Data>`, `getOverlayIconColor` renamed to `getColor`, `getOverlayIcon` removed, `appendContainerMarkers` returns `void` [#10820](https://github.com/eclipse-theia/theia/pull/10820)
+- [workspace] removed unused injections in `WorkspaceService`: `ApplicationShell`, `StorageService`, `LabelProvider`, `SelectionService`, `CommandRegistry`, `WorkspaceCommandContribution`. [#10868](https://github.com/eclipse-theia/theia/pull/10868)
 
 ## v1.23.0 - 2/24/2022
 

From 28cbb19d8ce39da7ca682993379d7ef6449bad6d Mon Sep 17 00:00:00 2001
From: Colin Grant <colin.grant@ericsson.com>
Date: Mon, 28 Mar 2022 11:20:20 -0600
Subject: [PATCH 7/8] Handle closing better

---
 packages/core/src/browser/saveable.ts             | 15 +++++++++++----
 .../core/src/browser/shell/application-shell.ts   |  6 +++++-
 .../custom-editors/custom-editor-widget.ts        | 15 +++++++++++++--
 3 files changed, 29 insertions(+), 7 deletions(-)

diff --git a/packages/core/src/browser/saveable.ts b/packages/core/src/browser/saveable.ts
index 5077ce374e6db..be2d4cbbe495c 100644
--- a/packages/core/src/browser/saveable.ts
+++ b/packages/core/src/browser/saveable.ts
@@ -103,7 +103,10 @@ export namespace Saveable {
         return waitForClosed(this);
     }
 
-    function createCloseWithSaving(getOtherSaveables?: () => Array<Widget | SaveableWidget>): (this: SaveableWidget, options?: SaveableWidget.CloseOptions) => Promise<void> {
+    function createCloseWithSaving(
+        getOtherSaveables?: () => Array<Widget | SaveableWidget>,
+        doSave?: (widget: Widget, options?: SaveOptions) => Promise<void>
+    ): (this: SaveableWidget, options?: SaveableWidget.CloseOptions) => Promise<void> {
         let closing = false;
         return async function (this: SaveableWidget, options: SaveableWidget.CloseOptions): Promise<void> {
             if (closing) { return; }
@@ -123,7 +126,7 @@ export namespace Saveable {
                 });
                 if (typeof result === 'boolean') {
                     if (result) {
-                        await Saveable.save(this);
+                        await (doSave?.(this) ?? Saveable.save(this));
                     }
                     await this.closeWithoutSaving();
                 }
@@ -163,7 +166,11 @@ export namespace Saveable {
         return !!saveable && !others.some(otherWidget => otherWidget !== widget && get(otherWidget) === saveable);
     }
 
-    export function apply(widget: Widget, getOtherSaveables?: () => Array<Widget | SaveableWidget>): SaveableWidget | undefined {
+    export function apply(
+        widget: Widget,
+        getOtherSaveables?: () => Array<Widget | SaveableWidget>,
+        doSave?: (widget: Widget, options?: SaveOptions) => Promise<void>,
+    ): SaveableWidget | undefined {
         if (SaveableWidget.is(widget)) {
             return widget;
         }
@@ -174,7 +181,7 @@ export namespace Saveable {
         const saveableWidget = widget as SaveableWidget;
         setDirty(saveableWidget, saveable.dirty);
         saveable.onDirtyChanged(() => setDirty(saveableWidget, saveable.dirty));
-        const closeWithSaving = createCloseWithSaving(getOtherSaveables);
+        const closeWithSaving = createCloseWithSaving(getOtherSaveables, doSave);
         return Object.assign(saveableWidget, {
             closeWithoutSaving,
             closeWithSaving,
diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts
index 282cbecc26a25..0f1407e2912a5 100644
--- a/packages/core/src/browser/shell/application-shell.ts
+++ b/packages/core/src/browser/shell/application-shell.ts
@@ -1072,7 +1072,11 @@ export class ApplicationShell extends Widget {
         }
         this.tracker.add(widget);
         this.checkActivation(widget);
-        Saveable.apply(widget, () => this.widgets.filter((maybeSaveable): maybeSaveable is Widget & SaveableSource => !!Saveable.get(maybeSaveable)));
+        Saveable.apply(
+            widget,
+            () => this.widgets.filter((maybeSaveable): maybeSaveable is Widget & SaveableSource => !!Saveable.get(maybeSaveable)),
+            (toSave, options) => this.saveResourceService.save(toSave, options),
+        );
         if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
             for (const toTrack of widget.getTrackableWidgets()) {
                 this.track(toTrack);
diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts
index 80e48e1ffb93a..6b72ea49295ed 100644
--- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts
+++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editor-widget.ts
@@ -17,7 +17,8 @@
 import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
 import URI from '@theia/core/lib/common/uri';
 import { FileOperation } from '@theia/filesystem/lib/common/files';
-import { NavigatableWidget, Saveable, SaveableSource, SaveOptions } from '@theia/core/lib/browser';
+import { ApplicationShell, NavigatableWidget, Saveable, SaveableSource, SaveOptions } from '@theia/core/lib/browser';
+import { SaveResourceService } from '@theia/core/lib/browser/save-resource-service';
 import { Reference } from '@theia/core/lib/common/reference';
 import { WebviewWidget } from '../webview/webview';
 import { UndoRedoService } from './undo-redo-service';
@@ -37,7 +38,11 @@ export class CustomEditorWidget extends WebviewWidget implements SaveableSource,
     set modelRef(modelRef: Reference<CustomEditorModel>) {
         this._modelRef = modelRef;
         this.doUpdateContent();
-        Saveable.apply(this);
+        Saveable.apply(
+            this,
+            () => this.shell.widgets.filter(widget => !!Saveable.get(widget)),
+            (widget, options) => this.saveService.save(widget, options),
+        );
     }
     get saveable(): Saveable {
         return this._modelRef.object;
@@ -46,6 +51,12 @@ export class CustomEditorWidget extends WebviewWidget implements SaveableSource,
     @inject(UndoRedoService)
     protected readonly undoRedoService: UndoRedoService;
 
+    @inject(ApplicationShell)
+    protected readonly shell: ApplicationShell;
+
+    @inject(SaveResourceService)
+    protected readonly saveService: SaveResourceService;
+
     @postConstruct()
     protected override init(): void {
         super.init();

From 5b9f52aa4a1fd241d2e44312f9368708f80f9671 Mon Sep 17 00:00:00 2001
From: Colin Grant <colin.grant@ericsson.com>
Date: Mon, 28 Mar 2022 14:05:19 -0600
Subject: [PATCH 8/8] handle autosave

---
 packages/core/src/browser/save-resource-service.ts   | 2 +-
 packages/core/src/browser/shell/application-shell.ts | 4 +++-
 packages/monaco/src/browser/monaco-editor-model.ts   | 2 +-
 3 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/packages/core/src/browser/save-resource-service.ts b/packages/core/src/browser/save-resource-service.ts
index 0c2f92d5c05a1..cbf6871208bfd 100644
--- a/packages/core/src/browser/save-resource-service.ts
+++ b/packages/core/src/browser/save-resource-service.ts
@@ -31,7 +31,7 @@ export class SaveResourceService {
         return Saveable.isDirty(widget) && (this.canSaveNotSaveAs(widget) || this.canSaveAs(widget));
     }
 
-    protected canSaveNotSaveAs(widget?: Widget): widget is Widget & (Saveable | SaveableSource) {
+    canSaveNotSaveAs(widget?: Widget): widget is Widget & (Saveable | SaveableSource) {
         // By default, we never allow a document to be saved if it is untitled.
         return Boolean(widget && NavigatableWidget.getUri(widget)?.scheme !== UNTITLED_SCHEME);
     }
diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts
index 0f1407e2912a5..af80b4316bb4d 100644
--- a/packages/core/src/browser/shell/application-shell.ts
+++ b/packages/core/src/browser/shell/application-shell.ts
@@ -1859,7 +1859,9 @@ export class ApplicationShell extends Widget {
      */
     async saveAll(options?: SaveOptions): Promise<void> {
         for (const widget of this.widgets) {
-            await this.saveResourceService.save(widget, options);
+            if (this.saveResourceService.canSaveNotSaveAs(widget)) {
+                await this.saveResourceService.save(widget, options);
+            }
         }
     }
 
diff --git a/packages/monaco/src/browser/monaco-editor-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts
index d5f319ddfeb55..ce3ddc87f8d87 100644
--- a/packages/monaco/src/browser/monaco-editor-model.ts
+++ b/packages/monaco/src/browser/monaco-editor-model.ts
@@ -445,7 +445,7 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
     }
 
     protected doAutoSave(): void {
-        if (this.autoSave !== 'off') {
+        if (this.autoSave !== 'off' && this.resource.uri.scheme !== UNTITLED_SCHEME) {
             const token = this.cancelSave();
             this.toDisposeOnAutoSave.dispose();
             const handle = window.setTimeout(() => {