mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-09 19:38:32 +00:00
Compare commits
58 Commits
open-recen
...
2.0.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4e882d25d9 | ||
![]() |
f93f78039b | ||
![]() |
2b2463b834 | ||
![]() |
0773c3915c | ||
![]() |
2f5afe0d9c | ||
![]() |
b8370686ec | ||
![]() |
3b2d12eff9 | ||
![]() |
cdaaa5584d | ||
![]() |
3476de27f7 | ||
![]() |
b55cfc2052 | ||
![]() |
44751c370b | ||
![]() |
32d904ca36 | ||
![]() |
5424dfcf70 | ||
![]() |
b8bf1eefa2 | ||
![]() |
93291b6811 | ||
![]() |
87ebcbe77e | ||
![]() |
99b10942bb | ||
![]() |
960a2d0634 | ||
![]() |
e577de4e8e | ||
![]() |
f3ef95cfe2 | ||
![]() |
bc264d1adf | ||
![]() |
5444395f34 | ||
![]() |
2d2be1f6d0 | ||
![]() |
1e269ac83d | ||
![]() |
0c49709f26 | ||
![]() |
019b2d5588 | ||
![]() |
aa0807ca3f | ||
![]() |
61a11a0857 | ||
![]() |
0c20ae0e28 | ||
![]() |
945a8f4841 | ||
![]() |
ae76432944 | ||
![]() |
40807db65e | ||
![]() |
da22f1ed11 | ||
![]() |
32b70efd5c | ||
![]() |
6f07717369 | ||
![]() |
d6cb23f782 | ||
![]() |
9ac2638335 | ||
![]() |
96cf09d594 | ||
![]() |
8380c82028 | ||
![]() |
5eb2926407 | ||
![]() |
a4ab204400 | ||
![]() |
6416c431c6 | ||
![]() |
8f88aa69bf | ||
![]() |
3c2b2a0734 | ||
![]() |
39538f163f | ||
![]() |
9ef04bb8d6 | ||
![]() |
707f3bef61 | ||
![]() |
878395221a | ||
![]() |
6a35bbfa7e | ||
![]() |
42f6f43870 | ||
![]() |
6983c5bf7f | ||
![]() |
b3ab5cbd2a | ||
![]() |
8a5995920a | ||
![]() |
8de6cf84d9 | ||
![]() |
f5c36bb691 | ||
![]() |
364f8b8e51 | ||
![]() |
671d2eabd4 | ||
![]() |
9a65ef6ea8 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -9,7 +9,7 @@ contact_links:
|
||||
url: https://forum.arduino.cc/
|
||||
about: We can help you out on the Arduino Forum!
|
||||
- name: Issue report guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/issues.md#issue-report-guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/contributor-guide/issues.md#issue-report-guide
|
||||
about: Learn about submitting issue reports to this repository.
|
||||
- name: Contributor guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -45,6 +45,7 @@
|
||||
"@types/deepmerge": "^2.2.0",
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/is-valid-path": "^0.1.0",
|
||||
"@types/js-yaml": "^3.12.2",
|
||||
"@types/keytar": "^4.4.0",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
@@ -56,7 +57,7 @@
|
||||
"@types/temp": "^0.8.34",
|
||||
"@types/which": "^1.3.1",
|
||||
"ajv": "^6.5.3",
|
||||
"arduino-serial-plotter-webapp": "0.1.0",
|
||||
"arduino-serial-plotter-webapp": "0.2.0",
|
||||
"async-mutex": "^0.3.0",
|
||||
"atob": "^2.1.2",
|
||||
"auth0-js": "^9.14.0",
|
||||
@@ -158,10 +159,10 @@
|
||||
],
|
||||
"arduino": {
|
||||
"cli": {
|
||||
"version": "0.27.1"
|
||||
"version": "0.28.0"
|
||||
},
|
||||
"fwuploader": {
|
||||
"version": "2.2.0"
|
||||
"version": "2.2.2"
|
||||
},
|
||||
"clangd": {
|
||||
"version": "14.0.0"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
// The version to use.
|
||||
const version = '1.9.1';
|
||||
const version = '1.10.0';
|
||||
|
||||
(async () => {
|
||||
const os = require('os');
|
||||
|
@@ -53,8 +53,6 @@ import {
|
||||
DockPanelRenderer as TheiaDockPanelRenderer,
|
||||
TabBarRendererFactory,
|
||||
ContextMenuRenderer,
|
||||
createTreeContainer,
|
||||
TreeWidget,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||
import {
|
||||
@@ -207,12 +205,8 @@ import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } f
|
||||
import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution';
|
||||
import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager';
|
||||
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
|
||||
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
|
||||
import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget';
|
||||
import { SearchInWorkspaceFactory as TheiaSearchInWorkspaceFactory } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';
|
||||
import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory';
|
||||
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
|
||||
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
|
||||
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
|
||||
import {
|
||||
MonacoEditorFactory,
|
||||
@@ -337,6 +331,10 @@ 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';
|
||||
import { UserFields } from './contributions/user-fields';
|
||||
import { UpdateIndexes } from './contributions/update-indexes';
|
||||
import { InterfaceScale } from './contributions/interface-scale';
|
||||
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
|
||||
|
||||
const registerArduinoThemes = () => {
|
||||
const themes: MonacoThemeJson[] = [
|
||||
@@ -401,6 +399,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
LibraryListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService)
|
||||
@@ -467,6 +466,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
BoardsListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
@@ -604,9 +604,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(MonacoEditorProvider).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoEditorProvider).toService(MonacoEditorProvider);
|
||||
|
||||
bind(SearchInWorkspaceWidget).toSelf();
|
||||
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
|
||||
|
||||
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
|
||||
bind(EditorManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorManager).toService(EditorManager);
|
||||
@@ -616,17 +613,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.to(SearchInWorkspaceFactory)
|
||||
.inSingletonScope();
|
||||
|
||||
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(
|
||||
({ container }) => {
|
||||
const childContainer = createTreeContainer(container);
|
||||
childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf();
|
||||
childContainer
|
||||
.rebind(TreeWidget)
|
||||
.toService(SearchInWorkspaceResultTreeWidget);
|
||||
return childContainer.get(SearchInWorkspaceResultTreeWidget);
|
||||
}
|
||||
);
|
||||
|
||||
// Show a disconnected status bar, when the daemon is not available
|
||||
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaApplicationConnectionStatusContribution).toService(
|
||||
@@ -761,7 +747,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, OpenBoardsConfig);
|
||||
Contribution.configure(bind, SketchFilesTracker);
|
||||
Contribution.configure(bind, CheckForUpdates);
|
||||
Contribution.configure(bind, UserFields);
|
||||
Contribution.configure(bind, DeleteSketch);
|
||||
Contribution.configure(bind, UpdateIndexes);
|
||||
Contribution.configure(bind, InterfaceScale);
|
||||
|
||||
bindContributionProvider(bind, StartupTaskProvider);
|
||||
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
|
||||
|
@@ -249,6 +249,14 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
),
|
||||
default: true,
|
||||
},
|
||||
'arduino.sketch.inoBlueprint': {
|
||||
type: 'string',
|
||||
markdownDescription: nls.localize(
|
||||
'arduino/preferences/sketch/inoBlueprint',
|
||||
'Absolute filesystem path to the default `.ino` blueprint file. If specified, the content of the blueprint file will be used for every new sketch created by the IDE. The sketches will be generated with the default Arduino content if not specified. Unaccessible blueprint files are ignored. **A restart of the IDE is needed** for this setting to take effect.'
|
||||
),
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -278,6 +286,7 @@ export interface ArduinoConfiguration {
|
||||
'arduino.auth.registerUri': string;
|
||||
'arduino.survey.notification': boolean;
|
||||
'arduino.cli.daemon.debug': boolean;
|
||||
'arduino.sketch.inoBlueprint': string;
|
||||
'arduino.checkForUpdates': boolean;
|
||||
}
|
||||
|
||||
|
@@ -34,6 +34,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
) {
|
||||
super({ ...props, maxWidth: 500 });
|
||||
|
||||
this.node.id = 'select-board-dialog-container';
|
||||
this.contentNode.classList.add('select-board-dialog');
|
||||
this.contentNode.appendChild(this.createDescription());
|
||||
|
||||
|
@@ -6,7 +6,6 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
Board,
|
||||
Port,
|
||||
AttachedBoardsChangeEvent,
|
||||
BoardWithPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
@@ -113,11 +112,14 @@ export class BoardsConfig extends React.Component<
|
||||
);
|
||||
}
|
||||
}),
|
||||
this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
|
||||
this.updatePorts(
|
||||
event.newState.ports,
|
||||
AttachedBoardsChangeEvent.diff(event).detached.ports
|
||||
)
|
||||
this.props.boardsServiceProvider.onAvailablePortsChanged(
|
||||
({ newState, oldState }) => {
|
||||
const removedPorts = oldState.filter(
|
||||
(oldPort) =>
|
||||
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
|
||||
);
|
||||
this.updatePorts(newState, removedPorts);
|
||||
}
|
||||
),
|
||||
this.props.boardsServiceProvider.onBoardsConfigChanged(
|
||||
({ selectedBoard, selectedPort }) => {
|
||||
@@ -132,7 +134,7 @@ export class BoardsConfig extends React.Component<
|
||||
this.props.notificationCenter.onPlatformDidUninstall(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onIndexDidUpdate(() =>
|
||||
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onDaemonDidStart(() =>
|
||||
@@ -259,9 +261,12 @@ export class BoardsConfig extends React.Component<
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
{this.renderContainer('boards', this.renderBoards.bind(this))}
|
||||
{this.renderContainer(
|
||||
'ports',
|
||||
nls.localize('arduino/board/boards', 'boards'),
|
||||
this.renderBoards.bind(this)
|
||||
)}
|
||||
{this.renderContainer(
|
||||
nls.localize('arduino/board/ports', 'ports'),
|
||||
this.renderPorts.bind(this),
|
||||
this.renderPortsFooter.bind(this)
|
||||
)}
|
||||
@@ -299,6 +304,18 @@ export class BoardsConfig extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
const boardsList = Array.from(distinctBoards.values()).map((board) => (
|
||||
<Item<BoardWithPackage>
|
||||
key={toKey(board)}
|
||||
item={board}
|
||||
label={board.name}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={board.missing}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="search">
|
||||
@@ -315,19 +332,17 @@ export class BoardsConfig extends React.Component<
|
||||
/>
|
||||
<i className="fa fa-search"></i>
|
||||
</div>
|
||||
<div className="boards list">
|
||||
{Array.from(distinctBoards.values()).map((board) => (
|
||||
<Item<BoardWithPackage>
|
||||
key={toKey(board)}
|
||||
item={board}
|
||||
label={board.name}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={board.missing}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{boardsList.length > 0 ? (
|
||||
<div className="boards list">{boardsList}</div>
|
||||
) : (
|
||||
<div className="no-result">
|
||||
{nls.localize(
|
||||
'arduino/board/noBoardsFound',
|
||||
'No boards found for "{0}"',
|
||||
query
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -342,7 +357,7 @@ export class BoardsConfig extends React.Component<
|
||||
);
|
||||
}
|
||||
return !ports.length ? (
|
||||
<div className="loading noselect">
|
||||
<div className="no-result">
|
||||
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
|
||||
</div>
|
||||
) : (
|
||||
@@ -374,7 +389,9 @@ export class BoardsConfig extends React.Component<
|
||||
defaultChecked={this.state.showAllPorts}
|
||||
onChange={this.toggleFilterPorts}
|
||||
/>
|
||||
<span>Show all ports</span>
|
||||
<span>
|
||||
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
@@ -111,7 +111,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
const { label } = commands.get(commandId)!;
|
||||
this.menuRegistry.registerMenuAction(menuPath, {
|
||||
commandId,
|
||||
order: `${i}`,
|
||||
order: String(i).padStart(4),
|
||||
label,
|
||||
});
|
||||
return Disposable.create(() =>
|
||||
|
@@ -63,7 +63,10 @@ export class BoardsServiceProvider
|
||||
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
|
||||
AvailableBoard[]
|
||||
>();
|
||||
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
|
||||
protected readonly onAvailablePortsChangedEmitter = new Emitter<{
|
||||
newState: Port[];
|
||||
oldState: Port[];
|
||||
}>();
|
||||
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
|
||||
|
||||
/**
|
||||
@@ -120,8 +123,12 @@ export class BoardsServiceProvider
|
||||
const { boards: attachedBoards, ports: availablePorts } =
|
||||
AvailablePorts.split(state);
|
||||
this._attachedBoards = attachedBoards;
|
||||
const oldState = this._availablePorts.slice();
|
||||
this._availablePorts = availablePorts;
|
||||
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||
this.onAvailablePortsChangedEmitter.fire({
|
||||
newState: this._availablePorts.slice(),
|
||||
oldState,
|
||||
});
|
||||
|
||||
await this.reconcileAvailableBoards();
|
||||
|
||||
@@ -229,8 +236,12 @@ export class BoardsServiceProvider
|
||||
}
|
||||
|
||||
this._attachedBoards = event.newState.boards;
|
||||
const oldState = this._availablePorts.slice();
|
||||
this._availablePorts = event.newState.ports;
|
||||
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||
this.onAvailablePortsChangedEmitter.fire({
|
||||
newState: this._availablePorts.slice(),
|
||||
oldState,
|
||||
});
|
||||
this.reconcileAvailableBoards().then(() => {
|
||||
const { uploadInProgress } = event;
|
||||
// avoid attempting "auto-selection" while an
|
||||
@@ -398,14 +409,16 @@ export class BoardsServiceProvider
|
||||
}
|
||||
|
||||
async selectedBoardUserFields(): Promise<BoardUserField[]> {
|
||||
if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
|
||||
if (!this._boardsConfig.selectedBoard) {
|
||||
return [];
|
||||
}
|
||||
const fqbn = this._boardsConfig.selectedBoard.fqbn;
|
||||
if (!fqbn) {
|
||||
return [];
|
||||
}
|
||||
const protocol = this._boardsConfig.selectedPort.protocol;
|
||||
// Protocol must be set to `default` when uploading without a port selected:
|
||||
// https://arduino.github.io/arduino-cli/dev/platform-specification/#sketch-upload-configuration
|
||||
const protocol = this._boardsConfig.selectedPort?.protocol || 'default';
|
||||
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
|
||||
}
|
||||
|
||||
@@ -600,7 +613,7 @@ export class BoardsServiceProvider
|
||||
boardsConfig.selectedBoard &&
|
||||
availableBoards.every(({ selected }) => !selected)
|
||||
) {
|
||||
let port = boardsConfig.selectedPort
|
||||
let port = boardsConfig.selectedPort;
|
||||
// If the selected board has the same port of an unknown board
|
||||
// that is already in availableBoards we might get a duplicate port.
|
||||
// So we remove the one already in the array and add the selected one.
|
||||
@@ -611,7 +624,7 @@ export class BoardsServiceProvider
|
||||
// get the "Unknown board port" that we will substitute,
|
||||
// then we can include it in the "availableBoard object"
|
||||
// pushed below; to ensure addressLabel is included
|
||||
port = availableBoards[found].port
|
||||
port = availableBoards[found].port;
|
||||
availableBoards.splice(found, 1);
|
||||
}
|
||||
availableBoards.push({
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
import type {
|
||||
import {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
@@ -24,7 +25,16 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
});
|
||||
}
|
||||
|
||||
override async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
protected canParse(uri: URI): boolean {
|
||||
try {
|
||||
BoardSearch.UriParser.parse(uri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): BoardSearch | undefined {
|
||||
return BoardSearch.UriParser.parse(uri);
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ import { CurrentSketch } from '../../common/protocol/sketches-service-client-imp
|
||||
@injectable()
|
||||
export class AddFile extends SketchContribution {
|
||||
@inject(FileDialogService)
|
||||
protected readonly fileDialogService: FileDialogService;
|
||||
private readonly fileDialogService: FileDialogService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddFile.Commands.ADD_FILE, {
|
||||
@@ -31,7 +31,7 @@ export class AddFile extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
protected async addFile(): Promise<void> {
|
||||
private async addFile(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
@@ -41,6 +41,7 @@ export class AddFile extends SketchContribution {
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
modal: true,
|
||||
});
|
||||
if (!toAddUri) {
|
||||
return;
|
||||
|
@@ -17,13 +17,13 @@ import { nls } from '@theia/core/lib/common';
|
||||
@injectable()
|
||||
export class AddZipLibrary extends SketchContribution {
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
private readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(ResponseServiceClient)
|
||||
protected readonly responseService: ResponseServiceClient;
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
|
||||
@@ -43,23 +43,26 @@ export class AddZipLibrary extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
async addZipLibrary(): Promise<void> {
|
||||
private async addZipLibrary(): Promise<void> {
|
||||
const homeUri = await this.envVariableServer.getHomeDirUri();
|
||||
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
|
||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
|
||||
title: nls.localize(
|
||||
'arduino/selectZip',
|
||||
"Select a zip file containing the library you'd like to add"
|
||||
),
|
||||
defaultPath,
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/library/zipLibrary', 'Library'),
|
||||
extensions: ['zip'],
|
||||
},
|
||||
],
|
||||
});
|
||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/selectZip',
|
||||
"Select a zip file containing the library you'd like to add"
|
||||
),
|
||||
defaultPath,
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/library/zipLibrary', 'Library'),
|
||||
extensions: ['zip'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
if (!canceled && filePaths.length) {
|
||||
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
|
||||
try {
|
||||
|
@@ -28,7 +28,7 @@ export class ArchiveSketch extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
protected async archiveSketch(): Promise<void> {
|
||||
private async archiveSketch(): Promise<void> {
|
||||
const [sketch, config] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
@@ -43,13 +43,16 @@ export class ArchiveSketch extends SketchContribution {
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri).resolve(archiveBasename)
|
||||
);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveSketchAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
});
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveSketchAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
);
|
||||
if (!filePath || canceled) {
|
||||
return;
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@ import {
|
||||
DisposableCollection,
|
||||
Disposable,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { firstToUpperCase } from '../../common/utils';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { BoardsListWidget } from '../boards/boards-list-widget';
|
||||
@@ -200,14 +199,15 @@ PID: ${PID}`;
|
||||
});
|
||||
|
||||
// Installed boards
|
||||
for (const board of installedBoards) {
|
||||
installedBoards.forEach((board, index) => {
|
||||
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
|
||||
|
||||
const packageLabel =
|
||||
packageName +
|
||||
`${manuallyInstalled
|
||||
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||
: ''
|
||||
`${
|
||||
manuallyInstalled
|
||||
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||
: ''
|
||||
}`;
|
||||
// Platform submenu
|
||||
const platformMenuPath = [...boardsPackagesGroup, packageId];
|
||||
@@ -240,14 +240,18 @@ PID: ${PID}`;
|
||||
};
|
||||
|
||||
// Board menu
|
||||
const menuAction = { commandId: id, label: name };
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label: name,
|
||||
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
|
||||
);
|
||||
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
|
||||
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
|
||||
}
|
||||
});
|
||||
|
||||
// Installed ports
|
||||
const registerPorts = (
|
||||
@@ -267,8 +271,12 @@ PID: ${PID}`;
|
||||
];
|
||||
const placeholder = new PlaceholderMenuNode(
|
||||
menuPath,
|
||||
`${firstToUpperCase(protocol)} ports`,
|
||||
{ order: protocolOrder.toString() }
|
||||
nls.localize(
|
||||
'arduino/board/typeOfPorts',
|
||||
'{0} ports',
|
||||
Port.Protocols.protocolLabel(protocol)
|
||||
),
|
||||
{ order: protocolOrder.toString().padStart(4) }
|
||||
);
|
||||
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -279,11 +287,13 @@ PID: ${PID}`;
|
||||
|
||||
// First we show addresses with recognized boards connected,
|
||||
// then all the rest.
|
||||
const sortedIDs = Object.keys(ports).sort((left: string, right: string): number => {
|
||||
const [, leftBoards] = ports[left];
|
||||
const [, rightBoards] = ports[right];
|
||||
return rightBoards.length - leftBoards.length;
|
||||
});
|
||||
const sortedIDs = Object.keys(ports).sort(
|
||||
(left: string, right: string): number => {
|
||||
const [, leftBoards] = ports[left];
|
||||
const [, rightBoards] = ports[right];
|
||||
return rightBoards.length - leftBoards.length;
|
||||
}
|
||||
);
|
||||
|
||||
for (let i = 0; i < sortedIDs.length; i++) {
|
||||
const portID = sortedIDs[i];
|
||||
@@ -319,7 +329,7 @@ PID: ${PID}`;
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label,
|
||||
order: `${protocolOrder + i + 1}`,
|
||||
order: String(protocolOrder + i + 1).padStart(4),
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -351,7 +361,7 @@ PID: ${PID}`;
|
||||
}
|
||||
|
||||
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
|
||||
const allBoards = await this.boardsService.searchBoards({});
|
||||
const allBoards = await this.boardsService.getInstalledBoards();
|
||||
return allBoards.filter(InstalledBoardWithPackage.is);
|
||||
}
|
||||
}
|
||||
|
@@ -37,16 +37,17 @@ export class CheckForIDEUpdates extends Contribution {
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
if (!checkForUpdates) {
|
||||
return;
|
||||
}
|
||||
this.updater
|
||||
.init(
|
||||
this.preferences.get('arduino.ide.updateChannel'),
|
||||
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||
)
|
||||
.then(() => this.updater.checkForUpdates(true))
|
||||
.then(() => {
|
||||
if (!this.preferences['arduino.checkForUpdates']) {
|
||||
return;
|
||||
}
|
||||
return this.updater.checkForUpdates(true);
|
||||
})
|
||||
.then(async (updateInfo) => {
|
||||
if (!updateInfo) return;
|
||||
const versionToSkip = await this.localStorage.getData<string>(
|
||||
|
@@ -12,7 +12,6 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||
|
||||
import {
|
||||
@@ -61,6 +60,7 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { NotificationManager } from '../theia/messages/notifications-manager';
|
||||
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { WorkspaceService } from '../theia/workspace/workspace-service';
|
||||
|
||||
export {
|
||||
Command,
|
||||
|
@@ -49,30 +49,6 @@ export class EditContributions extends Contribution {
|
||||
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
|
||||
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
|
||||
execute: async () => {
|
||||
const settings = await this.settingsService.settings();
|
||||
if (settings.autoScaleInterface) {
|
||||
settings.interfaceScale = settings.interfaceScale + 1;
|
||||
} else {
|
||||
settings.editorFontSize = settings.editorFontSize + 1;
|
||||
}
|
||||
await this.settingsService.update(settings);
|
||||
await this.settingsService.save();
|
||||
},
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
|
||||
execute: async () => {
|
||||
const settings = await this.settingsService.settings();
|
||||
if (settings.autoScaleInterface) {
|
||||
settings.interfaceScale = settings.interfaceScale - 1;
|
||||
} else {
|
||||
settings.editorFontSize = settings.editorFontSize - 1;
|
||||
}
|
||||
await this.settingsService.update(settings);
|
||||
await this.settingsService.save();
|
||||
},
|
||||
});
|
||||
/* Tools */ registry.registerCommand(
|
||||
EditContributions.Commands.AUTO_FORMAT,
|
||||
{ execute: () => this.run('editor.action.formatDocument') }
|
||||
@@ -147,23 +123,6 @@ ${value}
|
||||
order: '3',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/increaseFontSize',
|
||||
'Increase Font Size'
|
||||
),
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/decreaseFontSize',
|
||||
'Decrease Font Size'
|
||||
),
|
||||
order: '1',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND.id,
|
||||
label: nls.localize('vscode/findController/startFindAction', 'Find'),
|
||||
@@ -220,15 +179,6 @@ ${value}
|
||||
when: 'editorFocus',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+=',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+-',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND.id,
|
||||
keybinding: 'CtrlCmd+F',
|
||||
@@ -315,12 +265,6 @@ export namespace EditContributions {
|
||||
export const USE_FOR_FIND: Command = {
|
||||
id: 'arduino-for-find',
|
||||
};
|
||||
export const INCREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-increase-font-size',
|
||||
};
|
||||
export const DECREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-decrease-font-size',
|
||||
};
|
||||
export const AUTO_FORMAT: Command = {
|
||||
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
|
||||
};
|
||||
|
@@ -21,16 +21,23 @@ import {
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
|
||||
import {
|
||||
Board,
|
||||
SketchRef,
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
Sketch,
|
||||
CoreService,
|
||||
} from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export abstract class Examples extends SketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
@@ -38,6 +45,9 @@ export abstract class Examples extends SketchContribution {
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@@ -50,10 +60,16 @@ export abstract class Examples extends SketchContribution {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
||||
protected handleBoardChanged(board: Board | undefined): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
protected abstract update(options?: {
|
||||
board?: Board | undefined;
|
||||
forceRefresh?: boolean;
|
||||
}): void;
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
try {
|
||||
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
||||
@@ -149,23 +165,54 @@ export abstract class Examples extends SketchContribution {
|
||||
protected createHandler(uri: string): CommandHandler {
|
||||
return {
|
||||
execute: async () => {
|
||||
const sketch = await this.sketchService.cloneExample(uri);
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
const sketch = await this.clone(uri);
|
||||
if (sketch) {
|
||||
try {
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async clone(uri: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchService.cloneExample(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BuiltInExamples extends Examples {
|
||||
override async onReady(): Promise<void> {
|
||||
this.register(); // no `await`
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
protected async register(): Promise<void> {
|
||||
protected override async update(): Promise<void> {
|
||||
let sketchContainers: SketchContainer[] | undefined;
|
||||
try {
|
||||
sketchContainers = await this.examplesService.builtIns();
|
||||
@@ -197,29 +244,34 @@ export class BuiltInExamples extends Examples {
|
||||
@injectable()
|
||||
export class LibraryExamples extends Examples {
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.register());
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.register());
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.update());
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.update());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
this.register(); // no `await`
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
protected override handleBoardChanged(board: Board | undefined): void {
|
||||
this.register(board);
|
||||
this.update({ board });
|
||||
}
|
||||
|
||||
protected async register(
|
||||
board: Board | undefined = this.boardsServiceClient.boardsConfig
|
||||
.selectedBoard
|
||||
protected override async update(
|
||||
options: { board?: Board; forceRefresh?: boolean } = {
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
}
|
||||
): Promise<void> {
|
||||
const { board, forceRefresh } = options;
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
if (forceRefresh) {
|
||||
await this.coreService.refresh();
|
||||
}
|
||||
const fqbn = board?.fqbn;
|
||||
const name = board?.name;
|
||||
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
|
||||
|
@@ -7,6 +7,8 @@ import {
|
||||
} from '../../common/protocol';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
const Arduino_BuiltIn = 'Arduino_BuiltIn';
|
||||
|
||||
@injectable()
|
||||
export class FirstStartupInstaller extends Contribution {
|
||||
@inject(LocalStorageService)
|
||||
@@ -25,8 +27,8 @@ export class FirstStartupInstaller extends Contribution {
|
||||
id: 'arduino:avr',
|
||||
});
|
||||
const builtInLibrary = (
|
||||
await this.libraryService.search({ query: 'Arduino_BuiltIn' })
|
||||
)[0];
|
||||
await this.libraryService.search({ query: Arduino_BuiltIn })
|
||||
).find(({ name }) => name === Arduino_BuiltIn); // Filter by `name` to ensure "exact match". See: https://github.com/arduino/arduino-ide/issues/1526.
|
||||
|
||||
let avrPackageError: Error | undefined;
|
||||
let builtInLibraryError: Error | undefined;
|
||||
@@ -84,7 +86,7 @@ export class FirstStartupInstaller extends Contribution {
|
||||
}
|
||||
if (builtInLibraryError) {
|
||||
this.messageService.error(
|
||||
`Could not install ${builtInLibrary.name} library: ${builtInLibraryError}`
|
||||
`Could not install ${Arduino_BuiltIn} library: ${builtInLibraryError}`
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,7 @@ export class IndexesUpdateProgress extends Contribution {
|
||||
| undefined;
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onIndexWillUpdate((progressId) =>
|
||||
this.notificationCenter.onIndexUpdateWillStart(({ progressId }) =>
|
||||
this.getOrCreateProgress(progressId)
|
||||
);
|
||||
this.notificationCenter.onIndexUpdateDidProgress((progress) => {
|
||||
@@ -24,7 +24,7 @@ export class IndexesUpdateProgress extends Contribution {
|
||||
delegate.report(progress)
|
||||
);
|
||||
});
|
||||
this.notificationCenter.onIndexDidUpdate((progressId) => {
|
||||
this.notificationCenter.onIndexUpdateDidComplete(({ progressId }) => {
|
||||
this.cancelProgress(progressId);
|
||||
});
|
||||
this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => {
|
||||
|
@@ -0,0 +1,228 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
Contribution,
|
||||
Command,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import {
|
||||
CommandRegistry,
|
||||
DisposableCollection,
|
||||
MaybePromise,
|
||||
nls,
|
||||
} from '@theia/core/lib/common';
|
||||
|
||||
import { Settings } from '../dialogs/settings/settings';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import debounce = require('lodash.debounce');
|
||||
|
||||
@injectable()
|
||||
export class InterfaceScale extends Contribution {
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
private readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
private readonly menuActionsDisposables = new DisposableCollection();
|
||||
private fontScalingEnabled: InterfaceScale.FontScalingEnabled = {
|
||||
increase: true,
|
||||
decrease: true,
|
||||
};
|
||||
|
||||
private currentSettings: Settings;
|
||||
private updateSettingsDebounced = debounce(
|
||||
async () => {
|
||||
await this.settingsService.update(this.currentSettings);
|
||||
await this.settingsService.save();
|
||||
},
|
||||
100,
|
||||
{ maxWait: 200 }
|
||||
);
|
||||
|
||||
override onStart(): MaybePromise<void> {
|
||||
const updateCurrent = (settings: Settings) => {
|
||||
this.currentSettings = settings;
|
||||
this.updateFontScalingEnabled();
|
||||
};
|
||||
this.settingsService.onDidChange((settings) => updateCurrent(settings));
|
||||
this.settingsService.settings().then((settings) => updateCurrent(settings));
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(InterfaceScale.Commands.INCREASE_FONT_SIZE, {
|
||||
execute: () => this.updateFontSize('increase'),
|
||||
isEnabled: () => this.fontScalingEnabled.increase,
|
||||
});
|
||||
registry.registerCommand(InterfaceScale.Commands.DECREASE_FONT_SIZE, {
|
||||
execute: () => this.updateFontSize('decrease'),
|
||||
isEnabled: () => this.fontScalingEnabled.decrease,
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
const increaseFontSizeMenuAction = {
|
||||
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/increaseFontSize',
|
||||
'Increase Font Size'
|
||||
),
|
||||
order: '0',
|
||||
};
|
||||
const decreaseFontSizeMenuAction = {
|
||||
commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/decreaseFontSize',
|
||||
'Decrease Font Size'
|
||||
),
|
||||
order: '1',
|
||||
};
|
||||
|
||||
if (this.fontScalingEnabled.increase) {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
increaseFontSizeMenuAction
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuNode(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
increaseFontSizeMenuAction.label,
|
||||
{ order: increaseFontSizeMenuAction.order }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (this.fontScalingEnabled.decrease) {
|
||||
this.menuActionsDisposables.push(
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
decreaseFontSizeMenuAction
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.menuActionsDisposables.push(
|
||||
this.menuRegistry.registerMenuNode(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
decreaseFontSizeMenuAction.label,
|
||||
{ order: decreaseFontSizeMenuAction.order }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
|
||||
private updateFontScalingEnabled(): void {
|
||||
let fontScalingEnabled = {
|
||||
increase: true,
|
||||
decrease: true,
|
||||
};
|
||||
|
||||
if (this.currentSettings.autoScaleInterface) {
|
||||
fontScalingEnabled = {
|
||||
increase:
|
||||
this.currentSettings.interfaceScale + InterfaceScale.ZoomLevel.STEP <=
|
||||
InterfaceScale.ZoomLevel.MAX,
|
||||
decrease:
|
||||
this.currentSettings.interfaceScale - InterfaceScale.ZoomLevel.STEP >=
|
||||
InterfaceScale.ZoomLevel.MIN,
|
||||
};
|
||||
} else {
|
||||
fontScalingEnabled = {
|
||||
increase:
|
||||
this.currentSettings.editorFontSize + InterfaceScale.FontSize.STEP <=
|
||||
InterfaceScale.FontSize.MAX,
|
||||
decrease:
|
||||
this.currentSettings.editorFontSize - InterfaceScale.FontSize.STEP >=
|
||||
InterfaceScale.FontSize.MIN,
|
||||
};
|
||||
}
|
||||
|
||||
const isChanged = Object.keys(fontScalingEnabled).some(
|
||||
(key: keyof InterfaceScale.FontScalingEnabled) =>
|
||||
fontScalingEnabled[key] !== this.fontScalingEnabled[key]
|
||||
);
|
||||
if (isChanged) {
|
||||
this.fontScalingEnabled = fontScalingEnabled;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
private updateFontSize(mode: 'increase' | 'decrease'): void {
|
||||
if (this.currentSettings.autoScaleInterface) {
|
||||
mode === 'increase'
|
||||
? (this.currentSettings.interfaceScale += InterfaceScale.ZoomLevel.STEP)
|
||||
: (this.currentSettings.interfaceScale -=
|
||||
InterfaceScale.ZoomLevel.STEP);
|
||||
} else {
|
||||
mode === 'increase'
|
||||
? (this.currentSettings.editorFontSize += InterfaceScale.FontSize.STEP)
|
||||
: (this.currentSettings.editorFontSize -= InterfaceScale.FontSize.STEP);
|
||||
}
|
||||
this.updateFontScalingEnabled();
|
||||
this.updateSettingsDebounced();
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+=',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+-',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace InterfaceScale {
|
||||
export namespace Commands {
|
||||
export const INCREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-increase-font-size',
|
||||
};
|
||||
export const DECREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-decrease-font-size',
|
||||
};
|
||||
}
|
||||
|
||||
export namespace ZoomLevel {
|
||||
export const MIN = -8;
|
||||
export const MAX = 9;
|
||||
export const STEP = 1;
|
||||
|
||||
export function toPercentage(scale: number): number {
|
||||
return scale * 20 + 100;
|
||||
}
|
||||
export function fromPercentage(percentage: number): number {
|
||||
return (percentage - 100) / 20;
|
||||
}
|
||||
export namespace Step {
|
||||
export function toPercentage(step: number): number {
|
||||
return step * 20;
|
||||
}
|
||||
export function fromPercentage(percentage: number): number {
|
||||
return percentage / 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace FontSize {
|
||||
export const MIN = 8;
|
||||
export const MAX = 72;
|
||||
export const STEP = 2;
|
||||
}
|
||||
|
||||
export interface FontScalingEnabled {
|
||||
increase: boolean;
|
||||
decrease: boolean;
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
SketchContribution,
|
||||
URI,
|
||||
@@ -17,11 +16,6 @@ export class NewSketch extends SketchContribution {
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
||||
execute: () => this.newSketch(),
|
||||
});
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
@@ -54,8 +48,5 @@ export namespace NewSketch {
|
||||
export const NEW_SKETCH: Command = {
|
||||
id: 'arduino-new-sketch',
|
||||
};
|
||||
export const NEW_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-new-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class OpenRecentSketch extends SketchContribution {
|
||||
@@ -33,7 +34,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
||||
protected toDispose = new DisposableCollection();
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||
@@ -42,8 +43,12 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update(forceUpdate?: boolean): void {
|
||||
this.sketchService
|
||||
.recentlyOpenedSketches()
|
||||
.recentlyOpenedSketches(forceUpdate)
|
||||
.then((sketches) => this.refreshMenu(sketches));
|
||||
}
|
||||
|
||||
@@ -62,19 +67,25 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
|
||||
protected register(sketches: Sketch[]): void {
|
||||
const order = 0;
|
||||
this.toDispose.dispose();
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const toDispose = this.toDisposeBeforeRegister.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
const command = { id: `arduino-open-recent--${uri}` };
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.commandRegistry.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
),
|
||||
execute: async () => {
|
||||
try {
|
||||
await this.commandRegistry.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.update(true);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.menuRegistry.registerMenuAction(
|
||||
@@ -85,8 +96,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
order: String(order),
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeRegister.set(
|
||||
sketch.uri,
|
||||
this.toDispose.pushAll([
|
||||
new DisposableCollection(
|
||||
Disposable.create(() =>
|
||||
this.commandRegistry.unregisterCommand(command)
|
||||
@@ -94,8 +104,8 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(command)
|
||||
)
|
||||
)
|
||||
);
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { Later } from '../../common/nls';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
import { Sketch, SketchesError } from '../../common/protocol';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
@@ -10,9 +10,19 @@ import {
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
import { promptMoveSketch } from './open-sketch';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
import { Deferred, wait } from '@theia/core/lib/common/promise-util';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService';
|
||||
|
||||
@injectable()
|
||||
export class OpenSketchFiles extends SketchContribution {
|
||||
@inject(VSCodeContextKeyService)
|
||||
private readonly contextKeyService: VSCodeContextKeyService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
|
||||
execute: (uri: URI) => this.openSketchFiles(uri),
|
||||
@@ -55,9 +65,25 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
}
|
||||
});
|
||||
}
|
||||
const { workspaceError } = this.workspaceService;
|
||||
// This happens when the IDE2 has been started (from either a terminal or clicking on an `ino` file) with a /path/to/invalid/sketch. (#964)
|
||||
if (SketchesError.InvalidName.is(workspaceError)) {
|
||||
await this.promptMove(workspaceError);
|
||||
}
|
||||
} catch (err) {
|
||||
// This happens when the user gracefully closed IDE2, all went well
|
||||
// but the main sketch file was renamed outside of IDE2 and when the user restarts the IDE2
|
||||
// the workspace path still exists, but the sketch path is not valid anymore. (#964)
|
||||
if (SketchesError.InvalidName.is(err)) {
|
||||
const movedSketch = await this.promptMove(err);
|
||||
if (!movedSketch) {
|
||||
// If user did not accept the move, or move was not possible, force reload with a fallback.
|
||||
return this.openFallbackSketch();
|
||||
}
|
||||
}
|
||||
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.openFallbackSketch();
|
||||
return this.openFallbackSketch();
|
||||
} else {
|
||||
console.error(err);
|
||||
const message =
|
||||
@@ -71,6 +97,31 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
}
|
||||
}
|
||||
|
||||
private async promptMove(
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
invalidMainSketchUri: string;
|
||||
}
|
||||
>
|
||||
): Promise<Sketch | undefined> {
|
||||
const { invalidMainSketchUri } = err.data;
|
||||
requestAnimationFrame(() => this.messageService.error(err.message));
|
||||
await wait(10); // let IDE2 toast the error message.
|
||||
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
|
||||
fileService: this.fileService,
|
||||
sketchService: this.sketchService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
if (movedSketch) {
|
||||
this.workspaceService.open(new URI(movedSketch.uri), {
|
||||
preserveWindow: true,
|
||||
});
|
||||
return movedSketch;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async openFallbackSketch(): Promise<void> {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
||||
@@ -84,8 +135,48 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
const widget = this.editorManager.all.find(
|
||||
(widget) => widget.editor.uri.toString() === uri
|
||||
);
|
||||
const disposables = new DisposableCollection();
|
||||
if (!widget || forceOpen) {
|
||||
return this.editorManager.open(
|
||||
const deferred = new Deferred<EditorWidget>();
|
||||
disposables.push(
|
||||
this.editorManager.onCreated((editor) => {
|
||||
if (editor.editor.uri.toString() === uri) {
|
||||
if (editor.isVisible) {
|
||||
disposables.dispose();
|
||||
deferred.resolve(editor);
|
||||
} else {
|
||||
// In Theia, the promise resolves after opening the editor, but the editor is neither attached to the DOM, nor visible.
|
||||
// This is a hack to first get an event from monaco after the widget update request, then IDE2 waits for the next monaco context key event.
|
||||
// Here, the monaco context key event is not used, but this is the first event after the editor is visible in the UI.
|
||||
disposables.push(
|
||||
(editor.editor as MonacoEditor).onDidResize((dimension) => {
|
||||
if (dimension) {
|
||||
const isKeyOwner = (
|
||||
arg: unknown
|
||||
): arg is { key: string } => {
|
||||
if (typeof arg === 'object') {
|
||||
const object = arg as Record<string, unknown>;
|
||||
return typeof object['key'] === 'string';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
disposables.push(
|
||||
this.contextKeyService.onDidChangeContext((e) => {
|
||||
// `commentIsEmpty` is the first context key change event received from monaco after the editor is for real visible in the UI.
|
||||
if (isKeyOwner(e) && e.key === 'commentIsEmpty') {
|
||||
deferred.resolve(editor);
|
||||
disposables.dispose();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
this.editorManager.open(
|
||||
new URI(uri),
|
||||
options ?? {
|
||||
mode: 'reveal',
|
||||
@@ -93,6 +184,20 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
counter: 0,
|
||||
}
|
||||
);
|
||||
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
|
||||
const result = await Promise.race([
|
||||
deferred.promise,
|
||||
wait(timeout).then(() => {
|
||||
disposables.dispose();
|
||||
return 'timeout';
|
||||
}),
|
||||
]);
|
||||
if (result === 'timeout') {
|
||||
console.warn(
|
||||
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,115 +1,50 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
SketchesError,
|
||||
SketchesService,
|
||||
SketchRef,
|
||||
} from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
SketchContribution,
|
||||
Sketch,
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
SketchContribution,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import { BuiltInExamples } from './examples';
|
||||
import { Sketchbook } from './sketchbook';
|
||||
import { SketchContainer } from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
export type SketchLocation = string | URI | SketchRef;
|
||||
export namespace SketchLocation {
|
||||
export function toUri(location: SketchLocation): URI {
|
||||
if (typeof location === 'string') {
|
||||
return new URI(location);
|
||||
} else if (SketchRef.is(location)) {
|
||||
return toUri(location.uri);
|
||||
} else {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
export function is(arg: unknown): arg is SketchLocation {
|
||||
return typeof arg === 'string' || arg instanceof URI || SketchRef.is(arg);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class OpenSketch extends SketchContribution {
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(BuiltInExamples)
|
||||
protected readonly builtInExamples: BuiltInExamples;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(Sketchbook)
|
||||
protected readonly sketchbook: Sketchbook;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
||||
execute: (arg) =>
|
||||
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
|
||||
});
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (_: Widget, target: EventTarget) => {
|
||||
const container = await this.sketchService.getSketches({
|
||||
exclude: ['**/hardware/**'],
|
||||
});
|
||||
if (SketchContainer.isEmpty(container)) {
|
||||
this.openSketch();
|
||||
} else {
|
||||
this.toDispose.dispose();
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
|
||||
{
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: nls.localize(
|
||||
'vscode/workspaceActions/openFileFolder',
|
||||
'Open...'
|
||||
),
|
||||
}
|
||||
);
|
||||
this.toDispose.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
OpenSketch.Commands.OPEN_SKETCH
|
||||
)
|
||||
)
|
||||
);
|
||||
this.sketchbook.registerRecursively(
|
||||
[...container.children, ...container.sketches],
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
try {
|
||||
const containers = await this.examplesService.builtIns();
|
||||
for (const container of containers) {
|
||||
this.builtInExamples.registerRecursively(
|
||||
container,
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error when collecting built-in examples.', e);
|
||||
}
|
||||
const options = {
|
||||
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
y:
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
execute: async (arg) => {
|
||||
const toOpen = !SketchLocation.is(arg)
|
||||
? await this.selectSketch()
|
||||
: arg;
|
||||
if (toOpen) {
|
||||
return this.openSketch(toOpen);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -130,30 +65,40 @@ export class OpenSketch extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
async openSketch(
|
||||
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
|
||||
): Promise<void> {
|
||||
const sketch = await toOpen;
|
||||
if (sketch) {
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> {
|
||||
if (!toOpen) {
|
||||
return;
|
||||
}
|
||||
const uri = SketchLocation.toUri(toOpen);
|
||||
try {
|
||||
await this.sketchService.loadSketch(uri.toString());
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
this.workspaceService.open(uri);
|
||||
}
|
||||
|
||||
protected async selectSketch(): Promise<Sketch | undefined> {
|
||||
private async selectSketch(): Promise<Sketch | undefined> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri)
|
||||
);
|
||||
const { filePaths } = await remote.dialog.showOpenDialog({
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
|
||||
extensions: ['ino', 'pde'],
|
||||
},
|
||||
],
|
||||
});
|
||||
const { filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
|
||||
extensions: ['ino', 'pde'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
if (!filePaths.length) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -169,45 +114,11 @@ export class OpenSketch extends SketchContribution {
|
||||
return sketch;
|
||||
}
|
||||
if (Sketch.isSketchFile(sketchFileUri)) {
|
||||
const name = new URI(sketchFileUri).path.name;
|
||||
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||
],
|
||||
message: nls.localize(
|
||||
'arduino/sketch/movingMsg',
|
||||
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
||||
nameWithExt,
|
||||
name
|
||||
),
|
||||
return promptMoveSketch(sketchFileUri, {
|
||||
fileService: this.fileService,
|
||||
sketchService: this.sketchService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
if (response === 1) {
|
||||
// OK
|
||||
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
|
||||
const exists = await this.fileService.exists(newSketchUri);
|
||||
if (exists) {
|
||||
await remote.dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
||||
message: nls.localize(
|
||||
'arduino/sketch/cantOpen',
|
||||
'A folder named "{0}" already exists. Can\'t open sketch.',
|
||||
name
|
||||
),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
await this.fileService.createFolder(newSketchUri);
|
||||
await this.fileService.move(
|
||||
new URI(sketchFileUri),
|
||||
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||
);
|
||||
return this.sketchService.getSketchFolder(newSketchUri.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,8 +128,57 @@ export namespace OpenSketch {
|
||||
export const OPEN_SKETCH: Command = {
|
||||
id: 'arduino-open-sketch',
|
||||
};
|
||||
export const OPEN_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function promptMoveSketch(
|
||||
sketchFileUri: string | URI,
|
||||
options: {
|
||||
fileService: FileService;
|
||||
sketchService: SketchesService;
|
||||
labelProvider: LabelProvider;
|
||||
}
|
||||
): Promise<Sketch | undefined> {
|
||||
const { fileService, sketchService, labelProvider } = options;
|
||||
const uri =
|
||||
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
|
||||
const name = uri.path.name;
|
||||
const nameWithExt = labelProvider.getName(uri);
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||
],
|
||||
message: nls.localize(
|
||||
'arduino/sketch/movingMsg',
|
||||
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
||||
nameWithExt,
|
||||
name
|
||||
),
|
||||
});
|
||||
if (response === 1) {
|
||||
// OK
|
||||
const newSketchUri = uri.parent.resolve(name);
|
||||
const exists = await fileService.exists(newSketchUri);
|
||||
if (exists) {
|
||||
await remote.dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
||||
message: nls.localize(
|
||||
'arduino/sketch/cantOpen',
|
||||
'A folder named "{0}" already exists. Can\'t open sketch.',
|
||||
name
|
||||
),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
await fileService.createFolder(newSketchUri);
|
||||
await fileService.move(
|
||||
uri,
|
||||
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||
);
|
||||
return sketchService.getSketchFolder(newSketchUri.toString());
|
||||
}
|
||||
}
|
||||
|
@@ -50,7 +50,7 @@ export class SaveAsSketch extends SketchContribution {
|
||||
/**
|
||||
* Resolves `true` if the sketch was successfully saved as something.
|
||||
*/
|
||||
async saveAs(
|
||||
private async saveAs(
|
||||
{
|
||||
execOnlyIfTemp,
|
||||
openAfterMove,
|
||||
@@ -58,7 +58,10 @@ export class SaveAsSketch extends SketchContribution {
|
||||
markAsRecentlyOpened,
|
||||
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||
): Promise<boolean> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
const [sketch, configuration] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
@@ -68,27 +71,38 @@ export class SaveAsSketch extends SketchContribution {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const sketchbookDirUri = new URI(configuration.sketchDirUri);
|
||||
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
|
||||
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
|
||||
// Otherwise, it proposes the parent folder of the current sketch.
|
||||
const containerDirUri = isTemp
|
||||
? sketchbookDirUri
|
||||
: !sketchbookDirUri.isEqualOrParent(sketchUri)
|
||||
? sketchbookDirUri
|
||||
: sketchUri.parent;
|
||||
const exists = await this.fileService.exists(
|
||||
containerDirUri.resolve(sketch.name)
|
||||
);
|
||||
|
||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||
const sketchDirUri = new URI(
|
||||
(await this.configService.getConfiguration()).sketchDirUri
|
||||
);
|
||||
const exists = await this.fileService.exists(
|
||||
sketchDirUri.resolve(sketch.name)
|
||||
);
|
||||
const defaultUri = sketchDirUri.resolve(
|
||||
const defaultUri = containerDirUri.resolve(
|
||||
exists
|
||||
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
|
||||
: sketch.name
|
||||
);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveFolderAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
});
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveFolderAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
);
|
||||
if (!filePath || canceled) {
|
||||
return false;
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
import {
|
||||
SketchContribution,
|
||||
@@ -19,12 +18,6 @@ export class SaveSketch extends SketchContribution {
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
||||
execute: () => this.saveSketch(),
|
||||
});
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () =>
|
||||
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
@@ -68,8 +61,5 @@ export namespace SaveSketch {
|
||||
export const SAVE_SKETCH: Command = {
|
||||
id: 'arduino-save-sketch',
|
||||
};
|
||||
export const SAVE_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-save-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -176,7 +176,7 @@ export class SketchControl extends SketchContribution {
|
||||
{
|
||||
commandId: command.id,
|
||||
label: this.labelProvider.getName(uri),
|
||||
order: `${i}`,
|
||||
order: String(i).padStart(4),
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
|
@@ -1,32 +1,14 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||
import { CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
import { MenuModelRegistry } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Examples } from './examples';
|
||||
import {
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
SketchRef,
|
||||
} from '../../common/protocol';
|
||||
import { SketchContainer, SketchesError } from '../../common/protocol';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class Sketchbook extends Examples {
|
||||
@inject(CommandRegistry)
|
||||
protected override readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected override readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
override onStart(): void {
|
||||
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||
}
|
||||
@@ -35,10 +17,10 @@ export class Sketchbook extends Examples {
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
protected override update(): void {
|
||||
this.sketchService.getSketches({}).then((container) => {
|
||||
this.register(container);
|
||||
this.mainMenuManager.update();
|
||||
this.menuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +32,7 @@ export class Sketchbook extends Examples {
|
||||
);
|
||||
}
|
||||
|
||||
protected register(container: SketchContainer): void {
|
||||
private register(container: SketchContainer): void {
|
||||
this.toDispose.dispose();
|
||||
this.registerRecursively(
|
||||
[...container.children, ...container.sketches],
|
||||
@@ -62,23 +44,18 @@ export class Sketchbook extends Examples {
|
||||
protected override createHandler(uri: string): CommandHandler {
|
||||
return {
|
||||
execute: async () => {
|
||||
let sketch: SketchRef | undefined = undefined;
|
||||
try {
|
||||
sketch = await this.sketchService.loadSketch(uri);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
// To handle the following:
|
||||
// Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch.
|
||||
// Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items.
|
||||
this.messageService.error(err.message);
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
if (sketch) {
|
||||
await this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
uri
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
// Force update the menu items to remove the absent sketch.
|
||||
this.update();
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -0,0 +1,193 @@
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CoreService, IndexType } from '../../common/protocol';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||
import { Command, CommandRegistry, Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class UpdateIndexes extends Contribution {
|
||||
@inject(WindowServiceExt)
|
||||
private readonly windowService: WindowServiceExt;
|
||||
@inject(LocalStorageService)
|
||||
private readonly localStorage: LocalStorageService;
|
||||
@inject(CoreService)
|
||||
private readonly coreService: CoreService;
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.notificationCenter.onIndexUpdateDidComplete(({ summary }) =>
|
||||
Promise.all(
|
||||
Object.entries(summary).map(([type, updatedAt]) =>
|
||||
this.setLastUpdateDateTime(type as IndexType, updatedAt)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.checkForUpdates();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UpdateIndexes.Commands.UPDATE_INDEXES, {
|
||||
execute: () => this.updateIndexes(IndexType.All, true),
|
||||
});
|
||||
registry.registerCommand(UpdateIndexes.Commands.UPDATE_PLATFORM_INDEX, {
|
||||
execute: () => this.updateIndexes(['platform'], true),
|
||||
});
|
||||
registry.registerCommand(UpdateIndexes.Commands.UPDATE_LIBRARY_INDEX, {
|
||||
execute: () => this.updateIndexes(['library'], true),
|
||||
});
|
||||
}
|
||||
|
||||
private async checkForUpdates(): Promise<void> {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
if (!checkForUpdates) {
|
||||
console.debug(
|
||||
'[update-indexes]: `arduino.checkForUpdates` is `false`. Skipping updating the indexes.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.windowService.isFirstWindow()) {
|
||||
const summary = await this.coreService.indexUpdateSummaryBeforeInit();
|
||||
if (summary.message) {
|
||||
this.messageService.error(summary.message);
|
||||
}
|
||||
const typesToCheck = IndexType.All.filter((type) => !(type in summary));
|
||||
if (Object.keys(summary).length) {
|
||||
console.debug(
|
||||
`[update-indexes]: Detected an index update summary before the core gRPC client initialization. Updating local storage with ${JSON.stringify(
|
||||
summary
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
console.debug(
|
||||
'[update-indexes]: No index update summary was available before the core gRPC client initialization. Checking the status of the all the index types.'
|
||||
);
|
||||
}
|
||||
await Promise.allSettled([
|
||||
...Object.entries(summary).map(([type, updatedAt]) =>
|
||||
this.setLastUpdateDateTime(type as IndexType, updatedAt)
|
||||
),
|
||||
this.updateIndexes(typesToCheck),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateIndexes(
|
||||
types: IndexType[],
|
||||
force = false
|
||||
): Promise<void> {
|
||||
const updatedAt = new Date().toISOString();
|
||||
return Promise.all(
|
||||
types.map((type) => this.needsIndexUpdate(type, updatedAt, force))
|
||||
).then((needsIndexUpdateResults) => {
|
||||
const typesToUpdate = needsIndexUpdateResults.filter(IndexType.is);
|
||||
if (typesToUpdate.length) {
|
||||
console.debug(
|
||||
`[update-indexes]: Requesting the index update of type: ${JSON.stringify(
|
||||
typesToUpdate
|
||||
)} with date time: ${updatedAt}.`
|
||||
);
|
||||
return this.coreService.updateIndex({ types: typesToUpdate });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async needsIndexUpdate(
|
||||
type: IndexType,
|
||||
now: string,
|
||||
force = false
|
||||
): Promise<IndexType | false> {
|
||||
if (force) {
|
||||
console.debug(
|
||||
`[update-indexes]: Update for index type: '${type}' was forcefully requested.`
|
||||
);
|
||||
return type;
|
||||
}
|
||||
const lastUpdateIsoDateTime = await this.getLastUpdateDateTime(type);
|
||||
if (!lastUpdateIsoDateTime) {
|
||||
console.debug(
|
||||
`[update-indexes]: No last update date time was persisted for index type: '${type}'. Index update is required.`
|
||||
);
|
||||
return type;
|
||||
}
|
||||
const lastUpdateDateTime = Date.parse(lastUpdateIsoDateTime);
|
||||
if (Number.isNaN(lastUpdateDateTime)) {
|
||||
console.debug(
|
||||
`[update-indexes]: Invalid last update date time was persisted for index type: '${type}'. Last update date time was: ${lastUpdateDateTime}. Index update is required.`
|
||||
);
|
||||
return type;
|
||||
}
|
||||
const diff = new Date(now).getTime() - lastUpdateDateTime;
|
||||
const needsIndexUpdate = diff >= this.threshold;
|
||||
console.debug(
|
||||
`[update-indexes]: Update for index type '${type}' is ${
|
||||
needsIndexUpdate ? '' : 'not '
|
||||
}required. Now: ${now}, Last index update date time: ${new Date(
|
||||
lastUpdateDateTime
|
||||
).toISOString()}, diff: ${diff} ms, threshold: ${this.threshold} ms.`
|
||||
);
|
||||
return needsIndexUpdate ? type : false;
|
||||
}
|
||||
|
||||
private async getLastUpdateDateTime(
|
||||
type: IndexType
|
||||
): Promise<string | undefined> {
|
||||
const key = this.storageKeyOf(type);
|
||||
return this.localStorage.getData<string>(key);
|
||||
}
|
||||
|
||||
private async setLastUpdateDateTime(
|
||||
type: IndexType,
|
||||
updatedAt: string
|
||||
): Promise<void> {
|
||||
const key = this.storageKeyOf(type);
|
||||
return this.localStorage.setData<string>(key, updatedAt).finally(() => {
|
||||
console.debug(
|
||||
`[update-indexes]: Updated the last index update date time of '${type}' to ${updatedAt}.`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private storageKeyOf(type: IndexType): string {
|
||||
return `index-last-update-time--${type}`;
|
||||
}
|
||||
|
||||
private get threshold(): number {
|
||||
return 4 * 60 * 60 * 1_000; // four hours in millis
|
||||
}
|
||||
}
|
||||
export namespace UpdateIndexes {
|
||||
export namespace Commands {
|
||||
export const UPDATE_INDEXES: Command & { label: string } = {
|
||||
id: 'arduino-update-indexes',
|
||||
label: nls.localize(
|
||||
'arduino/updateIndexes/updateIndexes',
|
||||
'Update Indexes'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const UPDATE_PLATFORM_INDEX: Command & { label: string } = {
|
||||
id: 'arduino-update-package-index',
|
||||
label: nls.localize(
|
||||
'arduino/updateIndexes/updatePackageIndex',
|
||||
'Update Package Index'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const UPDATE_LIBRARY_INDEX: Command & { label: string } = {
|
||||
id: 'arduino-update-library-index',
|
||||
label: nls.localize(
|
||||
'arduino/updateIndexes/updateLibraryIndex',
|
||||
'Update Library Index'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { BoardUserField, CoreService, Port } from '../../common/protocol';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { CoreService, Port } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
Command,
|
||||
@@ -11,96 +11,36 @@ import {
|
||||
TabBarToolbarRegistry,
|
||||
CoreServiceContribution,
|
||||
} from './contribution';
|
||||
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
|
||||
import { deepClone, DisposableCollection, nls } from '@theia/core/lib/common';
|
||||
import { deepClone, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import type { VerifySketchParams } from './verify-sketch';
|
||||
import { UserFields } from './user-fields';
|
||||
|
||||
@injectable()
|
||||
export class UploadSketch extends CoreServiceContribution {
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(UserFieldsDialog)
|
||||
private readonly userFieldsDialog: UserFieldsDialog;
|
||||
|
||||
private boardRequiresUserFields = false;
|
||||
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||
private readonly menuActionsDisposables = new DisposableCollection();
|
||||
|
||||
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
private uploadInProgress = false;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
|
||||
const userFields =
|
||||
await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
this.boardRequiresUserFields = userFields.length > 0;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
});
|
||||
}
|
||||
|
||||
private selectedFqbnAddress(): string {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const fqbn = boardsConfig.selectedBoard?.fqbn;
|
||||
if (!fqbn) {
|
||||
return '';
|
||||
}
|
||||
const address =
|
||||
boardsConfig.selectedBoard?.port?.address ||
|
||||
boardsConfig.selectedPort?.address;
|
||||
if (!address) {
|
||||
return '';
|
||||
}
|
||||
return fqbn + '|' + address;
|
||||
}
|
||||
@inject(UserFields)
|
||||
private readonly userFields: UserFields;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
|
||||
execute: async () => {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (
|
||||
this.boardRequiresUserFields &&
|
||||
key &&
|
||||
!this.cachedUserFields.has(key)
|
||||
) {
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = (
|
||||
await this.boardsServiceProvider.selectedBoardUserFields()
|
||||
).map((f) => ({ ...f }));
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.cachedUserFields.set(key, result);
|
||||
if (await this.userFields.checkUserFieldsDialog()) {
|
||||
this.uploadSketch();
|
||||
}
|
||||
this.uploadSketch();
|
||||
},
|
||||
isEnabled: () => !this.uploadInProgress,
|
||||
});
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
|
||||
execute: async () => {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (!key) {
|
||||
return;
|
||||
if (await this.userFields.checkUserFieldsDialog(true)) {
|
||||
this.uploadSketch();
|
||||
}
|
||||
|
||||
const cached = this.cachedUserFields.get(key);
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = (
|
||||
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
|
||||
).map((f) => ({ ...f }));
|
||||
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.cachedUserFields.set(key, result);
|
||||
this.uploadSketch();
|
||||
},
|
||||
isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
|
||||
isEnabled: () => !this.uploadInProgress && this.userFields.isRequired(),
|
||||
});
|
||||
registry.registerCommand(
|
||||
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
|
||||
@@ -120,45 +60,20 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/upload', 'Upload'),
|
||||
order: '1',
|
||||
})
|
||||
);
|
||||
if (this.boardRequiresUserFields) {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
order: '2',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
{ order: '2' }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||
label: nls.localize(
|
||||
'arduino/sketch/uploadUsingProgrammer',
|
||||
'Upload Using Programmer'
|
||||
),
|
||||
order: '3',
|
||||
})
|
||||
);
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/upload', 'Upload'),
|
||||
order: '1',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||
label: nls.localize(
|
||||
'arduino/sketch/uploadUsingProgrammer',
|
||||
'Upload Using Programmer'
|
||||
),
|
||||
order: '3',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
@@ -215,18 +130,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This does not belong here.
|
||||
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
|
||||
if (
|
||||
uploadOptions.userFields.length === 0 &&
|
||||
this.boardRequiresUserFields
|
||||
) {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/sketch/userFieldsNotFoundError',
|
||||
"Can't find user fields for connected board"
|
||||
)
|
||||
);
|
||||
if (!this.userFields.checkUserFieldsForUpload()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,6 +146,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
} catch (e) {
|
||||
this.userFields.notifyFailedWithError(e);
|
||||
this.handleError(e);
|
||||
} finally {
|
||||
this.uploadInProgress = false;
|
||||
@@ -258,7 +163,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return undefined;
|
||||
}
|
||||
const userFields = this.userFields();
|
||||
const userFields = this.userFields.getUserFields();
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
|
||||
await Promise.all([
|
||||
@@ -301,10 +206,6 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
return port;
|
||||
}
|
||||
|
||||
private userFields(): BoardUserField[] {
|
||||
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
|
||||
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
|
||||
@@ -328,7 +229,7 @@ export namespace UploadSketch {
|
||||
id: 'arduino-upload-with-configuration-sketch',
|
||||
label: nls.localize(
|
||||
'arduino/sketch/configureAndUpload',
|
||||
'Configure And Upload'
|
||||
'Configure and Upload'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
|
147
arduino-ide-extension/src/browser/contributions/user-fields.ts
Normal file
147
arduino-ide-extension/src/browser/contributions/user-fields.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
||||
import { BoardUserField, CoreError } from '../../common/protocol';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { MenuModelRegistry, Contribution } from './contribution';
|
||||
import { UploadSketch } from './upload-sketch';
|
||||
|
||||
@injectable()
|
||||
export class UserFields extends Contribution {
|
||||
private boardRequiresUserFields = false;
|
||||
private userFieldsSet = false;
|
||||
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||
private readonly menuActionsDisposables = new DisposableCollection();
|
||||
|
||||
@inject(UserFieldsDialog)
|
||||
private readonly userFieldsDialog: UserFieldsDialog;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
|
||||
const userFields =
|
||||
await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
this.boardRequiresUserFields = userFields.length > 0;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
if (this.boardRequiresUserFields) {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
order: '2',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
{ order: '2' }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private selectedFqbnAddress(): string | undefined {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const fqbn = boardsConfig.selectedBoard?.fqbn;
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const address =
|
||||
boardsConfig.selectedBoard?.port?.address ||
|
||||
boardsConfig.selectedPort?.address ||
|
||||
'';
|
||||
return fqbn + '|' + address;
|
||||
}
|
||||
|
||||
private async showUserFieldsDialog(
|
||||
key: string
|
||||
): Promise<BoardUserField[] | undefined> {
|
||||
const cached = this.cachedUserFields.get(key);
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = cached
|
||||
? cached.slice()
|
||||
: await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.userFieldsSet = true;
|
||||
this.cachedUserFields.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async checkUserFieldsDialog(forceOpen = false): Promise<boolean> {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
/*
|
||||
If the board requires to be configured with user fields, we want
|
||||
to show the user fields dialog, but only if they weren't already
|
||||
filled in or if they were filled in, but the previous upload failed.
|
||||
*/
|
||||
if (
|
||||
!forceOpen &&
|
||||
(!this.boardRequiresUserFields ||
|
||||
(this.cachedUserFields.has(key) && this.userFieldsSet))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const userFieldsFilledIn = Boolean(await this.showUserFieldsDialog(key));
|
||||
return userFieldsFilledIn;
|
||||
}
|
||||
|
||||
checkUserFieldsForUpload(): boolean {
|
||||
// TODO: This does not belong here.
|
||||
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
|
||||
if (!this.boardRequiresUserFields || this.getUserFields().length > 0) {
|
||||
this.userFieldsSet = true;
|
||||
return true;
|
||||
}
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/sketch/userFieldsNotFoundError',
|
||||
"Can't find user fields for connected board"
|
||||
)
|
||||
);
|
||||
this.userFieldsSet = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
getUserFields(): BoardUserField[] {
|
||||
const fqbnAddress = this.selectedFqbnAddress();
|
||||
if (!fqbnAddress) {
|
||||
return [];
|
||||
}
|
||||
return this.cachedUserFields.get(fqbnAddress) ?? [];
|
||||
}
|
||||
|
||||
isRequired(): boolean {
|
||||
return this.boardRequiresUserFields;
|
||||
}
|
||||
|
||||
notifyFailedWithError(e: Error): void {
|
||||
if (this.boardRequiresUserFields && CoreError.UploadFailed.is(e)) {
|
||||
this.userFieldsSet = false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -171,6 +171,9 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
const firstButton = this.widget.node.querySelector('button');
|
||||
firstButton?.focus();
|
||||
|
||||
this.widget.busyCallback = this.busyCallback.bind(this);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
|
@@ -115,6 +115,8 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
const firstButton = this.widget.node.querySelector('button');
|
||||
firstButton?.focus();
|
||||
this.widget.busyCallback = this.busyCallback.bind(this);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
|
@@ -10,6 +10,7 @@ import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/fil
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
AdditionalUrls,
|
||||
CompilerWarnings,
|
||||
CompilerWarningLiterals,
|
||||
Network,
|
||||
ProxySettings,
|
||||
@@ -22,14 +23,22 @@ import {
|
||||
LanguageInfo,
|
||||
} from '@theia/core/lib/common/i18n/localization';
|
||||
import SettingsStepInput from './settings-step-input';
|
||||
import { InterfaceScale } from '../../contributions/interface-scale';
|
||||
|
||||
const maxScale = 280;
|
||||
const minScale = -60;
|
||||
const scaleStep = 20;
|
||||
const maxScale = InterfaceScale.ZoomLevel.toPercentage(
|
||||
InterfaceScale.ZoomLevel.MAX
|
||||
);
|
||||
const minScale = InterfaceScale.ZoomLevel.toPercentage(
|
||||
InterfaceScale.ZoomLevel.MIN
|
||||
);
|
||||
const scaleStep = InterfaceScale.ZoomLevel.Step.toPercentage(
|
||||
InterfaceScale.ZoomLevel.STEP
|
||||
);
|
||||
|
||||
const maxFontSize = InterfaceScale.FontSize.MAX;
|
||||
const minFontSize = InterfaceScale.FontSize.MIN;
|
||||
const fontSizeStep = InterfaceScale.FontSize.STEP;
|
||||
|
||||
const maxFontSize = 72;
|
||||
const minFontSize = 0;
|
||||
const fontSizeStep = 2;
|
||||
export class SettingsComponent extends React.Component<
|
||||
SettingsComponent.Props,
|
||||
SettingsComponent.State
|
||||
@@ -171,7 +180,8 @@ export class SettingsComponent extends React.Component<
|
||||
<div className="column">
|
||||
<div className="flex-line">
|
||||
<SettingsStepInput
|
||||
value={this.state.editorFontSize}
|
||||
key={`font-size-stepper-${String(this.state.editorFontSize)}`}
|
||||
initialValue={this.state.editorFontSize}
|
||||
setSettingsStateValue={this.setFontSize}
|
||||
step={fontSizeStep}
|
||||
maxValue={maxFontSize}
|
||||
@@ -190,13 +200,18 @@ export class SettingsComponent extends React.Component<
|
||||
</label>
|
||||
<div>
|
||||
<SettingsStepInput
|
||||
value={scalePercentage}
|
||||
key={`scale-stepper-${String(scalePercentage)}`}
|
||||
initialValue={scalePercentage}
|
||||
setSettingsStateValue={this.setInterfaceScale}
|
||||
step={scaleStep}
|
||||
maxValue={maxScale}
|
||||
minValue={minScale}
|
||||
unitOfMeasure="%"
|
||||
classNames={{ input: 'theia-input small with-margin' }}
|
||||
classNames={{
|
||||
input: 'theia-input small with-margin',
|
||||
buttonsContainer:
|
||||
'settings-step-input-buttons-container-perc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -260,7 +275,7 @@ export class SettingsComponent extends React.Component<
|
||||
>
|
||||
{CompilerWarningLiterals.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
{CompilerWarnings.labelOf(value)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -398,10 +413,22 @@ export class SettingsComponent extends React.Component<
|
||||
</form>
|
||||
<div className="flex-line proxy-settings">
|
||||
<div className="column">
|
||||
<div className="flex-line">Host name:</div>
|
||||
<div className="flex-line">Port number:</div>
|
||||
<div className="flex-line">Username:</div>
|
||||
<div className="flex-line">Password:</div>
|
||||
<div className="flex-line">{`${nls.localize(
|
||||
'arduino/preferences/proxySettings/hostname',
|
||||
'Host name'
|
||||
)}:`}</div>
|
||||
<div className="flex-line">{`${nls.localize(
|
||||
'arduino/preferences/proxySettings/port',
|
||||
'Port number'
|
||||
)}:`}</div>
|
||||
<div className="flex-line">{`${nls.localize(
|
||||
'arduino/preferences/proxySettings/username',
|
||||
'Username'
|
||||
)}:`}</div>
|
||||
<div className="flex-line">{`${nls.localize(
|
||||
'arduino/preferences/proxySettings/password',
|
||||
'Password'
|
||||
)}:`}</div>
|
||||
</div>
|
||||
<div className="column stretch">
|
||||
<div className="flex-line">
|
||||
@@ -502,6 +529,7 @@ export class SettingsComponent extends React.Component<
|
||||
canSelectFiles: false,
|
||||
canSelectMany: false,
|
||||
canSelectFolders: true,
|
||||
modal: true,
|
||||
});
|
||||
if (uri) {
|
||||
const sketchbookPath = await this.props.fileService.fsPath(uri);
|
||||
@@ -540,8 +568,7 @@ export class SettingsComponent extends React.Component<
|
||||
};
|
||||
|
||||
private setInterfaceScale = (percentage: number) => {
|
||||
const interfaceScale = (percentage - 100) / 20;
|
||||
|
||||
const interfaceScale = InterfaceScale.ZoomLevel.fromPercentage(percentage);
|
||||
this.setState({ interfaceScale });
|
||||
};
|
||||
|
||||
|
@@ -155,7 +155,6 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
|
||||
|
||||
this.textArea = document.createElement('textarea');
|
||||
this.textArea.className = 'theia-input';
|
||||
this.textArea.setAttribute('style', 'flex: 0;');
|
||||
this.textArea.value = urls
|
||||
.filter((url) => url.trim())
|
||||
.filter((url) => !!url)
|
||||
@@ -181,10 +180,10 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
|
||||
);
|
||||
this.contentNode.appendChild(anchor);
|
||||
|
||||
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
|
||||
this.appendCloseButton(
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel')
|
||||
);
|
||||
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
|
||||
}
|
||||
|
||||
get value(): string[] {
|
||||
|
@@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface SettingsStepInputProps {
|
||||
value: number;
|
||||
initialValue: number;
|
||||
setSettingsStateValue: (value: number) => void;
|
||||
step: number;
|
||||
maxValue: number;
|
||||
@@ -15,7 +15,7 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
|
||||
props: SettingsStepInputProps
|
||||
) => {
|
||||
const {
|
||||
value,
|
||||
initialValue,
|
||||
setSettingsStateValue,
|
||||
step,
|
||||
maxValue,
|
||||
@@ -24,18 +24,35 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
|
||||
classNames,
|
||||
} = props;
|
||||
|
||||
const [valueState, setValueState] = React.useState<{
|
||||
currentValue: number;
|
||||
isEmptyString: boolean;
|
||||
}>({
|
||||
currentValue: initialValue,
|
||||
isEmptyString: false,
|
||||
});
|
||||
const { currentValue, isEmptyString } = valueState;
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number => {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
};
|
||||
|
||||
const resetToInitialState = (): void => {
|
||||
setValueState({
|
||||
currentValue: initialValue,
|
||||
isEmptyString: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onStep = (
|
||||
roundingOperation: 'ceil' | 'floor',
|
||||
stepOperation: (a: number, b: number) => number
|
||||
): void => {
|
||||
const valueRoundedToScale = Math[roundingOperation](value / step) * step;
|
||||
const valueRoundedToScale =
|
||||
Math[roundingOperation](currentValue / step) * step;
|
||||
const calculatedValue =
|
||||
valueRoundedToScale === value
|
||||
? stepOperation(value, step)
|
||||
valueRoundedToScale === currentValue
|
||||
? stepOperation(currentValue, step)
|
||||
: valueRoundedToScale;
|
||||
const newValue = clamp(calculatedValue, minValue, maxValue);
|
||||
|
||||
@@ -52,33 +69,53 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
|
||||
|
||||
const onUserInput = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const { value: eventValue } = event.target;
|
||||
|
||||
if (eventValue === '') {
|
||||
setSettingsStateValue(0);
|
||||
}
|
||||
|
||||
const number = Number(eventValue);
|
||||
|
||||
if (!isNaN(number) && number !== value) {
|
||||
const newValue = clamp(number, minValue, maxValue);
|
||||
|
||||
setSettingsStateValue(newValue);
|
||||
}
|
||||
setValueState({
|
||||
currentValue: Number(eventValue),
|
||||
isEmptyString: eventValue === '',
|
||||
});
|
||||
};
|
||||
|
||||
const upDisabled = value >= maxValue;
|
||||
const downDisabled = value <= minValue;
|
||||
/* Prevent the user from entering invalid values */
|
||||
const onBlur = (event: React.FocusEvent): void => {
|
||||
if (
|
||||
(currentValue === initialValue && !isEmptyString) ||
|
||||
event.currentTarget.contains(event.relatedTarget as Node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clampedValue = clamp(currentValue, minValue, maxValue);
|
||||
if (clampedValue === initialValue || isNaN(currentValue) || isEmptyString) {
|
||||
resetToInitialState();
|
||||
return;
|
||||
}
|
||||
|
||||
setSettingsStateValue(clampedValue);
|
||||
};
|
||||
|
||||
const valueIsNotWithinRange =
|
||||
currentValue < minValue || currentValue > maxValue;
|
||||
const isDisabledException =
|
||||
valueIsNotWithinRange || isEmptyString || isNaN(currentValue);
|
||||
|
||||
const upDisabled = isDisabledException || currentValue >= maxValue;
|
||||
const downDisabled = isDisabledException || currentValue <= minValue;
|
||||
|
||||
return (
|
||||
<div className="settings-step-input-container">
|
||||
<div className="settings-step-input-container" onBlur={onBlur}>
|
||||
<input
|
||||
className={classnames('settings-step-input-element', classNames?.input)}
|
||||
value={value.toString()}
|
||||
value={isEmptyString ? '' : String(currentValue)}
|
||||
onChange={onUserInput}
|
||||
type="number"
|
||||
pattern="[0-9]+"
|
||||
/>
|
||||
<div className="settings-step-input-buttons-container">
|
||||
<div
|
||||
className={classnames(
|
||||
'settings-step-input-buttons-container',
|
||||
classNames?.buttonsContainer
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="settings-step-input-button settings-step-input-up-button"
|
||||
disabled={upDisabled}
|
||||
|
@@ -16,9 +16,9 @@ export const UserFieldsComponent = ({
|
||||
const [boardUserFields, setBoardUserFields] = React.useState<
|
||||
BoardUserField[]
|
||||
>(initialBoardUserFields);
|
||||
|
||||
const [uploadButtonDisabled, setUploadButtonDisabled] =
|
||||
React.useState<boolean>(true);
|
||||
const firstInputElement = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setBoardUserFields(initialBoardUserFields);
|
||||
@@ -48,7 +48,10 @@ export const UserFieldsComponent = ({
|
||||
React.useEffect(() => {
|
||||
updateUserFields(boardUserFields);
|
||||
setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields));
|
||||
}, [boardUserFields]);
|
||||
if (firstInputElement.current) {
|
||||
firstInputElement.current.focus();
|
||||
}
|
||||
}, [boardUserFields, updateUserFields]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -71,6 +74,7 @@ export const UserFieldsComponent = ({
|
||||
field.label
|
||||
)}
|
||||
onChange={updateUserField(index)}
|
||||
ref={index === 0 ? firstInputElement : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -13,7 +13,7 @@ import { BoardUserField } from '../../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class UserFieldsDialogWidget extends ReactWidget {
|
||||
protected _currentUserFields: BoardUserField[] = [];
|
||||
private _currentUserFields: BoardUserField[] = [];
|
||||
|
||||
constructor(private cancel: () => void, private accept: () => Promise<void>) {
|
||||
super();
|
||||
@@ -34,7 +34,7 @@ export class UserFieldsDialogWidget extends ReactWidget {
|
||||
});
|
||||
}
|
||||
|
||||
protected setUserFields(userFields: BoardUserField[]): void {
|
||||
private setUserFields(userFields: BoardUserField[]): void {
|
||||
this._currentUserFields = userFields;
|
||||
}
|
||||
|
||||
|
@@ -119,20 +119,16 @@ export class LibraryListWidget extends ListWidget<
|
||||
message.appendChild(question);
|
||||
const result = await new MessageBoxDialog({
|
||||
title: nls.localize(
|
||||
'arduino/library/dependenciesForLibrary',
|
||||
'Dependencies for library {0}:{1}',
|
||||
item.name,
|
||||
version
|
||||
'arduino/library/installLibraryDependencies',
|
||||
'Install library dependencies'
|
||||
),
|
||||
message,
|
||||
buttons: [
|
||||
nls.localize('arduino/library/installAll', 'Install all'),
|
||||
nls.localize(
|
||||
'arduino/library/installOnly',
|
||||
'Install {0} only',
|
||||
item.name
|
||||
'arduino/library/installWithoutDependencies',
|
||||
'Install without dependencies'
|
||||
),
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||
nls.localize('arduino/library/installAll', 'Install All'),
|
||||
],
|
||||
maxWidth: 740, // Aligned with `settings-dialog.css`.
|
||||
}).open();
|
||||
@@ -140,11 +136,11 @@ export class LibraryListWidget extends ListWidget<
|
||||
if (result) {
|
||||
const { response } = result;
|
||||
if (response === 0) {
|
||||
// All
|
||||
installDependencies = true;
|
||||
} else if (response === 1) {
|
||||
// Current only
|
||||
installDependencies = false;
|
||||
} else if (response === 1) {
|
||||
// All
|
||||
installDependencies = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -201,7 +197,13 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
|
||||
options.buttons || [nls.localize('vscode/issueMainService/ok', 'OK')]
|
||||
).forEach((text, index) => {
|
||||
const button = this.createButton(text);
|
||||
button.classList.add(index === 0 ? 'main' : 'secondary');
|
||||
const isPrimaryButton =
|
||||
index === (options.buttons ? options.buttons.length - 1 : 0);
|
||||
button.title = text;
|
||||
button.classList.add(
|
||||
isPrimaryButton ? 'main' : 'secondary',
|
||||
'message-box-dialog-button'
|
||||
);
|
||||
this.controlPanel.appendChild(button);
|
||||
this.toDisposeOnDetach.push(
|
||||
addEventListener(button, 'click', () => {
|
||||
|
@@ -1,16 +1,17 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { MenuModelRegistry } from '@theia/core';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { LibraryPackage, LibrarySearch } from '../../common/protocol';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetFrontendContribution
|
||||
extends AbstractViewContribution<LibraryListWidget>
|
||||
implements FrontendApplicationContribution
|
||||
{
|
||||
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
LibraryPackage,
|
||||
LibrarySearch
|
||||
> {
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: LibraryListWidget.WIDGET_ID,
|
||||
@@ -24,10 +25,6 @@ export class LibraryListWidgetFrontendContribution
|
||||
});
|
||||
}
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
}
|
||||
|
||||
override registerMenus(menus: MenuModelRegistry): void {
|
||||
if (this.toggleCommand) {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
@@ -40,4 +37,17 @@ export class LibraryListWidgetFrontendContribution
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected canParse(uri: URI): boolean {
|
||||
try {
|
||||
LibrarySearch.UriParser.parse(uri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): LibrarySearch | undefined {
|
||||
return LibrarySearch.UriParser.parse(uri);
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,9 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import {
|
||||
IndexUpdateDidCompleteParams,
|
||||
IndexUpdateDidFailParams,
|
||||
IndexUpdateWillStartParams,
|
||||
NotificationServiceClient,
|
||||
NotificationServiceServer,
|
||||
} from '../common/protocol/notification-service';
|
||||
@@ -29,48 +32,48 @@ export class NotificationCenter
|
||||
implements NotificationServiceClient, FrontendApplicationContribution
|
||||
{
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly server: JsonRpcProxy<NotificationServiceServer>;
|
||||
private readonly server: JsonRpcProxy<NotificationServiceServer>;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
protected readonly indexDidUpdateEmitter = new Emitter<string>();
|
||||
protected readonly indexWillUpdateEmitter = new Emitter<string>();
|
||||
protected readonly indexUpdateDidProgressEmitter =
|
||||
private readonly indexUpdateDidCompleteEmitter =
|
||||
new Emitter<IndexUpdateDidCompleteParams>();
|
||||
private readonly indexUpdateWillStartEmitter =
|
||||
new Emitter<IndexUpdateWillStartParams>();
|
||||
private readonly indexUpdateDidProgressEmitter =
|
||||
new Emitter<ProgressMessage>();
|
||||
protected readonly indexUpdateDidFailEmitter = new Emitter<{
|
||||
progressId: string;
|
||||
message: string;
|
||||
}>();
|
||||
protected readonly daemonDidStartEmitter = new Emitter<string>();
|
||||
protected readonly daemonDidStopEmitter = new Emitter<void>();
|
||||
protected readonly configDidChangeEmitter = new Emitter<{
|
||||
private readonly indexUpdateDidFailEmitter =
|
||||
new Emitter<IndexUpdateDidFailParams>();
|
||||
private readonly daemonDidStartEmitter = new Emitter<string>();
|
||||
private readonly daemonDidStopEmitter = new Emitter<void>();
|
||||
private readonly configDidChangeEmitter = new Emitter<{
|
||||
config: Config | undefined;
|
||||
}>();
|
||||
protected readonly platformDidInstallEmitter = new Emitter<{
|
||||
private readonly platformDidInstallEmitter = new Emitter<{
|
||||
item: BoardsPackage;
|
||||
}>();
|
||||
protected readonly platformDidUninstallEmitter = new Emitter<{
|
||||
private readonly platformDidUninstallEmitter = new Emitter<{
|
||||
item: BoardsPackage;
|
||||
}>();
|
||||
protected readonly libraryDidInstallEmitter = new Emitter<{
|
||||
private readonly libraryDidInstallEmitter = new Emitter<{
|
||||
item: LibraryPackage;
|
||||
}>();
|
||||
protected readonly libraryDidUninstallEmitter = new Emitter<{
|
||||
private readonly libraryDidUninstallEmitter = new Emitter<{
|
||||
item: LibraryPackage;
|
||||
}>();
|
||||
protected readonly attachedBoardsDidChangeEmitter =
|
||||
private readonly attachedBoardsDidChangeEmitter =
|
||||
new Emitter<AttachedBoardsChangeEvent>();
|
||||
protected readonly recentSketchesChangedEmitter = new Emitter<{
|
||||
private readonly recentSketchesChangedEmitter = new Emitter<{
|
||||
sketches: Sketch[];
|
||||
}>();
|
||||
private readonly onAppStateDidChangeEmitter =
|
||||
new Emitter<FrontendApplicationState>();
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.indexWillUpdateEmitter,
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.indexUpdateWillStartEmitter,
|
||||
this.indexUpdateDidProgressEmitter,
|
||||
this.indexDidUpdateEmitter,
|
||||
this.indexUpdateDidCompleteEmitter,
|
||||
this.indexUpdateDidFailEmitter,
|
||||
this.daemonDidStartEmitter,
|
||||
this.daemonDidStopEmitter,
|
||||
@@ -82,8 +85,8 @@ export class NotificationCenter
|
||||
this.attachedBoardsDidChangeEmitter
|
||||
);
|
||||
|
||||
readonly onIndexDidUpdate = this.indexDidUpdateEmitter.event;
|
||||
readonly onIndexWillUpdate = this.indexDidUpdateEmitter.event;
|
||||
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event;
|
||||
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event;
|
||||
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
|
||||
readonly onIndexUpdateDidFail = this.indexUpdateDidFailEmitter.event;
|
||||
readonly onDaemonDidStart = this.daemonDidStartEmitter.event;
|
||||
@@ -112,26 +115,20 @@ export class NotificationCenter
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
notifyIndexWillUpdate(progressId: string): void {
|
||||
this.indexWillUpdateEmitter.fire(progressId);
|
||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
|
||||
this.indexUpdateWillStartEmitter.fire(params);
|
||||
}
|
||||
|
||||
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
|
||||
this.indexUpdateDidProgressEmitter.fire(progressMessage);
|
||||
}
|
||||
|
||||
notifyIndexDidUpdate(progressId: string): void {
|
||||
this.indexDidUpdateEmitter.fire(progressId);
|
||||
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void {
|
||||
this.indexUpdateDidCompleteEmitter.fire(params);
|
||||
}
|
||||
|
||||
notifyIndexUpdateDidFail({
|
||||
progressId,
|
||||
message,
|
||||
}: {
|
||||
progressId: string;
|
||||
message: string;
|
||||
}): void {
|
||||
this.indexUpdateDidFailEmitter.fire({ progressId, message });
|
||||
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void {
|
||||
this.indexUpdateDidFailEmitter.fire(params);
|
||||
}
|
||||
|
||||
notifyDaemonDidStart(port: string): void {
|
||||
|
@@ -6,6 +6,53 @@ import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||
import { MonitorModel } from '../../monitor-model';
|
||||
import { Unknown } from '../../../common/nls';
|
||||
|
||||
class HistoryList {
|
||||
private readonly items: string[] = [];
|
||||
private index = -1;
|
||||
|
||||
constructor(private readonly size = 100) {}
|
||||
|
||||
push(val: string): void {
|
||||
if (val !== this.items[this.items.length - 1]) {
|
||||
this.items.push(val);
|
||||
}
|
||||
while (this.items.length > this.size) {
|
||||
this.items.shift();
|
||||
}
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
previous(): string {
|
||||
if (this.index === -1) {
|
||||
this.index = this.items.length - 1;
|
||||
return this.items[this.index];
|
||||
}
|
||||
if (this.hasPrevious) {
|
||||
return this.items[--this.index];
|
||||
}
|
||||
return this.items[this.index];
|
||||
}
|
||||
|
||||
private get hasPrevious(): boolean {
|
||||
return this.index >= 1;
|
||||
}
|
||||
|
||||
next(): string {
|
||||
if (this.index === this.items.length - 1) {
|
||||
this.index = -1;
|
||||
return '';
|
||||
}
|
||||
if (this.hasNext) {
|
||||
return this.items[++this.index];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private get hasNext(): boolean {
|
||||
return this.index >= 0 && this.index !== this.items.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SerialMonitorSendInput {
|
||||
export interface Props {
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
@@ -16,6 +63,7 @@ export namespace SerialMonitorSendInput {
|
||||
export interface State {
|
||||
text: string;
|
||||
connected: boolean;
|
||||
history: HistoryList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +75,7 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
|
||||
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
|
||||
super(props);
|
||||
this.state = { text: '', connected: true };
|
||||
this.state = { text: '', connected: true, history: new HistoryList() };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onSend = this.onSend.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
@@ -90,7 +138,7 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
protected setRef = (element: HTMLElement | null) => {
|
||||
protected setRef = (element: HTMLElement | null): void => {
|
||||
if (this.props.resolveFocus) {
|
||||
this.props.resolveFocus(element || undefined);
|
||||
}
|
||||
@@ -110,7 +158,17 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
if (keyCode) {
|
||||
const { key } = keyCode;
|
||||
if (key === Key.ENTER) {
|
||||
const { text } = this.state;
|
||||
this.onSend();
|
||||
if (text) {
|
||||
this.state.history.push(text);
|
||||
}
|
||||
} else if (key === Key.ARROW_UP) {
|
||||
this.setState({ text: this.state.history.previous() });
|
||||
} else if (key === Key.ARROW_DOWN) {
|
||||
this.setState({ text: this.state.history.next() });
|
||||
} else if (key === Key.ESCAPE) {
|
||||
this.setState({ text: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
CLOSE_PLOTTER_WINDOW,
|
||||
SHOW_PLOTTER_WINDOW,
|
||||
} from '../../../common/ipc-communication';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
const queryString = require('query-string');
|
||||
|
||||
@@ -107,7 +108,12 @@ export class PlotterFrontendContribution extends Contribution {
|
||||
if (wsPort) {
|
||||
this.open(wsPort);
|
||||
} else {
|
||||
this.messageService.error(`Couldn't open serial plotter`);
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/contributions/plotter/couldNotOpen',
|
||||
"Couldn't open serial plotter"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,11 @@
|
||||
#select-board-dialog-container > .dialogBlock {
|
||||
width: 640px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
div#select-board-dialog {
|
||||
margin: 5px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div#select-board-dialog .selectBoardContainer {
|
||||
@@ -7,12 +13,17 @@ div#select-board-dialog .selectBoardContainer {
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.select-board-dialog .head {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.dialogContent.select-board-dialog {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.dialogContent.select-board-dialog > div.head .title {
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
@@ -63,6 +74,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .left.container .content {
|
||||
@@ -131,6 +143,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
|
||||
#select-board-dialog .selectBoardContainer .list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .ports.list {
|
||||
@@ -282,3 +295,11 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#select-board-dialog .no-result {
|
||||
text-transform: uppercase;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 10px 5px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
@@ -9,7 +9,8 @@
|
||||
total = padding + margin = 96px
|
||||
*/
|
||||
max-width: calc(100% - 96px) !important;
|
||||
min-width: unset;
|
||||
|
||||
min-width: 424px;
|
||||
max-height: 560px;
|
||||
padding: 0 28px;
|
||||
}
|
||||
@@ -85,3 +86,4 @@
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
}
|
||||
.firmware-uploader-dialog .arduino-select__control {
|
||||
height: 31px;
|
||||
background: var(--theia-menubar-selectionBackground) !important;
|
||||
background: var(--theia-input-background) !important;
|
||||
}
|
||||
|
||||
.firmware-uploader-dialog .dialogRow > button{
|
||||
@@ -28,4 +28,4 @@
|
||||
|
||||
.firmware-uploader-dialog .status-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@@ -55,7 +55,8 @@
|
||||
/* Makes the sidepanel a bit wider when opening the widget */
|
||||
.p-DockPanel-widget {
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
min-height: 20px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
/* Overrule the default Theia CSS button styles. */
|
||||
@@ -108,6 +109,13 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
|
||||
background-color: var(--theia-secondaryButton-background);
|
||||
}
|
||||
|
||||
button.theia-button.message-box-dialog-button {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* To make the progress-bar slightly thicker, and use the color from the status bar */
|
||||
.theia-progress-bar-container {
|
||||
width: 100%;
|
||||
@@ -136,6 +144,9 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* High Contrast Theme rules */
|
||||
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
|
||||
|
@@ -111,13 +111,13 @@
|
||||
font-weight: bold;
|
||||
max-height: calc(1em + 4px);
|
||||
color: var(--theia-button-foreground);
|
||||
content: 'INSTALLED';
|
||||
content: attr(install);
|
||||
}
|
||||
|
||||
.component-list-item .header .installed:hover:before {
|
||||
background-color: var(--theia-button-foreground);
|
||||
color: var(--theia-button-background);
|
||||
content: 'UNINSTALL';
|
||||
content: attr(uninstall);
|
||||
}
|
||||
|
||||
.component-list-item[min-width~="170px"] .footer {
|
||||
@@ -131,7 +131,7 @@
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.component-list-item:hover .footer > * {
|
||||
.component-list-item .footer > * {
|
||||
display: inline-block;
|
||||
margin: 5px 0px 0px 10px;
|
||||
}
|
||||
@@ -160,4 +160,4 @@
|
||||
|
||||
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before {
|
||||
border: 1px solid var(--theia-button-border);
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,16 @@
|
||||
background: var(--theia-editorGroupHeader-tabsBackground);
|
||||
}
|
||||
|
||||
/* Avoid the Intellisense widget may be cover by the bottom panel partially.
|
||||
TODO: This issue may be resolved after monaco-editor upgrade */
|
||||
#theia-main-content-panel {
|
||||
z-index: auto
|
||||
}
|
||||
|
||||
#theia-main-content-panel div[id^="code-editor-opener"] {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item.arduino-tool-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
@@ -87,8 +97,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--theia-titleBar-activeBackground);
|
||||
|
||||
background-color: var(--theia-titleBar-activeBackground);
|
||||
}
|
||||
|
||||
#arduino-toolbar-container {
|
||||
@@ -243,3 +252,10 @@
|
||||
outline: 1px solid var(--theia-contrastBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.markdown-hover:first-child p {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.monaco-hover .hover-row.markdown-hover:first-child .monaco-tokenized-source {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
@@ -14,6 +14,10 @@
|
||||
font-family: monospace
|
||||
}
|
||||
|
||||
.serial-monitor-messages pre {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.serial-monitor .head {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
|
@@ -88,9 +88,13 @@
|
||||
}
|
||||
|
||||
.additional-urls-dialog textarea {
|
||||
width: 100%;
|
||||
resize: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent.additional-urls-dialog {
|
||||
display: block;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: 0 1px;
|
||||
margin: 0 -1px;
|
||||
}
|
||||
|
@@ -2,17 +2,17 @@
|
||||
position: relative
|
||||
}
|
||||
|
||||
.settings-step-input-element::-webkit-inner-spin-button,
|
||||
.settings-step-input-element::-webkit-inner-spin-button,
|
||||
.settings-step-input-element::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-step-input-buttons-container {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translate(0px, -50%);
|
||||
height: calc(100% - 4px);
|
||||
@@ -21,7 +21,11 @@
|
||||
background: var(--theia-input-background);
|
||||
}
|
||||
|
||||
.settings-step-input-container:hover > .settings-step-input-buttons-container {
|
||||
.settings-step-input-buttons-container-perc {
|
||||
right: 14px;
|
||||
}
|
||||
|
||||
.settings-step-input-container:hover>.settings-step-input-buttons-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -43,4 +47,4 @@
|
||||
|
||||
.settings-step-input-button:hover {
|
||||
background: rgba(128, 128, 128, 0.8);
|
||||
}
|
||||
}
|
@@ -1,8 +1,6 @@
|
||||
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
|
||||
import { duration } from '../../../common/decorators';
|
||||
|
||||
export class AboutDialog extends TheiaAboutDialog {
|
||||
@duration({ name: 'theia-about#init' })
|
||||
protected override async init(): Promise<void> {
|
||||
// NOOP
|
||||
// IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time.
|
||||
|
@@ -1,44 +0,0 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import {
|
||||
SearchInWorkspaceFileNode,
|
||||
SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget,
|
||||
} from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
|
||||
import { MEMORY_TEXT } from '@theia/core/lib/common/resource';
|
||||
|
||||
/**
|
||||
* Workaround for https://github.com/eclipse-theia/theia/pull/9192/.
|
||||
*/
|
||||
@injectable()
|
||||
export class SearchInWorkspaceResultTreeWidget extends TheiaSearchInWorkspaceResultTreeWidget {
|
||||
protected override async createReplacePreview(
|
||||
node: SearchInWorkspaceFileNode
|
||||
): Promise<URI> {
|
||||
const fileUri = new URI(node.fileUri).withScheme('file');
|
||||
const openedEditor = this.editorManager.all.find(
|
||||
({ editor }) => editor.uri.toString() === fileUri.toString()
|
||||
);
|
||||
let content: string;
|
||||
if (openedEditor) {
|
||||
content = openedEditor.editor.document.getText();
|
||||
} else {
|
||||
const resource = await this.fileResourceResolver.resolve(fileUri);
|
||||
content = await resource.readContents();
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
node.children.map((l) => {
|
||||
const leftPositionedNodes = node.children.filter(
|
||||
(rl) => rl.line === l.line && rl.character < l.character
|
||||
);
|
||||
const diff =
|
||||
(this._replaceTerm.length - this.searchTerm.length) *
|
||||
leftPositionedNodes.length;
|
||||
const start = lines[l.line - 1].substr(0, l.character - 1 + diff);
|
||||
const end = lines[l.line - 1].substr(l.character - 1 + diff + l.length);
|
||||
lines[l.line - 1] = start + this._replaceTerm + end;
|
||||
});
|
||||
|
||||
return fileUri.withScheme(MEMORY_TEXT).withQuery(lines.join('\n'));
|
||||
}
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
import { injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { Key, KeyCode } from '@theia/core/lib/browser';
|
||||
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
|
||||
|
||||
/**
|
||||
* Workaround for https://github.com/eclipse-theia/theia/pull/9183.
|
||||
*/
|
||||
@injectable()
|
||||
export class SearchInWorkspaceWidget extends TheiaSearchInWorkspaceWidget {
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.title.iconClass = 'fa fa-arduino-search';
|
||||
}
|
||||
|
||||
protected override renderGlobField(kind: 'include' | 'exclude'): React.ReactNode {
|
||||
const currentValue = this.searchInWorkspaceOptions[kind];
|
||||
const value = (currentValue && currentValue.join(', ')) || '';
|
||||
return (
|
||||
<div className="glob-field">
|
||||
<div className="label">{'files to ' + kind}</div>
|
||||
<input
|
||||
className="theia-input"
|
||||
type="text"
|
||||
size={1}
|
||||
defaultValue={value}
|
||||
id={kind + '-glob-field'}
|
||||
onKeyUp={(e) => {
|
||||
if (e.target) {
|
||||
const targetValue = (e.target as HTMLInputElement).value || '';
|
||||
let shouldSearch =
|
||||
Key.ENTER.keyCode ===
|
||||
KeyCode.createKeyCode(e.nativeEvent).key?.keyCode;
|
||||
const currentOptions = (this.searchInWorkspaceOptions[kind] || [])
|
||||
.slice()
|
||||
.map((s) => s.trim())
|
||||
.sort();
|
||||
const candidateOptions = this.splitOnComma(targetValue)
|
||||
.map((s) => s.trim())
|
||||
.sort();
|
||||
const sameAs = (left: string[], right: string[]) => {
|
||||
if (left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
if (left[i] !== right[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!sameAs(currentOptions, candidateOptions)) {
|
||||
this.searchInWorkspaceOptions[kind] =
|
||||
this.splitOnComma(targetValue);
|
||||
shouldSearch = true;
|
||||
}
|
||||
if (shouldSearch) {
|
||||
this.resultTreeWidget.search(
|
||||
this.searchTerm,
|
||||
this.searchInWorkspaceOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onFocus={
|
||||
kind === 'include'
|
||||
? this.handleFocusIncludesInputBox
|
||||
: this.handleFocusExcludesInputBox
|
||||
}
|
||||
onBlur={
|
||||
kind === 'include'
|
||||
? this.handleBlurIncludesInputBox
|
||||
: this.handleBlurExcludesInputBox
|
||||
}
|
||||
></input>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -17,7 +17,6 @@ import {
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
import { SaveAsSketch } from '../../contributions/save-as-sketch';
|
||||
import { SingleTextInputDialog } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
@@ -161,20 +160,26 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
|
||||
return;
|
||||
}
|
||||
const initialValue = uri.path.base;
|
||||
const dialog = new SingleTextInputDialog({
|
||||
title: nls.localize('theia/workspace/newFileName', 'New name for file'),
|
||||
initialValue,
|
||||
initialSelectionRange: {
|
||||
start: 0,
|
||||
end: uri.path.name.length,
|
||||
const parentUri = parent.resource;
|
||||
|
||||
const dialog = new WorkspaceInputDialog(
|
||||
{
|
||||
title: nls.localize('theia/workspace/newFileName', 'New name for file'),
|
||||
initialValue,
|
||||
parentUri,
|
||||
initialSelectionRange: {
|
||||
start: 0,
|
||||
end: uri.path.name.length,
|
||||
},
|
||||
validate: (name, mode) => {
|
||||
if (initialValue === name && mode === 'preview') {
|
||||
return false;
|
||||
}
|
||||
return this.validateFileName(name, parent, false);
|
||||
},
|
||||
},
|
||||
validate: (name, mode) => {
|
||||
if (initialValue === name && mode === 'preview') {
|
||||
return false;
|
||||
}
|
||||
return this.validateFileName(name, parent, false);
|
||||
},
|
||||
});
|
||||
this.labelProvider
|
||||
);
|
||||
const newName = await dialog.open();
|
||||
const newNameWithExt = this.maybeAppendInoExt(newName);
|
||||
if (newNameWithExt) {
|
||||
|
@@ -14,9 +14,11 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
|
||||
constructor(
|
||||
@inject(WorkspaceInputDialogProps)
|
||||
protected override readonly props: WorkspaceInputDialogProps,
|
||||
@inject(LabelProvider) protected override readonly labelProvider: LabelProvider
|
||||
@inject(LabelProvider)
|
||||
protected override readonly labelProvider: LabelProvider
|
||||
) {
|
||||
super(props, labelProvider);
|
||||
this.node.classList.add('workspace-input-dialog');
|
||||
this.appendCloseButton(
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel')
|
||||
);
|
||||
@@ -41,4 +43,14 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
|
||||
this.errorMessageNode.innerText = DialogError.getMessage(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected override appendCloseButton(text: string): HTMLButtonElement {
|
||||
this.closeButton = this.createButton(text);
|
||||
this.controlPanel.insertBefore(
|
||||
this.closeButton,
|
||||
this.controlPanel.lastChild
|
||||
);
|
||||
this.closeButton.classList.add('secondary');
|
||||
return this.closeButton;
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import {
|
||||
import {
|
||||
SketchesService,
|
||||
Sketch,
|
||||
SketchesError,
|
||||
} from '../../../common/protocol/sketches-service';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import {
|
||||
@@ -38,6 +39,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
private readonly providers: ContributionProvider<StartupTaskProvider>;
|
||||
|
||||
private version?: string;
|
||||
private _workspaceError: Error | undefined;
|
||||
|
||||
async onStart(application: FrontendApplication): Promise<void> {
|
||||
const info = await this.applicationServer.getApplicationInfo();
|
||||
@@ -51,6 +53,10 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
this.onCurrentWidgetChange({ newValue, oldValue: null });
|
||||
}
|
||||
|
||||
get workspaceError(): Error | undefined {
|
||||
return this._workspaceError;
|
||||
}
|
||||
|
||||
protected override async toFileStat(
|
||||
uri: string | URI | undefined
|
||||
): Promise<FileStat | undefined> {
|
||||
@@ -59,6 +65,31 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
const newSketchUri = await this.sketchService.createNewSketch();
|
||||
return this.toFileStat(newSketchUri.uri);
|
||||
}
|
||||
// When opening a file instead of a directory, IDE2 (and Theia) expects a workspace JSON file.
|
||||
// Nothing will work if the workspace file is invalid. Users tend to start (see #964) IDE2 from the `.ino` files,
|
||||
// so here, IDE2 tries to load the sketch via the CLI from the main sketch file URI.
|
||||
// If loading the sketch is OK, IDE2 starts and uses the sketch folder as the workspace root instead of the sketch file.
|
||||
// If loading fails due to invalid name error, IDE2 loads a temp sketch and preserves the startup error, and offers the sketch move to the user later.
|
||||
// If loading the sketch fails, create a fallback sketch and open the new temp sketch folder as the workspace root.
|
||||
if (stat.isFile && stat.resource.path.ext === '.ino') {
|
||||
try {
|
||||
const sketch = await this.sketchService.loadSketch(
|
||||
stat.resource.toString()
|
||||
);
|
||||
return this.toFileStat(sketch.uri);
|
||||
} catch (err) {
|
||||
if (SketchesError.InvalidName.is(err)) {
|
||||
this._workspaceError = err;
|
||||
const newSketchUri = await this.sketchService.createNewSketch();
|
||||
return this.toFileStat(newSketchUri.uri);
|
||||
} else if (SketchesError.NotFound.is(err)) {
|
||||
this._workspaceError = err;
|
||||
const newSketchUri = await this.sketchService.createNewSketch();
|
||||
return this.toFileStat(newSketchUri.uri);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return stat;
|
||||
}
|
||||
|
||||
|
@@ -61,10 +61,10 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="theia-button"
|
||||
className="theia-button uppercase"
|
||||
onClick={() => shell.openExternal('https://create.arduino.cc/editor')}
|
||||
>
|
||||
{nls.localize('cloud/GoToCloud', 'GO TO CLOUD')}
|
||||
{nls.localize('arduino/cloud/goToCloud', 'Go to Cloud')}
|
||||
</button>
|
||||
<div className="center item"></div>
|
||||
</div>
|
||||
|
@@ -14,34 +14,21 @@ export class ComponentListItem<
|
||||
)[0];
|
||||
this.state = {
|
||||
selectedVersion: version,
|
||||
focus: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
override componentDidUpdate(
|
||||
prevProps: ComponentListItem.Props<T>,
|
||||
prevState: ComponentListItem.State
|
||||
): void {
|
||||
if (this.state.focus !== prevState.focus) {
|
||||
this.props.onFocusDidChange();
|
||||
}
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { item, itemRenderer } = this.props;
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => this.setState({ focus: true })}
|
||||
onMouseLeave={() => this.setState({ focus: false })}
|
||||
>
|
||||
<>
|
||||
{itemRenderer.renderItem(
|
||||
Object.assign(this.state, { item }),
|
||||
this.install.bind(this),
|
||||
this.uninstall.bind(this),
|
||||
this.onVersionChange.bind(this)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,11 +64,9 @@ export namespace ComponentListItem {
|
||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly onFocusDidChange: () => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
selectedVersion?: Installable.Version;
|
||||
focus: boolean;
|
||||
}
|
||||
}
|
||||
|
@@ -125,7 +125,7 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
|
||||
rowIndex={index}
|
||||
parent={parent}
|
||||
>
|
||||
{({ measure, registerChild }) => (
|
||||
{({ registerChild }) => (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<div ref={registerChild} style={style}>
|
||||
@@ -135,7 +135,6 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
|
||||
itemRenderer={this.props.itemRenderer}
|
||||
install={this.props.install}
|
||||
uninstall={this.props.uninstall}
|
||||
onFocusDidChange={() => measure()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@@ -111,19 +111,7 @@ export class FilterableListContainer<
|
||||
const { searchable } = this.props;
|
||||
searchable
|
||||
.search(searchOptions)
|
||||
.then((items) => this.setState({ items: this.sort(items) }));
|
||||
}
|
||||
|
||||
protected sort(items: T[]): T[] {
|
||||
const { itemLabel, itemDeprecated } = this.props;
|
||||
return items.sort((left, right) => {
|
||||
// always put deprecated items at the bottom of the list
|
||||
if (itemDeprecated(left)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return itemLabel(left).localeCompare(itemLabel(right));
|
||||
});
|
||||
.then((items) => this.setState({ items: this.props.sort(items) }));
|
||||
}
|
||||
|
||||
protected async install(
|
||||
@@ -139,7 +127,7 @@ export class FilterableListContainer<
|
||||
run: ({ progressId }) => install({ item, progressId, version }),
|
||||
});
|
||||
const items = await searchable.search(this.state.searchOptions);
|
||||
this.setState({ items: this.sort(items) });
|
||||
this.setState({ items: this.props.sort(items) });
|
||||
}
|
||||
|
||||
protected async uninstall(item: T): Promise<void> {
|
||||
@@ -167,7 +155,7 @@ export class FilterableListContainer<
|
||||
run: ({ progressId }) => uninstall({ item, progressId }),
|
||||
});
|
||||
const items = await searchable.search(this.state.searchOptions);
|
||||
this.setState({ items: this.sort(items) });
|
||||
this.setState({ items: this.props.sort(items) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +192,7 @@ export namespace FilterableListContainer {
|
||||
progressId: string;
|
||||
}) => Promise<void>;
|
||||
readonly commandService: CommandService;
|
||||
readonly sort: (items: T[]) => T[];
|
||||
}
|
||||
|
||||
export interface State<T, S extends Searchable.Options> {
|
||||
|
@@ -28,7 +28,7 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
uninstall: (item: T) => Promise<void>,
|
||||
onVersionChange: (version: Installable.Version) => void
|
||||
): React.ReactNode {
|
||||
const { item, focus } = input;
|
||||
const { item } = input;
|
||||
let nameAndAuthor: JSX.Element;
|
||||
if (item.name && item.author) {
|
||||
const name = <span className="name">{item.name}</span>;
|
||||
@@ -55,7 +55,14 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
item.installedVersion
|
||||
)}
|
||||
</span>
|
||||
<span className="installed" onClick={onClickUninstall} />
|
||||
<span
|
||||
className="installed uppercase"
|
||||
onClick={onClickUninstall}
|
||||
{...{
|
||||
install: nls.localize('arduino/component/installed', 'Installed'),
|
||||
uninstall: nls.localize('arduino/component/uninstall', 'Uninstall'),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -70,10 +77,10 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
const onClickInstall = () => install(item);
|
||||
const installButton = item.installable && (
|
||||
<button
|
||||
className="theia-button secondary install"
|
||||
className="theia-button secondary install uppercase"
|
||||
onClick={onClickInstall}
|
||||
>
|
||||
{nls.localize('arduino/component/install', 'INSTALL')}
|
||||
{nls.localize('arduino/component/install', 'Install')}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -120,12 +127,10 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
{description}
|
||||
</div>
|
||||
<div className="info">{moreInfo}</div>
|
||||
{focus && (
|
||||
<div className="footer">
|
||||
{versions}
|
||||
{installButton}
|
||||
</div>
|
||||
)}
|
||||
<div className="footer">
|
||||
{versions}
|
||||
{installButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,9 +1,15 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import {
|
||||
OpenerOptions,
|
||||
OpenHandler,
|
||||
} from '@theia/core/lib/browser/opener-service';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { URI } from '@theia/core/lib/common/uri';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { Searchable } from '../../../common/protocol';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ListWidget } from './list-widget';
|
||||
import { Searchable } from '../../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidgetFrontendContribution<
|
||||
@@ -11,14 +17,49 @@ export abstract class ListWidgetFrontendContribution<
|
||||
S extends Searchable.Options
|
||||
>
|
||||
extends AbstractViewContribution<ListWidget<T, S>>
|
||||
implements FrontendApplicationContribution
|
||||
implements FrontendApplicationContribution, OpenHandler
|
||||
{
|
||||
readonly id: string = `http-opener-${this.viewId}`;
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
// TS requires at least one method from `FrontendApplicationContribution`.
|
||||
// Expected to be empty.
|
||||
this.openView();
|
||||
}
|
||||
|
||||
override registerMenus(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
override registerMenus(_: MenuModelRegistry): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
canHandle(uri: URI, _?: OpenerOptions): number {
|
||||
// `500` is the default HTTP opener in Theia. IDE2 has higher priority.
|
||||
// https://github.com/eclipse-theia/theia/blob/b75b6144b0ffea06a549294903c374fa642135e4/packages/core/src/browser/http-open-handler.ts#L39
|
||||
return this.canParse(uri) ? 501 : 0;
|
||||
}
|
||||
|
||||
async open(
|
||||
uri: URI,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_?: OpenerOptions | undefined
|
||||
): Promise<void> {
|
||||
const searchOptions = this.parse(uri);
|
||||
if (!searchOptions) {
|
||||
console.warn(
|
||||
`Failed to parse URI into a search options. URI: ${uri.toString()}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const widget = await this.openView({
|
||||
activate: true,
|
||||
reveal: true,
|
||||
});
|
||||
if (!widget) {
|
||||
console.warn(`Failed to open view for URI: ${uri.toString()}`);
|
||||
return;
|
||||
}
|
||||
widget.refresh(searchOptions);
|
||||
}
|
||||
|
||||
protected abstract canParse(uri: URI): boolean;
|
||||
protected abstract parse(uri: URI): S | undefined;
|
||||
}
|
||||
|
@@ -51,9 +51,11 @@ export abstract class ListWidget<
|
||||
*/
|
||||
protected firstActivate = true;
|
||||
|
||||
protected readonly defaultSortComparator: (left: T, right: T) => number;
|
||||
|
||||
constructor(protected options: ListWidget.Options<T, S>) {
|
||||
super();
|
||||
const { id, label, iconClass } = options;
|
||||
const { id, label, iconClass, itemDeprecated, itemLabel } = options;
|
||||
this.id = id;
|
||||
this.title.label = label;
|
||||
this.title.caption = label;
|
||||
@@ -63,12 +65,23 @@ export abstract class ListWidget<
|
||||
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
||||
this.scrollOptions = undefined;
|
||||
this.toDispose.push(this.searchOptionsChangeEmitter);
|
||||
|
||||
this.defaultSortComparator = (left, right): number => {
|
||||
// always put deprecated items at the bottom of the list
|
||||
if (itemDeprecated(left)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return itemLabel(left).localeCompare(itemLabel(right));
|
||||
};
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onIndexDidUpdate(() => this.refresh(undefined)),
|
||||
this.notificationCenter.onIndexUpdateDidComplete(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
this.notificationCenter.onDaemonDidStart(() => this.refresh(undefined)),
|
||||
this.notificationCenter.onDaemonDidStop(() => this.refresh(undefined)),
|
||||
]);
|
||||
@@ -128,6 +141,30 @@ export abstract class ListWidget<
|
||||
return this.options.installable.uninstall({ item, progressId });
|
||||
}
|
||||
|
||||
protected filterableListSort = (items: T[]): T[] => {
|
||||
const isArduinoTypeComparator = (left: T, right: T) => {
|
||||
const aIsArduinoType = left.types.includes('Arduino');
|
||||
const bIsArduinoType = right.types.includes('Arduino');
|
||||
|
||||
if (aIsArduinoType && !bIsArduinoType && !left.deprecated) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!aIsArduinoType && bIsArduinoType && !right.deprecated) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
return items.sort((left, right) => {
|
||||
return (
|
||||
isArduinoTypeComparator(left, right) ||
|
||||
this.defaultSortComparator(left, right)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<FilterableListContainer<T, S>
|
||||
@@ -145,6 +182,7 @@ export abstract class ListWidget<
|
||||
messageService={this.messageService}
|
||||
commandService={this.commandService}
|
||||
responseService={this.responseService}
|
||||
sort={this.filterableListSort}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -3,7 +3,14 @@ import { Searchable } from './searchable';
|
||||
import { Installable } from './installable';
|
||||
import { ArduinoComponent } from './arduino-component';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { All, Contributed, Partner, Type, Updatable } from '../nls';
|
||||
import {
|
||||
All,
|
||||
Contributed,
|
||||
Partner,
|
||||
Type as TypeLabel,
|
||||
Updatable,
|
||||
} from '../nls';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export type AvailablePorts = Record<string, [Port, Array<Board>]>;
|
||||
export namespace AvailablePorts {
|
||||
@@ -141,6 +148,7 @@ export interface BoardsService
|
||||
fqbn: string;
|
||||
}): Promise<BoardsPackage | undefined>;
|
||||
searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]>;
|
||||
getInstalledBoards(): Promise<BoardWithPackage[]>;
|
||||
getBoardUserFields(options: {
|
||||
fqbn: string;
|
||||
protocol: string;
|
||||
@@ -151,6 +159,7 @@ export interface BoardSearch extends Searchable.Options {
|
||||
readonly type?: BoardSearch.Type;
|
||||
}
|
||||
export namespace BoardSearch {
|
||||
export const Default: BoardSearch = { type: 'All' };
|
||||
export const TypeLiterals = [
|
||||
'All',
|
||||
'Updatable',
|
||||
@@ -161,6 +170,11 @@ export namespace BoardSearch {
|
||||
'Arduino@Heart',
|
||||
] as const;
|
||||
export type Type = typeof TypeLiterals[number];
|
||||
export namespace Type {
|
||||
export function is(arg: unknown): arg is Type {
|
||||
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
|
||||
}
|
||||
}
|
||||
export const TypeLabels: Record<Type, string> = {
|
||||
All: All,
|
||||
Updatable: Updatable,
|
||||
@@ -177,8 +191,41 @@ export namespace BoardSearch {
|
||||
keyof Omit<BoardSearch, 'query'>,
|
||||
string
|
||||
> = {
|
||||
type: Type,
|
||||
type: TypeLabel,
|
||||
};
|
||||
export namespace UriParser {
|
||||
export const authority = 'boardsmanager';
|
||||
export function parse(uri: URI): BoardSearch | undefined {
|
||||
if (uri.scheme !== 'http') {
|
||||
throw new Error(
|
||||
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
if (uri.authority !== authority) {
|
||||
throw new Error(
|
||||
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
|
||||
if (segments.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
let searchOptions: BoardSearch | undefined = undefined;
|
||||
const [type] = segments;
|
||||
if (!type) {
|
||||
searchOptions = BoardSearch.Default;
|
||||
} else if (BoardSearch.Type.is(type)) {
|
||||
searchOptions = { type };
|
||||
}
|
||||
if (searchOptions) {
|
||||
return {
|
||||
...searchOptions,
|
||||
...Searchable.UriParser.parseQuery(uri),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Port {
|
||||
@@ -285,6 +332,29 @@ export namespace Port {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace Protocols {
|
||||
export const KnownProtocolLiterals = ['serial', 'network'] as const;
|
||||
export type KnownProtocol = typeof KnownProtocolLiterals[number];
|
||||
export namespace KnownProtocol {
|
||||
export function is(protocol: unknown): protocol is KnownProtocol {
|
||||
return (
|
||||
typeof protocol === 'string' &&
|
||||
KnownProtocolLiterals.indexOf(protocol as KnownProtocol) >= 0
|
||||
);
|
||||
}
|
||||
}
|
||||
export const ProtocolLabels: Record<KnownProtocol, string> = {
|
||||
serial: nls.localize('arduino/portProtocol/serial', 'Serial'),
|
||||
network: nls.localize('arduino/portProtocol/network', 'Network'),
|
||||
};
|
||||
export function protocolLabel(protocol: string): string {
|
||||
if (KnownProtocol.is(protocol)) {
|
||||
return ProtocolLabels[protocol];
|
||||
}
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface BoardsPackage extends ArduinoComponent {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
import type {
|
||||
Location,
|
||||
@@ -10,6 +11,7 @@ import type {
|
||||
} from '../../common/protocol/boards-service';
|
||||
import type { Programmer } from './boards-service';
|
||||
import type { Sketch } from './sketches-service';
|
||||
import { IndexUpdateSummary } from './notification-service';
|
||||
|
||||
export const CompilerWarningLiterals = [
|
||||
'None',
|
||||
@@ -18,6 +20,17 @@ export const CompilerWarningLiterals = [
|
||||
'All',
|
||||
] as const;
|
||||
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
|
||||
export namespace CompilerWarnings {
|
||||
export function labelOf(warning: CompilerWarnings): string {
|
||||
return CompilerWarningLabels[warning];
|
||||
}
|
||||
const CompilerWarningLabels: Record<CompilerWarnings, string> = {
|
||||
None: nls.localize('arduino/core/compilerWarnings/none', 'None'),
|
||||
Default: nls.localize('arduino/core/compilerWarnings/default', 'Default'),
|
||||
More: nls.localize('arduino/core/compilerWarnings/more', 'More'),
|
||||
All: nls.localize('arduino/core/compilerWarnings/all', 'All'),
|
||||
};
|
||||
}
|
||||
export namespace CoreError {
|
||||
export interface ErrorLocationRef {
|
||||
readonly message: string;
|
||||
@@ -96,6 +109,37 @@ export interface CoreService {
|
||||
compile(options: CoreService.Options.Compile): Promise<void>;
|
||||
upload(options: CoreService.Options.Upload): Promise<void>;
|
||||
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
|
||||
/**
|
||||
* Refreshes the underling core gRPC client for the Arduino CLI.
|
||||
*/
|
||||
refresh(): Promise<void>;
|
||||
/**
|
||||
* Updates the index of the given index types and refreshes (`init`) the underlying core gRPC client.
|
||||
* If `types` is empty, only the refresh part will be executed.
|
||||
*/
|
||||
updateIndex({ types }: { types: IndexType[] }): Promise<void>;
|
||||
/**
|
||||
* If the IDE2 detects invalid or missing indexes on core client init,
|
||||
* IDE2 tries to update the indexes before the first frontend connects.
|
||||
* Use this method to determine whether the backend has already updated
|
||||
* the indexes before updating them.
|
||||
*
|
||||
* If yes, the connected frontend can update the local storage with the most
|
||||
* recent index update date-time for a particular index type,
|
||||
* and IDE2 can avoid the double indexes update.
|
||||
*/
|
||||
indexUpdateSummaryBeforeInit(): Promise<Readonly<IndexUpdateSummary>>;
|
||||
}
|
||||
|
||||
export const IndexTypeLiterals = ['platform', 'library'] as const;
|
||||
export type IndexType = typeof IndexTypeLiterals[number];
|
||||
export namespace IndexType {
|
||||
export function is(arg: unknown): arg is IndexType {
|
||||
return (
|
||||
typeof arg === 'string' && IndexTypeLiterals.includes(arg as IndexType)
|
||||
);
|
||||
}
|
||||
export const All: IndexType[] = IndexTypeLiterals.filter(is);
|
||||
}
|
||||
|
||||
export namespace CoreService {
|
||||
|
@@ -8,9 +8,10 @@ import {
|
||||
Partner,
|
||||
Recommended,
|
||||
Retired,
|
||||
Type,
|
||||
Type as TypeLabel,
|
||||
Updatable,
|
||||
} from '../nls';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export const LibraryServicePath = '/services/library-service';
|
||||
export const LibraryService = Symbol('LibraryService');
|
||||
@@ -55,6 +56,7 @@ export interface LibrarySearch extends Searchable.Options {
|
||||
readonly topic?: LibrarySearch.Topic;
|
||||
}
|
||||
export namespace LibrarySearch {
|
||||
export const Default: LibrarySearch = { type: 'All', topic: 'All' };
|
||||
export const TypeLiterals = [
|
||||
'All',
|
||||
'Updatable',
|
||||
@@ -66,6 +68,11 @@ export namespace LibrarySearch {
|
||||
'Retired',
|
||||
] as const;
|
||||
export type Type = typeof TypeLiterals[number];
|
||||
export namespace Type {
|
||||
export function is(arg: unknown): arg is Type {
|
||||
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
|
||||
}
|
||||
}
|
||||
export const TypeLabels: Record<Type, string> = {
|
||||
All: All,
|
||||
Updatable: Updatable,
|
||||
@@ -90,6 +97,11 @@ export namespace LibrarySearch {
|
||||
'Uncategorized',
|
||||
] as const;
|
||||
export type Topic = typeof TopicLiterals[number];
|
||||
export namespace Topic {
|
||||
export function is(arg: unknown): arg is Topic {
|
||||
return typeof arg === 'string' && TopicLiterals.includes(arg as Topic);
|
||||
}
|
||||
}
|
||||
export const TopicLabels: Record<Topic, string> = {
|
||||
All: All,
|
||||
Communication: nls.localize(
|
||||
@@ -126,8 +138,60 @@ export namespace LibrarySearch {
|
||||
string
|
||||
> = {
|
||||
topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
|
||||
type: Type,
|
||||
type: TypeLabel,
|
||||
};
|
||||
export namespace UriParser {
|
||||
export const authority = 'librarymanager';
|
||||
export function parse(uri: URI): LibrarySearch | undefined {
|
||||
if (uri.scheme !== 'http') {
|
||||
throw new Error(
|
||||
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
if (uri.authority !== authority) {
|
||||
throw new Error(
|
||||
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
|
||||
// Special magic handling for `Signal Input/Output`.
|
||||
// TODO: IDE2 deserves a better lib/boards URL spec.
|
||||
// https://github.com/arduino/arduino-ide/issues/1442#issuecomment-1252136377
|
||||
if (segments.length === 3) {
|
||||
const [type, topicHead, topicTail] = segments;
|
||||
const maybeTopic = `${topicHead}/${topicTail}`;
|
||||
if (
|
||||
LibrarySearch.Topic.is(maybeTopic) &&
|
||||
maybeTopic === 'Signal Input/Output' &&
|
||||
LibrarySearch.Type.is(type)
|
||||
) {
|
||||
return {
|
||||
type,
|
||||
topic: maybeTopic,
|
||||
...Searchable.UriParser.parseQuery(uri),
|
||||
};
|
||||
}
|
||||
}
|
||||
let searchOptions: LibrarySearch | undefined = undefined;
|
||||
const [type, topic] = segments;
|
||||
if (!type && !topic) {
|
||||
searchOptions = LibrarySearch.Default;
|
||||
} else if (LibrarySearch.Type.is(type)) {
|
||||
if (!topic) {
|
||||
searchOptions = { ...LibrarySearch.Default, type };
|
||||
} else if (LibrarySearch.Topic.is(topic)) {
|
||||
searchOptions = { type, topic };
|
||||
}
|
||||
}
|
||||
if (searchOptions) {
|
||||
return {
|
||||
...searchOptions,
|
||||
...Searchable.UriParser.parseQuery(uri),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace LibraryService {
|
||||
@@ -162,12 +226,6 @@ export enum LibraryLocation {
|
||||
}
|
||||
|
||||
export interface LibraryPackage extends ArduinoComponent {
|
||||
/**
|
||||
* Same as [`Library#real_name`](https://arduino.github.io/arduino-cli/latest/rpc/commands/#library).
|
||||
* Should be used for the UI, and `name` is used to uniquely identify a library. It does not have an ID.
|
||||
*/
|
||||
readonly label: string;
|
||||
|
||||
/**
|
||||
* An array of string that should be included into the `ino` file if this library is used.
|
||||
* For example, including `SD` will prepend `#include <SD.h>` to the `ino` file. While including `Bridge`
|
||||
|
@@ -5,27 +5,62 @@ import type {
|
||||
Config,
|
||||
ProgressMessage,
|
||||
Sketch,
|
||||
IndexType,
|
||||
} from '../protocol';
|
||||
import type { LibraryPackage } from './library-service';
|
||||
|
||||
/**
|
||||
* Values are [ISO 8601](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
|
||||
* strings representing the date-time when the update of the index has been completed.
|
||||
*/
|
||||
export type IndexUpdateSummary = {
|
||||
[T in IndexType]: string;
|
||||
} & { message?: string };
|
||||
export interface IndexUpdateParams {
|
||||
/**
|
||||
* Application unique ID of the progress.
|
||||
*/
|
||||
readonly progressId: string;
|
||||
/**
|
||||
* The type of the index is which is being updated.
|
||||
*/
|
||||
readonly types: IndexType[];
|
||||
}
|
||||
export type IndexUpdateWillStartParams = IndexUpdateParams;
|
||||
export interface IndexUpdateDidCompleteParams
|
||||
extends Omit<IndexUpdateParams, 'types'> {
|
||||
readonly summary: IndexUpdateSummary;
|
||||
}
|
||||
export interface IndexUpdateDidFailParams extends IndexUpdateParams {
|
||||
/**
|
||||
* Describes the reason of the index update failure.
|
||||
*/
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export interface NotificationServiceClient {
|
||||
notifyIndexWillUpdate(progressId: string): void;
|
||||
// Index
|
||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void;
|
||||
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void;
|
||||
notifyIndexDidUpdate(progressId: string): void;
|
||||
notifyIndexUpdateDidFail({
|
||||
progressId,
|
||||
message,
|
||||
}: {
|
||||
progressId: string;
|
||||
message: string;
|
||||
}): void;
|
||||
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void;
|
||||
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void;
|
||||
|
||||
// Daemon
|
||||
notifyDaemonDidStart(port: string): void;
|
||||
notifyDaemonDidStop(): void;
|
||||
|
||||
// CLI config
|
||||
notifyConfigDidChange(event: { config: Config | undefined }): void;
|
||||
|
||||
// Platforms
|
||||
notifyPlatformDidInstall(event: { item: BoardsPackage }): void;
|
||||
notifyPlatformDidUninstall(event: { item: BoardsPackage }): void;
|
||||
|
||||
// Libraries
|
||||
notifyLibraryDidInstall(event: { item: LibraryPackage }): void;
|
||||
notifyLibraryDidUninstall(event: { item: LibraryPackage }): void;
|
||||
|
||||
// Boards discovery
|
||||
notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void;
|
||||
notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void;
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export interface Searchable<T, O extends Searchable.Options> {
|
||||
search(options: O): Promise<T[]>;
|
||||
}
|
||||
@@ -8,4 +10,24 @@ export namespace Searchable {
|
||||
*/
|
||||
readonly query?: string;
|
||||
}
|
||||
export namespace UriParser {
|
||||
/**
|
||||
* Parses the `URI#fragment` into a query term.
|
||||
*/
|
||||
export function parseQuery(uri: URI): { query: string } {
|
||||
return { query: uri.fragment };
|
||||
}
|
||||
/**
|
||||
* Splits the `URI#path#toString` on the `/` POSIX separator into decoded segments. The first, empty segment representing the root is omitted.
|
||||
* Examples:
|
||||
* - `/` -> `['']`
|
||||
* - `/All` -> `['All']`
|
||||
* - `/All/Device%20Control` -> `['All', 'Device Control']`
|
||||
* - `/All/Display` -> `['All', 'Display']`
|
||||
* - `/Updatable/Signal%20Input%2FOutput` -> `['Updatable', 'Signal Input', 'Output']` (**caveat**!)
|
||||
*/
|
||||
export function normalizedSegmentsOf(uri: URI): string[] {
|
||||
return uri.path.toString().split('/').slice(1).map(decodeURIComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import URI from '@theia/core/lib/common/uri';
|
||||
export namespace SketchesError {
|
||||
export const Codes = {
|
||||
NotFound: 5001,
|
||||
InvalidName: 5002,
|
||||
};
|
||||
export const NotFound = ApplicationError.declare(
|
||||
Codes.NotFound,
|
||||
@@ -14,6 +15,15 @@ export namespace SketchesError {
|
||||
};
|
||||
}
|
||||
);
|
||||
export const InvalidName = ApplicationError.declare(
|
||||
Codes.InvalidName,
|
||||
(message: string, invalidMainSketchUri: string) => {
|
||||
return {
|
||||
message,
|
||||
data: { invalidMainSketchUri },
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const SketchesServicePath = '/services/sketches-service';
|
||||
@@ -21,16 +31,9 @@ export const SketchesService = Symbol('SketchesService');
|
||||
export interface SketchesService {
|
||||
/**
|
||||
* Resolves to a sketch container representing the hierarchical structure of the sketches.
|
||||
* If `uri` is not given, `directories.user` will be user instead. Specify `exclude` global patterns to filter folders from the sketch container.
|
||||
* If `exclude` is not set `['**\/libraries\/**', '**\/hardware\/**']` will be used instead.
|
||||
* If `uri` is not given, `directories.user` will be user instead.
|
||||
*/
|
||||
getSketches({
|
||||
uri,
|
||||
exclude,
|
||||
}: {
|
||||
uri?: string;
|
||||
exclude?: string[];
|
||||
}): Promise<SketchContainer>;
|
||||
getSketches({ uri }: { uri?: string }): Promise<SketchContainer>;
|
||||
|
||||
/**
|
||||
* This is the TS implementation of `SketchLoad` from the CLI and should be replaced with a gRPC call eventually.
|
||||
@@ -71,7 +74,7 @@ export interface SketchesService {
|
||||
copy(sketch: Sketch, options: { destinationUri: string }): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, resolved `undefined`.
|
||||
* Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, the promise resolves to `undefined`.
|
||||
*/
|
||||
getSketchFolder(uri: string): Promise<Sketch | undefined>;
|
||||
|
||||
@@ -82,8 +85,10 @@ export interface SketchesService {
|
||||
|
||||
/**
|
||||
* Resolves to an array of sketches in inverse chronological order. The newest is the first.
|
||||
* If `forceUpdate` is `true`, the array of recently opened sketches will be recalculated.
|
||||
* Invalid and missing sketches will be removed from the list. It's `false` by default.
|
||||
*/
|
||||
recentlyOpenedSketches(): Promise<Sketch[]>;
|
||||
recentlyOpenedSketches(forceUpdate?: boolean): Promise<Sketch[]>;
|
||||
|
||||
/**
|
||||
* Archives the sketch, resolves to the archive URI.
|
||||
@@ -114,6 +119,19 @@ export namespace SketchRef {
|
||||
uri: typeof uriLike === 'string' ? uriLike : uriLike.toString(),
|
||||
};
|
||||
}
|
||||
export function is(arg: unknown): arg is SketchRef {
|
||||
if (typeof arg === 'object') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const object = arg as any;
|
||||
return (
|
||||
'name' in object &&
|
||||
typeof object['name'] === 'string' &&
|
||||
'uri' in object &&
|
||||
typeof object['name'] === 'string'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export interface Sketch extends SketchRef {
|
||||
readonly mainFileUri: string; // `MainFile`
|
||||
@@ -122,14 +140,25 @@ export interface Sketch extends SketchRef {
|
||||
readonly rootFolderFileUris: string[]; // `RootFolderFiles` (does not include the main sketch file)
|
||||
}
|
||||
export namespace Sketch {
|
||||
export function is(arg: any): arg is Sketch {
|
||||
return (
|
||||
!!arg &&
|
||||
'name' in arg &&
|
||||
'uri' in arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
typeof arg.uri === 'string'
|
||||
);
|
||||
export function is(arg: unknown): arg is Sketch {
|
||||
if (!SketchRef.is(arg)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof arg === 'object') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const object = arg as any;
|
||||
return (
|
||||
'mainFileUri' in object &&
|
||||
typeof object['mainFileUri'] === 'string' &&
|
||||
'otherSketchFileUris' in object &&
|
||||
Array.isArray(object['otherSketchFileUris']) &&
|
||||
'additionalFileUris' in object &&
|
||||
Array.isArray(object['additionalFileUris']) &&
|
||||
'rootFolderFileUris' in object &&
|
||||
Array.isArray(object['rootFolderFileUris'])
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export namespace Extensions {
|
||||
export const MAIN = ['.ino', '.pde'];
|
||||
|
@@ -18,6 +18,7 @@ import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl';
|
||||
import { ElectronMainApplication } from './theia/electron-main-application';
|
||||
import { ElectronMainWindowServiceImpl } from './theia/electron-main-window-service';
|
||||
import { TheiaElectronWindow } from './theia/theia-electron-window';
|
||||
import { ElectronNativeKeymap } from '@theia/core/lib/electron-main/electron-native-keymap';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(ElectronMainApplication).toSelf().inSingletonScope();
|
||||
@@ -58,4 +59,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.inSingletonScope();
|
||||
|
||||
bind(IsTempSketch).toSelf().inSingletonScope();
|
||||
|
||||
// https://github.com/eclipse-theia/theia/issues/11688
|
||||
bind(ElectronNativeKeymap).toSelf().inSingletonScope();
|
||||
bind(ElectronMainApplicationContribution).toService(ElectronNativeKeymap);
|
||||
});
|
||||
|
@@ -8,8 +8,8 @@ import {
|
||||
} from '@theia/core/electron-shared/electron';
|
||||
import { fork } from 'child_process';
|
||||
import { AddressInfo } from 'net';
|
||||
import { join, dirname } from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import { join, isAbsolute, resolve } from 'path';
|
||||
import { promises as fs, Stats } from 'fs';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
|
||||
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
CLOSE_PLOTTER_WINDOW,
|
||||
SHOW_PLOTTER_WINDOW,
|
||||
} from '../../common/ipc-communication';
|
||||
import isValidPath = require('is-valid-path');
|
||||
|
||||
app.commandLine.appendSwitch('disable-http-cache');
|
||||
|
||||
@@ -69,8 +70,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
|
||||
// See: https://github.com/electron-userland/electron-builder/issues/2468
|
||||
// Regression in Theia: https://github.com/eclipse-theia/theia/issues/8701
|
||||
console.log(`${config.applicationName} ${app.getVersion()}`);
|
||||
app.on('ready', () => app.setName(config.applicationName));
|
||||
this.attachFileAssociations();
|
||||
const cwd = process.cwd();
|
||||
this.attachFileAssociations(cwd);
|
||||
this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
|
||||
this._config = config;
|
||||
this.hookApplicationEvents();
|
||||
@@ -84,7 +87,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
return this.launch({
|
||||
secondInstance: false,
|
||||
argv: this.processArgv.getProcessArgvWithoutBin(process.argv),
|
||||
cwd: process.cwd(),
|
||||
cwd,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,7 +122,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
let traceFile: string | undefined;
|
||||
if (appPath) {
|
||||
const tracesPath = join(appPath, 'traces');
|
||||
await fs.promises.mkdir(tracesPath, { recursive: true });
|
||||
await fs.mkdir(tracesPath, { recursive: true });
|
||||
traceFile = join(tracesPath, `trace-${new Date().toISOString()}.trace`);
|
||||
}
|
||||
console.log('>>> Content tracing has started...');
|
||||
@@ -135,14 +138,18 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
})();
|
||||
}
|
||||
|
||||
private attachFileAssociations(): void {
|
||||
private attachFileAssociations(cwd: string): void {
|
||||
// OSX: register open-file event
|
||||
if (os.isOSX) {
|
||||
app.on('open-file', async (event, uri) => {
|
||||
app.on('open-file', async (event, path) => {
|
||||
event.preventDefault();
|
||||
if (uri.endsWith('.ino') && (await fs.pathExists(uri))) {
|
||||
this.openFilePromise.reject();
|
||||
await this.openSketch(dirname(uri));
|
||||
const resolvedPath = await this.resolvePath(path, cwd);
|
||||
if (resolvedPath) {
|
||||
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
|
||||
if (sketchFolderPath) {
|
||||
this.openFilePromise.reject(new InterruptWorkspaceRestoreError());
|
||||
await this.openSketch(sketchFolderPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
setTimeout(() => this.openFilePromise.resolve(), 500);
|
||||
@@ -151,8 +158,68 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
}
|
||||
}
|
||||
|
||||
private async isValidSketchPath(uri: string): Promise<boolean | undefined> {
|
||||
return typeof uri === 'string' && (await fs.pathExists(uri));
|
||||
/**
|
||||
* The `path` argument is valid, if accessible and either pointing to a `.ino` file,
|
||||
* or it's a directory, and one of the files in the directory is an `.ino` file.
|
||||
*
|
||||
* If `undefined`, `path` was pointing to neither an accessible sketch file nor a sketch folder.
|
||||
*
|
||||
* The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
|
||||
* The `path` must be an absolute, resolved path.
|
||||
*/
|
||||
private async isValidSketchPath(path: string): Promise<string | undefined> {
|
||||
let stats: Stats | undefined = undefined;
|
||||
try {
|
||||
stats = await fs.stat(path);
|
||||
} catch (err) {
|
||||
if ('code' in err && err.code === 'ENOENT') {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (!stats) {
|
||||
return undefined;
|
||||
}
|
||||
if (stats.isFile() && path.endsWith('.ino')) {
|
||||
return path;
|
||||
}
|
||||
try {
|
||||
const entries = await fs.readdir(path, { withFileTypes: true });
|
||||
const sketchFilename = entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
|
||||
.map(({ name }) => name)
|
||||
.sort((left, right) => left.localeCompare(right))[0];
|
||||
if (sketchFilename) {
|
||||
return join(path, sketchFilename);
|
||||
}
|
||||
// If no sketches found in the folder, but the folder exists,
|
||||
// return with the path of the empty folder and let IDE2's frontend
|
||||
// figure out the workspace root.
|
||||
return path;
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async resolvePath(
|
||||
maybePath: string,
|
||||
cwd: string
|
||||
): Promise<string | undefined> {
|
||||
if (!isValidPath(maybePath)) {
|
||||
return undefined;
|
||||
}
|
||||
if (isAbsolute(maybePath)) {
|
||||
return maybePath;
|
||||
}
|
||||
try {
|
||||
const resolved = await fs.realpath(resolve(cwd, maybePath));
|
||||
return resolved;
|
||||
} catch (err) {
|
||||
if ('code' in err && err.code === 'ENOENT') {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async launch(
|
||||
@@ -163,12 +230,15 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
// 1. The `open-file` command has been received by the app, rejecting the promise
|
||||
// 2. A short timeout resolves the promise automatically, falling back to the usual app launch
|
||||
await this.openFilePromise.promise;
|
||||
} catch {
|
||||
// Application has received the `open-file` event and will skip the default application launch
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err instanceof InterruptWorkspaceRestoreError) {
|
||||
// Application has received the `open-file` event and will skip the default application launch
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!os.isOSX && (await this.launchFromArgs(params))) {
|
||||
if (await this.launchFromArgs(params)) {
|
||||
// Application has received a file in its arguments and will skip the default application launch
|
||||
return;
|
||||
}
|
||||
@@ -182,7 +252,13 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
`Restoring workspace roots: ${workspaces.map(({ file }) => file)}`
|
||||
);
|
||||
for (const workspace of workspaces) {
|
||||
if (await this.isValidSketchPath(workspace.file)) {
|
||||
const resolvedPath = await this.resolvePath(workspace.file, params.cwd);
|
||||
if (!resolvedPath) {
|
||||
continue;
|
||||
}
|
||||
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
|
||||
if (sketchFolderPath) {
|
||||
workspace.file = sketchFolderPath;
|
||||
if (this.isTempSketch.is(workspace.file)) {
|
||||
console.info(
|
||||
`Skipped opening sketch. The sketch was detected as temporary. Workspace path: ${workspace.file}.`
|
||||
@@ -205,38 +281,40 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
): Promise<boolean> {
|
||||
// Copy to prevent manipulation of original array
|
||||
const argCopy = [...params.argv];
|
||||
let uri: string | undefined;
|
||||
for (const possibleUri of argCopy) {
|
||||
if (
|
||||
possibleUri.endsWith('.ino') &&
|
||||
(await this.isValidSketchPath(possibleUri))
|
||||
) {
|
||||
uri = possibleUri;
|
||||
let path: string | undefined;
|
||||
for (const maybePath of argCopy) {
|
||||
const resolvedPath = await this.resolvePath(maybePath, params.cwd);
|
||||
if (!resolvedPath) {
|
||||
continue;
|
||||
}
|
||||
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
|
||||
if (sketchFolderPath) {
|
||||
path = sketchFolderPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (uri) {
|
||||
await this.openSketch(dirname(uri));
|
||||
if (path) {
|
||||
await this.openSketch(path);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async openSketch(
|
||||
workspace: WorkspaceOptions | string
|
||||
workspaceOrPath: WorkspaceOptions | string
|
||||
): Promise<BrowserWindow> {
|
||||
const options = await this.getLastWindowOptions();
|
||||
let file: string;
|
||||
if (typeof workspace === 'object') {
|
||||
options.x = workspace.x;
|
||||
options.y = workspace.y;
|
||||
options.width = workspace.width;
|
||||
options.height = workspace.height;
|
||||
options.isMaximized = workspace.isMaximized;
|
||||
options.isFullScreen = workspace.isFullScreen;
|
||||
file = workspace.file;
|
||||
if (typeof workspaceOrPath === 'object') {
|
||||
options.x = workspaceOrPath.x;
|
||||
options.y = workspaceOrPath.y;
|
||||
options.width = workspaceOrPath.width;
|
||||
options.height = workspaceOrPath.height;
|
||||
options.isMaximized = workspaceOrPath.isMaximized;
|
||||
options.isFullScreen = workspaceOrPath.isFullScreen;
|
||||
file = workspaceOrPath.file;
|
||||
} else {
|
||||
file = workspace;
|
||||
file = workspaceOrPath;
|
||||
}
|
||||
const [uri, electronWindow] = await Promise.all([
|
||||
this.createWindowUri(),
|
||||
@@ -486,3 +564,12 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
return this._firstWindowId;
|
||||
}
|
||||
}
|
||||
|
||||
class InterruptWorkspaceRestoreError extends Error {
|
||||
constructor() {
|
||||
super(
|
||||
"Received 'open-file' event. Interrupting the default launch workflow."
|
||||
);
|
||||
Object.setPrototypeOf(this, InterruptWorkspaceRestoreError.prototype);
|
||||
}
|
||||
}
|
||||
|
@@ -162,8 +162,6 @@ export class ArduinoDaemonImpl
|
||||
'--config-file',
|
||||
`"${cliConfigPath}"`,
|
||||
'-v',
|
||||
'--log-format',
|
||||
'json',
|
||||
];
|
||||
if (debug) {
|
||||
args.push('--debug');
|
||||
|
@@ -4,7 +4,6 @@ import {
|
||||
ArduinoFirmwareUploader,
|
||||
ArduinoFirmwareUploaderPath,
|
||||
} from '../common/protocol/arduino-firmware-uploader';
|
||||
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import {
|
||||
BackendApplicationContribution,
|
||||
@@ -26,7 +25,7 @@ import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connec
|
||||
import { CoreClientProvider } from './core-client-provider';
|
||||
import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core';
|
||||
import { DefaultWorkspaceServer } from './theia/workspace/default-workspace-server';
|
||||
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common';
|
||||
import { WorkspaceServer as TheiaWorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
|
||||
import { SketchesServiceImpl } from './sketches-service-impl';
|
||||
import {
|
||||
SketchesService,
|
||||
@@ -40,7 +39,6 @@ import {
|
||||
ArduinoDaemon,
|
||||
ArduinoDaemonPath,
|
||||
} from '../common/protocol/arduino-daemon';
|
||||
|
||||
import { ConfigServiceImpl } from './config-service-impl';
|
||||
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
|
||||
@@ -332,6 +330,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
'fwuploader', // Arduino Firmware uploader
|
||||
'discovery-log', // Boards discovery
|
||||
'config', // Logger for the CLI config reading and manipulation
|
||||
'sketches-service', // For creating, loading, and cloning sketches
|
||||
MonitorManagerName, // Logger for the monitor manager and its services
|
||||
MonitorServiceName,
|
||||
].forEach((name) => bindChildLogger(bind, name));
|
||||
|
@@ -6,7 +6,7 @@ import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node';
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { v4 } from 'uuid';
|
||||
import { Unknown } from '../common/nls';
|
||||
import {
|
||||
@@ -78,14 +78,6 @@ export class BoardDiscovery
|
||||
|
||||
onStart(): void {
|
||||
this.start();
|
||||
this.onClientDidRefresh(() => this.restart());
|
||||
}
|
||||
|
||||
private async restart(): Promise<void> {
|
||||
this.logger.info('restarting before stop');
|
||||
await this.stop();
|
||||
this.logger.info('restarting after stop');
|
||||
return this.start();
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
|
@@ -32,6 +32,8 @@ import { CoreClientAware } from './core-client-provider';
|
||||
import {
|
||||
BoardDetailsRequest,
|
||||
BoardDetailsResponse,
|
||||
BoardListAllRequest,
|
||||
BoardListAllResponse,
|
||||
BoardSearchRequest,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/board_pb';
|
||||
import {
|
||||
@@ -199,8 +201,28 @@ export class BoardsServiceImpl
|
||||
const req = new BoardSearchRequest();
|
||||
req.setSearchArgs(query || '');
|
||||
req.setInstance(instance);
|
||||
return this.handleListBoards(client.boardSearch.bind(client), req);
|
||||
}
|
||||
|
||||
async getInstalledBoards(): Promise<BoardWithPackage[]> {
|
||||
const { instance, client } = await this.coreClient;
|
||||
const req = new BoardListAllRequest();
|
||||
req.setInstance(instance);
|
||||
return this.handleListBoards(client.boardListAll.bind(client), req);
|
||||
}
|
||||
|
||||
private async handleListBoards(
|
||||
getBoards: (
|
||||
request: BoardListAllRequest | BoardSearchRequest,
|
||||
callback: (
|
||||
error: ServiceError | null,
|
||||
response: BoardListAllResponse
|
||||
) => void
|
||||
) => void,
|
||||
request: BoardListAllRequest | BoardSearchRequest
|
||||
): Promise<BoardWithPackage[]> {
|
||||
const boards = await new Promise<BoardWithPackage[]>((resolve, reject) => {
|
||||
client.boardSearch(req, (error, resp) => {
|
||||
getBoards(request, (error, resp) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
|
@@ -220,6 +220,9 @@ export class UpdateIndexRequest extends jspb.Message {
|
||||
getInstance(): cc_arduino_cli_commands_v1_common_pb.Instance | undefined;
|
||||
setInstance(value?: cc_arduino_cli_commands_v1_common_pb.Instance): UpdateIndexRequest;
|
||||
|
||||
getIgnoreCustomPackageIndexes(): boolean;
|
||||
setIgnoreCustomPackageIndexes(value: boolean): UpdateIndexRequest;
|
||||
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): UpdateIndexRequest.AsObject;
|
||||
@@ -234,6 +237,7 @@ export class UpdateIndexRequest extends jspb.Message {
|
||||
export namespace UpdateIndexRequest {
|
||||
export type AsObject = {
|
||||
instance?: cc_arduino_cli_commands_v1_common_pb.Instance.AsObject,
|
||||
ignoreCustomPackageIndexes: boolean,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1811,7 +1811,8 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.toObject = functio
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f)
|
||||
instance: (f = msg.getInstance()) && cc_arduino_cli_commands_v1_common_pb.Instance.toObject(includeInstance, f),
|
||||
ignoreCustomPackageIndexes: jspb.Message.getBooleanFieldWithDefault(msg, 2, false)
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -1853,6 +1854,10 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.deserializeBinaryFromReader
|
||||
reader.readMessage(value,cc_arduino_cli_commands_v1_common_pb.Instance.deserializeBinaryFromReader);
|
||||
msg.setInstance(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setIgnoreCustomPackageIndexes(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
@@ -1890,6 +1895,13 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.serializeBinaryToWriter = fu
|
||||
cc_arduino_cli_commands_v1_common_pb.Instance.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
f = message.getIgnoreCustomPackageIndexes();
|
||||
if (f) {
|
||||
writer.writeBool(
|
||||
2,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1930,6 +1942,24 @@ proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.hasInstance = func
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool ignore_custom_package_indexes = 2;
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.getIgnoreCustomPackageIndexes = function() {
|
||||
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 2, false));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.UpdateIndexRequest} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.UpdateIndexRequest.prototype.setIgnoreCustomPackageIndexes = function(value) {
|
||||
return jspb.Message.setProto3BooleanField(this, 2, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -28,21 +28,26 @@ export namespace Instance {
|
||||
}
|
||||
|
||||
export class DownloadProgress extends jspb.Message {
|
||||
getUrl(): string;
|
||||
setUrl(value: string): DownloadProgress;
|
||||
|
||||
getFile(): string;
|
||||
setFile(value: string): DownloadProgress;
|
||||
hasStart(): boolean;
|
||||
clearStart(): void;
|
||||
getStart(): DownloadProgressStart | undefined;
|
||||
setStart(value?: DownloadProgressStart): DownloadProgress;
|
||||
|
||||
getTotalSize(): number;
|
||||
setTotalSize(value: number): DownloadProgress;
|
||||
|
||||
getDownloaded(): number;
|
||||
setDownloaded(value: number): DownloadProgress;
|
||||
hasUpdate(): boolean;
|
||||
clearUpdate(): void;
|
||||
getUpdate(): DownloadProgressUpdate | undefined;
|
||||
setUpdate(value?: DownloadProgressUpdate): DownloadProgress;
|
||||
|
||||
getCompleted(): boolean;
|
||||
setCompleted(value: boolean): DownloadProgress;
|
||||
|
||||
hasEnd(): boolean;
|
||||
clearEnd(): void;
|
||||
getEnd(): DownloadProgressEnd | undefined;
|
||||
setEnd(value?: DownloadProgressEnd): DownloadProgress;
|
||||
|
||||
|
||||
getMessageCase(): DownloadProgress.MessageCase;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): DownloadProgress.AsObject;
|
||||
@@ -55,12 +60,97 @@ export class DownloadProgress extends jspb.Message {
|
||||
}
|
||||
|
||||
export namespace DownloadProgress {
|
||||
export type AsObject = {
|
||||
start?: DownloadProgressStart.AsObject,
|
||||
update?: DownloadProgressUpdate.AsObject,
|
||||
end?: DownloadProgressEnd.AsObject,
|
||||
}
|
||||
|
||||
export enum MessageCase {
|
||||
MESSAGE_NOT_SET = 0,
|
||||
|
||||
START = 1,
|
||||
|
||||
UPDATE = 2,
|
||||
|
||||
END = 3,
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DownloadProgressStart extends jspb.Message {
|
||||
getUrl(): string;
|
||||
setUrl(value: string): DownloadProgressStart;
|
||||
|
||||
getLabel(): string;
|
||||
setLabel(value: string): DownloadProgressStart;
|
||||
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): DownloadProgressStart.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: DownloadProgressStart): DownloadProgressStart.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: DownloadProgressStart, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): DownloadProgressStart;
|
||||
static deserializeBinaryFromReader(message: DownloadProgressStart, reader: jspb.BinaryReader): DownloadProgressStart;
|
||||
}
|
||||
|
||||
export namespace DownloadProgressStart {
|
||||
export type AsObject = {
|
||||
url: string,
|
||||
file: string,
|
||||
totalSize: number,
|
||||
label: string,
|
||||
}
|
||||
}
|
||||
|
||||
export class DownloadProgressUpdate extends jspb.Message {
|
||||
getDownloaded(): number;
|
||||
setDownloaded(value: number): DownloadProgressUpdate;
|
||||
|
||||
getTotalSize(): number;
|
||||
setTotalSize(value: number): DownloadProgressUpdate;
|
||||
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): DownloadProgressUpdate.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: DownloadProgressUpdate): DownloadProgressUpdate.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: DownloadProgressUpdate, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): DownloadProgressUpdate;
|
||||
static deserializeBinaryFromReader(message: DownloadProgressUpdate, reader: jspb.BinaryReader): DownloadProgressUpdate;
|
||||
}
|
||||
|
||||
export namespace DownloadProgressUpdate {
|
||||
export type AsObject = {
|
||||
downloaded: number,
|
||||
completed: boolean,
|
||||
totalSize: number,
|
||||
}
|
||||
}
|
||||
|
||||
export class DownloadProgressEnd extends jspb.Message {
|
||||
getSuccess(): boolean;
|
||||
setSuccess(value: boolean): DownloadProgressEnd;
|
||||
|
||||
getMessage(): string;
|
||||
setMessage(value: string): DownloadProgressEnd;
|
||||
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): DownloadProgressEnd.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: DownloadProgressEnd): DownloadProgressEnd.AsObject;
|
||||
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
|
||||
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
|
||||
static serializeBinaryToWriter(message: DownloadProgressEnd, writer: jspb.BinaryWriter): void;
|
||||
static deserializeBinary(bytes: Uint8Array): DownloadProgressEnd;
|
||||
static deserializeBinaryFromReader(message: DownloadProgressEnd, reader: jspb.BinaryReader): DownloadProgressEnd;
|
||||
}
|
||||
|
||||
export namespace DownloadProgressEnd {
|
||||
export type AsObject = {
|
||||
success: boolean,
|
||||
message: string,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,10 @@ var global = Function('return this')();
|
||||
|
||||
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.Board', null, global);
|
||||
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgress', null, global);
|
||||
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase', null, global);
|
||||
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgressEnd', null, global);
|
||||
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgressStart', null, global);
|
||||
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate', null, global);
|
||||
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.InstalledPlatformReference', null, global);
|
||||
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.Instance', null, global);
|
||||
goog.exportSymbol('proto.cc.arduino.cli.commands.v1.Platform', null, global);
|
||||
@@ -55,7 +59,7 @@ if (goog.DEBUG && !COMPILED) {
|
||||
* @constructor
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_);
|
||||
};
|
||||
goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgress, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
@@ -65,6 +69,69 @@ if (goog.DEBUG && !COMPILED) {
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgress';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
* server response, or constructed directly in Javascript. The array is used
|
||||
* in place and becomes part of the constructed object. It is not cloned.
|
||||
* If no data is provided, the constructed object will be empty, but still
|
||||
* valid.
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||
};
|
||||
goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgressStart, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgressStart';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
* server response, or constructed directly in Javascript. The array is used
|
||||
* in place and becomes part of the constructed object. It is not cloned.
|
||||
* If no data is provided, the constructed object will be empty, but still
|
||||
* valid.
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||
};
|
||||
goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
* server response, or constructed directly in Javascript. The array is used
|
||||
* in place and becomes part of the constructed object. It is not cloned.
|
||||
* If no data is provided, the constructed object will be empty, but still
|
||||
* valid.
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||
};
|
||||
goog.inherits(proto.cc.arduino.cli.commands.v1.DownloadProgressEnd, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.displayName = 'proto.cc.arduino.cli.commands.v1.DownloadProgressEnd';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
@@ -322,6 +389,33 @@ proto.cc.arduino.cli.commands.v1.Instance.prototype.setId = function(value) {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Oneof group definitions for this message. Each group defines the field
|
||||
* numbers belonging to that group. When of these fields' value is set, all
|
||||
* other fields in the group are cleared. During deserialization, if multiple
|
||||
* fields are encountered for a group, only the last value seen will be kept.
|
||||
* @private {!Array<!Array<number>>}
|
||||
* @const
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_ = [[1,2,3]];
|
||||
|
||||
/**
|
||||
* @enum {number}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase = {
|
||||
MESSAGE_NOT_SET: 0,
|
||||
START: 1,
|
||||
UPDATE: 2,
|
||||
END: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getMessageCase = function() {
|
||||
return /** @type {proto.cc.arduino.cli.commands.v1.DownloadProgress.MessageCase} */(jspb.Message.computeOneofCase(this, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0]));
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
@@ -353,11 +447,9 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.toObject = function(
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
url: jspb.Message.getFieldWithDefault(msg, 1, ""),
|
||||
file: jspb.Message.getFieldWithDefault(msg, 2, ""),
|
||||
totalSize: jspb.Message.getFieldWithDefault(msg, 3, 0),
|
||||
downloaded: jspb.Message.getFieldWithDefault(msg, 4, 0),
|
||||
completed: jspb.Message.getBooleanFieldWithDefault(msg, 5, false)
|
||||
start: (f = msg.getStart()) && proto.cc.arduino.cli.commands.v1.DownloadProgressStart.toObject(includeInstance, f),
|
||||
update: (f = msg.getUpdate()) && proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.toObject(includeInstance, f),
|
||||
end: (f = msg.getEnd()) && proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.toObject(includeInstance, f)
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -395,24 +487,19 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.deserializeBinaryFromReader =
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setUrl(value);
|
||||
var value = new proto.cc.arduino.cli.commands.v1.DownloadProgressStart;
|
||||
reader.readMessage(value,proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinaryFromReader);
|
||||
msg.setStart(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setFile(value);
|
||||
var value = new proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate;
|
||||
reader.readMessage(value,proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinaryFromReader);
|
||||
msg.setUpdate(value);
|
||||
break;
|
||||
case 3:
|
||||
var value = /** @type {number} */ (reader.readInt64());
|
||||
msg.setTotalSize(value);
|
||||
break;
|
||||
case 4:
|
||||
var value = /** @type {number} */ (reader.readInt64());
|
||||
msg.setDownloaded(value);
|
||||
break;
|
||||
case 5:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setCompleted(value);
|
||||
var value = new proto.cc.arduino.cli.commands.v1.DownloadProgressEnd;
|
||||
reader.readMessage(value,proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinaryFromReader);
|
||||
msg.setEnd(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
@@ -442,6 +529,251 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.serializeBinary = fu
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getStart();
|
||||
if (f != null) {
|
||||
writer.writeMessage(
|
||||
1,
|
||||
f,
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
f = message.getUpdate();
|
||||
if (f != null) {
|
||||
writer.writeMessage(
|
||||
2,
|
||||
f,
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
f = message.getEnd();
|
||||
if (f != null) {
|
||||
writer.writeMessage(
|
||||
3,
|
||||
f,
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional DownloadProgressStart start = 1;
|
||||
* @return {?proto.cc.arduino.cli.commands.v1.DownloadProgressStart}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getStart = function() {
|
||||
return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgressStart} */ (
|
||||
jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.DownloadProgressStart, 1));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {?proto.cc.arduino.cli.commands.v1.DownloadProgressStart|undefined} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setStart = function(value) {
|
||||
return jspb.Message.setOneofWrapperField(this, 1, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0], value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the message field making it undefined.
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.clearStart = function() {
|
||||
return this.setStart(undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.hasStart = function() {
|
||||
return jspb.Message.getField(this, 1) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional DownloadProgressUpdate update = 2;
|
||||
* @return {?proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getUpdate = function() {
|
||||
return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} */ (
|
||||
jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate, 2));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {?proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate|undefined} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setUpdate = function(value) {
|
||||
return jspb.Message.setOneofWrapperField(this, 2, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0], value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the message field making it undefined.
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.clearUpdate = function() {
|
||||
return this.setUpdate(undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.hasUpdate = function() {
|
||||
return jspb.Message.getField(this, 2) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional DownloadProgressEnd end = 3;
|
||||
* @return {?proto.cc.arduino.cli.commands.v1.DownloadProgressEnd}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getEnd = function() {
|
||||
return /** @type{?proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} */ (
|
||||
jspb.Message.getWrapperField(this, proto.cc.arduino.cli.commands.v1.DownloadProgressEnd, 3));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {?proto.cc.arduino.cli.commands.v1.DownloadProgressEnd|undefined} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setEnd = function(value) {
|
||||
return jspb.Message.setOneofWrapperField(this, 3, proto.cc.arduino.cli.commands.v1.DownloadProgress.oneofGroups_[0], value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the message field making it undefined.
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.clearEnd = function() {
|
||||
return this.setEnd(undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.hasEnd = function() {
|
||||
return jspb.Message.getField(this, 3) != null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||
* Optional fields that are not set will be set to undefined.
|
||||
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||
* For the list of reserved names please see:
|
||||
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||
* JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.cc.arduino.cli.commands.v1.DownloadProgressStart.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Static version of the {@see toObject} method.
|
||||
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||
* the JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
url: jspb.Message.getFieldWithDefault(msg, 1, ""),
|
||||
label: jspb.Message.getFieldWithDefault(msg, 2, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
obj.$jspbMessageInstance = msg;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.cc.arduino.cli.commands.v1.DownloadProgressStart;
|
||||
return proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
}
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setUrl(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setLabel(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getUrl();
|
||||
if (f.length > 0) {
|
||||
@@ -450,34 +782,13 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.serializeBinaryToWriter = func
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getFile();
|
||||
f = message.getLabel();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
2,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getTotalSize();
|
||||
if (f !== 0) {
|
||||
writer.writeInt64(
|
||||
3,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getDownloaded();
|
||||
if (f !== 0) {
|
||||
writer.writeInt64(
|
||||
4,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getCompleted();
|
||||
if (f) {
|
||||
writer.writeBool(
|
||||
5,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -485,89 +796,355 @@ proto.cc.arduino.cli.commands.v1.DownloadProgress.serializeBinaryToWriter = func
|
||||
* optional string url = 1;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getUrl = function() {
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.getUrl = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setUrl = function(value) {
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.setUrl = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 1, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string file = 2;
|
||||
* optional string label = 2;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getFile = function() {
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.getLabel = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressStart} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setFile = function(value) {
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressStart.prototype.setLabel = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 2, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* optional int64 total_size = 3;
|
||||
* Creates an object representation of this proto.
|
||||
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||
* Optional fields that are not set will be set to undefined.
|
||||
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||
* For the list of reserved names please see:
|
||||
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||
* JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Static version of the {@see toObject} method.
|
||||
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||
* the JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
downloaded: jspb.Message.getFieldWithDefault(msg, 1, 0),
|
||||
totalSize: jspb.Message.getFieldWithDefault(msg, 2, 0)
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
obj.$jspbMessageInstance = msg;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate;
|
||||
return proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
}
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = /** @type {number} */ (reader.readInt64());
|
||||
msg.setDownloaded(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = /** @type {number} */ (reader.readInt64());
|
||||
msg.setTotalSize(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getDownloaded();
|
||||
if (f !== 0) {
|
||||
writer.writeInt64(
|
||||
1,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getTotalSize();
|
||||
if (f !== 0) {
|
||||
writer.writeInt64(
|
||||
2,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional int64 downloaded = 1;
|
||||
* @return {number}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getTotalSize = function() {
|
||||
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 3, 0));
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.getDownloaded = function() {
|
||||
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setTotalSize = function(value) {
|
||||
return jspb.Message.setProto3IntField(this, 3, value);
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.setDownloaded = function(value) {
|
||||
return jspb.Message.setProto3IntField(this, 1, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional int64 downloaded = 4;
|
||||
* optional int64 total_size = 2;
|
||||
* @return {number}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getDownloaded = function() {
|
||||
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 4, 0));
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.getTotalSize = function() {
|
||||
return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setDownloaded = function(value) {
|
||||
return jspb.Message.setProto3IntField(this, 4, value);
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressUpdate.prototype.setTotalSize = function(value) {
|
||||
return jspb.Message.setProto3IntField(this, 2, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||
* Optional fields that are not set will be set to undefined.
|
||||
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||
* For the list of reserved names please see:
|
||||
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||
* JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool completed = 5;
|
||||
* Static version of the {@see toObject} method.
|
||||
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||
* the JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
success: jspb.Message.getBooleanFieldWithDefault(msg, 1, false),
|
||||
message: jspb.Message.getFieldWithDefault(msg, 2, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
obj.$jspbMessageInstance = msg;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.cc.arduino.cli.commands.v1.DownloadProgressEnd;
|
||||
return proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
}
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setSuccess(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setMessage(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getSuccess();
|
||||
if (f) {
|
||||
writer.writeBool(
|
||||
1,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getMessage();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
2,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool success = 1;
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.getCompleted = function() {
|
||||
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 5, false));
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.getSuccess = function() {
|
||||
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 1, false));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgress} returns this
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgress.prototype.setCompleted = function(value) {
|
||||
return jspb.Message.setProto3BooleanField(this, 5, value);
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.setSuccess = function(value) {
|
||||
return jspb.Message.setProto3BooleanField(this, 1, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string message = 2;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.getMessage = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.DownloadProgressEnd} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.DownloadProgressEnd.prototype.setMessage = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 2, value);
|
||||
};
|
||||
|
||||
|
||||
|
@@ -765,9 +765,6 @@ export class Library extends jspb.Message {
|
||||
getContainerPlatform(): string;
|
||||
setContainerPlatform(value: string): Library;
|
||||
|
||||
getRealName(): string;
|
||||
setRealName(value: string): Library;
|
||||
|
||||
getDotALinkage(): boolean;
|
||||
setDotALinkage(value: boolean): Library;
|
||||
|
||||
@@ -836,7 +833,6 @@ export namespace Library {
|
||||
sourceDir: string,
|
||||
utilityDir: string,
|
||||
containerPlatform: string,
|
||||
realName: string,
|
||||
dotALinkage: boolean,
|
||||
precompiled: boolean,
|
||||
ldFlags: string,
|
||||
|
@@ -5447,7 +5447,6 @@ proto.cc.arduino.cli.commands.v1.Library.toObject = function(includeInstance, ms
|
||||
sourceDir: jspb.Message.getFieldWithDefault(msg, 11, ""),
|
||||
utilityDir: jspb.Message.getFieldWithDefault(msg, 12, ""),
|
||||
containerPlatform: jspb.Message.getFieldWithDefault(msg, 14, ""),
|
||||
realName: jspb.Message.getFieldWithDefault(msg, 16, ""),
|
||||
dotALinkage: jspb.Message.getBooleanFieldWithDefault(msg, 17, false),
|
||||
precompiled: jspb.Message.getBooleanFieldWithDefault(msg, 18, false),
|
||||
ldFlags: jspb.Message.getFieldWithDefault(msg, 19, ""),
|
||||
@@ -5548,10 +5547,6 @@ proto.cc.arduino.cli.commands.v1.Library.deserializeBinaryFromReader = function(
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setContainerPlatform(value);
|
||||
break;
|
||||
case 16:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setRealName(value);
|
||||
break;
|
||||
case 17:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setDotALinkage(value);
|
||||
@@ -5724,13 +5719,6 @@ proto.cc.arduino.cli.commands.v1.Library.serializeBinaryToWriter = function(mess
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getRealName();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
16,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getDotALinkage();
|
||||
if (f) {
|
||||
writer.writeBool(
|
||||
@@ -6084,24 +6072,6 @@ proto.cc.arduino.cli.commands.v1.Library.prototype.setContainerPlatform = functi
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string real_name = 16;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.Library.prototype.getRealName = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 16, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.cc.arduino.cli.commands.v1.Library} returns this
|
||||
*/
|
||||
proto.cc.arduino.cli.commands.v1.Library.prototype.setRealName = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 16, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool dot_a_linkage = 17;
|
||||
* @return {boolean}
|
||||
|
@@ -26,7 +26,6 @@ import { DefaultCliConfig, CLI_CONFIG } from './cli-config';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { deepClone } from '@theia/core';
|
||||
import { duration } from '../common/decorators';
|
||||
|
||||
const deepmerge = require('deepmerge');
|
||||
|
||||
@@ -56,7 +55,10 @@ export class ConfigServiceImpl
|
||||
this.loadCliConfig().then(async (cliConfig) => {
|
||||
this.cliConfig = cliConfig;
|
||||
if (this.cliConfig) {
|
||||
const config = await this.mapCliConfigToAppConfig(this.cliConfig);
|
||||
const [config] = await Promise.all([
|
||||
this.mapCliConfigToAppConfig(this.cliConfig),
|
||||
this.ensureUserDirExists(this.cliConfig),
|
||||
]);
|
||||
if (config) {
|
||||
this.config = config;
|
||||
this.ready.resolve();
|
||||
@@ -126,7 +128,6 @@ export class ConfigServiceImpl
|
||||
return this.daemon.getVersion();
|
||||
}
|
||||
|
||||
@duration()
|
||||
protected async loadCliConfig(
|
||||
initializeIfAbsent = true
|
||||
): Promise<DefaultCliConfig | undefined> {
|
||||
@@ -263,4 +264,11 @@ export class ConfigServiceImpl
|
||||
grpc.credentials.createInsecure()
|
||||
) as SettingsServiceClient;
|
||||
}
|
||||
|
||||
// #1445
|
||||
private async ensureUserDirExists(
|
||||
cliConfig: DefaultCliConfig
|
||||
): Promise<void> {
|
||||
await fs.mkdir(cliConfig.directories.user, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
|
||||
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
import {
|
||||
@@ -19,8 +19,15 @@ import {
|
||||
UpdateLibrariesIndexResponse,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
|
||||
import * as commandsGrpcPb from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
|
||||
import { NotificationServiceServer } from '../common/protocol';
|
||||
import { Deferred, retry } from '@theia/core/lib/common/promise-util';
|
||||
import {
|
||||
IndexType,
|
||||
IndexUpdateDidCompleteParams,
|
||||
IndexUpdateSummary,
|
||||
IndexUpdateDidFailParams,
|
||||
IndexUpdateWillStartParams,
|
||||
NotificationServiceServer,
|
||||
} from '../common/protocol';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import {
|
||||
Status as RpcStatus,
|
||||
Status,
|
||||
@@ -32,6 +39,7 @@ import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import {
|
||||
IndexesUpdateProgressHandler,
|
||||
ExecuteWithProgress,
|
||||
DownloadResult,
|
||||
} from './grpc-progressible';
|
||||
import type { DefaultCliConfig } from './cli-config';
|
||||
import { ServiceError } from './service-error';
|
||||
@@ -45,16 +53,19 @@ export class CoreClientProvider {
|
||||
@inject(NotificationServiceServer)
|
||||
private readonly notificationService: NotificationServiceServer;
|
||||
|
||||
private ready = new Deferred<void>();
|
||||
private pending: Deferred<CoreClientProvider.Client> | undefined;
|
||||
private _client: CoreClientProvider.Client | undefined;
|
||||
private readonly toDisposeBeforeCreate = new DisposableCollection();
|
||||
/**
|
||||
* See `CoreService#indexUpdateSummaryBeforeInit`.
|
||||
*/
|
||||
private readonly beforeInitSummary = {} as IndexUpdateSummary;
|
||||
private readonly toDisposeOnCloseClient = new DisposableCollection();
|
||||
private readonly toDisposeAfterDidCreate = new DisposableCollection();
|
||||
private readonly onClientReadyEmitter =
|
||||
new Emitter<CoreClientProvider.Client>();
|
||||
private readonly onClientReady = this.onClientReadyEmitter.event;
|
||||
private readonly onClientDidRefreshEmitter =
|
||||
new Emitter<CoreClientProvider.Client>();
|
||||
|
||||
private ready = new Deferred<void>();
|
||||
private pending: Deferred<CoreClientProvider.Client> | undefined;
|
||||
private _client: CoreClientProvider.Client | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
@@ -65,7 +76,9 @@ export class CoreClientProvider {
|
||||
});
|
||||
this.daemon.onDaemonStarted((port) => this.create(port));
|
||||
this.daemon.onDaemonStopped(() => this.closeClient());
|
||||
this.configService.onConfigChange(() => this.refreshIndexes());
|
||||
this.configService.onConfigChange(
|
||||
() => this.client.then((client) => this.updateIndex(client, ['platform'])) // Assuming 3rd party URL changes. No library index update is required.
|
||||
);
|
||||
}
|
||||
|
||||
get tryGetClient(): CoreClientProvider.Client | undefined {
|
||||
@@ -80,7 +93,7 @@ export class CoreClientProvider {
|
||||
if (!this.pending) {
|
||||
this.pending = new Deferred();
|
||||
this.toDisposeAfterDidCreate.pushAll([
|
||||
Disposable.create(() => (this.pending = undefined)),
|
||||
Disposable.create(() => (this.pending = undefined)), // TODO: reject all pending requests before unsetting the ref?
|
||||
this.onClientReady((client) => {
|
||||
this.pending?.resolve(client);
|
||||
this.toDisposeAfterDidCreate.dispose();
|
||||
@@ -90,8 +103,9 @@ export class CoreClientProvider {
|
||||
return this.pending.promise;
|
||||
}
|
||||
|
||||
get onClientDidRefresh(): Event<CoreClientProvider.Client> {
|
||||
return this.onClientDidRefreshEmitter.event;
|
||||
async refresh(): Promise<void> {
|
||||
const client = await this.client;
|
||||
await this.initInstance(client);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +115,7 @@ export class CoreClientProvider {
|
||||
this.closeClient();
|
||||
const address = this.address(port);
|
||||
const client = await this.createClient(address);
|
||||
this.toDisposeBeforeCreate.pushAll([
|
||||
this.toDisposeOnCloseClient.pushAll([
|
||||
Disposable.create(() => client.client.close()),
|
||||
Disposable.create(() => {
|
||||
this.ready.reject(
|
||||
@@ -113,7 +127,6 @@ export class CoreClientProvider {
|
||||
}),
|
||||
]);
|
||||
await this.initInstanceWithFallback(client);
|
||||
setTimeout(async () => this.refreshIndexes(), 10_000); // Update the indexes asynchronously
|
||||
return this.useClient(client);
|
||||
}
|
||||
|
||||
@@ -136,12 +149,17 @@ export class CoreClientProvider {
|
||||
try {
|
||||
await this.initInstance(client);
|
||||
} catch (err) {
|
||||
if (err instanceof IndexUpdateRequiredBeforeInitError) {
|
||||
if (err instanceof MustUpdateIndexesBeforeInitError) {
|
||||
console.error(
|
||||
'The primary packages indexes are missing. Running indexes update before initializing the core gRPC client',
|
||||
err.message
|
||||
);
|
||||
await this.updateIndexes(client); // TODO: this should run without the 3rd party URLs
|
||||
await this.updateIndex(client, Array.from(err.indexTypesToUpdate));
|
||||
const updatedAt = new Date().toISOString();
|
||||
// Clients will ask for it after they connect.
|
||||
err.indexTypesToUpdate.forEach(
|
||||
(type) => (this.beforeInitSummary[type] = updatedAt)
|
||||
);
|
||||
await this.initInstance(client);
|
||||
console.info(
|
||||
`Downloaded the primary package indexes, and successfully initialized the core gRPC client.`
|
||||
@@ -165,7 +183,7 @@ export class CoreClientProvider {
|
||||
}
|
||||
|
||||
private closeClient(): void {
|
||||
return this.toDisposeBeforeCreate.dispose();
|
||||
return this.toDisposeOnCloseClient.dispose();
|
||||
}
|
||||
|
||||
private async createClient(
|
||||
@@ -248,45 +266,66 @@ export class CoreClientProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all indexes and runs an init to [reload the indexes](https://github.com/arduino/arduino-cli/pull/1274#issue-866154638).
|
||||
* `update3rdPartyPlatforms` has not effect if `types` is `['library']`.
|
||||
*/
|
||||
private async refreshIndexes(): Promise<void> {
|
||||
const client = this._client;
|
||||
if (client) {
|
||||
const progressHandler = this.createProgressHandler();
|
||||
try {
|
||||
await this.updateIndexes(client, progressHandler);
|
||||
async updateIndex(
|
||||
client: CoreClientProvider.Client,
|
||||
types: IndexType[]
|
||||
): Promise<void> {
|
||||
let error: unknown | undefined = undefined;
|
||||
const progressHandler = this.createProgressHandler(types);
|
||||
try {
|
||||
const updates: Promise<void>[] = [];
|
||||
if (types.includes('platform')) {
|
||||
updates.push(this.updatePlatformIndex(client, progressHandler));
|
||||
}
|
||||
if (types.includes('library')) {
|
||||
updates.push(this.updateLibraryIndex(client, progressHandler));
|
||||
}
|
||||
await Promise.all(updates);
|
||||
} catch (err) {
|
||||
// This is suboptimal but the core client must be re-initialized even if the index update has failed and the request was rejected.
|
||||
error = err;
|
||||
} finally {
|
||||
// IDE2 reloads the index only and if only at least one download success is available.
|
||||
if (
|
||||
progressHandler.results.some(
|
||||
(result) => !DownloadResult.isError(result)
|
||||
)
|
||||
) {
|
||||
await this.initInstance(client);
|
||||
// notify clients about the index update only after the client has been "re-initialized" and the new content is available.
|
||||
progressHandler.reportEnd();
|
||||
this.onClientDidRefreshEmitter.fire(client);
|
||||
} catch (err) {
|
||||
console.error('Failed to update indexes', err);
|
||||
progressHandler.reportError(
|
||||
ServiceError.is(err) ? err.details : String(err)
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
console.error(`Failed to update ${types.join(', ')} indexes.`, error);
|
||||
const downloadErrors = progressHandler.results
|
||||
.filter(DownloadResult.isError)
|
||||
.map(({ url, message }) => `${message}: ${url}`)
|
||||
.join(' ');
|
||||
const message = ServiceError.is(error)
|
||||
? `${error.details}${downloadErrors ? ` ${downloadErrors}` : ''}`
|
||||
: String(error);
|
||||
// IDE2 keeps only the most recent error message. Previous errors might have been fixed with the fallback initialization.
|
||||
this.beforeInitSummary.message = message;
|
||||
// Toast the error message, so tha the user has chance to fix it if it was a client error (HTTP 4xx).
|
||||
progressHandler.reportError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateIndexes(
|
||||
client: CoreClientProvider.Client,
|
||||
progressHandler?: IndexesUpdateProgressHandler
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
this.updateIndex(client, progressHandler),
|
||||
this.updateLibraryIndex(client, progressHandler),
|
||||
]);
|
||||
get indexUpdateSummaryBeforeInit(): IndexUpdateSummary {
|
||||
return { ...this.beforeInitSummary };
|
||||
}
|
||||
|
||||
private async updateIndex(
|
||||
private async updatePlatformIndex(
|
||||
client: CoreClientProvider.Client,
|
||||
progressHandler?: IndexesUpdateProgressHandler
|
||||
): Promise<void> {
|
||||
return this.doUpdateIndex(
|
||||
() =>
|
||||
client.client.updateIndex(
|
||||
new UpdateIndexRequest().setInstance(client.instance)
|
||||
new UpdateIndexRequest().setInstance(client.instance) // Always updates both the primary and the 3rd party package indexes.
|
||||
),
|
||||
progressHandler,
|
||||
'platform-index'
|
||||
@@ -318,50 +357,45 @@ export class CoreClientProvider {
|
||||
task?: string
|
||||
): Promise<void> {
|
||||
const progressId = progressHandler?.progressId;
|
||||
return retry(
|
||||
() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
responseProvider()
|
||||
.on(
|
||||
'data',
|
||||
ExecuteWithProgress.createDataCallback({
|
||||
responseService: {
|
||||
appendToOutput: ({ chunk: message }) => {
|
||||
console.log(
|
||||
`core-client-provider${task ? ` [${task}]` : ''}`,
|
||||
message
|
||||
);
|
||||
progressHandler?.reportProgress(message);
|
||||
},
|
||||
},
|
||||
progressId,
|
||||
})
|
||||
)
|
||||
.on('error', reject)
|
||||
.on('end', resolve);
|
||||
}),
|
||||
50,
|
||||
3
|
||||
);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
responseProvider()
|
||||
.on(
|
||||
'data',
|
||||
ExecuteWithProgress.createDataCallback({
|
||||
responseService: {
|
||||
appendToOutput: ({ chunk: message }) => {
|
||||
console.log(
|
||||
`core-client-provider${task ? ` [${task}]` : ''}`,
|
||||
message
|
||||
);
|
||||
progressHandler?.reportProgress(message);
|
||||
},
|
||||
},
|
||||
reportResult: (result) => progressHandler?.reportResult(result),
|
||||
progressId,
|
||||
})
|
||||
)
|
||||
.on('error', reject)
|
||||
.on('end', resolve);
|
||||
});
|
||||
}
|
||||
|
||||
private createProgressHandler(): IndexesUpdateProgressHandler {
|
||||
private createProgressHandler(
|
||||
types: IndexType[]
|
||||
): IndexesUpdateProgressHandler {
|
||||
const additionalUrlsCount =
|
||||
this.configService.cliConfiguration?.board_manager?.additional_urls
|
||||
?.length ?? 0;
|
||||
return new IndexesUpdateProgressHandler(
|
||||
additionalUrlsCount,
|
||||
(progressMessage) =>
|
||||
return new IndexesUpdateProgressHandler(types, additionalUrlsCount, {
|
||||
onProgress: (progressMessage) =>
|
||||
this.notificationService.notifyIndexUpdateDidProgress(progressMessage),
|
||||
({ progressId, message }) =>
|
||||
this.notificationService.notifyIndexUpdateDidFail({
|
||||
progressId,
|
||||
message,
|
||||
}),
|
||||
(progressId) =>
|
||||
this.notificationService.notifyIndexWillUpdate(progressId),
|
||||
(progressId) => this.notificationService.notifyIndexDidUpdate(progressId)
|
||||
);
|
||||
onError: (params: IndexUpdateDidFailParams) =>
|
||||
this.notificationService.notifyIndexUpdateDidFail(params),
|
||||
onStart: (params: IndexUpdateWillStartParams) =>
|
||||
this.notificationService.notifyIndexUpdateWillStart(params),
|
||||
onComplete: (params: IndexUpdateDidCompleteParams) =>
|
||||
this.notificationService.notifyIndexUpdateDidComplete(params),
|
||||
});
|
||||
}
|
||||
|
||||
private address(port: string): string {
|
||||
@@ -405,6 +439,7 @@ export namespace CoreClientProvider {
|
||||
export abstract class CoreClientAware {
|
||||
@inject(CoreClientProvider)
|
||||
private readonly coreClientProvider: CoreClientProvider;
|
||||
|
||||
/**
|
||||
* Returns with a promise that resolves when the core client is initialized and ready.
|
||||
*/
|
||||
@@ -412,20 +447,38 @@ export abstract class CoreClientAware {
|
||||
return this.coreClientProvider.client;
|
||||
}
|
||||
|
||||
protected get onClientDidRefresh(): Event<CoreClientProvider.Client> {
|
||||
return this.coreClientProvider.onClientDidRefresh;
|
||||
/**
|
||||
* Updates the index of the given `type` and returns with a promise which resolves when the core gPRC client has been reinitialized.
|
||||
*/
|
||||
async updateIndex({ types }: { types: IndexType[] }): Promise<void> {
|
||||
const client = await this.coreClient;
|
||||
return this.coreClientProvider.updateIndex(client, types);
|
||||
}
|
||||
|
||||
async indexUpdateSummaryBeforeInit(): Promise<IndexUpdateSummary> {
|
||||
await this.coreClient;
|
||||
return this.coreClientProvider.indexUpdateSummaryBeforeInit;
|
||||
}
|
||||
|
||||
refresh(): Promise<void> {
|
||||
return this.coreClientProvider.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
class IndexUpdateRequiredBeforeInitError extends Error {
|
||||
constructor(causes: RpcStatus.AsObject[]) {
|
||||
class MustUpdateIndexesBeforeInitError extends Error {
|
||||
readonly indexTypesToUpdate: Set<IndexType>;
|
||||
constructor(causes: [RpcStatus.AsObject, IndexType][]) {
|
||||
super(`The index of the cores and libraries must be updated before initializing the core gRPC client.
|
||||
The following problems were detected during the gRPC client initialization:
|
||||
${causes
|
||||
.map(({ code, message }) => ` - code: ${code}, message: ${message}`)
|
||||
.map(
|
||||
([{ code, message }, type]) =>
|
||||
`[${type}-index] - code: ${code}, message: ${message}`
|
||||
)
|
||||
.join('\n')}
|
||||
`);
|
||||
Object.setPrototypeOf(this, IndexUpdateRequiredBeforeInitError.prototype);
|
||||
Object.setPrototypeOf(this, MustUpdateIndexesBeforeInitError.prototype);
|
||||
this.indexTypesToUpdate = new Set(causes.map(([, type]) => type));
|
||||
if (!causes.length) {
|
||||
throw new Error(`expected non-empty 'causes'`);
|
||||
}
|
||||
@@ -435,41 +488,66 @@ ${causes
|
||||
function isIndexUpdateRequiredBeforeInit(
|
||||
status: RpcStatus[],
|
||||
cliConfig: DefaultCliConfig
|
||||
): IndexUpdateRequiredBeforeInitError | undefined {
|
||||
const causes = status
|
||||
.filter((s) =>
|
||||
IndexUpdateRequiredBeforeInit.map((predicate) =>
|
||||
predicate(s, cliConfig)
|
||||
).some(Boolean)
|
||||
)
|
||||
.map((s) => RpcStatus.toObject(false, s));
|
||||
): MustUpdateIndexesBeforeInitError | undefined {
|
||||
const causes = status.reduce((acc, curr) => {
|
||||
for (const [predicate, type] of IndexUpdateRequiredPredicates) {
|
||||
if (predicate(curr, cliConfig)) {
|
||||
acc.push([curr.toObject(false), type]);
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as [RpcStatus.AsObject, IndexType][]);
|
||||
return causes.length
|
||||
? new IndexUpdateRequiredBeforeInitError(causes)
|
||||
? new MustUpdateIndexesBeforeInitError(causes)
|
||||
: undefined;
|
||||
}
|
||||
const IndexUpdateRequiredBeforeInit = [
|
||||
isPackageIndexMissingStatus,
|
||||
isDiscoveryNotFoundStatus,
|
||||
interface Predicate {
|
||||
(
|
||||
status: RpcStatus,
|
||||
{
|
||||
directories: { data },
|
||||
}: DefaultCliConfig
|
||||
): boolean;
|
||||
}
|
||||
const IndexUpdateRequiredPredicates: [Predicate, IndexType][] = [
|
||||
[isPrimaryPackageIndexMissingStatus, 'platform'],
|
||||
[isDiscoveryNotFoundStatus, 'platform'],
|
||||
[isLibraryIndexMissingStatus, 'library'],
|
||||
];
|
||||
function isPackageIndexMissingStatus(
|
||||
// Loading index file: loading json index file /path/to/package_index.json: open /path/to/package_index.json: no such file or directory
|
||||
function isPrimaryPackageIndexMissingStatus(
|
||||
status: RpcStatus,
|
||||
{ directories: { data } }: DefaultCliConfig
|
||||
): boolean {
|
||||
const predicate = ({ message }: RpcStatus.AsObject) =>
|
||||
message.includes('loading json index file') &&
|
||||
(message.includes(join(data, 'package_index.json')) ||
|
||||
message.includes(join(data, 'library_index.json')));
|
||||
message.includes(join(data, 'package_index.json'));
|
||||
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
|
||||
return evaluate(status, predicate);
|
||||
}
|
||||
// Error loading hardware platform: discovery $TOOL_NAME not found
|
||||
function isDiscoveryNotFoundStatus(status: RpcStatus): boolean {
|
||||
const predicate = ({ message }: RpcStatus.AsObject) =>
|
||||
message.includes('discovery') &&
|
||||
(message.includes('not found') || message.includes('not installed'));
|
||||
(message.includes('not found') ||
|
||||
message.includes('loading hardware platform'));
|
||||
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L740
|
||||
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/loader.go#L744
|
||||
return evaluate(status, predicate);
|
||||
}
|
||||
// Loading index file: reading library_index.json: open /path/to/library_index.json: no such file or directory
|
||||
function isLibraryIndexMissingStatus(
|
||||
status: RpcStatus,
|
||||
{ directories: { data } }: DefaultCliConfig
|
||||
): boolean {
|
||||
const predicate = ({ message }: RpcStatus.AsObject) =>
|
||||
message.includes('index file') &&
|
||||
message.includes('reading') &&
|
||||
message.includes(join(data, 'library_index.json'));
|
||||
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
|
||||
return evaluate(status, predicate);
|
||||
}
|
||||
function evaluate(
|
||||
subject: RpcStatus,
|
||||
predicate: (error: RpcStatus.AsObject) => boolean
|
||||
|
@@ -11,14 +11,10 @@ import {
|
||||
SketchContainer,
|
||||
} from '../common/protocol/sketches-service';
|
||||
import { ExamplesService } from '../common/protocol/examples-service';
|
||||
import {
|
||||
LibraryLocation,
|
||||
LibraryPackage,
|
||||
LibraryService,
|
||||
} from '../common/protocol';
|
||||
import { duration } from '../common/decorators';
|
||||
import { LibraryLocation, LibraryPackage } from '../common/protocol';
|
||||
import { URI } from '@theia/core/lib/common/uri';
|
||||
import { Path } from '@theia/core/lib/common/path';
|
||||
import { LibraryServiceImpl } from './library-service-impl';
|
||||
|
||||
interface BuiltInSketchRef {
|
||||
readonly name: string;
|
||||
@@ -84,8 +80,8 @@ export class BuiltInExamplesServiceImpl {
|
||||
|
||||
@injectable()
|
||||
export class ExamplesServiceImpl implements ExamplesService {
|
||||
@inject(LibraryService)
|
||||
private readonly libraryService: LibraryService;
|
||||
@inject(LibraryServiceImpl)
|
||||
private readonly libraryService: LibraryServiceImpl;
|
||||
|
||||
@inject(BuiltInExamplesServiceImpl)
|
||||
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
|
||||
@@ -94,7 +90,6 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
return this.builtInExamplesService.builtIns();
|
||||
}
|
||||
|
||||
@duration()
|
||||
async installed({ fqbn }: { fqbn?: string }): Promise<{
|
||||
user: SketchContainer[];
|
||||
current: SketchContainer[];
|
||||
@@ -129,11 +124,11 @@ export class ExamplesServiceImpl implements ExamplesService {
|
||||
* location of the examples. Otherwise it creates the example container from the direct examples FS paths.
|
||||
*/
|
||||
private async tryGroupExamples({
|
||||
label,
|
||||
name,
|
||||
exampleUris,
|
||||
installDirUri,
|
||||
}: LibraryPackage): Promise<SketchContainer> {
|
||||
const container = SketchContainer.create(label);
|
||||
const container = SketchContainer.create(name);
|
||||
if (!installDirUri || !exampleUris.length) {
|
||||
return container;
|
||||
}
|
||||
|
@@ -1,4 +1,11 @@
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
IndexType,
|
||||
IndexUpdateDidCompleteParams,
|
||||
IndexUpdateDidFailParams,
|
||||
IndexUpdateSummary,
|
||||
IndexUpdateWillStartParams,
|
||||
} from '../common/protocol';
|
||||
import {
|
||||
ProgressMessage,
|
||||
ResponseService,
|
||||
@@ -11,6 +18,9 @@ import {
|
||||
import {
|
||||
DownloadProgress,
|
||||
TaskProgress,
|
||||
DownloadProgressStart,
|
||||
DownloadProgressUpdate,
|
||||
DownloadProgressEnd,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
|
||||
import { CompileResponse } from './cli-protocol/cc/arduino/cli/commands/v1/compile_pb';
|
||||
import {
|
||||
@@ -81,7 +91,9 @@ namespace IndexProgressResponse {
|
||||
);
|
||||
}
|
||||
export function workUnit(response: IndexProgressResponse): UnitOfWork {
|
||||
return { download: response.getDownloadProgress() };
|
||||
return {
|
||||
download: response.getDownloadProgress(),
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -152,26 +164,33 @@ export namespace ExecuteWithProgress {
|
||||
*/
|
||||
readonly progressId?: string;
|
||||
readonly responseService: Partial<ResponseService>;
|
||||
/**
|
||||
* It's only relevant for index updates to build a summary of possible client (4xx) and server (5xx) errors when downloading the files during the index update. It's missing for lib/platform installations.
|
||||
*/
|
||||
readonly reportResult?: (result: DownloadResult) => void;
|
||||
}
|
||||
|
||||
export function createDataCallback<R extends ProgressResponse>({
|
||||
responseService,
|
||||
progressId,
|
||||
reportResult,
|
||||
}: ExecuteWithProgress.Options): (response: R) => void {
|
||||
const uuid = v4();
|
||||
let localFile = '';
|
||||
let localTotalSize = Number.NaN;
|
||||
let message = '';
|
||||
let url = '';
|
||||
return (response: R) => {
|
||||
if (DEBUG) {
|
||||
const json = toJson(response);
|
||||
if (json) {
|
||||
console.log(`Progress response [${uuid}]: ${json}`);
|
||||
console.debug(`[gRPC progress] Progress response [${uuid}]: ${json}`);
|
||||
}
|
||||
}
|
||||
const unitOfWork = resolve(response);
|
||||
const { task, download } = unitOfWork;
|
||||
if (!download && !task) {
|
||||
// report a fake unknown progress.
|
||||
// Report a fake unknown progress if progress ID is available.
|
||||
// When a progress ID is available, a connected client is setting the progress ID.
|
||||
// Hence, it's listening to progress updates.
|
||||
if (unitOfWork === UnitOfWork.Unknown && progressId) {
|
||||
if (progressId) {
|
||||
responseService.reportProgress?.({
|
||||
@@ -187,7 +206,7 @@ export namespace ExecuteWithProgress {
|
||||
// Technically, it does not cause an error, but could mess up the progress reporting.
|
||||
// See an example of an empty object `{}` repose here: https://github.com/arduino/arduino-ide/issues/906#issuecomment-1171145630.
|
||||
console.warn(
|
||||
"Implementation error. Neither 'download' nor 'task' is available."
|
||||
`Implementation error. None of the following properties were available on the response: 'task', 'download'`
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -219,43 +238,32 @@ export namespace ExecuteWithProgress {
|
||||
}
|
||||
}
|
||||
} else if (download) {
|
||||
if (download.getFile() && !localFile) {
|
||||
localFile = download.getFile();
|
||||
}
|
||||
if (download.getTotalSize() > 0 && Number.isNaN(localTotalSize)) {
|
||||
localTotalSize = download.getTotalSize();
|
||||
}
|
||||
|
||||
// This happens only once per file download.
|
||||
if (download.getTotalSize() && localFile) {
|
||||
responseService.appendToOutput?.({ chunk: `${localFile}\n` });
|
||||
}
|
||||
|
||||
if (progressId && localFile) {
|
||||
let work: ProgressMessage.Work | undefined = undefined;
|
||||
if (download.getDownloaded() > 0 && !Number.isNaN(localTotalSize)) {
|
||||
work = {
|
||||
total: localTotalSize,
|
||||
done: download.getDownloaded(),
|
||||
};
|
||||
}
|
||||
responseService.reportProgress?.({
|
||||
progressId,
|
||||
message: `Downloading ${localFile}`,
|
||||
work,
|
||||
});
|
||||
}
|
||||
if (download.getCompleted()) {
|
||||
// Discard local state.
|
||||
if (progressId && !Number.isNaN(localTotalSize)) {
|
||||
const phase = phaseOf(download);
|
||||
if (phase instanceof DownloadProgressStart) {
|
||||
message = phase.getLabel();
|
||||
url = phase.getUrl();
|
||||
responseService.appendToOutput?.({ chunk: `${message}\n` });
|
||||
} else if (phase instanceof DownloadProgressUpdate) {
|
||||
if (progressId && message) {
|
||||
responseService.reportProgress?.({
|
||||
progressId,
|
||||
message: '',
|
||||
work: { done: Number.NaN, total: Number.NaN },
|
||||
message,
|
||||
work: {
|
||||
total: phase.getTotalSize(),
|
||||
done: phase.getDownloaded(),
|
||||
},
|
||||
});
|
||||
}
|
||||
localFile = '';
|
||||
localTotalSize = Number.NaN;
|
||||
} else if (phase instanceof DownloadProgressEnd) {
|
||||
if (url && reportResult) {
|
||||
reportResult({
|
||||
url,
|
||||
message: phase.getMessage(),
|
||||
success: phase.getSuccess(),
|
||||
});
|
||||
}
|
||||
message = '';
|
||||
url = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -274,31 +282,40 @@ export namespace ExecuteWithProgress {
|
||||
return {};
|
||||
}
|
||||
function toJson(response: ProgressResponse): string | undefined {
|
||||
let object: Record<string, unknown> | undefined = undefined;
|
||||
if (response instanceof LibraryInstallResponse) {
|
||||
object = LibraryInstallResponse.toObject(false, response);
|
||||
} else if (response instanceof LibraryUninstallResponse) {
|
||||
object = LibraryUninstallResponse.toObject(false, response);
|
||||
} else if (response instanceof ZipLibraryInstallResponse) {
|
||||
object = ZipLibraryInstallResponse.toObject(false, response);
|
||||
} else if (response instanceof PlatformInstallResponse) {
|
||||
object = PlatformInstallResponse.toObject(false, response);
|
||||
} else if (response instanceof PlatformUninstallResponse) {
|
||||
object = PlatformUninstallResponse.toObject(false, response);
|
||||
} else if (response instanceof UpdateIndexResponse) {
|
||||
object = UpdateIndexResponse.toObject(false, response);
|
||||
} else if (response instanceof UpdateLibrariesIndexResponse) {
|
||||
object = UpdateLibrariesIndexResponse.toObject(false, response);
|
||||
} else if (response instanceof UpdateCoreLibrariesIndexResponse) {
|
||||
object = UpdateCoreLibrariesIndexResponse.toObject(false, response);
|
||||
} else if (response instanceof CompileResponse) {
|
||||
object = CompileResponse.toObject(false, response);
|
||||
return JSON.stringify(response.toObject(false));
|
||||
}
|
||||
function phaseOf(
|
||||
download: DownloadProgress
|
||||
): DownloadProgressStart | DownloadProgressUpdate | DownloadProgressEnd {
|
||||
let start: undefined | DownloadProgressStart = undefined;
|
||||
let update: undefined | DownloadProgressUpdate = undefined;
|
||||
let end: undefined | DownloadProgressEnd = undefined;
|
||||
if (download.hasStart()) {
|
||||
start = download.getStart();
|
||||
} else if (download.hasUpdate()) {
|
||||
update = download.getUpdate();
|
||||
} else if (download.hasEnd()) {
|
||||
end = download.getEnd();
|
||||
} else {
|
||||
throw new Error(
|
||||
`Download progress does not have a 'start', 'update', and 'end'. ${JSON.stringify(
|
||||
download.toObject(false)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
if (!object) {
|
||||
console.warn('Unhandled gRPC response', response);
|
||||
return undefined;
|
||||
if (start) {
|
||||
return start;
|
||||
} else if (update) {
|
||||
return update;
|
||||
} else if (end) {
|
||||
return end;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Download progress does not have a 'start', 'update', and 'end'. ${JSON.stringify(
|
||||
download.toObject(false)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,33 +323,39 @@ export class IndexesUpdateProgressHandler {
|
||||
private done = 0;
|
||||
private readonly total: number;
|
||||
readonly progressId: string;
|
||||
readonly results: DownloadResult[];
|
||||
|
||||
constructor(
|
||||
private types: IndexType[],
|
||||
additionalUrlsCount: number,
|
||||
private readonly onProgress: (progressMessage: ProgressMessage) => void,
|
||||
private readonly onError?: ({
|
||||
progressId,
|
||||
message,
|
||||
}: {
|
||||
progressId: string;
|
||||
message: string;
|
||||
}) => void,
|
||||
private readonly onStart?: (progressId: string) => void,
|
||||
private readonly onEnd?: (progressId: string) => void
|
||||
private readonly options: {
|
||||
onProgress: (progressMessage: ProgressMessage) => void;
|
||||
onError?: (params: IndexUpdateDidFailParams) => void;
|
||||
onStart?: (params: IndexUpdateWillStartParams) => void;
|
||||
onComplete?: (params: IndexUpdateDidCompleteParams) => void;
|
||||
}
|
||||
) {
|
||||
this.progressId = v4();
|
||||
this.total = IndexesUpdateProgressHandler.total(additionalUrlsCount);
|
||||
this.results = [];
|
||||
this.total = IndexesUpdateProgressHandler.total(types, additionalUrlsCount);
|
||||
// Note: at this point, the IDE2 backend might not have any connected clients, so this notification is not delivered to anywhere
|
||||
// Hence, clients must handle gracefully when no `willUpdate` is received before any `didProgress`.
|
||||
this.onStart?.(this.progressId);
|
||||
// Hence, clients must handle gracefully when no `willStart` event is received before any `didProgress`.
|
||||
this.options.onStart?.({ progressId: this.progressId, types });
|
||||
}
|
||||
|
||||
reportEnd(): void {
|
||||
this.onEnd?.(this.progressId);
|
||||
const updatedAt = new Date().toISOString();
|
||||
this.options.onComplete?.({
|
||||
progressId: this.progressId,
|
||||
summary: this.types.reduce((summary, type) => {
|
||||
summary[type] = updatedAt;
|
||||
return summary;
|
||||
}, {} as IndexUpdateSummary),
|
||||
});
|
||||
}
|
||||
|
||||
reportProgress(message: string): void {
|
||||
this.onProgress({
|
||||
this.options.onProgress({
|
||||
message,
|
||||
progressId: this.progressId,
|
||||
work: { total: this.total, done: ++this.done },
|
||||
@@ -340,15 +363,44 @@ export class IndexesUpdateProgressHandler {
|
||||
}
|
||||
|
||||
reportError(message: string): void {
|
||||
this.onError?.({ progressId: this.progressId, message });
|
||||
this.options.onError?.({
|
||||
progressId: this.progressId,
|
||||
message,
|
||||
types: this.types,
|
||||
});
|
||||
}
|
||||
|
||||
private static total(additionalUrlsCount: number): number {
|
||||
// +1 for the `package_index.tar.bz2` when updating the platform index.
|
||||
const totalPlatformIndexCount = additionalUrlsCount + 1;
|
||||
// The `library_index.json.gz` and `library_index.json.sig` when running the library index update.
|
||||
const totalLibraryIndexCount = 2;
|
||||
reportResult(result: DownloadResult): void {
|
||||
this.results.push(result);
|
||||
}
|
||||
|
||||
private static total(
|
||||
types: IndexType[],
|
||||
additionalUrlsCount: number
|
||||
): number {
|
||||
let total = 0;
|
||||
if (types.includes('library')) {
|
||||
// The `library_index.json.gz` and `library_index.json.sig` when running the library index update.
|
||||
total += 2;
|
||||
}
|
||||
if (types.includes('platform')) {
|
||||
// +1 for the `package_index.tar.bz2` when updating the platform index.
|
||||
total += additionalUrlsCount + 1;
|
||||
}
|
||||
// +1 for the `initInstance` call after the index update (`reportEnd`)
|
||||
return totalPlatformIndexCount + totalLibraryIndexCount + 1;
|
||||
return total + 1;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DownloadResult {
|
||||
readonly url: string;
|
||||
readonly success: boolean;
|
||||
readonly message?: string;
|
||||
}
|
||||
export namespace DownloadResult {
|
||||
export function isError(
|
||||
arg: DownloadResult
|
||||
): arg is DownloadResult & { message: string } {
|
||||
return !!arg.message && !arg.success;
|
||||
}
|
||||
}
|
||||
|
@@ -3,150 +3,45 @@ import {
|
||||
LocalizationRegistry,
|
||||
} from '@theia/core/lib/node/i18n/localization-contribution';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { join } from 'path';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoLocalizationContribution
|
||||
implements LocalizationContribution
|
||||
{
|
||||
// 0. index: locale
|
||||
// 1. index: optional JSON file to `require` (if differs from the locale)
|
||||
// If you touch the locales, please keep the alphabetical order. Also in the `package.json` for the VS Code language packs. Thank you! ❤️
|
||||
// Note that IDE2 has more translations than available VS Code language packs. (https://github.com/arduino/arduino-ide/issues/1447)
|
||||
private readonly locales: ReadonlyArray<[string, string?]> = [
|
||||
['bg'],
|
||||
['cs'],
|
||||
['de'],
|
||||
['es'],
|
||||
['fr'],
|
||||
['hu'],
|
||||
// ['id'], Does not have Transifex translations, but has a VS Code language pack available on Open VSX.
|
||||
['it'],
|
||||
['ja'],
|
||||
['ko'],
|
||||
['nl'],
|
||||
['pl'],
|
||||
['pt-br', 'pt'],
|
||||
['ru'],
|
||||
['tr'],
|
||||
['uk', 'uk_UA'],
|
||||
['zh-cn', 'zh'],
|
||||
];
|
||||
|
||||
async registerLocalizations(registry: LocalizationRegistry): Promise<void> {
|
||||
registry.registerLocalizationFromRequire(
|
||||
'af',
|
||||
require('../../../build/i18n/af.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'en',
|
||||
require('../../../build/i18n/en.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'fr',
|
||||
require('../../../build/i18n/fr.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'ko',
|
||||
require('../../../build/i18n/ko.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'pt-br',
|
||||
require('../../../build/i18n/pt.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'uk_UA',
|
||||
require('../../../build/i18n/uk_UA.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'ar',
|
||||
require('../../../build/i18n/ar.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'es',
|
||||
require('../../../build/i18n/es.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'he',
|
||||
require('../../../build/i18n/he.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'my_MM',
|
||||
require('../../../build/i18n/my_MM.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'ro',
|
||||
require('../../../build/i18n/ro.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'zh-cn',
|
||||
require('../../../build/i18n/zh.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'bg',
|
||||
require('../../../build/i18n/bg.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'eu',
|
||||
require('../../../build/i18n/eu.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'hu',
|
||||
require('../../../build/i18n/hu.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'ne',
|
||||
require('../../../build/i18n/ne.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'ru',
|
||||
require('../../../build/i18n/ru.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'zh_TW',
|
||||
require('../../../build/i18n/zh_TW.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'de',
|
||||
require('../../../build/i18n/de.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'fa',
|
||||
require('../../../build/i18n/fa.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'it',
|
||||
require('../../../build/i18n/it.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'nl',
|
||||
require('../../../build/i18n/nl.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'sv_SE',
|
||||
require('../../../build/i18n/sv_SE.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'el',
|
||||
require('../../../build/i18n/el.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'fil',
|
||||
require('../../../build/i18n/fil.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'ja',
|
||||
require('../../../build/i18n/ja.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'pl',
|
||||
require('../../../build/i18n/pl.json')
|
||||
);
|
||||
|
||||
registry.registerLocalizationFromRequire(
|
||||
'tr',
|
||||
require('../../../build/i18n/tr.json')
|
||||
);
|
||||
for (const [locale, jsonFilename] of this.locales) {
|
||||
registry.registerLocalizationFromRequire(
|
||||
locale,
|
||||
require(join(
|
||||
__dirname,
|
||||
`../../../build/i18n/${jsonFilename ?? locale}.json`
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -23,8 +23,8 @@ export class LocalizationBackendContribution extends TheiaLocalizationBackendCon
|
||||
app.get('/i18n/:locale', async (req, res) => {
|
||||
let locale = req.params.locale;
|
||||
/*
|
||||
Waiting for the deploy of the language plugins is neecessary to avoid checking the available
|
||||
languages before they're finished to be loaded: https://github.com/eclipse-theia/theia/issues/11471
|
||||
Waiting for the deploy of the language plugins is necessary to avoid checking the available
|
||||
languages before they're finished to be loaded: https://github.com/eclipse-theia/theia/issues/11471
|
||||
*/
|
||||
const start = performance.now();
|
||||
await this.initialized.promise;
|
||||
|
@@ -66,7 +66,7 @@ export class LibraryServiceImpl
|
||||
if (installedLib.hasLibrary()) {
|
||||
const lib = installedLib.getLibrary();
|
||||
if (lib) {
|
||||
installedLibsIdx.set(lib.getRealName(), installedLib);
|
||||
installedLibsIdx.set(lib.getName(), installedLib);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,6 @@ export class LibraryServiceImpl
|
||||
return toLibrary(
|
||||
{
|
||||
name: library.getName(),
|
||||
label: library.getRealName(),
|
||||
installedVersion,
|
||||
installable: true,
|
||||
description: library.getSentence(),
|
||||
@@ -443,7 +442,6 @@ function toLibrary(
|
||||
): LibraryPackage {
|
||||
return {
|
||||
name: '',
|
||||
label: '',
|
||||
exampleUris: [],
|
||||
installable: false,
|
||||
deprecated: false,
|
||||
|
@@ -8,6 +8,9 @@ import type {
|
||||
Config,
|
||||
Sketch,
|
||||
ProgressMessage,
|
||||
IndexUpdateWillStartParams,
|
||||
IndexUpdateDidCompleteParams,
|
||||
IndexUpdateDidFailParams,
|
||||
} from '../common/protocol';
|
||||
|
||||
@injectable()
|
||||
@@ -16,8 +19,8 @@ export class NotificationServiceServerImpl
|
||||
{
|
||||
private readonly clients: NotificationServiceClient[] = [];
|
||||
|
||||
notifyIndexWillUpdate(progressId: string): void {
|
||||
this.clients.forEach((client) => client.notifyIndexWillUpdate(progressId));
|
||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
|
||||
this.clients.forEach((client) => client.notifyIndexUpdateWillStart(params));
|
||||
}
|
||||
|
||||
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
|
||||
@@ -26,20 +29,14 @@ export class NotificationServiceServerImpl
|
||||
);
|
||||
}
|
||||
|
||||
notifyIndexDidUpdate(progressId: string): void {
|
||||
this.clients.forEach((client) => client.notifyIndexDidUpdate(progressId));
|
||||
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void {
|
||||
this.clients.forEach((client) =>
|
||||
client.notifyIndexUpdateDidComplete(params)
|
||||
);
|
||||
}
|
||||
|
||||
notifyIndexUpdateDidFail({
|
||||
progressId,
|
||||
message,
|
||||
}: {
|
||||
progressId: string;
|
||||
message: string;
|
||||
}): void {
|
||||
this.clients.forEach((client) =>
|
||||
client.notifyIndexUpdateDidFail({ progressId, message })
|
||||
);
|
||||
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void {
|
||||
this.clients.forEach((client) => client.notifyIndexUpdateDidFail(params));
|
||||
}
|
||||
|
||||
notifyDaemonDidStart(port: string): void {
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import * as fs from 'fs';
|
||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||
import { promises as fs, realpath, lstat, Stats, constants, rm } from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as temp from 'temp';
|
||||
|
||||
import * as path from 'path';
|
||||
import * as glob from 'glob';
|
||||
import * as crypto from 'crypto';
|
||||
import * as PQueue from 'p-queue';
|
||||
import { ncp } from 'ncp';
|
||||
import { promisify } from 'util';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { FileUri } from '@theia/core/lib/node';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import { ConfigServiceImpl } from './config-service-impl';
|
||||
import {
|
||||
SketchesService,
|
||||
@@ -24,8 +25,6 @@ import {
|
||||
ArchiveSketchRequest,
|
||||
LoadSketchRequest,
|
||||
} from './cli-protocol/cc/arduino/cli/commands/v1/commands_pb';
|
||||
import { duration } from '../common/decorators';
|
||||
import * as glob from 'glob';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { ServiceError } from './service-error';
|
||||
import {
|
||||
@@ -33,6 +32,19 @@ import {
|
||||
maybeNormalizeDrive,
|
||||
TempSketchPrefix,
|
||||
} from './is-temp-sketch';
|
||||
import { join } from 'path';
|
||||
|
||||
const RecentSketches = 'recent-sketches.json';
|
||||
const DefaultIno = `void setup() {
|
||||
// put your setup code here, to run once:
|
||||
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// put your main code here, to run repeatedly:
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
@injectable()
|
||||
export class SketchesServiceImpl
|
||||
@@ -41,6 +53,16 @@ export class SketchesServiceImpl
|
||||
{
|
||||
private sketchSuffixIndex = 1;
|
||||
private lastSketchBaseName: string;
|
||||
private recentSketches: SketchWithDetails[] | undefined;
|
||||
private readonly markAsRecentSketchQueue = new PQueue({
|
||||
autoStart: true,
|
||||
concurrency: 1,
|
||||
});
|
||||
private inoContent: Deferred<string> | undefined;
|
||||
|
||||
@inject(ILogger)
|
||||
@named('sketches-service')
|
||||
private readonly logger: ILogger;
|
||||
|
||||
@inject(ConfigServiceImpl)
|
||||
private readonly configService: ConfigServiceImpl;
|
||||
@@ -54,28 +76,7 @@ export class SketchesServiceImpl
|
||||
@inject(IsTempSketch)
|
||||
private readonly isTempSketch: IsTempSketch;
|
||||
|
||||
async getSketches({
|
||||
uri,
|
||||
exclude,
|
||||
}: {
|
||||
uri?: string;
|
||||
exclude?: string[];
|
||||
}): Promise<SketchContainer> {
|
||||
const [/*old,*/ _new] = await Promise.all([
|
||||
// this.getSketchesOld({ uri, exclude }),
|
||||
this.getSketchesNew({ uri, exclude }),
|
||||
]);
|
||||
return _new;
|
||||
}
|
||||
|
||||
@duration()
|
||||
async getSketchesNew({
|
||||
uri,
|
||||
exclude,
|
||||
}: {
|
||||
uri?: string;
|
||||
exclude?: string[];
|
||||
}): Promise<SketchContainer> {
|
||||
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
|
||||
const root = await this.root(uri);
|
||||
const pathToAllSketchFiles = await new Promise<string[]>(
|
||||
(resolve, reject) => {
|
||||
@@ -138,7 +139,7 @@ export class SketchesServiceImpl
|
||||
for (const pathToSketchFile of pathToAllSketchFiles) {
|
||||
const relative = path.relative(root, pathToSketchFile);
|
||||
if (!relative) {
|
||||
console.warn(
|
||||
this.logger.warn(
|
||||
`Could not determine relative sketch path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Relative path was: ${relative}`
|
||||
);
|
||||
continue;
|
||||
@@ -146,7 +147,7 @@ export class SketchesServiceImpl
|
||||
const segments = relative.split(path.sep);
|
||||
if (segments.length < 2) {
|
||||
// folder name, and sketch name.
|
||||
console.warn(
|
||||
this.logger.warn(
|
||||
`Expected at least one segment relative path from the root <${root}> to the sketch <${pathToSketchFile}>. Skipping. Segments were: ${segments}.`
|
||||
);
|
||||
continue;
|
||||
@@ -160,7 +161,7 @@ export class SketchesServiceImpl
|
||||
''
|
||||
);
|
||||
if (sketchFileExtension !== '.ino' && sketchFileExtension !== '.pde') {
|
||||
console.warn(
|
||||
this.logger.warn(
|
||||
`Mismatching sketch file <${sketchFilename}> and sketch folder name <${sketchName}>. Skipping`
|
||||
);
|
||||
continue;
|
||||
@@ -169,7 +170,7 @@ export class SketchesServiceImpl
|
||||
if (child) {
|
||||
child.sketches.push({
|
||||
name: sketchName,
|
||||
uri: FileUri.create(pathToSketchFile).toString(),
|
||||
uri: FileUri.create(path.dirname(pathToSketchFile)).toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -191,36 +192,40 @@ export class SketchesServiceImpl
|
||||
const requestSketchPath = FileUri.fsPath(uri);
|
||||
req.setSketchPath(requestSketchPath);
|
||||
req.setInstance(instance);
|
||||
const stat = new Deferred<fs.Stats | Error>();
|
||||
fs.lstat(requestSketchPath, (err, result) =>
|
||||
const stat = new Deferred<Stats | Error>();
|
||||
lstat(requestSketchPath, (err, result) =>
|
||||
err ? stat.resolve(err) : stat.resolve(result)
|
||||
);
|
||||
const sketch = await new Promise<SketchWithDetails>((resolve, reject) => {
|
||||
client.loadSketch(req, async (err, resp) => {
|
||||
if (err) {
|
||||
reject(
|
||||
isNotFoundError(err)
|
||||
? SketchesError.NotFound(
|
||||
fixErrorMessage(
|
||||
err,
|
||||
requestSketchPath,
|
||||
this.configService.cliConfiguration?.directories.user
|
||||
),
|
||||
uri
|
||||
)
|
||||
: err
|
||||
);
|
||||
let rejectWith: unknown = err;
|
||||
if (isNotFoundError(err)) {
|
||||
const invalidMainSketchFilePath = await isInvalidSketchNameError(
|
||||
err,
|
||||
requestSketchPath
|
||||
);
|
||||
if (invalidMainSketchFilePath) {
|
||||
rejectWith = SketchesError.InvalidName(
|
||||
err.details,
|
||||
FileUri.create(invalidMainSketchFilePath).toString()
|
||||
);
|
||||
} else {
|
||||
rejectWith = SketchesError.NotFound(err.details, uri);
|
||||
}
|
||||
}
|
||||
reject(rejectWith);
|
||||
return;
|
||||
}
|
||||
const responseSketchPath = maybeNormalizeDrive(resp.getLocationPath());
|
||||
if (requestSketchPath !== responseSketchPath) {
|
||||
console.warn(
|
||||
this.logger.warn(
|
||||
`Warning! The request sketch path was different than the response sketch path from the CLI. This could be a potential bug. Request: <${requestSketchPath}>, response: <${responseSketchPath}>.`
|
||||
);
|
||||
}
|
||||
const resolvedStat = await stat.promise;
|
||||
if (resolvedStat instanceof Error) {
|
||||
console.error(
|
||||
this.logger.error(
|
||||
`The CLI could load the sketch from ${requestSketchPath}, but stating the folder has failed.`
|
||||
);
|
||||
reject(resolvedStat);
|
||||
@@ -254,89 +259,163 @@ export class SketchesServiceImpl
|
||||
private get recentSketchesFsPath(): Promise<string> {
|
||||
return this.envVariableServer
|
||||
.getConfigDirUri()
|
||||
.then((uri) => path.join(FileUri.fsPath(uri), 'recent-sketches.json'));
|
||||
.then((uri) => path.join(FileUri.fsPath(uri), RecentSketches));
|
||||
}
|
||||
|
||||
private async loadRecentSketches(
|
||||
fsPath: string
|
||||
): Promise<Record<string, number>> {
|
||||
private async loadRecentSketches(): Promise<Record<string, number>> {
|
||||
this.logger.debug(`>>> Loading recently opened sketches data.`);
|
||||
const fsPath = await this.recentSketchesFsPath;
|
||||
let data: Record<string, number> = {};
|
||||
try {
|
||||
const raw = await promisify(fs.readFile)(fsPath, {
|
||||
const raw = await fs.readFile(fsPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
data = JSON.parse(raw);
|
||||
} catch {}
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Could not parse recently opened sketches. Raw input was: ${raw}`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if ('code' in err && err.code === 'ENOENT') {
|
||||
this.logger.debug(
|
||||
`<<< '${RecentSketches}' does not exist yet. This is normal behavior. Falling back to empty data.`
|
||||
);
|
||||
return {};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
this.logger.debug(
|
||||
`<<< Successfully loaded recently opened sketches data: ${JSON.stringify(
|
||||
data
|
||||
)}`
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async markAsRecentlyOpened(uri: string): Promise<void> {
|
||||
let sketch: Sketch | undefined = undefined;
|
||||
try {
|
||||
sketch = await this.loadSketch(uri);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (await this.isTemp(sketch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fsPath = await this.recentSketchesFsPath;
|
||||
const data = await this.loadRecentSketches(fsPath);
|
||||
const now = Date.now();
|
||||
data[sketch.uri] = now;
|
||||
|
||||
let toDeleteUri: string | undefined = undefined;
|
||||
if (Object.keys(data).length > 10) {
|
||||
let min = Number.MAX_SAFE_INTEGER;
|
||||
for (const uri of Object.keys(data)) {
|
||||
if (min > data[uri]) {
|
||||
min = data[uri];
|
||||
toDeleteUri = uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toDeleteUri) {
|
||||
delete data[toDeleteUri];
|
||||
}
|
||||
|
||||
await promisify(fs.writeFile)(fsPath, JSON.stringify(data, null, 2));
|
||||
this.recentlyOpenedSketches().then((sketches) =>
|
||||
this.notificationService.notifyRecentSketchesDidChange({ sketches })
|
||||
private async saveRecentSketches(
|
||||
data: Record<string, number>
|
||||
): Promise<void> {
|
||||
this.logger.debug(
|
||||
`>>> Saving recently opened sketches data: ${JSON.stringify(data)}`
|
||||
);
|
||||
const fsPath = await this.recentSketchesFsPath;
|
||||
await fs.writeFile(fsPath, JSON.stringify(data, null, 2));
|
||||
this.logger.debug('<<< Successfully saved recently opened sketches data.');
|
||||
}
|
||||
|
||||
async recentlyOpenedSketches(): Promise<Sketch[]> {
|
||||
const configDirUri = await this.envVariableServer.getConfigDirUri();
|
||||
const fsPath = path.join(
|
||||
FileUri.fsPath(configDirUri),
|
||||
'recent-sketches.json'
|
||||
);
|
||||
let data: Record<string, number> = {};
|
||||
try {
|
||||
const raw = await promisify(fs.readFile)(fsPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
data = JSON.parse(raw);
|
||||
} catch {}
|
||||
async markAsRecentlyOpened(uri: string): Promise<void> {
|
||||
return this.markAsRecentSketchQueue.add(async () => {
|
||||
this.logger.debug(`Marking sketch at '${uri}' as recently opened.`);
|
||||
if (this.isTempSketch.is(FileUri.fsPath(uri))) {
|
||||
this.logger.debug(
|
||||
`Sketch at '${uri}' is pointing to a temp location. Not marking as recently opened.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sketches: SketchWithDetails[] = [];
|
||||
for (const uri of Object.keys(data).sort(
|
||||
(left, right) => data[right] - data[left]
|
||||
)) {
|
||||
let sketch: Sketch | undefined = undefined;
|
||||
try {
|
||||
const sketch = await this.loadSketch(uri);
|
||||
sketches.push(sketch);
|
||||
} catch {}
|
||||
}
|
||||
sketch = await this.loadSketch(uri);
|
||||
this.logger.debug(
|
||||
`Loaded sketch ${JSON.stringify(
|
||||
sketch
|
||||
)} before marking it as recently opened.`
|
||||
);
|
||||
} catch (err) {
|
||||
if (
|
||||
SketchesError.NotFound.is(err) ||
|
||||
SketchesError.InvalidName.is(err)
|
||||
) {
|
||||
this.logger.debug(
|
||||
`Could not load sketch from '${uri}'. Not marking as recently opened.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.logger.error(
|
||||
`Unexpected error occurred while loading sketch from '${uri}'.`,
|
||||
err
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return sketches;
|
||||
const data = await this.loadRecentSketches();
|
||||
const now = Date.now();
|
||||
this.logger.debug(
|
||||
`Marking sketch '${uri}' as recently opened with timestamp: '${now}'.`
|
||||
);
|
||||
data[sketch.uri] = now;
|
||||
|
||||
let toDelete: [string, number] | undefined = undefined;
|
||||
if (Object.keys(data).length > 10) {
|
||||
let min = Number.MAX_SAFE_INTEGER;
|
||||
for (const [uri, timestamp] of Object.entries(data)) {
|
||||
if (min > timestamp) {
|
||||
min = data[uri];
|
||||
toDelete = [uri, timestamp];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toDelete) {
|
||||
const [toDeleteUri] = toDelete;
|
||||
delete data[toDeleteUri];
|
||||
this.logger.debug(
|
||||
`Deleted sketch entry ${JSON.stringify(
|
||||
toDelete
|
||||
)} from recently opened.`
|
||||
);
|
||||
}
|
||||
|
||||
await this.saveRecentSketches(data);
|
||||
this.logger.debug(`Marked sketch '${uri}' as recently opened.`);
|
||||
const sketches = await this.recentlyOpenedSketches(data);
|
||||
this.notificationService.notifyRecentSketchesDidChange({ sketches });
|
||||
});
|
||||
}
|
||||
|
||||
async recentlyOpenedSketches(
|
||||
forceUpdate?: Record<string, number> | boolean
|
||||
): Promise<Sketch[]> {
|
||||
if (!this.recentSketches || forceUpdate) {
|
||||
const data =
|
||||
forceUpdate && typeof forceUpdate === 'object'
|
||||
? forceUpdate
|
||||
: await this.loadRecentSketches();
|
||||
const sketches: SketchWithDetails[] = [];
|
||||
let needsUpdate = false;
|
||||
for (const uri of Object.keys(data).sort(
|
||||
(left, right) => data[right] - data[left]
|
||||
)) {
|
||||
let sketch: SketchWithDetails | undefined = undefined;
|
||||
try {
|
||||
sketch = await this.loadSketch(uri);
|
||||
} catch {}
|
||||
if (!sketch) {
|
||||
needsUpdate = true;
|
||||
} else {
|
||||
sketches.push(sketch);
|
||||
}
|
||||
}
|
||||
if (needsUpdate) {
|
||||
const data = sketches.reduce((acc, curr) => {
|
||||
acc[curr.uri] = curr.mtimeMs;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
await this.saveRecentSketches(data);
|
||||
this.notificationService.notifyRecentSketchesDidChange({ sketches });
|
||||
}
|
||||
this.recentSketches = sketches;
|
||||
}
|
||||
return this.recentSketches;
|
||||
}
|
||||
|
||||
async cloneExample(uri: string): Promise<Sketch> {
|
||||
const sketch = await this.loadSketch(uri);
|
||||
const parentPath = await this.createTempFolder();
|
||||
const [sketch, parentPath] = await Promise.all([
|
||||
this.loadSketch(uri),
|
||||
this.createTempFolder(),
|
||||
]);
|
||||
const destinationUri = FileUri.create(
|
||||
path.join(parentPath, sketch.name)
|
||||
).toString();
|
||||
@@ -377,7 +456,7 @@ export class SketchesServiceImpl
|
||||
this.sketchSuffixIndex++
|
||||
)}`;
|
||||
// Note: we check the future destination folder (`directories.user`) for name collision and not the temp folder!
|
||||
const sketchExists = await promisify(fs.exists)(
|
||||
const sketchExists = await this.exists(
|
||||
path.join(sketchbookPath, sketchNameCandidate)
|
||||
);
|
||||
if (!sketchExists) {
|
||||
@@ -393,21 +472,11 @@ export class SketchesServiceImpl
|
||||
|
||||
const sketchDir = path.join(parentPath, sketchName);
|
||||
const sketchFile = path.join(sketchDir, `${sketchName}.ino`);
|
||||
await promisify(fs.mkdir)(sketchDir, { recursive: true });
|
||||
await promisify(fs.writeFile)(
|
||||
sketchFile,
|
||||
`void setup() {
|
||||
// put your setup code here, to run once:
|
||||
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// put your main code here, to run repeatedly:
|
||||
|
||||
}
|
||||
`,
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
const [inoContent] = await Promise.all([
|
||||
this.loadInoContent(),
|
||||
fs.mkdir(sketchDir, { recursive: true }),
|
||||
]);
|
||||
await fs.writeFile(sketchFile, inoContent, { encoding: 'utf8' });
|
||||
return this.loadSketch(FileUri.create(sketchDir).toString());
|
||||
}
|
||||
|
||||
@@ -424,7 +493,7 @@ void loop() {
|
||||
reject(createError);
|
||||
return;
|
||||
}
|
||||
fs.realpath.native(dirPath, (resolveError, resolvedDirPath) => {
|
||||
realpath.native(dirPath, (resolveError, resolvedDirPath) => {
|
||||
if (resolveError) {
|
||||
reject(resolveError);
|
||||
return;
|
||||
@@ -462,7 +531,7 @@ void loop() {
|
||||
const sketch = await this.loadSketch(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
if (SketchesError.NotFound.is(err) || SketchesError.InvalidName.is(err)) {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
@@ -478,7 +547,7 @@ void loop() {
|
||||
{ destinationUri }: { destinationUri: string }
|
||||
): Promise<string> {
|
||||
const source = FileUri.fsPath(sketch.uri);
|
||||
const exists = await promisify(fs.exists)(source);
|
||||
const exists = await this.exists(source);
|
||||
if (!exists) {
|
||||
throw new Error(`Sketch does not exist: ${sketch}`);
|
||||
}
|
||||
@@ -503,7 +572,7 @@ void loop() {
|
||||
);
|
||||
const newPath = path.join(destinationPath, `${newName}.ino`);
|
||||
if (oldPath !== newPath) {
|
||||
await promisify(fs.rename)(oldPath, newPath);
|
||||
await fs.rename(oldPath, newPath);
|
||||
}
|
||||
await this.loadSketch(FileUri.create(destinationPath).toString()); // Sanity check.
|
||||
resolve();
|
||||
@@ -520,7 +589,7 @@ void loop() {
|
||||
const destination = FileUri.fsPath(destinationUri);
|
||||
let tempDestination = await this.createTempFolder();
|
||||
tempDestination = path.join(tempDestination, sketch.name);
|
||||
await fs.promises.mkdir(tempDestination, { recursive: true });
|
||||
await fs.mkdir(tempDestination, { recursive: true });
|
||||
await copy(source, tempDestination);
|
||||
await copy(tempDestination, destination);
|
||||
return FileUri.create(destination).toString();
|
||||
@@ -531,8 +600,8 @@ void loop() {
|
||||
const { client } = await this.coreClient;
|
||||
const archivePath = FileUri.fsPath(destinationUri);
|
||||
// The CLI cannot override existing archives, so we have to wipe it manually: https://github.com/arduino/arduino-cli/issues/1160
|
||||
if (await promisify(fs.exists)(archivePath)) {
|
||||
await promisify(fs.unlink)(archivePath);
|
||||
if (await this.exists(archivePath)) {
|
||||
await fs.unlink(archivePath);
|
||||
}
|
||||
const req = new ArchiveSketchRequest();
|
||||
req.setSketchPath(FileUri.fsPath(sketch.uri));
|
||||
@@ -556,7 +625,7 @@ void loop() {
|
||||
|
||||
async getIdeTempFolderPath(sketch: Sketch): Promise<string> {
|
||||
const sketchPath = FileUri.fsPath(sketch.uri);
|
||||
await fs.promises.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
|
||||
await fs.readdir(sketchPath); // Validates the sketch folder and rejects if not accessible.
|
||||
const suffix = crypto.createHash('md5').update(sketchPath).digest('hex');
|
||||
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
|
||||
}
|
||||
@@ -564,57 +633,148 @@ void loop() {
|
||||
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) => {
|
||||
rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
|
||||
if (error) {
|
||||
console.error(`Failed to delete sketch at ${sketchPath}.`, error);
|
||||
this.logger.error(`Failed to delete sketch at ${sketchPath}.`, error);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log(`Successfully deleted sketch at ${sketchPath}.`);
|
||||
this.logger.info(`Successfully deleted sketch at ${sketchPath}.`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async exists(pathLike: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(pathLike, constants.R_OK | constants.W_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the default.ino from the settings or from default folder.
|
||||
private async readSettings(): Promise<Record<string, unknown> | undefined> {
|
||||
const configDirUri = await this.envVariableServer.getConfigDirUri();
|
||||
const configDirPath = FileUri.fsPath(configDirUri);
|
||||
|
||||
try {
|
||||
const raw = await fs.readFile(join(configDirPath, 'settings.json'), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
return this.tryParse(raw);
|
||||
} catch (err) {
|
||||
if ('code' in err && err.code === 'ENOENT') {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private tryParse(raw: string): Record<string, unknown> | undefined {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the default.ino from the settings or from default folder.
|
||||
private async loadInoContent(): Promise<string> {
|
||||
if (!this.inoContent) {
|
||||
this.inoContent = new Deferred<string>();
|
||||
const settings = await this.readSettings();
|
||||
if (settings) {
|
||||
const inoBlueprintPath = settings['arduino.sketch.inoBlueprint'];
|
||||
if (inoBlueprintPath && typeof inoBlueprintPath === 'string') {
|
||||
try {
|
||||
const inoContent = await fs.readFile(inoBlueprintPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
this.inoContent.resolve(inoContent);
|
||||
} catch (err) {
|
||||
if ('code' in err && err.code === 'ENOENT') {
|
||||
// Ignored. The custom `.ino` blueprint file is optional.
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.inoContent.resolve(DefaultIno);
|
||||
}
|
||||
|
||||
return this.inoContent.promise;
|
||||
}
|
||||
}
|
||||
|
||||
interface SketchWithDetails extends Sketch {
|
||||
readonly mtimeMs: number;
|
||||
}
|
||||
|
||||
// https://github.com/arduino/arduino-cli/issues/1797
|
||||
function fixErrorMessage(
|
||||
err: ServiceError,
|
||||
sketchPath: string,
|
||||
sketchbookPath: string | undefined
|
||||
): string {
|
||||
if (!sketchbookPath) {
|
||||
return err.details; // No way to repair the error message. The current sketchbook path is not available.
|
||||
}
|
||||
// Original: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing /Users/a.kitta/Documents/Arduino/Arduino.ino`
|
||||
// Fixed: `Can't open sketch: no valid sketch found in /Users/a.kitta/Documents/Arduino: missing $sketchPath`
|
||||
const message = err.details;
|
||||
const incorrectMessageSuffix = path.join(sketchbookPath, 'Arduino.ino');
|
||||
if (
|
||||
message.startsWith("Can't open sketch: no valid sketch found in") &&
|
||||
message.endsWith(`${incorrectMessageSuffix}`)
|
||||
) {
|
||||
const sketchName = path.basename(sketchPath);
|
||||
const correctMessagePrefix = message.substring(
|
||||
0,
|
||||
message.length - incorrectMessageSuffix.length
|
||||
);
|
||||
return `${correctMessagePrefix}${path.join(
|
||||
sketchPath,
|
||||
`${sketchName}.ino`
|
||||
)}`;
|
||||
}
|
||||
return err.details;
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to detect whether the error was caused by an invalid main sketch file name.
|
||||
* IDE2 should handle gracefully when there is an invalid sketch folder name. See the [spec](https://arduino.github.io/arduino-cli/latest/sketch-specification/#sketch-root-folder) for details.
|
||||
* The CLI does not have error codes (https://github.com/arduino/arduino-cli/issues/1762), so IDE2 parses the error message and tries to guess it.
|
||||
* Nothing guarantees that the invalid existing main sketch file still exits by the time client performs the sketch move.
|
||||
*/
|
||||
async function isInvalidSketchNameError(
|
||||
cliErr: unknown,
|
||||
requestSketchPath: string
|
||||
): Promise<string | undefined> {
|
||||
if (isNotFoundError(cliErr)) {
|
||||
const ino = requestSketchPath.endsWith('.ino');
|
||||
if (ino) {
|
||||
const sketchFolderPath = path.dirname(requestSketchPath);
|
||||
const sketchName = path.basename(sketchFolderPath);
|
||||
const pattern = `${invalidSketchNameErrorRegExpPrefix}${path.join(
|
||||
sketchFolderPath,
|
||||
`${sketchName}.ino`
|
||||
)}`.replace(/\\/g, '\\\\'); // make windows path separator with \\ to have a valid regexp.
|
||||
if (new RegExp(pattern, 'i').test(cliErr.details)) {
|
||||
try {
|
||||
await fs.access(requestSketchPath);
|
||||
return requestSketchPath;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const resources = await fs.readdir(requestSketchPath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
return (
|
||||
resources
|
||||
.filter((resource) => resource.isFile())
|
||||
.filter((resource) => resource.name.endsWith('.ino'))
|
||||
// A folder might contain multiple sketches. It's OK to ick the first one as IDE2 cannot do much,
|
||||
// but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them.
|
||||
.sort(({ name: left }, { name: right }) =>
|
||||
left.localeCompare(right)
|
||||
)
|
||||
.map(({ name }) => name)
|
||||
.map((name) => path.join(requestSketchPath, name))[0]
|
||||
);
|
||||
} catch (err) {
|
||||
if ('code' in err && err.code === 'ENOTDIR') {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const invalidSketchNameErrorRegExpPrefix =
|
||||
'.*: main file missing from sketch: ';
|
||||
|
||||
/*
|
||||
* When a new sketch is created, add a suffix to distinguish it
|
||||
* from other new sketches I created today.
|
||||
|
136
arduino-ide-extension/src/test/common/searchable.test.ts
Normal file
136
arduino-ide-extension/src/test/common/searchable.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { expect } from 'chai';
|
||||
import { BoardSearch, LibrarySearch, Searchable } from '../../common/protocol';
|
||||
|
||||
interface Expectation<S extends Searchable.Options> {
|
||||
readonly uri: string;
|
||||
readonly expected: S | undefined | string;
|
||||
}
|
||||
|
||||
describe('searchable', () => {
|
||||
describe('parse', () => {
|
||||
describe(BoardSearch.UriParser.authority, () => {
|
||||
(
|
||||
[
|
||||
{
|
||||
uri: 'http://boardsmanager#SAMD',
|
||||
expected: { query: 'SAMD', type: 'All' },
|
||||
},
|
||||
{
|
||||
uri: 'http://boardsmanager/Arduino%40Heart#littleBits',
|
||||
expected: { query: 'littleBits', type: 'Arduino@Heart' },
|
||||
},
|
||||
{
|
||||
uri: 'http://boardsmanager/too/many/segments#invalidPath',
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
uri: 'http://boardsmanager/random#invalidPath',
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
uri: 'https://boardsmanager/#invalidScheme',
|
||||
expected: `Invalid 'scheme'. Expected 'http'. URI was: https://boardsmanager/#invalidScheme.`,
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/#invalidAuthority',
|
||||
expected: `Invalid 'authority'. Expected: 'boardsmanager'. URI was: http://librarymanager/#invalidAuthority.`,
|
||||
},
|
||||
] as Expectation<BoardSearch>[]
|
||||
).map((expectation) => toIt(expectation, BoardSearch.UriParser.parse));
|
||||
});
|
||||
describe(LibrarySearch.UriParser.authority, () => {
|
||||
(
|
||||
[
|
||||
{
|
||||
uri: 'http://librarymanager#WiFiNINA',
|
||||
expected: { query: 'WiFiNINA', type: 'All', topic: 'All' },
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/All/Device%20Control#Servo',
|
||||
expected: {
|
||||
query: 'Servo',
|
||||
type: 'All',
|
||||
topic: 'Device Control',
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/All/Display#SparkFun',
|
||||
expected: {
|
||||
query: 'SparkFun',
|
||||
type: 'All',
|
||||
topic: 'Display',
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/Updatable/Display#SparkFun',
|
||||
expected: {
|
||||
query: 'SparkFun',
|
||||
type: 'Updatable',
|
||||
topic: 'Display',
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/All/Signal%20Input%2FOutput#debouncer',
|
||||
expected: {
|
||||
query: 'debouncer',
|
||||
type: 'All',
|
||||
topic: 'Signal Input/Output',
|
||||
},
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/too/many/segments#invalidPath',
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
uri: 'http://librarymanager/absent/invalid#invalidPath',
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
uri: 'https://librarymanager/#invalidScheme',
|
||||
expected: `Invalid 'scheme'. Expected 'http'. URI was: https://librarymanager/#invalidScheme.`,
|
||||
},
|
||||
{
|
||||
uri: 'http://boardsmanager/#invalidAuthority',
|
||||
expected: `Invalid 'authority'. Expected: 'librarymanager'. URI was: http://boardsmanager/#invalidAuthority.`,
|
||||
},
|
||||
] as Expectation<LibrarySearch>[]
|
||||
).map((expectation) => toIt(expectation, LibrarySearch.UriParser.parse));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function toIt<S extends Searchable.Options>(
|
||||
{ uri, expected }: Expectation<S>,
|
||||
run: (uri: URI) => Searchable.Options | undefined
|
||||
): Mocha.Test {
|
||||
return it(`should ${
|
||||
typeof expected === 'string'
|
||||
? `fail to parse '${uri}'`
|
||||
: !expected
|
||||
? `not parse '${uri}'`
|
||||
: `parse '${uri}' to ${JSON.stringify(expected)}`
|
||||
}`, () => {
|
||||
if (typeof expected === 'string') {
|
||||
try {
|
||||
run(new URI(uri));
|
||||
expect.fail(
|
||||
`Expected an error with message '${expected}' when parsing URI: ${uri}.`
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err).to.be.instanceOf(Error);
|
||||
expect(err.message).to.be.equal(expected);
|
||||
}
|
||||
} else {
|
||||
const actual = run(new URI(uri));
|
||||
if (!expected) {
|
||||
expect(actual).to.be.undefined;
|
||||
} else {
|
||||
expect(actual).to.be.deep.equal(
|
||||
expected,
|
||||
`Was: ${JSON.stringify(actual)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@@ -11,14 +11,14 @@ There are several ways you can get involved:
|
||||
| - Support<br/>- Question<br/>- Discussion | Post on the [**Arduino Forum**][forum] |
|
||||
| - Bug report<br/>- Feature request | Issue report (see the guide [**here**][issues]) |
|
||||
| Testing | Beta testing, PR review (see the guide [**here**][beta-testing]) |
|
||||
| Translation | [Transifex project][translate] |
|
||||
| Translation | See the guide [**here**][translate] |
|
||||
| - Bug fix<br/>- Enhancement | Pull request (see the guide [**here**][prs]) |
|
||||
| Monetary | - [Donate][donate]<br/>- [Sponsor][sponsor]<br/>- [Buy official products][store] |
|
||||
|
||||
[forum]: https://forum.arduino.cc
|
||||
[issues]: contributor-guide/issues.md#issue-report-guide
|
||||
[beta-testing]: contributor-guide/beta-testing.md#beta-testing-guide
|
||||
[translate]: https://www.transifex.com/arduino-1/ide2/dashboard/
|
||||
[translate]: contributor-guide/translation.md#translator-guide
|
||||
[prs]: contributor-guide/pull-requests.md#pull-request-guide
|
||||
[donate]: https://www.arduino.cc/en/donate/
|
||||
[sponsor]: https://github.com/sponsors/arduino
|
||||
|
33
docs/contributor-guide/translation.md
Normal file
33
docs/contributor-guide/translation.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Translator Guide
|
||||
|
||||
The text of the Arduino IDE interface is translated into several languages. The language can be selected in the dialog opened via **File > Preferences** in the Arduino IDE menus (**Arduino IDE > Preferences** for macOS users).
|
||||
|
||||
Translating text and improving on existing translations is a valuable contribution to the project, helping make Arduino accessible to everyone.
|
||||
|
||||
The translations for the text found in the Arduino IDE come from several sources:
|
||||
|
||||
## Arduino IDE Text
|
||||
|
||||
Translations of Arduino IDE's text is done in the "**Arduino IDE 2.0**" project on the **Transifex** localization platform:
|
||||
|
||||
https://explore.transifex.com/arduino-1/ide2/
|
||||
|
||||
## Base Application Text
|
||||
|
||||
Arduino IDE leverages the localization data available for the [**VS Code**](https://code.visualstudio.com/) editor to localize shared UI text. This reduces the translation work required to add a new language to the text specific to the Arduino IDE project.
|
||||
|
||||
For this reason, some of Arduino IDE's text is not found in the **Transifex** project. Suggestions for corrections or improvement to this text are made by submitting an issue to the `microsoft/vscode-loc` GitHub repository.
|
||||
|
||||
Before submitting an issue, please check the existing issues to make sure it wasn't already reported:<br />
|
||||
https://github.com/microsoft/vscode-loc/issues
|
||||
|
||||
After that, submit an issue here:<br />
|
||||
https://github.com/microsoft/vscode-loc/issues/new
|
||||
|
||||
## Arduino CLI Text
|
||||
|
||||
The [**Arduino CLI**](https://arduino.github.io/arduino-cli/latest/) tool handles non-GUI operations for the Arduino IDE. Some of the text printed in the "**Output**" panel and in notifications originates from **Arduino CLI**.
|
||||
|
||||
Translations of Arduino CLI's text is done in the "**Arduino CLI**" Transifex project:
|
||||
|
||||
https://explore.transifex.com/arduino-1/arduino-cli/
|
@@ -1,6 +1,10 @@
|
||||
# Release Procedure
|
||||
|
||||
## 🗺️ Merge localization sync PR
|
||||
## Steps
|
||||
|
||||
The following are the steps to follow to make a release of Arduino IDE:
|
||||
|
||||
### 1. 🗺️ Merge localization sync PR
|
||||
|
||||
A pull request titled "**Update translation files**" is submitted periodically by the "**github-actions**" bot to pull in the localization data from [**Transifex**](https://www.transifex.com/arduino-1/ide2/dashboard/).
|
||||
|
||||
@@ -10,24 +14,24 @@ It will be shown in these search results:
|
||||
|
||||
https://github.com/arduino/arduino-ide/pulls/app%2Fgithub-actions
|
||||
|
||||
## ⚙ Create the release on GitHub
|
||||
### 2. 👀 Check version of packages
|
||||
|
||||
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.
|
||||
The [`version` field](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#version) of the project's `package.json` metadata files received a patch version bump (e.g., `2.0.1` -> `2.0.2`) at the time of the previous release.
|
||||
|
||||
To do so, you can make use of the `update:version` script.
|
||||
If this is a patch release, the current metadata values are correct and no action is needed.
|
||||
|
||||
For example, if you want to release the version `<YOUR_VERSION>`, you should run the following commands:
|
||||
The changes contained in this release might be considered to change the project's "API". If so, a patch version bump will not be appropriate and the version must be adjusted in compliance with the [**Semantic Versioning Specification**](https://semver.org/).
|
||||
|
||||
```text
|
||||
git checkout main
|
||||
git pull
|
||||
git checkout -b version-<YOUR_VERSION>
|
||||
yarn update:version <YOUR_VERSION>
|
||||
git commit -am <YOUR_VERSION>
|
||||
git push origin version-<YOUR_VERSION>
|
||||
```
|
||||
Follow the instructions for updating the version metadata [**here**](#update-version-metadata).
|
||||
|
||||
replacing `<YOUR_VERSION>` with the version you want to release. Then create a PR and merge it.
|
||||
#### Examples
|
||||
|
||||
If the version number of the previous release was `2.0.1`:
|
||||
|
||||
- If this is considered a minor release (non-breaking changes to the "API"), the `version` values must be changed to `2.1.0`.
|
||||
- If this is considered a major release (breaking changes to the "API"), the `version` values must be changed to `3.0.0`.
|
||||
|
||||
### 3. 🚢 Create the release on GitHub
|
||||
|
||||
Then, you need to **create and push the new tag** and wait for the release to appear on [the "**Releases**" page](https://github.com/arduino/arduino-ide/releases).
|
||||
|
||||
@@ -42,7 +46,13 @@ git push origin <YOUR_VERSION>
|
||||
|
||||
Pushing a tag will trigger a **GitHub Actions** workflow on the `main` branch. Check the "**Arduino IDE**" workflow and see that everything goes right. If the workflow succeeds, a new release will be created automatically and you should see it on the ["**Releases**"](https://github.com/arduino/arduino-ide/releases) page.
|
||||
|
||||
## 📄 Create the changelog
|
||||
### 4. ⬆️ Bump version metadata of packages
|
||||
|
||||
In order for the version number of the tester and nightly builds to have correct precedence compared to the release version, the `version` field of the project's `package.json` files must be given a patch version bump (e.g., `2.0.1` -> `2.0.2`) **after** the creation of the release tag.
|
||||
|
||||
Follow the instructions for updating the version metadata [**here**](#update-version-metadata).
|
||||
|
||||
### 5. 📄 Create the changelog
|
||||
|
||||
**Create GitHub issues for the known issues** that we haven't solved in the current release:
|
||||
|
||||
@@ -61,7 +71,7 @@ Add a list of mentions of GitHub users who contributed to the release in any of
|
||||
|
||||
Add a "**Known Issues**" section at the bottom of the changelog.
|
||||
|
||||
## ✎ Update the "**Software**" Page
|
||||
### 6. ✎ Update the "**Software**" Page
|
||||
|
||||
Open a PR on the [bcmi-labs/wiki-content](https://github.com/bcmi-labs/wiki-content) repository to update the links and texts.
|
||||
|
||||
@@ -78,7 +88,7 @@ When the deploy workflow is done, check if links on the "**Software**" page are
|
||||
|
||||
https://www.arduino.cc/en/software#future-version-of-the-arduino-ide
|
||||
|
||||
## 😎 Brag about it
|
||||
### 7. 😎 Brag about it
|
||||
|
||||
- Ask in the `#product_releases` **Slack** channel to write a post for the social media and, if needed, a blog post.
|
||||
- Post a message on the forum (ask @per1234).<br />
|
||||
@@ -97,3 +107,28 @@ https://www.arduino.cc/en/software#future-version-of-the-arduino-ide
|
||||
>
|
||||
> To see the details, you can take a look at the [Changelog](https://github.com/arduino/arduino-ide/releases/tag/2.0.0-beta.12)
|
||||
> If you want to post about it on social media and you need more details feel free to ask us on #team_tooling! :wink:
|
||||
|
||||
## Operations
|
||||
|
||||
The following are detailed descriptions of operations performed during the release process:
|
||||
|
||||
<a id="update-version-metadata"></a>
|
||||
|
||||
### ⚙ Update version metadata of packages
|
||||
|
||||
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.
|
||||
|
||||
For example, if you want to update the version to `<YOUR_VERSION>`, you should run the following commands:
|
||||
|
||||
```text
|
||||
git checkout main
|
||||
git pull
|
||||
git checkout -b version-<YOUR_VERSION>
|
||||
yarn update:version <YOUR_VERSION>
|
||||
git commit -am <YOUR_VERSION>
|
||||
git push origin version-<YOUR_VERSION>
|
||||
```
|
||||
|
||||
replacing `<YOUR_VERSION>` with the version you want. Then create a PR and merge it.
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "electron-app",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "src-gen/frontend/electron-main.js",
|
||||
"dependencies": {
|
||||
@@ -21,7 +21,7 @@
|
||||
"@theia/process": "1.25.0",
|
||||
"@theia/terminal": "1.25.0",
|
||||
"@theia/workspace": "1.25.0",
|
||||
"arduino-ide-extension": "2.0.0"
|
||||
"arduino-ide-extension": "2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/cli": "1.25.0",
|
||||
@@ -48,6 +48,7 @@
|
||||
"comments": false,
|
||||
"strings": false
|
||||
},
|
||||
"editor.maxTokenizationLineLength": 500,
|
||||
"breadcrumbs.enabled": false,
|
||||
"workbench.tree.renderIndentGuides": "none",
|
||||
"explorer.compactFolders": false
|
||||
|
@@ -17,6 +17,53 @@ const {
|
||||
FrontendApplicationConfigProvider,
|
||||
} = require('@theia/core/lib/browser/frontend-application-config-provider');
|
||||
|
||||
function fetchFrom(path) {
|
||||
const { Endpoint } = require('@theia/core/lib/browser/endpoint');
|
||||
const endpoint = new Endpoint({ path }).getRestUrl().toString();
|
||||
return fetch(endpoint);
|
||||
}
|
||||
|
||||
async function loadTranslations() {
|
||||
const { nls } = require('@theia/core/lib/common/nls');
|
||||
const defaultLocale = typeof window === 'object' && window && window.localStorage.getItem(nls.localeId) || '';
|
||||
if (defaultLocale && !nls.locale) {
|
||||
Object.assign(nls, {
|
||||
locale: defaultLocale
|
||||
});
|
||||
}
|
||||
if (nls.locale) {
|
||||
const response = await fetchFrom(`/i18n/${nls.locale}`);
|
||||
nls.localization = await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBackendOS() {
|
||||
const response = await fetchFrom('/os');
|
||||
const osType = await response.text();
|
||||
const isWindows = osType === 'Windows';
|
||||
const isOSX = osType === 'OSX';
|
||||
OS.backend.isOSX = isOSX;
|
||||
OS.backend.isWindows = isWindows;
|
||||
OS.backend.type = () => osType;
|
||||
}
|
||||
|
||||
function customizeMonacoNls() {
|
||||
const MonacoNls = require('@theia/monaco-editor-core/esm/vs/nls');
|
||||
const { nls: TheiaNls } = require('@theia/core/lib/common/nls');
|
||||
const { Localization } = require('@theia/core/lib/common/i18n/localization');
|
||||
Object.assign(MonacoNls, {
|
||||
localize(_, label, ...args) {
|
||||
if (TheiaNls.locale) {
|
||||
const defaultKey = TheiaNls.getDefaultKey(label);
|
||||
if (defaultKey) {
|
||||
return TheiaNls.localize(defaultKey, label, ...args);
|
||||
}
|
||||
}
|
||||
return Localization.format(label, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// It is a mighty hack to support theme updates in the bundled IDE2.
|
||||
// If the custom theme registration happens before the restoration of the existing monaco themes, then any custom theme changes will be ignored.
|
||||
// This patch introduces a static deferred promise in the monaco-theming service that will be resolved when the restoration is ready.
|
||||
@@ -25,8 +72,14 @@ const {
|
||||
// This patch customizes the monaco theme service behavior before loading the DI containers via the preload.
|
||||
// The preload is called only once before the app loads. The Theia extensions are not loaded at that point, but the app config provider is ready.
|
||||
const preloader = require('@theia/core/lib/browser/preloader');
|
||||
const originalPreload = preloader.preload;
|
||||
preloader.preload = async function () {
|
||||
// Must require the monaco frontend module to activate the NLS customization for monaco.
|
||||
// Otherwise, the NLS customization would trigger after the monaco UI components with all their translations are already loaded.
|
||||
await Promise.allSettled([
|
||||
loadTranslations(),
|
||||
loadBackendOS(),
|
||||
]);
|
||||
customizeMonacoNls();
|
||||
const { MonacoThemingService } = require('@theia/monaco/lib/browser/monaco-theming-service');
|
||||
const { MonacoThemeServiceIsReady } = require('arduino-ide-extension/lib/browser/utils/window');
|
||||
const { Deferred } = require('@theia/core/lib/common/promise-util');
|
||||
@@ -42,7 +95,6 @@ preloader.preload = async function () {
|
||||
await this.restore();
|
||||
ready.resolve();
|
||||
}.bind(MonacoThemingService);
|
||||
return originalPreload();
|
||||
}.bind(preloader);
|
||||
|
||||
const lightTheme = 'arduino-theme';
|
||||
|
@@ -137,24 +137,5 @@
|
||||
"path": "arduino-ide/nightly"
|
||||
}
|
||||
]
|
||||
},
|
||||
"theiaPluginsDir": "plugins",
|
||||
"theiaPlugins": {
|
||||
"vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.52.1/file/vscode.cpp-1.52.1.vsix",
|
||||
"vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.5.vsix",
|
||||
"vscode-builtin-json": "https://open-vsx.org/api/vscode/json/1.46.1/file/vscode.json-1.46.1.vsix",
|
||||
"vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix",
|
||||
"cortex-debug": "https://open-vsx.org/api/marus25/cortex-debug/0.3.10/file/marus25.cortex-debug-0.3.10.vsix",
|
||||
"vscode-language-pack-nl": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-nl/1.48.3/file/MS-CEINTL.vscode-language-pack-nl-1.48.3.vsix",
|
||||
"vscode-language-pack-fr": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-fr/1.69.0/file/MS-CEINTL.vscode-language-pack-fr-1.69.0.vsix",
|
||||
"vscode-language-pack-zh-hans": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-zh-hans/1.69.0/file/MS-CEINTL.vscode-language-pack-zh-hans-1.69.0.vsix",
|
||||
"vscode-language-pack-de": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-de/1.69.0/file/MS-CEINTL.vscode-language-pack-de-1.69.0.vsix",
|
||||
"vscode-language-pack-ja": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-ja/1.69.0/file/MS-CEINTL.vscode-language-pack-ja-1.69.0.vsix",
|
||||
"vscode-language-pack-tr": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-tr/1.69.0/file/MS-CEINTL.vscode-language-pack-tr-1.69.0.vsix",
|
||||
"vscode-language-pack-it": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-it/1.69.0/file/MS-CEINTL.vscode-language-pack-it-1.69.0.vsix",
|
||||
"vscode-language-pack-ru":"https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-ru/1.69.0/file/MS-CEINTL.vscode-language-pack-ru-1.69.0.vsix",
|
||||
"vscode-language-pack-es": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-es/1.69.0/file/MS-CEINTL.vscode-language-pack-es-1.69.0.vsix",
|
||||
"vscode-language-pack-pt-BR": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-pt-BR/1.69.0/file/MS-CEINTL.vscode-language-pack-pt-BR-1.69.0.vsix",
|
||||
"vscode-language-pack-cs": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-cs/1.69.0/file/MS-CEINTL.vscode-language-pack-cs-1.69.0.vsix"
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user