diff --git a/packages/core/src/browser/saveable.ts b/packages/core/src/browser/saveable.ts index 17e809254bc9d..ab28884081209 100644 --- a/packages/core/src/browser/saveable.ts +++ b/packages/core/src/browser/saveable.ts @@ -21,7 +21,7 @@ import { MaybePromise } from '../common/types'; import { Key } from './keyboard/keys'; import { AbstractDialog } from './dialogs'; import { nls } from '../common/nls'; -import { Disposable, DisposableCollection, isObject } from '../common'; +import { Disposable, DisposableCollection, isObject, URI } from '../common'; import { BinaryBuffer } from '../common/buffer'; export type AutoSaveMode = 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange'; @@ -44,6 +44,10 @@ export interface Saveable { * Saves dirty changes. */ save(options?: SaveOptions): MaybePromise; + /** + * Performs the save operation with a new file name. + */ + saveAs?(options: SaveAsOptions): MaybePromise; /** * Reverts dirty changes. */ @@ -87,6 +91,7 @@ export class DelegatingSaveable implements Saveable { createSnapshot?(): Saveable.Snapshot; applySnapshot?(snapshot: object): void; serialize?(): Promise; + saveAs?(options: SaveAsOptions): MaybePromise; protected _delegate?: Saveable; protected toDispose = new DisposableCollection(); @@ -110,6 +115,7 @@ export class DelegatingSaveable implements Saveable { this.createSnapshot = delegate.createSnapshot?.bind(delegate); this.applySnapshot = delegate.applySnapshot?.bind(delegate); this.serialize = delegate.serialize?.bind(delegate); + this.saveAs = delegate.saveAs?.bind(delegate); } } @@ -341,6 +347,10 @@ export interface SaveOptions { readonly saveReason?: SaveReason; } +export interface SaveAsOptions extends SaveOptions { + readonly target: URI; +} + /** * The class name added to the dirty widget's title. */ diff --git a/packages/filesystem/src/browser/filesystem-saveable-service.ts b/packages/filesystem/src/browser/filesystem-saveable-service.ts index 804fad348b800..17c6ddcdf2ce3 100644 --- a/packages/filesystem/src/browser/filesystem-saveable-service.ts +++ b/packages/filesystem/src/browser/filesystem-saveable-service.ts @@ -46,7 +46,7 @@ export class FilesystemSaveableService extends SaveableService { override canSaveAs(widget: Widget | undefined): widget is Widget & SaveableSource & Navigatable { return widget !== undefined && Saveable.isSource(widget) - && (typeof widget.saveable.createSnapshot === 'function' || typeof widget.saveable.serialize === 'function') + && (typeof widget.saveable.createSnapshot === 'function' || typeof widget.saveable.serialize === 'function' || typeof widget.saveable.saveAs === 'function') && typeof widget.saveable.revert === 'function' && Navigatable.is(widget) && widget.getResourceUri() !== undefined; @@ -97,23 +97,30 @@ export class FilesystemSaveableService extends SaveableService { */ protected async saveSnapshot(sourceWidget: Widget & SaveableSource & Navigatable, target: URI, overwrite: boolean): Promise { const saveable = sourceWidget.saveable; - let buffer: BinaryBuffer; - if (saveable.serialize) { - buffer = await saveable.serialize(); - } else if (saveable.createSnapshot) { - const snapshot = saveable.createSnapshot(); - const content = Saveable.Snapshot.read(snapshot) ?? ''; - buffer = BinaryBuffer.fromString(content); + if (saveable.saveAs) { + // Some widgets have their own "Save As" implementation, such as the custom plugin editors + await saveable.saveAs({ + target + }); } else { - throw new Error('Cannot save the widget as the saveable does not provide a snapshot or a serialize method.'); - } - - if (await this.fileService.exists(target)) { - // Do not fire the `onDidCreate` event as the file already exists. - await this.fileService.writeFile(target, buffer); - } else { - // Ensure to actually call `create` as that fires the `onDidCreate` event. - await this.fileService.createFile(target, buffer, { overwrite }); + // Most other editors simply allow us to serialize the content and write it to the target file. + let buffer: BinaryBuffer; + if (saveable.serialize) { + buffer = await saveable.serialize(); + } else if (saveable.createSnapshot) { + const snapshot = saveable.createSnapshot(); + const content = Saveable.Snapshot.read(snapshot) ?? ''; + buffer = BinaryBuffer.fromString(content); + } else { + throw new Error('Cannot save the widget as the saveable does not provide a snapshot or a serialize method.'); + } + if (await this.fileService.exists(target)) { + // Do not fire the `onDidCreate` event as the file already exists. + await this.fileService.writeFile(target, buffer); + } else { + // Ensure to actually call `create` as that fires the `onDidCreate` event. + await this.fileService.createFile(target, buffer, { overwrite }); + } } await saveable.revert!(); await open(this.openerService, target, { widgetOptions: { ref: sourceWidget, mode: 'tab-replace' } }); diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 5c7e7d7e9fd1f..e69929f3336ad 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1900,8 +1900,8 @@ export interface CustomEditorsExt { $redo(resource: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise; $revert(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void; - $onSave(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; - $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise; + $save(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; + $saveAs(resource: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise; // $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; $onMoveCustomEditor(handle: string, newResource: UriComponents, viewType: string): Promise; } 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 ac804199fc89b..bd6cb0216fdf5 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,7 @@ 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 { ApplicationShell, DelegatingSaveable, NavigatableWidget, Saveable, SaveableSource, SaveOptions } from '@theia/core/lib/browser'; +import { ApplicationShell, DelegatingSaveable, NavigatableWidget, Saveable, SaveableSource } from '@theia/core/lib/browser'; import { SaveableService } from '@theia/core/lib/browser/saveable-service'; import { Reference } from '@theia/core/lib/common/reference'; import { WebviewWidget } from '../webview/webview'; @@ -74,18 +74,6 @@ export class CustomEditorWidget extends WebviewWidget implements CustomEditorWid this._modelRef.object?.redo(); } - async save(options?: SaveOptions): Promise { - await this._modelRef.object?.saveCustomEditor(options); - } - - async saveAs(source: URI, target: URI, options?: SaveOptions): Promise { - if (this._modelRef.object) { - const result = await this._modelRef.object.saveCustomEditorAs(source, target, options); - this.doMove(target); - return result; - } - } - getResourceUri(): URI | undefined { return this.resource; } diff --git a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts index ef5c2d7d60bf4..db87fbc58a4e1 100644 --- a/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts +++ b/packages/plugin-ext/src/main/browser/custom-editors/custom-editors-main.ts @@ -38,9 +38,10 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service'; import { WebviewsMainImpl } from '../webviews-main'; import { WidgetManager } from '@theia/core/lib/browser/widget-manager'; -import { ApplicationShell, LabelProvider, Saveable, SaveOptions } from '@theia/core/lib/browser'; +import { ApplicationShell, LabelProvider, Saveable, SaveAsOptions, SaveOptions } from '@theia/core/lib/browser'; import { WebviewPanelOptions } from '@theia/plugin'; import { EditorPreferences } from '@theia/editor/lib/browser'; +import { BinaryBuffer } from '@theia/core/lib/common/buffer'; const enum CustomEditorModelType { Custom, @@ -186,7 +187,7 @@ export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable { switch (modelType) { case CustomEditorModelType.Text: { - const model = CustomTextEditorModel.create(viewType, resource, this.textModelService, this.fileService); + const model = CustomTextEditorModel.create(viewType, resource, this.textModelService); return this.customEditorService.models.add(resource, viewType, model); } case CustomEditorModelType.Custom: { @@ -224,7 +225,7 @@ export interface CustomEditorModel extends Saveable, Disposable { revert(options?: Saveable.RevertOptions): Promise; saveCustomEditor(options?: SaveOptions): Promise; - saveCustomEditorAs(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise; + saveCustomEditorAs?(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise; undo(): void; redo(): void; @@ -329,7 +330,7 @@ export class MainCustomEditorModel implements CustomEditorModel { } const cancellationSource = new CancellationTokenSource(); - this.proxy.$revert(this.resource, this.viewType, cancellationSource.token); + await this.proxy.$revert(this.resource, this.viewType, cancellationSource.token); this.change(() => { this.isDirtyFromContentChange = false; this.currentEditIndex = this.savePoint; @@ -347,7 +348,7 @@ export class MainCustomEditorModel implements CustomEditorModel { } const cancelable = new CancellationTokenSource(); - const savePromise = this.proxy.$onSave(this.resource, this.viewType, cancelable.token); + const savePromise = this.proxy.$save(this.resource, this.viewType, cancelable.token); this.ongoingSave?.cancel(); this.ongoingSave = cancelable; @@ -367,10 +368,14 @@ export class MainCustomEditorModel implements CustomEditorModel { } } + async saveAs(options: SaveAsOptions): Promise { + await this.saveCustomEditorAs(new TheiaURI(this.resource), options.target, options); + } + async saveCustomEditorAs(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise { if (this.editable) { const source = new CancellationTokenSource(); - await this.proxy.$onSaveAs(this.resource, this.viewType, targetResource.toComponents(), source.token); + await this.proxy.$saveAs(this.resource, this.viewType, targetResource.toComponents(), source.token); this.change(() => { this.savePoint = this.currentEditIndex; }); @@ -450,19 +455,17 @@ export class CustomTextEditorModel implements CustomEditorModel { static async create( viewType: string, resource: TheiaURI, - editorModelService: EditorModelService, - fileService: FileService, + editorModelService: EditorModelService ): Promise { const model = await editorModelService.createModelReference(resource); model.object.suppressOpenEditorWhenDirty = true; - return new CustomTextEditorModel(viewType, resource, model, fileService); + return new CustomTextEditorModel(viewType, resource, model); } constructor( readonly viewType: string, readonly editorResource: TheiaURI, - private readonly model: Reference, - private readonly fileService: FileService, + private readonly model: Reference ) { this.toDispose.push( this.editorTextModel.onDirtyChanged(e => { @@ -507,13 +510,12 @@ export class CustomTextEditorModel implements CustomEditorModel { return this.saveCustomEditor(options); } - saveCustomEditor(options?: SaveOptions): Promise { - return this.editorTextModel.save(options); + serialize(): Promise { + return this.editorTextModel.serialize(); } - async saveCustomEditorAs(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise { - await this.saveCustomEditor(options); - await this.fileService.copy(resource, targetResource, { overwrite: false }); + saveCustomEditor(options?: SaveOptions): Promise { + return this.editorTextModel.save(options); } undo(): void { diff --git a/packages/plugin-ext/src/plugin/custom-editors.ts b/packages/plugin-ext/src/plugin/custom-editors.ts index 700151a37303d..7d52414bf3c2e 100644 --- a/packages/plugin-ext/src/plugin/custom-editors.ts +++ b/packages/plugin-ext/src/plugin/custom-editors.ts @@ -184,12 +184,12 @@ export class CustomEditorsExtImpl implements CustomEditorsExt { async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - return entry.undo(editId, isDirty); + await entry.undo(editId, isDirty); } async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - return entry.redo(editId, isDirty); + await entry.redo(editId, isDirty); } async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { @@ -198,16 +198,16 @@ export class CustomEditorsExtImpl implements CustomEditorsExt { await provider.revertCustomDocument(entry.document, cancellation); } - async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + async $save(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { const entry = this.getCustomDocumentEntry(viewType, resourceComponents); const provider = this.getCustomEditorProvider(viewType); await provider.saveCustomDocument(entry.document, cancellation); } - async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise { + async $saveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise { const entry = this.getCustomDocumentEntry(viewType, resourceComponents); const provider = this.getCustomEditorProvider(viewType); - return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation); + await provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation); } private getCustomEditorProvider(viewType: string): theia.CustomEditorProvider {