Avoid deleting the workspace when it's still in use.

- From now on, NSFW service disposes after last reference
is removed. No more 10sec delay.
 - Moved the temp workspace deletion to a startup task.
 - Can set initial task for the window from electron-main.
 - Removed the `browser-app`.

Closes #39

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-09-05 11:22:16 +02:00 committed by Akos Kitta
parent 0151e4c224
commit fdf6f0f9c8
38 changed files with 560 additions and 582 deletions

View File

@ -17,7 +17,6 @@ module.exports = {
'scripts/*',
'electron/*',
'electron-app/*',
'browser-app/*',
'plugins/*',
'arduino-ide-extension/src/node/cli-protocol',
],

31
.vscode/launch.json vendored
View File

@ -80,37 +80,6 @@
"port": 9222,
"webRoot": "${workspaceFolder}/electron-app"
},
{
"type": "node",
"request": "launch",
"name": "App (Browser)",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--no-app-auto-install",
"--plugins=local-dir:plugins"
],
"windows": {
"env": {
"NODE_ENV": "development",
"NODE_PRESERVE_SYMLINKS": "1"
}
},
"env": {
"NODE_ENV": "development"
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
],
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",

30
.vscode/tasks.json vendored
View File

@ -12,17 +12,6 @@
"clear": false
}
},
{
"label": "Arduino IDE - Start Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app start",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": true
}
},
{
"label": "Arduino IDE - Watch IDE Extension",
"type": "shell",
@ -34,17 +23,6 @@
"clear": false
}
},
{
"label": "Arduino IDE - Watch Browser App",
"type": "shell",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new",
"clear": false
}
},
{
"label": "Arduino IDE - Watch Electron App",
"type": "shell",
@ -56,14 +34,6 @@
"clear": false
}
},
{
"label": "Arduino IDE - Watch All [Browser]",
"type": "shell",
"dependsOn": [
"Arduino IDE - Watch IDE Extension",
"Arduino IDE - Watch Browser App"
]
},
{
"label": "Arduino IDE - Watch All [Electron]",
"type": "shell",

View File

@ -147,11 +147,9 @@
"frontend": "lib/browser/arduino-ide-frontend-module"
},
{
"frontend": "lib/browser/theia/core/browser-menu-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
},
{
"frontend": "lib/browser/theia/core/browser-window-module",
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
},
{

View File

@ -105,7 +105,8 @@ import {
} from '@theia/core/lib/browser/connection-status-service';
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
import { BoardsDataStore } from './boards/boards-data-store';
import { ILogger } from '@theia/core';
import { ILogger } from '@theia/core/lib/common/logger';
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
import {
FileSystemExt,
FileSystemExtPath,
@ -308,7 +309,7 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
import { CompilerErrors } from './contributions/compiler-errors';
import { WidgetManager } from './theia/core/widget-manager';
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
import { StartupTasks } from './widgets/sketchbook/startup-task';
import { StartupTasks } from './contributions/startup-task';
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
import { Daemon } from './contributions/daemon';
import { FirstStartupInstaller } from './contributions/first-startup-installer';
@ -334,6 +335,8 @@ import {
} from './widgets/component-list/filter-renderer';
import { CheckForUpdates } from './contributions/check-for-updates';
import { OutputEditorFactory } from './theia/output/output-editor-factory';
import { StartupTaskProvider } from '../electron-common/startup-task';
import { DeleteSketch } from './contributions/delete-sketch';
const registerArduinoThemes = () => {
const themes: MonacoThemeJson[] = [
@ -433,6 +436,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Boards service client to receive and delegate notifications from the backend.
bind(BoardsServiceProvider).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
bind(CommandContribution).toService(BoardsServiceProvider);
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
bind(FrontendApplicationContribution)
@ -757,6 +761,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, OpenBoardsConfig);
Contribution.configure(bind, SketchFilesTracker);
Contribution.configure(bind, CheckForUpdates);
Contribution.configure(bind, DeleteSketch);
bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
// Disabled the quick-pick customization from Theia when multiple formatters are available.
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.

View File

@ -413,53 +413,5 @@ export namespace BoardsConfig {
const { name } = selectedBoard;
return `${name}${port ? ` at ${port.address}` : ''}`;
}
export function setConfig(
config: Config | undefined,
urlToAttachTo: URL
): URL {
const copy = new URL(urlToAttachTo.toString());
if (!config) {
copy.searchParams.delete('boards-config');
return copy;
}
const selectedBoard = config.selectedBoard
? {
name: config.selectedBoard.name,
fqbn: config.selectedBoard.fqbn,
}
: undefined;
const selectedPort = config.selectedPort
? {
protocol: config.selectedPort.protocol,
address: config.selectedPort.address,
}
: undefined;
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
return copy;
}
export function getConfig(url: URL): Config | undefined {
const encoded = url.searchParams.get('boards-config');
if (!encoded) {
return undefined;
}
try {
const raw = decodeURIComponent(encoded);
const candidate = JSON.parse(raw);
if (typeof candidate === 'object') {
return candidate;
}
console.warn(
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
);
return undefined;
} catch (e) {
console.log(`Could not get board config from URL: ${url}.`, e);
return undefined;
}
}
}
}

View File

@ -1,7 +1,12 @@
import { injectable, inject } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { CommandService } from '@theia/core/lib/common/command';
import {
Command,
CommandContribution,
CommandRegistry,
CommandService,
} from '@theia/core/lib/common/command';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { RecursiveRequired } from '../../common/types';
@ -23,9 +28,18 @@ import { nls } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { Unknown } from '../../common/nls';
import {
StartupTask,
StartupTaskProvider,
} from '../../electron-common/startup-task';
@injectable()
export class BoardsServiceProvider implements FrontendApplicationContribution {
export class BoardsServiceProvider
implements
FrontendApplicationContribution,
StartupTaskProvider,
CommandContribution
{
@inject(ILogger)
protected logger: ILogger;
@ -50,6 +64,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
AvailableBoard[]
>();
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
@ -115,6 +130,13 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(USE_INHERITED_CONFIG, {
execute: (inheritedConfig: BoardsConfig.Config) =>
this.inheritedConfig.resolve(inheritedConfig),
});
}
get reconciled(): Promise<void> {
return this._reconciled.promise;
}
@ -655,11 +677,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
let storedLatestBoardsConfig = await this.getData<
BoardsConfig.Config | undefined
>('latest-boards-config');
// Try to get from the URL if it was not persisted.
// Try to get from the startup task. Wait for it, then timeout. Maybe it never arrives.
if (!storedLatestBoardsConfig) {
storedLatestBoardsConfig = BoardsConfig.Config.getConfig(
new URL(window.location.href)
);
storedLatestBoardsConfig = await Promise.race([
this.inheritedConfig.promise,
new Promise<undefined>((resolve) =>
setTimeout(() => resolve(undefined), 2_000)
),
]);
}
if (storedLatestBoardsConfig) {
this.latestBoardsConfig = storedLatestBoardsConfig;
@ -682,8 +707,31 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
key
);
}
tasks(): StartupTask[] {
return [
{
command: USE_INHERITED_CONFIG.id,
args: [this.boardsConfig],
},
];
}
}
/**
* It should be neither visible nor called from outside.
*
* This service creates a startup task with the current board config and
* passes the task to the electron-main process so that the new window
* can inherit the boards config state of this service.
*
* Note that the state is always set, but new windows might ignore it.
* For example, the new window already has a valid boards config persisted to the local storage.
*/
const USE_INHERITED_CONFIG: Command = {
id: 'arduino-use-inherited-boards-config',
};
/**
* Representation of a ready-to-use board, either the user has configured it or was automatically recognized by the CLI.
* An available board was not necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`.

View File

@ -0,0 +1,45 @@
import { injectable } from '@theia/core/shared/inversify';
import { SketchesError } from '../../common/protocol';
import {
Command,
CommandRegistry,
SketchContribution,
Sketch,
} from './contribution';
@injectable()
export class DeleteSketch extends SketchContribution {
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
execute: (uri: string) => this.deleteSketch(uri),
});
}
private async deleteSketch(uri: string): Promise<void> {
const sketch = await this.loadSketch(uri);
if (!sketch) {
console.info(`Sketch not found at ${uri}. Skipping deletion.`);
return;
}
return this.sketchService.deleteSketch(sketch);
}
private async loadSketch(uri: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchService.loadSketch(uri);
return sketch;
} catch (err) {
if (SketchesError.NotFound.is(err)) {
return undefined;
}
throw err;
}
}
}
export namespace DeleteSketch {
export namespace Commands {
export const DELETE_SKETCH: Command = {
id: 'arduino-delete-sketch',
};
}
}

View File

@ -12,21 +12,19 @@ import {
} from './contribution';
import { nls } from '@theia/core/lib/common';
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
import { EditorManager } from '@theia/editor/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { WorkspaceInput } from '@theia/workspace/lib/browser';
import { StartupTask } from '../../electron-common/startup-task';
import { DeleteSketch } from './delete-sketch';
@injectable()
export class SaveAsSketch extends SketchContribution {
@inject(ApplicationShell)
protected readonly applicationShell: ApplicationShell;
@inject(EditorManager)
protected override readonly editorManager: EditorManager;
private readonly applicationShell: ApplicationShell;
@inject(WindowService)
protected readonly windowService: WindowService;
private readonly windowService: WindowService;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
@ -107,21 +105,19 @@ export class SaveAsSketch extends SketchContribution {
this.sketchService.markAsRecentlyOpened(workspaceUri);
}
}
const options: WorkspaceInput & StartupTask.Owner = {
preserveWindow: true,
tasks: [],
};
if (workspaceUri && openAfterMove) {
this.windowService.setSafeToShutDown();
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
// 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.
options.tasks.push({
command: DeleteSketch.Commands.DELETE_SKETCH.id,
args: [sketch.uri],
});
}
this.workspaceService.open(new URI(workspaceUri), {
preserveWindow: true,
});
this.workspaceService.open(new URI(workspaceUri), options);
}
return !!workspaceUri;
}

View File

@ -4,7 +4,7 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { FileChangeType } from '@theia/filesystem/lib/common/files';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import { Sketch, SketchContribution, URI } from './contribution';
import { Sketch, SketchContribution } from './contribution';
import { OpenSketchFiles } from './open-sketch-files';
@injectable()
@ -31,7 +31,6 @@ export class SketchFilesTracker extends SketchContribution {
override onReady(): void {
this.sketchServiceClient.currentSketch().then(async (sketch) => {
if (CurrentSketch.isValid(sketch)) {
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
this.toDisposeOnStop.push(
this.fileService.onDidFilesChange(async (event) => {
for (const { type, resource } of event.changes) {

View File

@ -0,0 +1,52 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import type { IpcRendererEvent } from '@theia/core/electron-shared/electron';
import { ipcRenderer } from '@theia/core/electron-shared/electron';
import { injectable } from '@theia/core/shared/inversify';
import { StartupTask } from '../../electron-common/startup-task';
import { Contribution } from './contribution';
@injectable()
export class StartupTasks extends Contribution {
override onReady(): void {
ipcRenderer.once(
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
(_: IpcRendererEvent, args: unknown) => {
console.debug(
`Received the startup tasks from the electron main process. Args: ${JSON.stringify(
args
)}`
);
if (!StartupTask.has(args)) {
console.warn(`Could not detect 'tasks' from the signal. Skipping.`);
return;
}
const tasks = args.tasks;
if (tasks.length) {
console.log(`Executing startup tasks:`);
tasks.forEach(({ command, args = [] }) => {
console.log(
` - '${command}' ${
args.length ? `, args: ${JSON.stringify(args)}` : ''
}`
);
this.commandService
.executeCommand(command, ...args)
.catch((err) =>
console.error(
`Error occurred when executing the startup task '${command}'${
args?.length ? ` with args: '${JSON.stringify(args)}` : ''
}.`,
err
)
);
});
}
}
);
const { id } = remote.getCurrentWindow();
console.debug(
`Signalling app ready event to the electron main process. Sender ID: ${id}.`
);
ipcRenderer.send(StartupTask.Messaging.APP_READY_SIGNAL(id));
}
}

View File

@ -1,26 +0,0 @@
import { injectable } from '@theia/core/shared/inversify';
import {
BrowserMainMenuFactory as TheiaBrowserMainMenuFactory,
MenuBarWidget,
} from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager';
@injectable()
export class BrowserMainMenuFactory
extends TheiaBrowserMainMenuFactory
implements MainMenuManager
{
protected menuBar: MenuBarWidget | undefined;
override createMenuBar(): MenuBarWidget {
this.menuBar = super.createMenuBar();
return this.menuBar;
}
update(): void {
if (this.menuBar) {
this.menuBar.clearMenus();
this.fillMenuBar(this.menuBar);
}
}
}

View File

@ -1,18 +0,0 @@
import '../../../../src/browser/style/browser-menu.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import {
BrowserMenuBarContribution,
BrowserMainMenuFactory as TheiaBrowserMainMenuFactory,
} from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from '../../../common/main-menu-manager';
import { ArduinoMenuContribution } from './browser-menu-plugin';
import { BrowserMainMenuFactory } from './browser-main-menu-factory';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BrowserMainMenuFactory).toSelf().inSingletonScope();
bind(MainMenuManager).toService(BrowserMainMenuFactory);
rebind(TheiaBrowserMainMenuFactory).toService(BrowserMainMenuFactory);
rebind(BrowserMenuBarContribution)
.to(ArduinoMenuContribution)
.inSingletonScope();
});

View File

@ -1,10 +0,0 @@
import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service';
import { ContainerModule } from '@theia/core/shared/inversify';
import { DefaultWindowService } from './default-window-service';
import { WindowServiceExt } from './window-service-ext';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DefaultWindowService).toSelf().inSingletonScope();
rebind(TheiaDefaultWindowService).toService(DefaultWindowService);
bind(WindowServiceExt).toService(DefaultWindowService);
});

View File

@ -1,17 +0,0 @@
import { DefaultWindowService as TheiaDefaultWindowService } from '@theia/core/lib/browser/window/default-window-service';
import { injectable } from '@theia/core/shared/inversify';
import { WindowServiceExt } from './window-service-ext';
@injectable()
export class DefaultWindowService
extends TheiaDefaultWindowService
implements WindowServiceExt
{
/**
* The default implementation always resolves to `true`.
* IDE2 does not use it. It's currently an electron-only app.
*/
async isFirstWindow(): Promise<boolean> {
return true;
}
}

