// ***************************************************************************** // 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-only WITH Classpath-exception-2.0 // ***************************************************************************** import { environment, MessageService, nls } from '@theia/core'; import { inject, injectable } from '@theia/core/shared/inversify'; import { Navigatable, Saveable, SaveableSource, SaveOptions, Widget, open, OpenerService, ConfirmDialog, CommonCommands, LabelProvider } from '@theia/core/lib/browser'; 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 { @inject(MessageService) protected readonly messageService: MessageService; @inject(FileService) protected readonly fileService: FileService; @inject(FileDialogService) protected readonly fileDialogService: FileDialogService; @inject(OpenerService) protected readonly openerService: OpenerService; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; /** * This method ensures a few things about `widget`: * - `widget.getResourceUri()` actually returns a URI. * - `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.serialize === 'function' || typeof widget.saveable.saveAs === '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 { let exist: boolean = false; let overwrite: boolean = false; let selected: URI | undefined; const canSave = this.canSaveNotSaveAs(sourceWidget); const uri: URI = sourceWidget.getResourceUri()!; let filters: { [name: string]: string[] } = { 'All Files': ['*'] }; if (sourceWidget.saveable.filters) { filters = { ...sourceWidget.saveable.filters(), ...filters }; } do { selected = await this.fileDialogService.showSaveDialog( { title: CommonCommands.SAVE_AS.label!, filters: filters, inputValue: uri.path.base }); 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)) { return this.save(sourceWidget, options); } else if (selected) { try { await this.saveSnapshot(sourceWidget, selected, overwrite); return selected; } catch (e) { console.warn(e); } } } /** * Saves the current snapshot of the {@link sourceWidget} to the target file * and replaces the widget with a new one that contains the snapshot content * * @param sourceWidget widget to save as `target`. * @param target The new URI for the widget. * @param overwrite */ protected async saveSnapshot(sourceWidget: Widget & SaveableSource & Navigatable, target: URI, overwrite: boolean): Promise { const saveable = sourceWidget.saveable; if (saveable.saveAs) { // Some widgets have their own "Save As" implementation, such as the custom plugin editors await saveable.saveAs({ target }); } else { // 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' } }); } async confirmOverwrite(uri: URI): Promise { // 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?', this.labelProvider.getName(uri)) }).open(); return !!confirmed; } private isElectron(): boolean { return environment.electron.is(); } }