diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index f2b4af002..bcbd4747f 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -5,17 +5,14 @@ import { postConstruct, } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; -import { SketchesService } from '../common/protocol'; import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, } from '@theia/core'; import { - Dialog, FrontendApplication, FrontendApplicationContribution, - OnWillStopAction, } from '@theia/core/lib/browser'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; @@ -34,14 +31,9 @@ import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser'; import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu'; import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution'; import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; -import { - CurrentSketch, - SketchesServiceClientImpl, -} from '../common/protocol/sketches-service-client-impl'; import { ArduinoPreferences } from './arduino-preferences'; import { BoardsServiceProvider } from './boards/boards-service-provider'; import { BoardsToolBarItem } from './boards/boards-toolbar-item'; -import { SaveAsSketch } from './contributions/save-as-sketch'; import { ArduinoMenus } from './menu/arduino-menus'; import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution'; import { ArduinoToolbar } from './toolbar/arduino-toolbar'; @@ -63,18 +55,12 @@ export class ArduinoFrontendContribution @inject(BoardsServiceProvider) private readonly boardsServiceProvider: BoardsServiceProvider; - @inject(SketchesService) - private readonly sketchService: SketchesService; - @inject(CommandRegistry) private readonly commandRegistry: CommandRegistry; @inject(ArduinoPreferences) private readonly arduinoPreferences: ArduinoPreferences; - @inject(SketchesServiceClientImpl) - private readonly sketchServiceClient: SketchesServiceClientImpl; - @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; @@ -91,7 +77,7 @@ export class ArduinoFrontendContribution } } - async onStart(app: FrontendApplication): Promise { + onStart(app: FrontendApplication): void { this.arduinoPreferences.onPreferenceChanged((event) => { if (event.newValue !== event.oldValue) { switch (event.preferenceName) { @@ -303,58 +289,4 @@ export class ArduinoFrontendContribution } ); } - - // TODO: should be handled by `Close` contribution. https://github.com/arduino/arduino-ide/issues/1016 - onWillStop(): OnWillStopAction { - return { - reason: 'temp-sketch', - action: () => { - return this.showTempSketchDialog(); - }, - }; - } - - private async showTempSketchDialog(): Promise { - const sketch = await this.sketchServiceClient.currentSketch(); - if (!CurrentSketch.isValid(sketch)) { - return true; - } - const isTemp = await this.sketchService.isTemp(sketch); - if (!isTemp) { - return true; - } - const messageBoxResult = await remote.dialog.showMessageBox( - remote.getCurrentWindow(), - { - message: nls.localize( - 'arduino/sketch/saveTempSketch', - 'Save your sketch to open it again later.' - ), - title: nls.localize( - 'theia/core/quitTitle', - 'Are you sure you want to quit?' - ), - type: 'question', - buttons: [ - Dialog.CANCEL, - nls.localizeByDefault('Save As...'), - nls.localizeByDefault("Don't Save"), - ], - } - ); - const result = messageBoxResult.response; - if (result === 2) { - return true; - } else if (result === 1) { - return !!(await this.commandRegistry.executeCommand( - SaveAsSketch.Commands.SAVE_AS_SKETCH.id, - { - execOnlyIfTemp: false, - openAfterMove: false, - wipeOriginal: true, - } - )); - } - return false; - } } diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 622819e67..5c6c2bdea 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -321,6 +321,8 @@ import { MonacoThemeServiceIsReady } from './utils/window'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { StatusBarImpl } from './theia/core/status-bar'; import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser'; +import { EditorMenuContribution } from './theia/editor/editor-file'; +import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu'; const registerArduinoThemes = () => { const themes: MonacoThemeJson[] = [ @@ -640,6 +642,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(WindowContribution).toSelf().inSingletonScope(); rebind(TheiaWindowContribution).toService(WindowContribution); + // To remove `File` > `Close Editor`. + bind(EditorMenuContribution).toSelf().inSingletonScope(); + rebind(TheiaEditorMenuContribution).toService(EditorMenuContribution); + bind(ArduinoDaemon) .toDynamicValue((context) => WebSocketConnectionProvider.createProxy( diff --git a/arduino-ide-extension/src/browser/contributions/close.ts b/arduino-ide-extension/src/browser/contributions/close.ts index 45597613c..033d02edd 100644 --- a/arduino-ide-extension/src/browser/contributions/close.ts +++ b/arduino-ide-extension/src/browser/contributions/close.ts @@ -1,9 +1,14 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; +import { injectable } from '@theia/core/shared/inversify'; +import { toArray } from '@theia/core/shared/@phosphor/algorithm'; import * as remote from '@theia/core/electron-shared/@electron/remote'; import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; -import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; +import type { MaybePromise } from '@theia/core/lib/common/types'; +import type { + FrontendApplication, + OnWillStopAction, +} from '@theia/core/lib/browser/frontend-application'; +import { nls } from '@theia/core/lib/common/nls'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; -import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; import { ArduinoMenus } from '../menu/arduino-menus'; import { SketchContribution, @@ -11,27 +16,48 @@ import { CommandRegistry, MenuModelRegistry, KeybindingRegistry, + Sketch, URI, } from './contribution'; -import { nls } from '@theia/core/lib/common'; +import { Dialog } from '@theia/core/lib/browser/dialogs'; +import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; +import { SaveAsSketch } from './save-as-sketch'; /** * Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window. */ @injectable() export class Close extends SketchContribution { - @inject(EditorManager) - protected override readonly editorManager: EditorManager; + private shell: ApplicationShell | undefined; - protected shell: ApplicationShell; - - override onStart(app: FrontendApplication): void { + override onStart(app: FrontendApplication): MaybePromise { this.shell = app.shell; } override registerCommands(registry: CommandRegistry): void { registry.registerCommand(Close.Commands.CLOSE, { - execute: () => remote.getCurrentWindow().close() + execute: () => { + // Close current editor if closeable. + const { currentEditor } = this.editorManager; + if (currentEditor && currentEditor.title.closable) { + currentEditor.close(); + return; + } + + if (this.shell) { + // Close current widget from the main area if possible. + const { currentWidget } = this.shell; + if (currentWidget) { + const currentWidgetInMain = toArray( + this.shell.mainPanel.widgets() + ).find((widget) => widget === currentWidget); + if (currentWidgetInMain && currentWidgetInMain.title.closable) { + return currentWidgetInMain.close(); + } + } + } + return remote.getCurrentWindow().close(); + }, }); } @@ -50,6 +76,123 @@ export class Close extends SketchContribution { }); } + // `FrontendApplicationContribution#onWillStop` + onWillStop(): OnWillStopAction { + return { + reason: 'save-sketch', + action: () => { + return this.showSaveSketchDialog(); + }, + }; + } + + /** + * If returns with `true`, IDE2 will close. Otherwise, it won't. + */ + private async showSaveSketchDialog(): Promise { + const sketch = await this.isCurrentSketchTemp(); + if (!sketch) { + // Normal close workflow: if there are dirty editors prompt the user. + if (!this.shell) { + console.error( + `Could not get the application shell. Something went wrong.` + ); + return true; + } + if (this.shell.canSaveAll()) { + const prompt = await this.prompt(false); + switch (prompt) { + case Prompt.DoNotSave: + return true; + case Prompt.Cancel: + return false; + case Prompt.Save: { + await this.shell.saveAll(); + return true; + } + default: + throw new Error(`Unexpected prompt: ${prompt}`); + } + } + return true; + } + + // If non of the sketch files were ever touched, do not prompt the save dialog. (#1274) + const wereTouched = await Promise.all( + Sketch.uris(sketch).map((uri) => this.wasTouched(uri)) + ); + if (wereTouched.every((wasTouched) => !Boolean(wasTouched))) { + return true; + } + + const prompt = await this.prompt(true); + switch (prompt) { + case Prompt.DoNotSave: + return true; + case Prompt.Cancel: + return false; + case Prompt.Save: { + // If `save as` was canceled by user, the result will be `undefined`, otherwise the new URI. + const result = await this.commandService.executeCommand( + SaveAsSketch.Commands.SAVE_AS_SKETCH.id, + { + execOnlyIfTemp: false, + openAfterMove: false, + wipeOriginal: true, + markAsRecentlyOpened: true, + } + ); + return !!result; + } + default: + throw new Error(`Unexpected prompt: ${prompt}`); + } + } + + private async prompt(isTemp: boolean): Promise { + const { response } = await remote.dialog.showMessageBox( + remote.getCurrentWindow(), + { + message: nls.localize( + 'arduino/sketch/saveSketch', + 'Save your sketch to open it again later.' + ), + title: nls.localize( + 'theia/core/quitTitle', + 'Are you sure you want to quit?' + ), + type: 'question', + buttons: [ + nls.localizeByDefault("Don't Save"), + Dialog.CANCEL, + nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'), + ], + defaultId: 2, // `Save`/`Save As...` button index is the default. + } + ); + switch (response) { + case 0: + return Prompt.DoNotSave; + case 1: + return Prompt.Cancel; + case 2: + return Prompt.Save; + default: + throw new Error(`Unexpected response: ${response}`); + } + } + + private async isCurrentSketchTemp(): Promise { + const currentSketch = await this.sketchServiceClient.currentSketch(); + if (CurrentSketch.isValid(currentSketch)) { + const isTemp = await this.sketchService.isTemp(currentSketch); + if (isTemp) { + return currentSketch; + } + } + return false; + } + /** * If the file was ever touched/modified. We get this based on the `version` of the monaco model. */ @@ -59,13 +202,23 @@ export class Close extends SketchContribution { const { editor } = editorWidget; if (editor instanceof MonacoEditor) { const versionId = editor.getControl().getModel()?.getVersionId(); - if (Number.isInteger(versionId) && versionId! > 1) { + if (this.isInteger(versionId) && versionId > 1) { return true; } } } return false; } + + private isInteger(arg: unknown): arg is number { + return Number.isInteger(arg); + } +} + +enum Prompt { + Save, + DoNotSave, + Cancel, } export namespace Close { diff --git a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts index 36071db0e..2954a6038 100644 --- a/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/save-as-sketch.ts @@ -57,6 +57,7 @@ export class SaveAsSketch extends SketchContribution { execOnlyIfTemp, openAfterMove, wipeOriginal, + markAsRecentlyOpened, }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT ): Promise { const sketch = await this.sketchServiceClient.currentSketch(); @@ -102,18 +103,22 @@ export class SaveAsSketch extends SketchContribution { }); if (workspaceUri) { await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri); + if (markAsRecentlyOpened) { + this.sketchService.markAsRecentlyOpened(workspaceUri); + } } if (workspaceUri && openAfterMove) { + this.windowService.setSafeToShutDown(); if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) { - try { - await this.fileService.delete(new URI(sketch.uri), { - recursive: true, - }); - } catch { - /* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */ - } + // This window will navigate away. + // Explicitly stop the contribution to dispose the file watcher before deleting the temp sketch. + // Otherwise, users might see irrelevant _Unable to watch for file changes in this large workspace._ notification. + // https://github.com/arduino/arduino-ide/issues/39. + this.sketchServiceClient.onStop(); + // TODO: consider implementing the temp sketch deletion the following way: + // Open the other sketch with a `delete the temp sketch` startup-task. + this.sketchService.notifyDeleteSketch(sketch); // This is a notification and will execute on the backend. } - this.windowService.setSafeToShutDown(); this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true, }); @@ -170,12 +175,14 @@ export namespace SaveAsSketch { * Ignored if `openAfterMove` is `false`. */ readonly wipeOriginal?: boolean; + readonly markAsRecentlyOpened?: boolean; } export namespace Options { export const DEFAULT: Options = { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false, + markAsRecentlyOpened: false, }; } } diff --git a/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts b/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts index ad7704adb..fcd07cb67 100644 --- a/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts +++ b/arduino-ide-extension/src/browser/contributions/sketch-files-tracker.ts @@ -30,10 +30,7 @@ export class SketchFilesTracker extends SketchContribution { override onReady(): void { this.sketchServiceClient.currentSketch().then(async (sketch) => { - if ( - CurrentSketch.isValid(sketch) && - !(await this.sketchService.isTemp(sketch)) - ) { + if (CurrentSketch.isValid(sketch)) { this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri))); this.toDisposeOnStop.push( this.fileService.onDidFilesChange(async (event) => { diff --git a/arduino-ide-extension/src/browser/style/editor.css b/arduino-ide-extension/src/browser/style/editor.css index 19bb6f5d8..81c3a3fdd 100644 --- a/arduino-ide-extension/src/browser/style/editor.css +++ b/arduino-ide-extension/src/browser/style/editor.css @@ -1,8 +1,7 @@ /* Show the dirty indicator on unclosable widgets. On hover, it should still show the dot instead of the X. */ /* https://github.com/arduino/arduino-pro-ide/issues/380 */ -.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty > .p-TabBar-tabCloseIcon:hover { - background-size: 13px; - background-image: var(--theia-icon-circle); +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.a-mod-uncloseable.theia-mod-dirty > .p-TabBar-tabCloseIcon:before { + content: "\ea71"; } .monaco-list-row.show-file-icons.focused { diff --git a/arduino-ide-extension/src/browser/theia/core/application-shell.ts b/arduino-ide-extension/src/browser/theia/core/application-shell.ts index f0610a569..9f0ac6b74 100644 --- a/arduino-ide-extension/src/browser/theia/core/application-shell.ts +++ b/arduino-ide-extension/src/browser/theia/core/application-shell.ts @@ -1,73 +1,30 @@ -import { injectable, inject } from '@theia/core/shared/inversify'; -import { EditorWidget } from '@theia/editor/lib/browser'; -import { CommandService } from '@theia/core/lib/common/command'; -import { MessageService } from '@theia/core/lib/common/message-service'; -import { OutputWidget } from '@theia/output/lib/browser/output-widget'; -import { - ConnectionStatusService, - ConnectionStatus, -} from '@theia/core/lib/browser/connection-status-service'; import { ApplicationShell as TheiaApplicationShell, DockPanel, DockPanelRenderer as TheiaDockPanelRenderer, Panel, + SaveOptions, + SHELL_TABBAR_CONTEXT_MENU, TabBar, Widget, - SHELL_TABBAR_CONTEXT_MENU, } from '@theia/core/lib/browser'; -import { Sketch } from '../../../common/protocol'; -import { SaveAsSketch } from '../../contributions/save-as-sketch'; import { - CurrentSketch, - SketchesServiceClientImpl, -} from '../../../common/protocol/sketches-service-client-impl'; -import { nls } from '@theia/core/lib/common'; -import URI from '@theia/core/lib/common/uri'; + ConnectionStatus, + ConnectionStatusService, +} from '@theia/core/lib/browser/connection-status-service'; +import { nls } from '@theia/core/lib/common/nls'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { ToolbarAwareTabBar } from './tab-bars'; @injectable() export class ApplicationShell extends TheiaApplicationShell { - @inject(CommandService) - private readonly commandService: CommandService; - @inject(MessageService) private readonly messageService: MessageService; - @inject(SketchesServiceClientImpl) - private readonly sketchesServiceClient: SketchesServiceClientImpl; - @inject(ConnectionStatusService) private readonly connectionStatusService: ConnectionStatusService; - protected override track(widget: Widget): void { - super.track(widget); - if (widget instanceof OutputWidget) { - widget.title.closable = false; // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700 - } - if (widget instanceof EditorWidget) { - // Make the editor un-closeable asynchronously. - this.sketchesServiceClient.currentSketch().then((sketch) => { - if (CurrentSketch.isValid(sketch)) { - if (!this.isSketchFile(widget.editor.uri, sketch.uri)) { - return; - } - if (Sketch.isInSketch(widget.editor.uri, sketch)) { - widget.title.closable = false; - } - } - }); - } - } - - private isSketchFile(uri: URI, sketchUriString: string): boolean { - const sketchUri = new URI(sketchUriString); - if (uri.parent.isEqual(sketchUri)) { - return true; - } - return false; - } - override async addWidget( widget: Widget, options: Readonly = {} @@ -106,7 +63,7 @@ export class ApplicationShell extends TheiaApplicationShell { return topPanel; } - override async saveAll(): Promise { + override async saveAll(options?: SaveOptions): Promise { if ( this.connectionStatusService.currentStatus === ConnectionStatus.OFFLINE ) { @@ -118,12 +75,7 @@ export class ApplicationShell extends TheiaApplicationShell { ); return; // Theia does not reject on failed save: https://github.com/eclipse-theia/theia/pull/8803 } - await super.saveAll(); - const options = { execOnlyIfTemp: true, openAfterMove: true }; - await this.commandService.executeCommand( - SaveAsSketch.Commands.SAVE_AS_SKETCH.id, - options - ); + return super.saveAll(options); } } diff --git a/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts b/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts index c92d4972f..0785cad03 100644 --- a/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/theia/core/common-frontend-contribution.ts @@ -5,6 +5,7 @@ import { CommonCommands, } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommandRegistry } from '@theia/core/lib/common/command'; +import type { OnWillStopAction } from '@theia/core/lib/browser/frontend-application'; @injectable() export class CommonFrontendContribution extends TheiaCommonFrontendContribution { @@ -48,4 +49,9 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution registry.unregisterMenuAction(command); } } + + override onWillStop(): OnWillStopAction | undefined { + // This is NOOP here. All window close and app quit requests are handled in the `Close` contribution. + return undefined; + } } diff --git a/arduino-ide-extension/src/browser/theia/core/widget-manager.ts b/arduino-ide-extension/src/browser/theia/core/widget-manager.ts index adc32860f..2e98c2bfc 100644 --- a/arduino-ide-extension/src/browser/theia/core/widget-manager.ts +++ b/arduino-ide-extension/src/browser/theia/core/widget-manager.ts @@ -1,11 +1,78 @@ import type { MaybePromise } from '@theia/core'; import type { Widget } from '@theia/core/lib/browser'; import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager'; -import { injectable } from '@theia/core/shared/inversify'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; +import { EditorWidget } from '@theia/editor/lib/browser'; +import { OutputWidget } from '@theia/output/lib/browser/output-widget'; import deepEqual = require('deep-equal'); +import { + CurrentSketch, + SketchesServiceClientImpl, +} from '../../../common/protocol/sketches-service-client-impl'; @injectable() export class WidgetManager extends TheiaWidgetManager { + @inject(SketchesServiceClientImpl) + private readonly sketchesServiceClient: SketchesServiceClientImpl; + + @postConstruct() + protected init(): void { + this.sketchesServiceClient.onCurrentSketchDidChange((sketch) => + this.maybeSetWidgetUncloseable( + sketch, + ...Array.from(this.widgets.values()) + ) + ); + } + + override getOrCreateWidget( + factoryId: string, + options?: unknown + ): Promise { + const unresolvedWidget = super.getOrCreateWidget(factoryId, options); + unresolvedWidget.then(async (widget) => { + const sketch = await this.sketchesServiceClient.currentSketch(); + this.maybeSetWidgetUncloseable(sketch, widget); + }); + return unresolvedWidget; + } + + private maybeSetWidgetUncloseable( + sketch: CurrentSketch, + ...widgets: Widget[] + ): void { + const sketchFileUris = + CurrentSketch.isValid(sketch) && + new Set([sketch.mainFileUri, ...sketch.rootFolderFileUris]); + for (const widget of widgets) { + if (widget instanceof OutputWidget) { + this.setWidgetUncloseable(widget); // TODO: https://arduino.slack.com/archives/C01698YT7S4/p1598011990133700 + } else if (widget instanceof EditorWidget) { + // Make the editor un-closeable asynchronously. + const uri = widget.editor.uri.toString(); + if (!!sketchFileUris && sketchFileUris.has(uri)) { + this.setWidgetUncloseable(widget); + } + } + } + } + + private setWidgetUncloseable(widget: Widget): void { + const { title } = widget; + if (title.closable) { + title.closable = false; + } + // Show the dirty indicator on uncloseable widgets when hovering over the title. Instead of showing the `X` for close. + const uncloseableClass = 'a-mod-uncloseable'; + if (!title.className.includes(uncloseableClass)) { + title.className += title.className + ` ${uncloseableClass}`; + } + } + /** * Customized to find any existing widget based on `options` deepEquals instead of string equals. * See https://github.com/eclipse-theia/theia/issues/11309. diff --git a/arduino-ide-extension/src/browser/theia/editor/editor-file.ts b/arduino-ide-extension/src/browser/theia/editor/editor-file.ts new file mode 100644 index 000000000..721a7783d --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/editor/editor-file.ts @@ -0,0 +1,12 @@ +import { MenuModelRegistry } from '@theia/core'; +import { CommonCommands } from '@theia/core/lib/browser'; +import { injectable } from '@theia/core/shared/inversify'; +import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu'; + +@injectable() +export class EditorMenuContribution extends TheiaEditorMenuContribution { + override registerMenus(registry: MenuModelRegistry): void { + super.registerMenus(registry); + registry.unregisterMenuAction(CommonCommands.CLOSE_MAIN_TAB.id); + } +} diff --git a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts index 7aea3367d..34da46bfd 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts @@ -10,7 +10,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { Sketch, SketchesService } from '../../common/protocol'; import { ConfigService } from './config-service'; -import { SketchContainer, SketchRef } from './sketches-service'; +import { SketchContainer, SketchesError, SketchRef } from './sketches-service'; import { ARDUINO_CLOUD_FOLDER, REMOTE_SKETCHBOOK_FOLDER, @@ -79,6 +79,7 @@ export class SketchesServiceClientImpl this.sketches.set(sketch.uri, sketch); } this.toDispose.push( + // Watch changes in the sketchbook to update `File` > `Sketchbook` menu items. this.fileService.watch(new URI(sketchDirUri), { recursive: true, excludes: [], @@ -87,6 +88,45 @@ export class SketchesServiceClientImpl this.toDispose.push( this.fileService.onDidFilesChange(async (event) => { for (const { type, resource } of event.changes) { + // The file change events have higher precedence in the current sketch over the sketchbook. + if ( + CurrentSketch.isValid(this._currentSketch) && + new URI(this._currentSketch.uri).isEqualOrParent(resource) + ) { + // https://github.com/arduino/arduino-ide/pull/1351#pullrequestreview-1086666656 + // On a sketch file rename, the FS watcher will contain two changes: + // - Deletion of the original file, + // - Update of the new file, + // Hence, `UPDATE` events must be processed but only and if only there is a `DELETED` change in the same event. + // Otherwise, IDE2 would ask CLI to reload the sketch content on every save event in IDE2. + if ( + type === FileChangeType.UPDATED && + event.changes.length === 1 + ) { + // If the event contains only one `UPDATE` change, it cannot be a rename. + return; + } + + let reloadedSketch: Sketch | undefined = undefined; + try { + reloadedSketch = await this.sketchService.loadSketch( + this._currentSketch.uri + ); + } catch (err) { + if (!SketchesError.NotFound.is(err)) { + throw err; + } + } + + if (!reloadedSketch) { + return; + } + + if (!Sketch.sameAs(this._currentSketch, reloadedSketch)) { + this.useCurrentSketch(reloadedSketch, true); + } + return; + } // We track main sketch files changes only. // TODO: check sketch folder changes. One can rename the folder without renaming the `.ino` file. if (sketchbookUri.isEqualOrParent(resource)) { if (Sketch.isSketchFile(resource)) { @@ -125,12 +165,31 @@ export class SketchesServiceClientImpl .reachedState('started_contributions') .then(async () => { const currentSketch = await this.loadCurrentSketch(); - this._currentSketch = currentSketch; - this.currentSketchDidChangeEmitter.fire(this._currentSketch); - this.currentSketchLoaded.resolve(this._currentSketch); + if (CurrentSketch.isValid(currentSketch)) { + this.toDispose.pushAll([ + // Watch the file changes of the current sketch + this.fileService.watch(new URI(currentSketch.uri), { + recursive: true, + excludes: [], + }), + ]); + } + this.useCurrentSketch(currentSketch); }); } + private useCurrentSketch( + currentSketch: CurrentSketch, + reassignPromise = false + ) { + this._currentSketch = currentSketch; + if (reassignPromise) { + this.currentSketchLoaded = new Deferred(); + } + this.currentSketchLoaded.resolve(this._currentSketch); + this.currentSketchDidChangeEmitter.fire(this._currentSketch); + } + onStop(): void { this.toDispose.dispose(); } diff --git a/arduino-ide-extension/src/common/protocol/sketches-service.ts b/arduino-ide-extension/src/common/protocol/sketches-service.ts index 0394e6e94..719ecaacd 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service.ts @@ -95,6 +95,11 @@ export interface SketchesService { * Based on https://github.com/arduino/arduino-cli/blob/550179eefd2d2bca299d50a4af9e9bfcfebec649/arduino/builder/builder.go#L30-L38 */ getIdeTempFolderUri(sketch: Sketch): Promise; + + /** + * Notifies the backend to recursively delete the sketch folder with all its content. + */ + notifyDeleteSketch(sketch: Sketch): void; } export interface SketchRef { @@ -157,6 +162,74 @@ export namespace Sketch { const { mainFileUri, otherSketchFileUris, additionalFileUris } = sketch; return [mainFileUri, ...otherSketchFileUris, ...additionalFileUris]; } + const primitiveProps: Array = ['name', 'uri', 'mainFileUri']; + const arrayProps: Array = [ + 'additionalFileUris', + 'otherSketchFileUris', + 'rootFolderFileUris', + ]; + export function sameAs(left: Sketch, right: Sketch): boolean { + for (const prop of primitiveProps) { + const leftValue = left[prop]; + const rightValue = right[prop]; + assertIsNotArray(leftValue, prop, left); + assertIsNotArray(rightValue, prop, right); + if (leftValue !== rightValue) { + return false; + } + } + for (const prop of arrayProps) { + const leftValue = left[prop]; + const rightValue = right[prop]; + assertIsArray(leftValue, prop, left); + assertIsArray(rightValue, prop, right); + if (leftValue.length !== rightValue.length) { + return false; + } + } + for (const prop of arrayProps) { + const leftValue = left[prop]; + const rightValue = right[prop]; + assertIsArray(leftValue, prop, left); + assertIsArray(rightValue, prop, right); + if ( + toSortedString(leftValue as string[]) !== + toSortedString(rightValue as string[]) + ) { + return false; + } + } + return true; + } + function toSortedString(array: string[]): string { + return array.slice().sort().join(','); + } + function assertIsNotArray( + toTest: unknown, + prop: keyof Sketch, + object: Sketch + ): void { + if (Array.isArray(toTest)) { + throw new Error( + `Expected a non-array type. Got: ${toTest}. Property was: ${prop}. Object was: ${JSON.stringify( + object + )}` + ); + } + } + function assertIsArray( + toTest: unknown, + prop: keyof Sketch, + object: Sketch + ): void { + if (!Array.isArray(toTest)) { + throw new Error( + `Expected an array type. Got: ${toTest}. Property was: ${prop}. Object was: ${JSON.stringify( + object + )}` + ); + } + } } export interface SketchContainer { diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts index 101e44b56..7e827ff4a 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-module.ts @@ -4,31 +4,6 @@ import { ElectronMenuContribution as TheiaElectronMenuContribution } from '@thei import { MainMenuManager } from '../../../common/main-menu-manager'; import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { ElectronMenuContribution } from './electron-menu-contribution'; -import { nls } from '@theia/core/lib/common/nls'; - -import * as remote from '@theia/core/electron-shared/@electron/remote'; -import * as dialogs from '@theia/core/lib/browser/dialogs'; - -Object.assign(dialogs, { - confirmExit: async () => { - const messageBoxResult = await remote.dialog.showMessageBox( - remote.getCurrentWindow(), - { - message: nls.localize( - 'theia/core/quitMessage', - 'Any unsaved changes will not be saved.' - ), - title: nls.localize( - 'theia/core/quitTitle', - 'Are you sure you want to quit?' - ), - type: 'question', - buttons: [dialogs.Dialog.CANCEL, dialogs.Dialog.YES], - } - ); - return messageBoxResult.response === 1; - }, -}); export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(ElectronMenuContribution).toSelf().inSingletonScope(); diff --git a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts index ea25a4e09..b6767e57d 100644 --- a/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts +++ b/arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts @@ -16,6 +16,7 @@ import { ElectronMainWindowServiceExt, electronMainWindowServiceExtPath, } from '../electron-common/electron-main-window-service-ext'; +import { IsTempSketch } from '../node/is-temp-sketch'; import { ElectronMainWindowServiceExtImpl } from './electron-main-window-service-ext-impl'; import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl'; import { ElectronMainApplication } from './theia/electron-main-application'; @@ -62,4 +63,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { ) ) .inSingletonScope(); + + bind(IsTempSketch).toSelf().inSingletonScope(); }); diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts index 9c990f0a3..981c5ad68 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts @@ -1,4 +1,4 @@ -import { injectable } from '@theia/core/shared/inversify'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { app, BrowserWindow, @@ -22,6 +22,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util'; import * as os from '@theia/core/lib/common/os'; import { Restart } from '@theia/core/lib/electron-common/messaging/electron-messages'; import { TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/theia-electron-window'; +import { IsTempSketch } from '../../node/is-temp-sketch'; app.commandLine.appendSwitch('disable-http-cache'); @@ -54,6 +55,8 @@ const APP_STARTED_WITH_CONTENT_TRACE = @injectable() export class ElectronMainApplication extends TheiaElectronMainApplication { + @inject(IsTempSketch) + private readonly isTempSketch: IsTempSketch; private startup = false; private _firstWindowId: number | undefined; private openFilePromise = new Deferred(); @@ -176,6 +179,12 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { ); for (const workspace of workspaces) { if (await this.isValidSketchPath(workspace.file)) { + if (this.isTempSketch.is(workspace.file)) { + console.info( + `Skipped opening sketch. The sketch was detected as temporary. Workspace path: ${workspace.file}.` + ); + continue; + } useDefault = false; await this.openSketch(workspace); } @@ -405,6 +414,15 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { const workspaceUri = URI.file(workspace); const bounds = window.getNormalBounds(); const now = Date.now(); + // Do not try to reopen the sketch if it was temp. + // Unfortunately, IDE2 has two different logic of restoring recent sketches: the Theia default `recentworkspace.json` and there is the `recent-sketches.json`. + const file = workspaceUri.fsPath; + if (this.isTempSketch.is(file)) { + console.info( + `Ignored marking workspace as a closed sketch. The sketch was detected as temporary. Workspace URI: ${workspaceUri.toString()}.` + ); + return; + } console.log( `Marking workspace as a closed sketch. Workspace URI: ${workspaceUri.toString()}. Date: ${now}.` ); diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index b21f5d301..b007b1bc3 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -109,6 +109,7 @@ import { SurveyNotificationService, SurveyNotificationServicePath, } from '../common/protocol/survey-service'; +import { IsTempSketch } from './is-temp-sketch'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -419,4 +420,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { ) ) .inSingletonScope(); + + bind(IsTempSketch).toSelf().inSingletonScope(); }); diff --git a/arduino-ide-extension/src/node/board-discovery.ts b/arduino-ide-extension/src/node/board-discovery.ts index 4f4bf40ee..84ac101b6 100644 --- a/arduino-ide-extension/src/node/board-discovery.ts +++ b/arduino-ide-extension/src/node/board-discovery.ts @@ -219,7 +219,7 @@ export class BoardDiscovery } else { throw new Error(`Unhandled object type: ${arg}`); } - return JSON.stringify(object, null, 2); // TODO: remove `space`? + return JSON.stringify(object); } async start(): Promise { diff --git a/arduino-ide-extension/src/node/is-temp-sketch.ts b/arduino-ide-extension/src/node/is-temp-sketch.ts new file mode 100644 index 000000000..5c62716e9 --- /dev/null +++ b/arduino-ide-extension/src/node/is-temp-sketch.ts @@ -0,0 +1,47 @@ +import * as fs from 'fs'; +import * as tempDir from 'temp-dir'; +import { isWindows, isOSX } from '@theia/core/lib/common/os'; +import { injectable } from '@theia/core/shared/inversify'; +import { firstToLowerCase } from '../common/utils'; + +const Win32DriveRegex = /^[a-zA-Z]:\\/; +export const TempSketchPrefix = '.arduinoIDE-unsaved'; + +@injectable() +export class IsTempSketch { + // If on macOS, the `temp-dir` lib will make sure there is resolved realpath. + // If on Windows, the `C:\Users\KITTAA~1\AppData\Local\Temp` path will be resolved and normalized to `C:\Users\kittaakos\AppData\Local\Temp`. + // Note: VS Code URI normalizes the drive letter. `C:` will be converted into `c:`. + // https://github.com/Microsoft/vscode/issues/68325#issuecomment-462239992 + private readonly tempDirRealpath = isOSX + ? tempDir + : maybeNormalizeDrive(fs.realpathSync.native(tempDir)); + + is(sketchPath: string): boolean { + // Consider the following paths: + // macOS: + // - Temp folder: /var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T + // - Sketch folder: /private/var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-A0337D47F86B24A51DF3DBCF2CC17925 + // Windows: + // - Temp folder: C:\Users\KITTAA~1\AppData\Local\Temp + // - Sketch folder: c:\Users\kittaakos\AppData\Local\Temp\.arduinoIDE-unsaved2022431-21824-116kfaz.9ljl\sketch_may31a + // Both sketches are valid and temp, but this function will give a false-negative result if we use the default `os.tmpdir()` logic. + const normalizedSketchPath = maybeNormalizeDrive(sketchPath); + const result = + normalizedSketchPath.startsWith(this.tempDirRealpath) && + normalizedSketchPath.includes(TempSketchPrefix); + console.debug(`isTempSketch: ${result}. Input was ${normalizedSketchPath}`); + return result; + } +} + +/** + * If on Windows, will change the input `C:\\path\\to\\somewhere` to `c:\\path\\to\\somewhere`. + * Otherwise, returns with the argument. + */ +export function maybeNormalizeDrive(fsPath: string): string { + if (isWindows && Win32DriveRegex.test(fsPath)) { + return firstToLowerCase(fsPath); + } + return fsPath; +} diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index 055171ce1..bba68f941 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -2,14 +2,13 @@ import { injectable, inject } from '@theia/core/shared/inversify'; import * as fs from 'fs'; import * as os from 'os'; import * as temp from 'temp'; -import * as tempDir from 'temp-dir'; + import * as path from 'path'; import * as crypto from 'crypto'; import { ncp } from 'ncp'; import { promisify } from 'util'; import URI from '@theia/core/lib/common/uri'; import { FileUri } from '@theia/core/lib/node'; -import { isWindows, isOSX } from '@theia/core/lib/common/os'; import { ConfigServiceImpl } from './config-service-impl'; import { SketchesService, @@ -18,7 +17,6 @@ import { SketchContainer, SketchesError, } from '../common/protocol/sketches-service'; -import { firstToLowerCase } from '../common/utils'; import { NotificationServiceServerImpl } from './notification-service-server'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import { CoreClientAware } from './core-client-provider'; @@ -30,10 +28,11 @@ import { duration } from '../common/decorators'; import * as glob from 'glob'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { ServiceError } from './service-error'; - -const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/; - -const prefix = '.arduinoIDE-unsaved'; +import { + IsTempSketch, + maybeNormalizeDrive, + TempSketchPrefix, +} from './is-temp-sketch'; @injectable() export class SketchesServiceImpl @@ -42,22 +41,18 @@ export class SketchesServiceImpl { private sketchSuffixIndex = 1; private lastSketchBaseName: string; - // If on macOS, the `temp-dir` lib will make sure there is resolved realpath. - // If on Windows, the `C:\Users\KITTAA~1\AppData\Local\Temp` path will be resolved and normalized to `C:\Users\kittaakos\AppData\Local\Temp`. - // Note: VS Code URI normalizes the drive letter. `C:` will be converted into `c:`. - // https://github.com/Microsoft/vscode/issues/68325#issuecomment-462239992 - private tempDirRealpath = isOSX - ? tempDir - : maybeNormalizeDrive(fs.realpathSync.native(tempDir)); @inject(ConfigServiceImpl) - protected readonly configService: ConfigServiceImpl; + private readonly configService: ConfigServiceImpl; @inject(NotificationServiceServerImpl) - protected readonly notificationService: NotificationServiceServerImpl; + private readonly notificationService: NotificationServiceServerImpl; @inject(EnvVariablesServer) - protected readonly envVariableServer: EnvVariablesServer; + private readonly envVariableServer: EnvVariablesServer; + + @inject(IsTempSketch) + private readonly isTempSketch: IsTempSketch; async getSketches({ uri, @@ -424,7 +419,7 @@ void loop() { */ private createTempFolder(): Promise { return new Promise((resolve, reject) => { - temp.mkdir({ prefix }, (createError, dirPath) => { + temp.mkdir({ prefix: TempSketchPrefix }, (createError, dirPath) => { if (createError) { reject(createError); return; @@ -475,20 +470,7 @@ void loop() { } async isTemp(sketch: SketchRef): Promise { - // Consider the following paths: - // macOS: - // - Temp folder: /var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T - // - Sketch folder: /private/var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-A0337D47F86B24A51DF3DBCF2CC17925 - // Windows: - // - Temp folder: C:\Users\KITTAA~1\AppData\Local\Temp - // - Sketch folder: c:\Users\kittaakos\AppData\Local\Temp\.arduinoIDE-unsaved2022431-21824-116kfaz.9ljl\sketch_may31a - // Both sketches are valid and temp, but this function will give a false-negative result if we use the default `os.tmpdir()` logic. - const sketchPath = maybeNormalizeDrive(FileUri.fsPath(sketch.uri)); - const tempPath = this.tempDirRealpath; // https://github.com/sindresorhus/temp-dir - const result = - sketchPath.indexOf(prefix) !== -1 && sketchPath.startsWith(tempPath); - console.log('isTemp?', result, sketch.uri); - return result; + return this.isTempSketch.is(FileUri.fsPath(sketch.uri)); } async copy( @@ -578,6 +560,17 @@ void loop() { const suffix = crypto.createHash('md5').update(sketchPath).digest('hex'); return path.join(os.tmpdir(), `arduino-ide2-${suffix}`); } + + notifyDeleteSketch(sketch: Sketch): void { + const sketchPath = FileUri.fsPath(sketch.uri); + fs.rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => { + if (error) { + console.error(`Failed to delete sketch at ${sketchPath}.`, error); + } else { + console.error(`Successfully delete sketch at ${sketchPath}.`); + } + }); + } } interface SketchWithDetails extends Sketch { @@ -618,16 +611,6 @@ function isNotFoundError(err: unknown): err is ServiceError { return ServiceError.is(err) && err.code === 5; // `NOT_FOUND` https://grpc.github.io/grpc/core/md_doc_statuscodes.html } -/** - * If on Windows, will change the input `C:\\path\\to\\somewhere` to `c:\\path\\to\\somewhere`. - */ -function maybeNormalizeDrive(input: string): string { - if (isWindows && WIN32_DRIVE_REGEXP.test(input)) { - return firstToLowerCase(input); - } - return input; -} - /* * When a new sketch is created, add a suffix to distinguish it * from other new sketches I created today. diff --git a/arduino-ide-extension/src/node/theia/workspace/default-workspace-server.ts b/arduino-ide-extension/src/node/theia/workspace/default-workspace-server.ts index 3481f7de1..89a5e0698 100644 --- a/arduino-ide-extension/src/node/theia/workspace/default-workspace-server.ts +++ b/arduino-ide-extension/src/node/theia/workspace/default-workspace-server.ts @@ -1,26 +1,16 @@ import { promises as fs, constants } from 'fs'; import { injectable, inject } from '@theia/core/shared/inversify'; -import { ILogger } from '@theia/core/lib/common/logger'; import { DefaultWorkspaceServer as TheiaDefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server'; -import { ConfigService } from '../../../common/protocol/config-service'; import { SketchesService } from '../../../common/protocol'; import { FileUri } from '@theia/core/lib/node'; +import { IsTempSketch } from '../../is-temp-sketch'; @injectable() export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer { - @inject(ConfigService) - protected readonly configService: ConfigService; - - @inject(ILogger) - protected readonly logger: ILogger; - @inject(SketchesService) private readonly sketchesService: SketchesService; - - override async onStart(): Promise { - // NOOP - // No need to remove untitled workspaces. IDE2 does not use workspaces. - } + @inject(IsTempSketch) + private readonly isTempSketch: IsTempSketch; override async getMostRecentlyUsedWorkspace(): Promise { const uri = await super.getMostRecentlyUsedWorkspace(); @@ -51,6 +41,35 @@ export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer { return listUri; } + protected override async writeToUserHome( + data: RecentWorkspacePathsData + ): Promise { + return super.writeToUserHome(this.filterTempSketches(data)); + } + + protected override async readRecentWorkspacePathsFromUserHome(): Promise< + RecentWorkspacePathsData | undefined + > { + const data = await super.readRecentWorkspacePathsFromUserHome(); + return data ? this.filterTempSketches(data) : undefined; + } + + protected override async removeOldUntitledWorkspaces(): Promise { + // NOOP + // No need to remove untitled workspaces. IDE2 does not use workspaces. + } + + private filterTempSketches( + data: RecentWorkspacePathsData + ): RecentWorkspacePathsData { + const recentRoots = data.recentRoots.filter( + (uri) => !this.isTempSketch.is(FileUri.fsPath(uri)) + ); + return { + recentRoots, + }; + } + private async exists(uri: string): Promise { try { await fs.access(FileUri.fsPath(uri), constants.R_OK | constants.W_OK); @@ -60,3 +79,8 @@ export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer { } } } + +// Remove after https://github.com/eclipse-theia/theia/pull/11603 +interface RecentWorkspacePathsData { + recentRoots: string[]; +} diff --git a/i18n/en.json b/i18n/en.json index 014067cc6..65b02b5c6 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -324,8 +324,8 @@ "openRecent": "Open Recent", "openSketchInNewWindow": "Open Sketch in New Window", "saveFolderAs": "Save sketch folder as...", + "saveSketch": "Save your sketch to open it again later.", "saveSketchAs": "Save sketch folder as...", - "saveTempSketch": "Save your sketch to open it again later.", "showFolder": "Show Sketch Folder", "sketch": "Sketch", "sketchbook": "Sketchbook", @@ -362,7 +362,6 @@ "couldNotSave": "Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.", "daemonOffline": "CLI Daemon Offline", "offline": "Offline", - "quitMessage": "Any unsaved changes will not be saved.", "quitTitle": "Are you sure you want to quit?" }, "debug": {