View File

@ -1,7 +1,10 @@
import type { StartupTask } from '../../../electron-common/startup-task';
export const WindowServiceExt = Symbol('WindowServiceExt');
export interface WindowServiceExt {
/**
* Returns with a promise that resolves to `true` if the current window is the first window.
*/
isFirstWindow(): Promise<boolean>;
reload(options?: StartupTask.Owner): void;
}

View File

@ -1,54 +1,41 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { injectable, inject } from '@theia/core/shared/inversify';
import { injectable, inject, named } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { EditorWidget } from '@theia/editor/lib/browser';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { MessageService } from '@theia/core/lib/common/message-service';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { FocusTracker, Widget } from '@theia/core/lib/browser';
import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import {
DEFAULT_WINDOW_HASH,
NewWindowOptions,
} from '@theia/core/lib/common/window';
import {
WorkspaceInput,
WorkspaceService as TheiaWorkspaceService,
} from '@theia/workspace/lib/browser/workspace-service';
import { ConfigService } from '../../../common/protocol/config-service';
import {
SketchesService,
Sketch,
} from '../../../common/protocol/sketches-service';
import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { BoardsConfig } from '../../boards/boards-config';
import { FileStat } from '@theia/filesystem/lib/common/files';
import {
StartupTask,
StartupTasks,
} from '../../widgets/sketchbook/startup-task';
import { setURL } from '../../utils/window';
StartupTaskProvider,
} from '../../../electron-common/startup-task';
import { WindowServiceExt } from '../core/window-service-ext';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
@injectable()
export class WorkspaceService extends TheiaWorkspaceService {
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(LabelProvider)
protected override readonly labelProvider: LabelProvider;
@inject(MessageService)
protected override readonly messageService: MessageService;
private readonly sketchService: SketchesService;
@inject(ApplicationServer)
protected readonly applicationServer: ApplicationServer;
@inject(FrontendApplicationStateService)
protected readonly appStateService: FrontendApplicationStateService;
@inject(BoardsServiceProvider)
protected readonly boardsServiceProvider: BoardsServiceProvider;
private readonly applicationServer: ApplicationServer;
@inject(WindowServiceExt)
private readonly windowServiceExt: WindowServiceExt;
@inject(ContributionProvider)
@named(StartupTaskProvider)
private readonly providers: ContributionProvider<StartupTaskProvider>;
private version?: string;
@ -156,27 +143,33 @@ export class WorkspaceService extends TheiaWorkspaceService {
}
protected override reloadWindow(options?: WorkspaceInput): void {
if (StartupTasks.WorkspaceInput.is(options)) {
setURL(StartupTask.append(options.tasks, new URL(window.location.href)));
}
super.reloadWindow();
const tasks = this.tasks(options);
this.setURLFragment(this._workspace?.resource.path.toString() || '');
this.windowServiceExt.reload({ tasks });
}
protected override openNewWindow(
workspacePath: string,
options?: WorkspaceInput
): void {
const { boardsConfig } = this.boardsServiceProvider;
let url = BoardsConfig.Config.setConfig(
boardsConfig,
new URL(window.location.href)
); // Set the current boards config for the new browser window.
url.hash = workspacePath;
if (StartupTasks.WorkspaceInput.is(options)) {
url = StartupTask.append(options.tasks, url);
}
const tasks = this.tasks(options);
const url = new URL(window.location.href);
url.hash = encodeURI(workspacePath);
this.windowService.openNewWindow(
url.toString(),
Object.assign({} as NewWindowOptions, { tasks })
);
}
this.windowService.openNewWindow(url.toString());
private tasks(options?: WorkspaceInput): StartupTask[] {
const tasks = this.providers
.getContributions()
.map((contribution) => contribution.tasks())
.reduce((prev, curr) => prev.concat(curr), []);
if (StartupTask.has(options)) {
tasks.push(...options.tasks);
}
return tasks;
}
protected onCurrentWidgetChange({

View File

@ -1,92 +0,0 @@
import { injectable } from '@theia/core/shared/inversify';
import { WorkspaceInput as TheiaWorkspaceInput } from '@theia/workspace/lib/browser';
import { Contribution } from '../../contributions/contribution';
import { setURL } from '../../utils/window';
@injectable()
export class StartupTasks extends Contribution {
override onReady(): void {
const tasks = StartupTask.get(new URL(window.location.href));
console.log(`Executing startup tasks: ${JSON.stringify(tasks)}`);
tasks.forEach(({ command, args = [] }) =>
this.commandService
.executeCommand(command, ...args)
.catch((err) =>
console.error(
`Error occurred when executing the startup task '${command}'${
args?.length ? ` with args: '${JSON.stringify(args)}` : ''
}.`,
err
)
)
);
if (tasks.length) {
// Remove the startup tasks after the execution.
// Otherwise, IDE2 executes them again on a window reload event.
setURL(StartupTask.set([], new URL(window.location.href)));
console.info(`Removed startup tasks from URL.`);
}
}
}
export interface StartupTask {
command: string;
/**
* Must be JSON serializable.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
}
export namespace StartupTask {
const QUERY = 'startupTasks';
export function is(arg: unknown): arg is StartupTasks {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return 'command' in object && typeof object['command'] === 'string';
}
return false;
}
export function get(url: URL): StartupTask[] {
const { searchParams } = url;
const encodedTasks = searchParams.get(QUERY);
if (encodedTasks) {
const rawTasks = decodeURIComponent(encodedTasks);
const tasks = JSON.parse(rawTasks);
if (Array.isArray(tasks)) {
return tasks.filter((task) => {
if (StartupTask.is(task)) {
return true;
}
console.warn(`Was not a task: ${JSON.stringify(task)}. Ignoring.`);
return false;
});
} else {
debugger;
console.warn(`Startup tasks was not an array: ${rawTasks}. Ignoring.`);
}
}
return [];
}
export function set(tasks: StartupTask[], url: URL): URL {
const copy = new URL(url);
copy.searchParams.set(QUERY, encodeURIComponent(JSON.stringify(tasks)));
return copy;
}
export function append(tasks: StartupTask[], url: URL): URL {
return set([...get(url), ...tasks], url);
}
}
export namespace StartupTasks {
export interface WorkspaceInput extends TheiaWorkspaceInput {
tasks: StartupTask[];
}
export namespace WorkspaceInput {
export function is(
input: (TheiaWorkspaceInput & Partial<WorkspaceInput>) | undefined
): input is WorkspaceInput {
return !!input && !!input.tasks;
}
}
}

View File

@ -165,15 +165,6 @@ export class SketchesServiceClientImpl
.reachedState('started_contributions')
.then(async () => {
const currentSketch = await this.loadCurrentSketch();
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);
});
}

View File

@ -97,9 +97,9 @@ export interface SketchesService {
getIdeTempFolderUri(sketch: Sketch): Promise<string>;
/**
* Notifies the backend to recursively delete the sketch folder with all its content.
* Recursively deletes the sketch folder with all its content.
*/
notifyDeleteSketch(sketch: Sketch): void;
deleteSketch(sketch: Sketch): Promise<void>;
}
export interface SketchRef {

View File

@ -1,10 +1,14 @@
import * as remote from '@theia/core/electron-shared/@electron/remote';
import { ipcRenderer } from '@theia/core/electron-shared/electron';
import {
ConnectionStatus,
ConnectionStatusService,
} from '@theia/core/lib/browser/connection-status-service';
import { nls } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { NewWindowOptions } from '@theia/core/lib/common/window';
import { ElectronWindowService as TheiaElectronWindowService } from '@theia/core/lib/electron-browser/window/electron-window-service';
import { RELOAD_REQUESTED_SIGNAL } from '@theia/core/lib/electron-common/messaging/electron-messages';
import {
inject,
injectable,
@ -12,6 +16,7 @@ import {
} from '@theia/core/shared/inversify';
import { WindowServiceExt } from '../../../browser/theia/core/window-service-ext';
import { ElectronMainWindowServiceExt } from '../../../electron-common/electron-main-window-service-ext';
import { StartupTask } from '../../../electron-common/startup-task';
@injectable()
export class ElectronWindowService
@ -60,14 +65,30 @@ export class ElectronWindowService
return response === 0; // 'Yes', close the window.
}
private _firstWindow: boolean | undefined;
private _firstWindow: Deferred<boolean> | undefined;
async isFirstWindow(): Promise<boolean> {
if (this._firstWindow === undefined) {
this._firstWindow = new Deferred<boolean>();
const windowId = remote.getCurrentWindow().id; // This is expensive and synchronous so we check it once per FE.
this._firstWindow = await this.mainWindowServiceExt.isFirstWindow(
windowId
);
this.mainWindowServiceExt
.isFirstWindow(windowId)
.then((firstWindow) => this._firstWindow?.resolve(firstWindow));
}
return this._firstWindow.promise;
}
// Overridden because the default Theia implementation destroys the additional properties of the `options` arg, such as `tasks`.
override openNewWindow(url: string, options?: NewWindowOptions): undefined {
return this.delegate.openNewWindow(url, options);
}
// Overridden to support optional task owner params and make `tsc` happy.
override reload(options?: StartupTask.Owner): void {
if (options?.tasks && options.tasks.length) {
const { tasks } = options;
ipcRenderer.send(RELOAD_REQUESTED_SIGNAL, { tasks });
} else {
ipcRenderer.send(RELOAD_REQUESTED_SIGNAL);
}
return this._firstWindow;
}
}

View File

@ -0,0 +1,50 @@
export interface StartupTask {
command: string;
/**
* Must be JSON serializable.
* See the restrictions [here](https://www.electronjs.org/docs/latest/api/web-contents#contentssendchannel-args).
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any[];
}
export namespace StartupTask {
export function is(arg: unknown): arg is StartupTask {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'command' in object &&
typeof object['command'] === 'string' &&
(!('args' in object) || Array.isArray(object['args']))
);
}
return false;
}
export function has(arg: unknown): arg is unknown & Owner {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = arg as any;
return (
'tasks' in object &&
Array.isArray(object['tasks']) &&
object['tasks'].every(is)
);
}
return false;
}
export namespace Messaging {
export const STARTUP_TASKS_SIGNAL = 'arduino/startupTasks';
export function APP_READY_SIGNAL(id: number): string {
return `arduino/appReady${id}`;
}
}
export interface Owner {
readonly tasks: StartupTask[];
}
}
export const StartupTaskProvider = Symbol('StartupTaskProvider');
export interface StartupTaskProvider {
tasks(): StartupTask[];
}

View File

@ -12,12 +12,8 @@ import {
IDEUpdaterClient,
IDEUpdaterPath,
} from '../common/protocol/ide-updater';
import {
ElectronMainWindowServiceExt,
electronMainWindowServiceExtPath,
} from '../electron-common/electron-main-window-service-ext';
import { 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';
import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service';
@ -52,14 +48,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(TheiaElectronWindow).toSelf();
rebind(DefaultTheiaElectronWindow).toService(TheiaElectronWindow);
bind(ElectronMainWindowServiceExt)
.to(ElectronMainWindowServiceExtImpl)
.inSingletonScope();
bind(ElectronConnectionHandler)
.toDynamicValue(
(context) =>
new JsonRpcConnectionHandler(electronMainWindowServiceExtPath, () =>
context.container.get(ElectronMainWindowServiceExt)
context.container.get(ElectronMainWindowServiceImpl)
)
)
.inSingletonScope();

View File

@ -1,15 +0,0 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import { ElectronMainWindowServiceExt } from '../electron-common/electron-main-window-service-ext';
import { ElectronMainApplication } from './theia/electron-main-application';
@injectable()
export class ElectronMainWindowServiceExtImpl
implements ElectronMainWindowServiceExt
{
@inject(ElectronMainApplication)
private readonly app: ElectronMainApplication;
async isFirstWindow(windowId: number): Promise<boolean> {
return this.app.firstWindowId === windowId;
}
}

View File

@ -1,34 +1,63 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import type { NewWindowOptions } from '@theia/core/lib/common/window';
import type { BrowserWindow } from '@theia/core/electron-shared/electron';
import { ElectronMainWindowServiceImpl as TheiaElectronMainWindowService } from '@theia/core/lib/electron-main/electron-main-window-service-impl';
import { inject, injectable } from '@theia/core/shared/inversify';
import { ElectronMainWindowServiceExt } from '../../electron-common/electron-main-window-service-ext';
import { StartupTask } from '../../electron-common/startup-task';
import { ElectronMainApplication } from './electron-main-application';
import { NewWindowOptions } from '@theia/core/lib/common/window';
import { load } from './window';
@injectable()
export class ElectronMainWindowServiceImpl extends TheiaElectronMainWindowService {
export class ElectronMainWindowServiceImpl
extends TheiaElectronMainWindowService
implements ElectronMainWindowServiceExt
{
@inject(ElectronMainApplication)
protected override readonly app: ElectronMainApplication;
override openNewWindow(url: string, { external }: NewWindowOptions): undefined {
if (!external) {
const sanitizedUrl = this.sanitize(url);
const existing = this.app.browserWindows.find(
(window) => this.sanitize(window.webContents.getURL()) === sanitizedUrl
);
if (existing) {
existing.focus();
return;
}
}
return super.openNewWindow(url, { external });
async isFirstWindow(windowId: number): Promise<boolean> {
return this.app.firstWindowId === windowId;
}
private sanitize(url: string): string {
const copy = new URL(url);
const searchParams: string[] = [];
copy.searchParams.forEach((_, key) => searchParams.push(key));
for (const param of searchParams) {
copy.searchParams.delete(param);
override openNewWindow(url: string, options: NewWindowOptions): undefined {
// External window has highest precedence.
if (options?.external) {
return super.openNewWindow(url, options);
}
return copy.toString();
// Look for existing window with the same URL and focus it.
const existing = this.app.browserWindows.find(
({ webContents }) => webContents.getURL() === url
);
if (existing) {
existing.focus();
return undefined;
}
// Create new window and share the startup tasks.
if (StartupTask.has(options)) {
const { tasks } = options;
this.app.createWindow().then((electronWindow) => {
this.loadURL(electronWindow, url).then(() => {
electronWindow.webContents.send(
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
{ tasks }
);
});
});
return undefined;
}
// Default.
return super.openNewWindow(url, options);
}
private loadURL(
electronWindow: BrowserWindow,
url: string
): Promise<BrowserWindow> {
return load(electronWindow, (electronWindow) =>
electronWindow.loadURL(url)
);
}
}

View File

@ -1,12 +1,15 @@
import { injectable } from '@theia/core/shared/inversify';
import { ipcMain, IpcMainEvent } from '@theia/electron/shared/electron';
import { StopReason } from '@theia/core/lib/electron-common/messaging/electron-messages';
import {
RELOAD_REQUESTED_SIGNAL,
StopReason,
} from '@theia/core/lib/electron-common/messaging/electron-messages';
import { TheiaElectronWindow as DefaultTheiaElectronWindow } from '@theia/core/lib/electron-main/theia-electron-window';
import { FileUri } from '@theia/core/lib/node';
import URI from '@theia/core/lib/common/uri';
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
import { createDisposableListener } from '@theia/core/lib/electron-main/event-utils';
import { APPLICATION_STATE_CHANGE_SIGNAL } from '@theia/core/lib/electron-common/messaging/electron-messages';
import { StartupTask } from '../../electron-common/startup-task';
import { load } from './window';
@injectable()
export class TheiaElectronWindow extends DefaultTheiaElectronWindow {
@ -38,30 +41,42 @@ export class TheiaElectronWindow extends DefaultTheiaElectronWindow {
return false;
}
// Note: does the same as the Theia impl, but logs state changes.
protected override trackApplicationState(): void {
protected override reload(tasks?: StartupTask[]): void {
this.handleStopRequest(() => {
this.applicationState = 'init';
if (tasks && tasks.length) {
load(this._window, (electronWindow) => electronWindow.reload()).then(
(electronWindow) =>
electronWindow.webContents.send(
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
{ tasks }
)
);
} else {
this._window.reload();
}
}, StopReason.Reload);
}
protected override attachReloadListener(): void {
createDisposableListener(
ipcMain,
APPLICATION_STATE_CHANGE_SIGNAL,
(e: IpcMainEvent, state: FrontendApplicationState) => {
console.log(
'app-state-change',
`>>> new app state <${state} was received from sender <${e.sender.id}>. current window ID: ${this._window.id}`
);
RELOAD_REQUESTED_SIGNAL,
(e: IpcMainEvent, arg: unknown) => {
if (this.isSender(e)) {
this.applicationState = state;
console.log(
'app-state-change',
`<<< new app state is <${this.applicationState}> for window <${this._window.id}>`
);
} else {
console.log(
'app-state-change',
`<<< new app state <${state}> is ignored from <${e.sender.id}>. current window ID is <${this._window.id}>`
);
if (StartupTask.has(arg)) {
this.reload(arg.tasks);
} else {
this.reload();
}
}
},
this.toDispose
);
}
// https://github.com/eclipse-theia/theia/issues/11600#issuecomment-1240657481
protected override isSender(e: IpcMainEvent): boolean {
return e.sender.id === this._window.webContents.id;
}
}

View File

@ -0,0 +1,33 @@
import { MaybePromise } from '@theia/core';
import type { IpcMainEvent } from '@theia/core/electron-shared/electron';
import { BrowserWindow, ipcMain } from '@theia/core/electron-shared/electron';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { createDisposableListener } from '@theia/core/lib/electron-main/event-utils';
import { StartupTask } from '../../electron-common/startup-task';
/**
* Should be used to load (URL) or reload a window. The returning promise will resolve
* when the app is ready to receive startup tasks.
*/
export async function load(
electronWindow: BrowserWindow,
doLoad: (electronWindow: BrowserWindow) => MaybePromise<void>
): Promise<BrowserWindow> {
const { id } = electronWindow;
const toDispose = new DisposableCollection();
const channel = StartupTask.Messaging.APP_READY_SIGNAL(id);
return new Promise<BrowserWindow>((resolve, reject) => {
toDispose.push(
createDisposableListener(
ipcMain,
channel,
({ sender: webContents }: IpcMainEvent) => {
if (webContents.id === electronWindow.webContents.id) {
resolve(electronWindow);
}
}
)
);
Promise.resolve(doLoad(electronWindow)).catch(reject);
}).finally(() => toDispose.dispose());
}

View File

@ -1,4 +1,4 @@
import { ContainerModule } from '@theia/core/shared/inversify';
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { ArduinoDaemonImpl } from './arduino-daemon-impl';
import {
ArduinoFirmwareUploader,
@ -110,6 +110,7 @@ import {
SurveyNotificationServicePath,
} from '../common/protocol/survey-service';
import { IsTempSketch } from './is-temp-sketch';
import { rebindNsfwFileSystemWatcher } from './theia/filesystem/nsfw-watcher/nsfw-bindings';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
@ -288,6 +289,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
)
)
.inSingletonScope();
rebindNsfwFileSystemWatcher(rebind);
// Output service per connection.
bind(ConnectionContainerModule).toConstantValue(
@ -325,58 +327,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
})
);
// Logger for the Arduino daemon
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('daemon');
})
.inSingletonScope()
.whenTargetNamed('daemon');
// Logger for the Arduino daemon
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('fwuploader');
})
.inSingletonScope()
.whenTargetNamed('fwuploader');
// Logger for the "serial discovery".
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('discovery-log'); // TODO: revert
})
.inSingletonScope()
.whenTargetNamed('discovery-log'); // TODO: revert
// Logger for the CLI config service. From the CLI config (FS path aware), we make a URI-aware app config.
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('config');
})
.inSingletonScope()
.whenTargetNamed('config');
// Logger for the monitor manager and its services
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child(MonitorManagerName);
})
.inSingletonScope()
.whenTargetNamed(MonitorManagerName);
bind(ILogger)
.toDynamicValue((ctx) => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child(MonitorServiceName);
})
.inSingletonScope()
.whenTargetNamed(MonitorServiceName);
[
'daemon', // Logger for the Arduino daemon
'fwuploader', // Arduino Firmware uploader
'discovery-log', // Boards discovery
'config', // Logger for the CLI config reading and manipulation
MonitorManagerName, // Logger for the monitor manager and its services
MonitorServiceName,
].forEach((name) => bindChildLogger(bind, name));
// Remote sketchbook bindings
bind(AuthenticationServiceImpl).toSelf().inSingletonScope();
@ -423,3 +381,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(IsTempSketch).toSelf().inSingletonScope();
});
function bindChildLogger(bind: interfaces.Bind, name: string): void {
bind(ILogger)
.toDynamicValue(({ container }) =>
container.get<ILogger>(ILogger).child(name)
)
.inSingletonScope()
.whenTargetNamed(name);
}

View File

@ -561,14 +561,18 @@ void loop() {
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}.`);
}
async deleteSketch(sketch: Sketch): Promise<void> {
return new Promise<void>((resolve, reject) => {
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);
reject(error);
} else {
console.log(`Successfully deleted sketch at ${sketchPath}.`);
resolve();
}
});
});
}
}

