Skip to content

Commit

Permalink
Serialize saveables to disk for "Save As" (#13833)
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew authored Jun 20, 2024
1 parent a5f03f8 commit c7af872
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 9 deletions.
10 changes: 9 additions & 1 deletion packages/core/src/browser/saveable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Key } from './keyboard/keys';
import { AbstractDialog } from './dialogs';
import { nls } from '../common/nls';
import { DisposableCollection, isObject } from '../common';
import { BinaryBuffer } from '../common/buffer';

export type AutoSaveMode = 'off' | 'afterDelay' | 'onFocusChange' | 'onWindowChange';

Expand Down Expand Up @@ -53,6 +54,10 @@ export interface Saveable {
* Applies the given snapshot to the dirty state.
*/
applySnapshot?(snapshot: object): void;
/**
* Serializes the full state of the saveable item to a binary buffer.
*/
serialize?(): Promise<BinaryBuffer>;
}

export interface SaveableSource {
Expand All @@ -79,6 +84,7 @@ export class DelegatingSaveable implements Saveable {
revert?(options?: Saveable.RevertOptions): Promise<void>;
createSnapshot?(): Saveable.Snapshot;
applySnapshot?(snapshot: object): void;
serialize?(): Promise<BinaryBuffer>;

protected _delegate?: Saveable;
protected toDispose = new DisposableCollection();
Expand All @@ -101,6 +107,7 @@ export class DelegatingSaveable implements Saveable {
this.revert = delegate.revert?.bind(delegate);
this.createSnapshot = delegate.createSnapshot?.bind(delegate);
this.applySnapshot = delegate.applySnapshot?.bind(delegate);
this.serialize = delegate.serialize?.bind(delegate);
}

}
Expand All @@ -115,7 +122,8 @@ export namespace Saveable {
}

/**
* A snapshot of a saveable item. Contains the full content of the saveable file in a string serializable format.
* A snapshot of a saveable item.
* Applying a snapshot of a saveable on another (of the same type) using the `applySnapshot` should yield the state of the original saveable.
*/
export type Snapshot = { value: string } | { read(): string | null };
export namespace Snapshot {
Expand Down
22 changes: 16 additions & 6 deletions packages/filesystem/src/browser/filesystem-saveable-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { SaveableService } from '@theia/core/lib/browser/saveable-service';
import URI from '@theia/core/lib/common/uri';
import { FileService } from './file-service';
import { FileDialogService } from './file-dialog';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';

@injectable()
export class FilesystemSaveableService extends SaveableService {
Expand All @@ -39,13 +40,13 @@ export class FilesystemSaveableService extends SaveableService {
/**
* This method ensures a few things about `widget`:
* - `widget.getResourceUri()` actually returns a URI.
* - `widget.saveable.createSnapshot` is defined.
* - `widget.saveable.createSnapshot` or `widget.saveable.serialize` 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.createSnapshot === 'function' || typeof widget.saveable.serialize === 'function')
&& typeof widget.saveable.revert === 'function'
&& Navigatable.is(widget)
&& widget.getResourceUri() !== undefined;
Expand Down Expand Up @@ -96,14 +97,23 @@ export class FilesystemSaveableService extends SaveableService {
*/
protected async saveSnapshot(sourceWidget: Widget & SaveableSource & Navigatable, target: URI, overwrite: boolean): Promise<void> {
const saveable = sourceWidget.saveable;
const snapshot = saveable.createSnapshot!();
const content = Saveable.Snapshot.read(snapshot) ?? '';
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.write(target, content);
await this.fileService.writeFile(target, buffer);
} else {
// Ensure to actually call `create` as that fires the `onDidCreate` event.
await this.fileService.create(target, content, { overwrite });
await this.fileService.createFile(target, buffer, { overwrite });
}
await saveable.revert!();
await open(this.openerService, target, { widgetOptions: { ref: sourceWidget, mode: 'tab-replace' } });
Expand Down
5 changes: 5 additions & 0 deletions packages/monaco/src/browser/monaco-editor-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { IModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/se
import { createTextBufferFactoryFromStream } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel';
import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/browser/editor-generated-preference-schema';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';

export {
TextDocumentSaveReason
Expand Down Expand Up @@ -655,6 +656,10 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
this.model.setValue(value);
}

async serialize(): Promise<BinaryBuffer> {
return BinaryBuffer.fromString(this.model.getValue());
}

protected trace(loggable: Loggable): void {
if (this.logger) {
this.logger.debug((log: Log) =>
Expand Down
8 changes: 6 additions & 2 deletions packages/notebook/src/browser/view-model/notebook-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { inject, injectable, interfaces, postConstruct } from '@theia/core/share
import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import type { NotebookModelResolverService } from '../service/notebook-model-resolver-service';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';

export const NotebookModelFactory = Symbol('NotebookModelFactory');

Expand Down Expand Up @@ -176,8 +177,7 @@ export class NotebookModel implements Saveable, Disposable {
this.dirtyCells = [];
this.dirty = false;

const data = this.getData();
const serializedNotebook = await this.props.serializer.fromNotebook(data);
const serializedNotebook = await this.serialize();
this.fileService.writeFile(this.uri, serializedNotebook);

this.onDidSaveNotebookEmitter.fire();
Expand All @@ -189,6 +189,10 @@ export class NotebookModel implements Saveable, Disposable {
};
}

serialize(): Promise<BinaryBuffer> {
return this.props.serializer.fromNotebook(this.getData());
}

async applySnapshot(snapshot: Saveable.Snapshot): Promise<void> {
const rawData = Saveable.Snapshot.read(snapshot);
if (!rawData) {
Expand Down

0 comments on commit c7af872

Please sign in to comment.