diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 6dddd463989..d96918ef4f5 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1250,6 +1250,13 @@ declare module 'vscode' { */ save(): Thenable; + /** + * + * @param resource Resource being saved. + * @param targetResource Location to save to. + */ + saveAs(resource: Uri, targetResource: Uri): Thenable; + /** * Event triggered by extensions to signal to VS Code that an edit has occurred. * diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 14c9efb3e55..a27f62c02d5 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -276,6 +276,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma model.onUndo(edits => { this._proxy.$undoEdits(handle, edits); }); model.onRedo(edits => { this._proxy.$redoEdits(handle, edits); }); model.onWillSave(e => { e.waitUntil(this._proxy.$onSave(handle)); }); + model.onWillSaveAs(e => { e.waitUntil(this._proxy.$onSaveAs(handle, e.resource.toJSON(), e.targetResource.toJSON())); }); webviewInput.onDispose(() => { this._customEditorService.models.disposeModel(model); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 5a735828101..72d18a284d4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -590,11 +590,15 @@ export interface ExtHostWebviewsShape { $onMissingCsp(handle: WebviewPanelHandle, extensionId: string): void; $onDidChangeWebviewPanelViewStates(newState: WebviewPanelViewStateData): void; $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise; + $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; + $undoEdits(handle: WebviewPanelHandle, edits: readonly any[]): void; $redoEdits(handle: WebviewPanelHandle, edits: readonly any[]): void; + $onSave(handle: WebviewPanelHandle): Promise; + $onSaveAs(handle: WebviewPanelHandle, resource: UriComponents, targetResource: UriComponents): Promise; } export interface MainThreadUrlsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 3cf879271d3..d1081d0f11e 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -260,6 +260,11 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa await assertIsDefined(this._capabilities).editingCapability?.save(); } + + async _onSaveAs(resource: vscode.Uri, targetResource: vscode.Uri): Promise { + await assertIsDefined(this._capabilities).editingCapability?.saveAs(resource, targetResource); + } + private assertNotDisposed() { if (this._isDisposed) { throw new Error('Webview is disposed'); @@ -462,6 +467,11 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { return panel?._onSave(); } + async $onSaveAs(handle: WebviewPanelHandle, resource: UriComponents, targetResource: UriComponents): Promise { + const panel = this.getWebviewPanel(handle); + return panel?._onSaveAs(URI.revive(resource), URI.revive(targetResource)); + } + private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined { return this._webviewPanels.get(handle); } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 9d3653b9d8f..397cae24e28 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -9,6 +9,7 @@ import { UnownedDisposable } from 'vs/base/common/lifecycle'; import { basename } from 'vs/base/common/path'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; @@ -16,6 +17,7 @@ import { GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, Verbosity import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { WebviewEditorOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { @@ -33,6 +35,8 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { @IWebviewWorkbenchService webviewWorkbenchService: IWebviewWorkbenchService, @ILabelService private readonly labelService: ILabelService, @ICustomEditorService private readonly customEditorService: ICustomEditorService, + @IEditorService private readonly editorService: IEditorService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, ) { super(id, viewType, '', webview, webviewWorkbenchService, lifecycleService); this._editorResource = resource; @@ -101,9 +105,32 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { return this._model ? this._model.save(options) : Promise.resolve(false); } - public saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - // TODO@matt implement properly (see TextEditorInput#saveAs()) - return this._model ? this._model.save(options) : Promise.resolve(false); + public async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + if (!this._model) { + return false; + } + + // Preserve view state by opening the editor first. In addition + // this allows the user to review the contents of the editor. + // let viewState: IEditorViewState | undefined = undefined; + // const editor = await this.editorService.openEditor(this, undefined, group); + // if (isTextEditor(editor)) { + // viewState = editor.getViewState(); + // } + + let dialogPath = this._editorResource; + // if (this._editorResource.scheme === Schemas.untitled) { + // dialogPath = this.suggestFileName(resource); + // } + + const target = await this.promptForPath(this._editorResource, dialogPath, options?.availableFileSystems); + if (!target) { + return false; // save cancelled + } + + await this._model.saveAs(this._editorResource, target, options); + + return true; } public revert(options?: IRevertOptions): Promise { @@ -115,4 +142,12 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { this._register(this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); return await super.resolve(); } + + protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: readonly string[]): Promise { + + // Help user to find a name for the file by opening it first + await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true } }); + + return this.fileDialogService.pickFileToSave({});//this.getSaveDialogOptions(defaultUri, availableFileSystems)); + } } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 09b882531fb..2e08c304d7a 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -48,16 +48,29 @@ export interface ICustomEditorModelManager { disposeModel(model: ICustomEditorModel): void; } +export interface CustomEditorSaveEvent { + readonly resource: URI; + readonly waitUntil: (until: Promise) => void; +} + +export interface CustomEditorSaveAsEvent { + readonly resource: URI; + readonly targetResource: URI; + readonly waitUntil: (until: Promise) => void; +} + export interface ICustomEditorModel extends IWorkingCopy { readonly onUndo: Event; readonly onRedo: Event; - readonly onWillSave: Event<{ waitUntil: (until: Promise) => void }>; + readonly onWillSave: Event; + readonly onWillSaveAs: Event; undo(): void; redo(): void; revert(options?: IRevertOptions): Promise; save(options?: ISaveOptions): Promise; + saveAs(resource: URI, targetResource: URI, currentOptions?: ISaveOptions): Promise; makeEdit(data: string): void; } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index ee49e9aaa14..2b36d9569d2 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { ICustomEditorModel, CustomEditorEdit } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { ICustomEditorModel, CustomEditorEdit, CustomEditorSaveAsEvent, CustomEditorSaveEvent } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; @@ -47,9 +47,12 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel protected readonly _onRedo = this._register(new Emitter()); readonly onRedo = this._onRedo.event; - protected readonly _onWillSave = this._register(new Emitter<{ waitUntil: (until: Promise) => void }>()); + protected readonly _onWillSave = this._register(new Emitter()); readonly onWillSave = this._onWillSave.event; + protected readonly _onWillSaveAs = this._register(new Emitter()); + readonly onWillSaveAs = this._onWillSaveAs.event; + public makeEdit(data: string): void { this._edits.splice(this._currentEditIndex + 1, this._edits.length - this._currentEditIndex, data); this._currentEditIndex = this._edits.length - 1; @@ -62,7 +65,10 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel public async save(_options?: ISaveOptions): Promise { const untils: Promise[] = []; - const handler = { waitUntil: (until: Promise) => untils.push(until) }; + const handler: CustomEditorSaveEvent = { + resource: this._resource, + waitUntil: (until: Promise) => untils.push(until) + }; try { this._onWillSave.fire(handler); @@ -77,6 +83,27 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel return true; } + public async saveAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise { + const untils: Promise[] = []; + const handler: CustomEditorSaveAsEvent = { + resource, + targetResource, + waitUntil: (until: Promise) => untils.push(until) + }; + + try { + this._onWillSaveAs.fire(handler); + await Promise.all(untils); + } catch { + return false; + } + + this._savePoint = this._currentEditIndex; + this.updateDirty(); + + return true; + } + public async revert(_options?: IRevertOptions) { if (this._currentEditIndex === this._savePoint) { return true;