View File

@ -0,0 +1,31 @@
import * as yargs from '@theia/core/shared/yargs';
import { JsonRpcProxyFactory } from '@theia/core';
import { NoDelayDisposalTimeoutNsfwFileSystemWatcherService } from './nsfw-filesystem-service';
import type { IPCEntryPoint } from '@theia/core/lib/node/messaging/ipc-protocol';
import type { FileSystemWatcherServiceClient } from '@theia/filesystem/lib/common/filesystem-watcher-protocol';
const options: {
verbose: boolean;
} = yargs
.option('verbose', {
default: false,
alias: 'v',
type: 'boolean',
})
.option('nsfwOptions', {
alias: 'o',
type: 'string',
coerce: JSON.parse,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}).argv as any;
export default <IPCEntryPoint>((connection) => {
const server = new NoDelayDisposalTimeoutNsfwFileSystemWatcherService(
options
);
const factory = new JsonRpcProxyFactory<FileSystemWatcherServiceClient>(
server
);
server.setClient(factory.createProxy());
factory.listen(connection);
});

View File

@ -0,0 +1,42 @@
import { join } from 'path';
import { interfaces } from '@theia/core/shared/inversify';
import {
NsfwFileSystemWatcherServiceProcessOptions,
NSFW_SINGLE_THREADED,
spawnNsfwFileSystemWatcherServiceProcess,
} from '@theia/filesystem/lib/node/filesystem-backend-module';
import { FileSystemWatcherService } from '@theia/filesystem/lib/common/filesystem-watcher-protocol';
import { NsfwFileSystemWatcherServerOptions } from '@theia/filesystem/lib/node/nsfw-watcher/nsfw-filesystem-service';
import { FileSystemWatcherServiceDispatcher } from '@theia/filesystem/lib/node/filesystem-watcher-dispatcher';
import { NoDelayDisposalTimeoutNsfwFileSystemWatcherService } from './nsfw-filesystem-service';
export function rebindNsfwFileSystemWatcher(rebind: interfaces.Rebind): void {
rebind<NsfwFileSystemWatcherServiceProcessOptions>(
NsfwFileSystemWatcherServiceProcessOptions
).toConstantValue({
entryPoint: join(__dirname, 'index.js'),
});
rebind<FileSystemWatcherService>(FileSystemWatcherService)
.toDynamicValue((context) =>
NSFW_SINGLE_THREADED
? createNsfwFileSystemWatcherService(context)
: spawnNsfwFileSystemWatcherServiceProcess(context)
)
.inSingletonScope();
}
function createNsfwFileSystemWatcherService({
container,
}: interfaces.Context): FileSystemWatcherService {
const options = container.get<NsfwFileSystemWatcherServerOptions>(
NsfwFileSystemWatcherServerOptions
);
const dispatcher = container.get<FileSystemWatcherServiceDispatcher>(
FileSystemWatcherServiceDispatcher
);
const server = new NoDelayDisposalTimeoutNsfwFileSystemWatcherService(
options
);
server.setClient(dispatcher);
return server;
}

View File

@ -0,0 +1,32 @@
import { Minimatch } from 'minimatch';
import type { WatchOptions } from '@theia/filesystem/lib/common/filesystem-watcher-protocol';
import {
NsfwFileSystemWatcherService,
NsfwWatcher,
} from '@theia/filesystem/lib/node/nsfw-watcher/nsfw-filesystem-service';
// Dispose the watcher immediately when the last reference is removed. By default, Theia waits 10 sec.
// https://github.com/eclipse-theia/theia/issues/11639#issuecomment-1238980708
const NoDelay = 0;
export class NoDelayDisposalTimeoutNsfwFileSystemWatcherService extends NsfwFileSystemWatcherService {
protected override createWatcher(
clientId: number,
fsPath: string,
options: WatchOptions
): NsfwWatcher {
const watcherOptions = {
ignored: options.ignored.map(
(pattern) => new Minimatch(pattern, { dot: true })
),
};
return new NsfwWatcher(
clientId,
fsPath,
watcherOptions,
this.options,
this.maybeClient,
NoDelay
);
}
}

View File

@ -1,62 +0,0 @@
{
"private": true,
"name": "browser-app",
"version": "2.0.0",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@theia/core": "1.25.0",
"@theia/debug": "1.25.0",
"@theia/editor": "1.25.0",
"@theia/file-search": "1.25.0",
"@theia/filesystem": "1.25.0",
"@theia/keymaps": "1.25.0",
"@theia/messages": "1.25.0",
"@theia/monaco": "1.25.0",
"@theia/navigator": "1.25.0",
"@theia/plugin-ext": "1.25.0",
"@theia/plugin-ext-vscode": "1.25.0",
"@theia/preferences": "1.25.0",
"@theia/process": "1.25.0",
"@theia/terminal": "1.25.0",
"@theia/workspace": "1.25.0",
"arduino-ide-extension": "2.0.0"
},
"devDependencies": {
"@theia/cli": "1.25.0"
},
"scripts": {
"prepare": "theia build --mode development",
"start": "theia start --plugins=local-dir:../plugins",
"watch": "theia build --watch --mode development"
},
"theia": {
"frontend": {
"config": {
"applicationName": "Arduino IDE",
"defaultTheme": "arduino-theme",
"preferences": {
"files.autoSave": "afterDelay",
"editor.minimap.enabled": false,
"editor.tabSize": 2,
"editor.scrollBeyondLastLine": false,
"editor.quickSuggestions": {
"other": false,
"comments": false,
"strings": false
},
"breadcrumbs.enabled": false
}
}
},
"backend": {
"config": {
"configDirName": ".arduinoIDE"
}
},
"generator": {
"config": {
"preloadTemplate": "<div class='theia-preload' style='background-color: rgb(237, 241, 242);'></div>"
}
}
}
}

View File

@ -1,20 +0,0 @@
/**
* This file can be edited to customize webpack configuration.
* To reset delete this file and rerun theia build again.
*/
// @ts-check
const config = require('./gen-webpack.config.js');
config.resolve.fallback['http'] = false;
config.resolve.fallback['fs'] = false;
/**
* Expose bundled modules on window.theia.moduleName namespace, e.g.
* window['theia']['@theia/core/lib/common/uri'].
* Such syntax can be used by external code, for instance, for testing.
config.module.rules.push({
test: /\.js$/,
loader: require.resolve('@theia/application-manager/lib/expose-loader')
}); */
module.exports = config;

View File

@ -12,7 +12,7 @@ https://github.com/arduino/arduino-ide/pulls/app%2Fgithub-actions
## ⚙ Create the release on GitHub
First of all, you need to **set the new version in all the `package.json` files** across the app (`./package.json`, `./arduino-ide-extension/package.json`, `./browser-app/package.json`, `./electron-app/package.json`), create a PR, and merge it on the `main` branch.
First of all, you need to **set the new version in all the `package.json` files** across the app (`./package.json`, `./arduino-ide-extension/package.json`, and `./electron-app/package.json`), create a PR, and merge it on the `main` branch.
To do so, you can make use of the `update:version` script.

View File

@ -119,14 +119,14 @@
}
verifyVersions(allDependencies);
//-------------------------------------------------------------+
// Save some time: no need to build the `browser-app` example. |
//-------------------------------------------------------------+
//---------------------------------------------------------------------------------------------------+
// Save some time: no need to build the projects that are not needed in final app. Currently unused. |
//---------------------------------------------------------------------------------------------------+
//@ts-ignore
let pkg = require('../working-copy/package.json');
const workspaces = pkg.workspaces;
// We cannot remove the `electron-app`. Otherwise, there is not way to collect the unused dependencies.
const dependenciesToRemove = ['browser-app'];
const dependenciesToRemove = [];
for (const dependencyToRemove of dependenciesToRemove) {
const index = workspaces.indexOf(dependencyToRemove);
if (index !== -1) {

View File

@ -41,7 +41,7 @@
},
"scripts": {
"prepare": "lerna run prepare && yarn download:plugins",
"cleanup": "npx rimraf ./**/node_modules && rm -rf ./node_modules ./.browser_modules ./arduino-ide-extension/build ./arduino-ide-extension/downloads ./arduino-ide-extension/Examples ./arduino-ide-extension/lib ./browser-app/lib ./browser-app/src-gen ./browser-app/gen-webpack.config.js ./electron-app/lib ./electron-app/src-gen ./electron-app/gen-webpack.config.js",
"cleanup": "npx rimraf ./**/node_modules && rm -rf ./node_modules ./.browser_modules ./arduino-ide-extension/build ./arduino-ide-extension/downloads ./arduino-ide-extension/Examples ./arduino-ide-extension/lib ./electron-app/lib ./electron-app/src-gen ./electron-app/gen-webpack.config.js",
"rebuild:browser": "theia rebuild:browser",
"rebuild:electron": "theia rebuild:electron",
"start": "yarn --cwd ./electron-app start",
@ -49,7 +49,7 @@
"test": "lerna run test",
"download:plugins": "theia download:plugins",
"update:version": "node ./scripts/update-version.js",
"i18n:generate": "theia nls-extract -e vscode -f \"+(arduino-ide-extension|browser-app|electron-app|plugins)/**/*.ts?(x)\" -o ./i18n/en.json",
"i18n:generate": "theia nls-extract -e vscode -f \"+(arduino-ide-extension|electron-app|plugins)/**/*.ts?(x)\" -o ./i18n/en.json",
"i18n:check": "yarn i18n:generate && git add -N ./i18n && git diff --exit-code ./i18n",
"i18n:push": "node ./scripts/i18n/transifex-push.js ./i18n/en.json",
"i18n:pull": "node ./scripts/i18n/transifex-pull.js ./i18n/",
@ -69,8 +69,7 @@
},
"workspaces": [
"arduino-ide-extension",
"electron-app",
"browser-app"
"electron-app"
],
"theiaPluginsDir": "plugins",
"theiaPlugins": {

View File

@ -27,7 +27,6 @@ console.log(`🛠️ Updating current version from '${currentVersion}' to '${tar
for (const toUpdate of [
path.join(repoRootPath, 'package.json'),
path.join(repoRootPath, 'electron-app', 'package.json'),
path.join(repoRootPath, 'browser-app', 'package.json'),
path.join(repoRootPath, 'arduino-ide-extension', 'package.json')
]) {
process.stdout.write(` Updating ${toUpdate}'...`);