mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-09-27 21:58:31 +00:00
Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
88cd571080 | ||
![]() |
9ffe421fab | ||
![]() |
d68bc4abdb | ||
![]() |
4f07515ee8 | ||
![]() |
25b545d4c4 | ||
![]() |
79b6b7ecc0 | ||
![]() |
5d264ef5b6 | ||
![]() |
f63ee85fa3 | ||
![]() |
083a7069f0 | ||
![]() |
f5621db85d | ||
![]() |
658f117e93 | ||
![]() |
6140ae525c | ||
![]() |
afb02da806 | ||
![]() |
692f29fe1a | ||
![]() |
40e797966f | ||
![]() |
a15a94a339 | ||
![]() |
ca687cfe40 | ||
![]() |
32e17745f1 | ||
![]() |
432f3654df | ||
![]() |
197cea2a60 | ||
![]() |
b2bf368db9 | ||
![]() |
287b2e3f41 | ||
![]() |
da0fecfd0f | ||
![]() |
76f9f635d8 | ||
![]() |
3f05396222 | ||
![]() |
644e6079b3 | ||
![]() |
1d342cdbd0 | ||
![]() |
908ec4c544 | ||
![]() |
7c86f1f9d3 | ||
![]() |
f8c01e379c | ||
![]() |
af468a73bc | ||
![]() |
d3a863911c | ||
![]() |
c4172ee8e1 | ||
![]() |
ed8ed15168 | ||
![]() |
32f0426f01 | ||
![]() |
200c00244b |
@@ -15,7 +15,6 @@ module.exports = {
|
||||
'.browser_modules/*',
|
||||
'docs/*',
|
||||
'scripts/*',
|
||||
'electron/*',
|
||||
'electron-app/*',
|
||||
'plugins/*',
|
||||
'arduino-ide-extension/src/node/cli-protocol',
|
||||
|
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
@@ -180,10 +180,14 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
echo -e "$BODY"
|
||||
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
|
||||
echo "BODY=$OUTPUT_SAFE_BODY" >> $GITHUB_OUTPUT
|
||||
|
||||
# Set workflow step output
|
||||
# See: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
DELIMITER="$RANDOM"
|
||||
echo "BODY<<$DELIMITER" >> $GITHUB_OUTPUT
|
||||
echo "$BODY" >> $GITHUB_OUTPUT
|
||||
echo "$DELIMITER" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "$BODY" > CHANGELOG.txt
|
||||
|
||||
- name: Upload Changelog [GitHub Actions]
|
||||
@@ -231,7 +235,7 @@ jobs:
|
||||
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Publish Release [GitHub]
|
||||
uses: svenstaro/upload-release-action@2.3.0
|
||||
uses: svenstaro/upload-release-action@2.4.1
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
|
||||
|
5
.github/workflows/check-certificates.yml
vendored
5
.github/workflows/check-certificates.yml
vendored
@@ -59,7 +59,9 @@ jobs:
|
||||
(
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-noout -passin env:CERTIFICATE_PASSWORD
|
||||
-legacy \
|
||||
-noout \
|
||||
-passin env:CERTIFICATE_PASSWORD
|
||||
) || (
|
||||
echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
|
||||
exit 1
|
||||
@@ -87,6 +89,7 @@ jobs:
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-clcerts \
|
||||
-legacy \
|
||||
-nodes \
|
||||
-passin env:CERTIFICATE_PASSWORD
|
||||
) | (
|
||||
|
2
.github/workflows/check-i18n-task.yml
vendored
2
.github/workflows/check-i18n-task.yml
vendored
@@ -48,6 +48,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check for errors
|
||||
run: yarn i18n:check
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -4,7 +4,7 @@ node_modules/
|
||||
lib/
|
||||
downloads/
|
||||
build/
|
||||
Examples/
|
||||
arduino-ide-extension/Examples/
|
||||
!electron/build/
|
||||
src-gen/
|
||||
webpack.config.js
|
||||
@@ -21,3 +21,5 @@ scripts/themes/tokens
|
||||
.env
|
||||
# content trace files for electron
|
||||
electron-app/traces
|
||||
# any Arduino LS generated log files
|
||||
inols*.log
|
||||
|
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -14,7 +14,6 @@
|
||||
".",
|
||||
"--log-level=debug",
|
||||
"--hostname=localhost",
|
||||
"--no-cluster",
|
||||
"--app-project-path=${workspaceRoot}/electron-app",
|
||||
"--remote-debugging-port=9222",
|
||||
"--no-app-auto-install",
|
||||
@@ -52,7 +51,6 @@
|
||||
".",
|
||||
"--log-level=debug",
|
||||
"--hostname=localhost",
|
||||
"--no-cluster",
|
||||
"--app-project-path=${workspaceRoot}/electron-app",
|
||||
"--remote-debugging-port=9222",
|
||||
"--no-app-auto-install",
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -2,6 +2,9 @@
|
||||
"files.exclude": {
|
||||
"**/lib": false
|
||||
},
|
||||
"search.exclude": {
|
||||
"arduino-ide-extension/src/test/node/__test_sketchbook__": true
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.4",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
|
||||
"watch": "tsc -w",
|
||||
"test": "mocha \"./lib/test/**/*.test.js\"",
|
||||
"test:slow": "mocha \"./lib/test/**/*.slow-test.js\" --slow 5000",
|
||||
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -66,10 +67,13 @@
|
||||
"auth0-js": "^9.14.0",
|
||||
"btoa": "^1.2.1",
|
||||
"classnames": "^2.3.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"dateformat": "^3.0.3",
|
||||
"deepmerge": "2.0.1",
|
||||
"electron-updater": "^4.6.5",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"filename-reserved-regex": "^2.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"google-protobuf": "^3.20.1",
|
||||
"hash.js": "^1.1.7",
|
||||
@@ -168,7 +172,7 @@
|
||||
"version": "14.0.0"
|
||||
},
|
||||
"languageServer": {
|
||||
"version": "0.7.2"
|
||||
"version": "0.7.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ import {
|
||||
SketchesService,
|
||||
SketchesServicePath,
|
||||
} from '../common/protocol/sketches-service';
|
||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
||||
import { SketchesServiceClientImpl } from './sketches-service-client-impl';
|
||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
@@ -127,7 +127,7 @@ import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/
|
||||
import { PreferencesContribution } from './theia/preferences/preferences-contribution';
|
||||
import { QuitApp } from './contributions/quit-app';
|
||||
import { SketchControl } from './contributions/sketch-control';
|
||||
import { Settings } from './contributions/settings';
|
||||
import { OpenSettings } from './contributions/open-settings';
|
||||
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
|
||||
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
|
||||
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
|
||||
@@ -323,8 +323,8 @@ import { NewCloudSketch } from './contributions/new-cloud-sketch';
|
||||
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
|
||||
import { WindowTitleUpdater } from './theia/core/window-title-updater';
|
||||
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
|
||||
import { ThemeService } from './theia/core/theming';
|
||||
import { ThemeService as TheiaThemeService } from '@theia/core/lib/browser/theming';
|
||||
import { ThemeServiceWithDB } from './theia/core/theming';
|
||||
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
|
||||
import { MonacoThemingService } from './theia/monaco/monaco-theming-service';
|
||||
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service';
|
||||
@@ -343,6 +343,10 @@ import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
|
||||
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
|
||||
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
|
||||
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
|
||||
import { ConfigServiceClient } from './config/config-service-client';
|
||||
import { ValidateSketch } from './contributions/validate-sketch';
|
||||
import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
|
||||
import { CreateFeatures } from './create/create-features';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Commands and toolbar items
|
||||
@@ -404,6 +408,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
bind(ConfigServiceClient).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
|
||||
|
||||
// Boards service
|
||||
bind(BoardsService)
|
||||
@@ -691,7 +697,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, EditContributions);
|
||||
Contribution.configure(bind, QuitApp);
|
||||
Contribution.configure(bind, SketchControl);
|
||||
Contribution.configure(bind, Settings);
|
||||
Contribution.configure(bind, OpenSettings);
|
||||
Contribution.configure(bind, BurnBootloader);
|
||||
Contribution.configure(bind, BuiltInExamples);
|
||||
Contribution.configure(bind, LibraryExamples);
|
||||
@@ -726,6 +732,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, UpdateIndexes);
|
||||
Contribution.configure(bind, InterfaceScale);
|
||||
Contribution.configure(bind, NewCloudSketch);
|
||||
Contribution.configure(bind, ValidateSketch);
|
||||
Contribution.configure(bind, RenameCloudSketch);
|
||||
|
||||
bindContributionProvider(bind, StartupTaskProvider);
|
||||
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
|
||||
@@ -886,6 +894,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
);
|
||||
bind(CreateApi).toSelf().inSingletonScope();
|
||||
bind(SketchCache).toSelf().inSingletonScope();
|
||||
bind(CreateFeatures).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(CreateFeatures);
|
||||
|
||||
bind(ShareSketchDialog).toSelf().inSingletonScope();
|
||||
bind(AuthenticationClientService).toSelf().inSingletonScope();
|
||||
@@ -959,8 +969,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater);
|
||||
|
||||
// register Arduino themes
|
||||
bind(ThemeService).toSelf().inSingletonScope();
|
||||
rebind(TheiaThemeService).toService(ThemeService);
|
||||
bind(ThemeServiceWithDB).toSelf().inSingletonScope();
|
||||
rebind(TheiaThemeServiceWithDB).toService(ThemeServiceWithDB);
|
||||
bind(MonacoThemingService).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
|
||||
|
||||
|
@@ -1,68 +0,0 @@
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { isWindows } from '@theia/core/lib/common/os';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
|
||||
/**
|
||||
* Class for determining the default workspace location from the
|
||||
* `location.hash`, the historical workspace locations, and recent sketch files.
|
||||
*
|
||||
* The following logic is used for determining the default workspace location:
|
||||
* - `hash` points to an existing location?
|
||||
* - Yes
|
||||
* - `validate location`. Is valid sketch location?
|
||||
* - Yes
|
||||
* - Done.
|
||||
* - No
|
||||
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
|
||||
* - No
|
||||
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
|
||||
*/
|
||||
namespace ArduinoWorkspaceRootResolver {
|
||||
export interface InitOptions {
|
||||
readonly isValid: (uri: string) => MaybePromise<boolean>;
|
||||
}
|
||||
export interface ResolveOptions {
|
||||
readonly hash?: string;
|
||||
readonly recentWorkspaces: string[];
|
||||
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
|
||||
readonly recentSketches: string[];
|
||||
}
|
||||
}
|
||||
export class ArduinoWorkspaceRootResolver {
|
||||
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {}
|
||||
|
||||
async resolve(
|
||||
options: ArduinoWorkspaceRootResolver.ResolveOptions
|
||||
): Promise<{ uri: string } | undefined> {
|
||||
const { hash, recentWorkspaces, recentSketches } = options;
|
||||
for (const uri of [
|
||||
this.hashToUri(hash),
|
||||
...recentWorkspaces,
|
||||
...recentSketches,
|
||||
].filter(notEmpty)) {
|
||||
const valid = await this.isValid(uri);
|
||||
if (valid) {
|
||||
return { uri };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected isValid(uri: string): MaybePromise<boolean> {
|
||||
return this.options.isValid(uri);
|
||||
}
|
||||
|
||||
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
|
||||
// This is important for Windows only and a NOOP on POSIX.
|
||||
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
|
||||
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
|
||||
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
|
||||
protected hashToUri(hash: string | undefined): string | undefined {
|
||||
if (hash && hash.length > 1 && hash.startsWith('#')) {
|
||||
const path = decodeURI(hash.slice(1)).replace(/\\/g, '/'); // Trim the leading `#`, decode the URI and replace Windows separators
|
||||
return URI.file(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)).toString();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
Board,
|
||||
Port,
|
||||
BoardConfig as ProtocolBoardConfig,
|
||||
BoardWithPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
@@ -18,10 +19,7 @@ import { nls } from '@theia/core/lib/common';
|
||||
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
|
||||
|
||||
export namespace BoardsConfig {
|
||||
export interface Config {
|
||||
selectedBoard?: Board;
|
||||
selectedPort?: Port;
|
||||
}
|
||||
export type Config = ProtocolBoardConfig;
|
||||
|
||||
export interface Props {
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
@@ -80,16 +80,16 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
string,
|
||||
Disposable & { label: string }
|
||||
>();
|
||||
let selectedValue = '';
|
||||
for (const value of values) {
|
||||
const id = `${fqbn}-${option}--${value.value}`;
|
||||
const command = { id };
|
||||
const selectedValue = value.value;
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.boardsDataStore.selectConfigOption({
|
||||
fqbn,
|
||||
option,
|
||||
selectedValue,
|
||||
selectedValue: value.value,
|
||||
}),
|
||||
isToggled: () => value.selected,
|
||||
};
|
||||
@@ -100,8 +100,14 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
{ label: value.label }
|
||||
)
|
||||
);
|
||||
if (value.selected) {
|
||||
selectedValue = value.label;
|
||||
}
|
||||
}
|
||||
this.menuRegistry.registerSubmenu(menuPath, label);
|
||||
this.menuRegistry.registerSubmenu(
|
||||
menuPath,
|
||||
`${label}${selectedValue ? `: "${selectedValue}"` : ''}`
|
||||
);
|
||||
this.toDisposeOnBoardChange.pushAll([
|
||||
...commands.values(),
|
||||
Disposable.create(() =>
|
||||
|
@@ -30,11 +30,11 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
@inject(LocalStorageService)
|
||||
protected readonly storageService: LocalStorageService;
|
||||
|
||||
protected readonly onChangedEmitter = new Emitter<void>();
|
||||
protected readonly onChangedEmitter = new Emitter<string[]>();
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
|
||||
let shouldFireChanged = false;
|
||||
const dataDidChangePerFqbn: string[] = [];
|
||||
for (const fqbn of item.boards
|
||||
.map(({ fqbn }) => fqbn)
|
||||
.filter(notEmpty)
|
||||
@@ -49,18 +49,18 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
data = details.configOptions;
|
||||
if (data.length) {
|
||||
await this.storageService.setData(key, data);
|
||||
shouldFireChanged = true;
|
||||
dataDidChangePerFqbn.push(fqbn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldFireChanged) {
|
||||
this.fireChanged();
|
||||
if (dataDidChangePerFqbn.length) {
|
||||
this.fireChanged(...dataDidChangePerFqbn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get onChanged(): Event<void> {
|
||||
get onChanged(): Event<string[]> {
|
||||
return this.onChangedEmitter.event;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
fqbn,
|
||||
data: { ...data, selectedProgrammer },
|
||||
});
|
||||
this.fireChanged();
|
||||
this.fireChanged(fqbn);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
return false;
|
||||
}
|
||||
await this.setData({ fqbn, data });
|
||||
this.fireChanged();
|
||||
this.fireChanged(fqbn);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -190,8 +190,8 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
}
|
||||
}
|
||||
|
||||
protected fireChanged(): void {
|
||||
this.onChangedEmitter.fire();
|
||||
protected fireChanged(...fqbn: string[]): void {
|
||||
this.onChangedEmitter.fire(fqbn);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -30,7 +30,6 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemDeprecated: (item: BoardsPackage) => item.deprecated,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All' },
|
||||
|
@@ -0,0 +1,102 @@
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { ConfigService, ConfigState } from '../../common/protocol';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class ConfigServiceClient implements FrontendApplicationContribution {
|
||||
@inject(ConfigService)
|
||||
private readonly delegate: ConfigService;
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
@inject(MessageService)
|
||||
private readonly messageService: MessageService;
|
||||
|
||||
private readonly didChangeSketchDirUriEmitter = new Emitter<
|
||||
URI | undefined
|
||||
>();
|
||||
private readonly didChangeDataDirUriEmitter = new Emitter<URI | undefined>();
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.didChangeSketchDirUriEmitter,
|
||||
this.didChangeDataDirUriEmitter
|
||||
);
|
||||
|
||||
private config: ConfigState | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.appStateService.reachedState('ready').then(async () => {
|
||||
const config = await this.delegate.getConfiguration();
|
||||
this.use(config);
|
||||
});
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onConfigDidChange((config) => this.use(config));
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get onDidChangeSketchDirUri(): Event<URI | undefined> {
|
||||
return this.didChangeSketchDirUriEmitter.event;
|
||||
}
|
||||
|
||||
get onDidChangeDataDirUri(): Event<URI | undefined> {
|
||||
return this.didChangeDataDirUriEmitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI config related error messages if any.
|
||||
*/
|
||||
tryGetMessages(): string[] | undefined {
|
||||
return this.config?.messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* `directories.user`
|
||||
*/
|
||||
tryGetSketchDirUri(): URI | undefined {
|
||||
return this.config?.config?.sketchDirUri
|
||||
? new URI(this.config?.config?.sketchDirUri)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* `directories.data`
|
||||
*/
|
||||
tryGetDataDirUri(): URI | undefined {
|
||||
return this.config?.config?.dataDirUri
|
||||
? new URI(this.config?.config?.dataDirUri)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private use(config: ConfigState): void {
|
||||
const oldConfig = deepClone(this.config);
|
||||
this.config = config;
|
||||
if (oldConfig?.config?.sketchDirUri !== this.config?.config?.sketchDirUri) {
|
||||
this.didChangeSketchDirUriEmitter.fire(this.tryGetSketchDirUri());
|
||||
}
|
||||
if (oldConfig?.config?.dataDirUri !== this.config?.config?.dataDirUri) {
|
||||
this.didChangeDataDirUriEmitter.fire(this.tryGetDataDirUri());
|
||||
}
|
||||
if (this.config.messages?.length) {
|
||||
const message = this.config.messages.join(' ');
|
||||
// toast the error later otherwise it might not show up in IDE2
|
||||
setTimeout(() => this.messageService.error(message), 1_000);
|
||||
}
|
||||
}
|
||||
}
|
@@ -41,22 +41,16 @@ export class About extends Contribution {
|
||||
}
|
||||
|
||||
async showAbout(): Promise<void> {
|
||||
const {
|
||||
version,
|
||||
commit,
|
||||
status: cliStatus,
|
||||
} = await this.configService.getVersion();
|
||||
const version = await this.configService.getVersion();
|
||||
const buildDate = this.buildDate;
|
||||
const detail = (showAll: boolean) =>
|
||||
nls.localize(
|
||||
'arduino/about/detail',
|
||||
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}{4} [{5}]\n\n{6}',
|
||||
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}',
|
||||
remote.app.getVersion(),
|
||||
buildDate ? buildDate : nls.localize('', 'dev build'),
|
||||
buildDate && showAll ? ` (${this.ago(buildDate)})` : '',
|
||||
version,
|
||||
cliStatus ? ` ${cliStatus}` : '',
|
||||
commit,
|
||||
nls.localize(
|
||||
'arduino/about/copyright',
|
||||
'Copyright © {0} Arduino SA',
|
||||
|
@@ -7,10 +7,11 @@ import {
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
URI,
|
||||
Sketch,
|
||||
} from './contribution';
|
||||
import { FileDialogService } from '@theia/filesystem/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class AddFile extends SketchContribution {
|
||||
@@ -46,9 +47,7 @@ export class AddFile extends SketchContribution {
|
||||
if (!toAddUri) {
|
||||
return;
|
||||
}
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const filename = toAddUri.path.base;
|
||||
const targetUri = sketchUri.resolve('data').resolve(filename);
|
||||
const { uri: targetUri, filename } = this.resolveTarget(sketch, toAddUri);
|
||||
const exists = await this.fileService.exists(targetUri);
|
||||
if (exists) {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
@@ -80,6 +79,22 @@ export class AddFile extends SketchContribution {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/arduino/arduino-ide/issues/284#issuecomment-1364533662
|
||||
// File the file to add has one of the following extension, it goes to the sketch folder root: .ino, .h, .cpp, .c, .S
|
||||
// Otherwise, the files goes to the `data` folder inside the sketch folder root.
|
||||
private resolveTarget(
|
||||
sketch: Sketch,
|
||||
toAddUri: URI
|
||||
): { uri: URI; filename: string } {
|
||||
const path = toAddUri.path;
|
||||
const filename = path.base;
|
||||
let root = new URI(sketch.uri);
|
||||
if (!Sketch.Extensions.CODE_FILES.includes(path.ext)) {
|
||||
root = root.resolve('data');
|
||||
}
|
||||
return { uri: root.resolve(filename), filename: filename };
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AddFile {
|
||||
|
@@ -2,7 +2,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
@@ -16,9 +15,6 @@ import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class AddZipLibrary extends SketchContribution {
|
||||
@inject(EnvVariablesServer)
|
||||
private readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
SketchContribution,
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class ArchiveSketch extends SketchContribution {
|
||||
@@ -29,10 +28,7 @@ export class ArchiveSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
private async archiveSketch(): Promise<void> {
|
||||
const [sketch, config] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
@@ -40,9 +36,9 @@ export class ArchiveSketch extends SketchContribution {
|
||||
new Date(),
|
||||
'yymmdd'
|
||||
)}a.zip`;
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri).resolve(archiveBasename)
|
||||
);
|
||||
const defaultContainerUri = await this.defaultUri();
|
||||
const defaultUri = defaultContainerUri.resolve(archiveBasename);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
@@ -60,7 +56,7 @@ export class ArchiveSketch extends SketchContribution {
|
||||
if (!destinationUri) {
|
||||
return;
|
||||
}
|
||||
await this.sketchService.archive(sketch, destinationUri.toString());
|
||||
await this.sketchesService.archive(sketch, destinationUri.toString());
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/sketch/createdArchive',
|
||||
|
@@ -20,6 +20,7 @@ import {
|
||||
InstalledBoardWithPackage,
|
||||
AvailablePorts,
|
||||
Port,
|
||||
getBoardInfo,
|
||||
} from '../../common/protocol';
|
||||
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
@@ -49,52 +50,28 @@ export class BoardSelection extends SketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
|
||||
execute: async () => {
|
||||
const { selectedBoard, selectedPort } =
|
||||
this.boardsServiceProvider.boardsConfig;
|
||||
if (!selectedBoard) {
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/board/selectBoardForInfo',
|
||||
'Please select a board to obtain board info.'
|
||||
)
|
||||
);
|
||||
const boardInfo = await getBoardInfo(
|
||||
this.boardsServiceProvider.boardsConfig.selectedPort,
|
||||
this.boardsService.getState()
|
||||
);
|
||||
if (typeof boardInfo === 'string') {
|
||||
this.messageService.info(boardInfo);
|
||||
return;
|
||||
}
|
||||
if (!selectedBoard.fqbn) {
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/board/platformMissing',
|
||||
"The platform for the selected '{0}' board is not installed.",
|
||||
selectedBoard.name
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!selectedPort) {
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/board/selectPortForInfo',
|
||||
'Please select a port to obtain board info.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
const boardDetails = await this.boardsService.getBoardDetails({
|
||||
fqbn: selectedBoard.fqbn,
|
||||
});
|
||||
if (boardDetails) {
|
||||
const { VID, PID } = boardDetails;
|
||||
const detail = `BN: ${selectedBoard.name}
|
||||
const { BN, VID, PID, SN } = boardInfo;
|
||||
const detail = `
|
||||
BN: ${BN}
|
||||
VID: ${VID}
|
||||
PID: ${PID}`;
|
||||
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
type: 'info',
|
||||
detail,
|
||||
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
|
||||
});
|
||||
}
|
||||
PID: ${PID}
|
||||
SN: ${SN}
|
||||
`.trim();
|
||||
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
type: 'info',
|
||||
detail,
|
||||
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -155,10 +132,7 @@ PID: ${PID}`;
|
||||
);
|
||||
|
||||
// Ports submenu
|
||||
const portsSubmenuPath = [
|
||||
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||
'2_ports',
|
||||
];
|
||||
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
|
||||
const portsSubmenuLabel = config.selectedPort?.address;
|
||||
this.menuModelRegistry.registerSubmenu(
|
||||
portsSubmenuPath,
|
||||
|
@@ -20,7 +20,7 @@ import {
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
|
||||
/**
|
||||
@@ -185,7 +185,7 @@ export class Close extends SketchContribution {
|
||||
private async isCurrentSketchTemp(): Promise<false | Sketch> {
|
||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||
if (CurrentSketch.isValid(currentSketch)) {
|
||||
const isTemp = await this.sketchService.isTemp(currentSketch);
|
||||
const isTemp = await this.sketchesService.isTemp(currentSketch);
|
||||
if (isTemp) {
|
||||
return currentSketch;
|
||||
}
|
||||
|
@@ -0,0 +1,121 @@
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateApi } from '../create/create-api';
|
||||
import { CreateFeatures } from '../create/create-features';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { Create, isNotFound } from '../create/typings';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
|
||||
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
|
||||
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
|
||||
import { SketchContribution } from './contribution';
|
||||
|
||||
export function sketchAlreadyExists(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/alreadyExists',
|
||||
"Cloud sketch '{0}' already exists.",
|
||||
input
|
||||
);
|
||||
}
|
||||
export function sketchNotFound(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/notFound',
|
||||
"Could not pull the cloud sketch '{0}'. It does not exist.",
|
||||
input
|
||||
);
|
||||
}
|
||||
export const synchronizingSketchbook = nls.localize(
|
||||
'arduino/cloudSketch/synchronizingSketchbook',
|
||||
'Synchronizing sketchbook...'
|
||||
);
|
||||
export function pullingSketch(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/pulling',
|
||||
"Synchronizing sketchbook, pulling '{0}'...",
|
||||
input
|
||||
);
|
||||
}
|
||||
export function pushingSketch(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/pushing',
|
||||
"Synchronizing sketchbook, pushing '{0}'...",
|
||||
input
|
||||
);
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class CloudSketchContribution extends SketchContribution {
|
||||
@inject(SketchbookWidgetContribution)
|
||||
private readonly widgetContribution: SketchbookWidgetContribution;
|
||||
@inject(CreateApi)
|
||||
protected readonly createApi: CreateApi;
|
||||
@inject(CreateFeatures)
|
||||
protected readonly createFeatures: CreateFeatures;
|
||||
|
||||
protected async treeModel(): Promise<
|
||||
(CloudSketchbookTreeModel & { root: CompositeTreeNode }) | undefined
|
||||
> {
|
||||
const { enabled, session } = this.createFeatures;
|
||||
if (enabled && session) {
|
||||
const widget = await this.widgetContribution.widget;
|
||||
const treeModel = this.treeModelFrom(widget);
|
||||
if (treeModel) {
|
||||
const root = treeModel.root;
|
||||
if (CompositeTreeNode.is(root)) {
|
||||
return treeModel as CloudSketchbookTreeModel & {
|
||||
root: CompositeTreeNode;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async pull(
|
||||
sketch: Create.Sketch
|
||||
): Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (!treeModel) {
|
||||
return undefined;
|
||||
}
|
||||
const id = CreateUri.toUri(sketch).path.toString();
|
||||
const node = treeModel.getNode(id);
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
`Could not find cloud sketchbook tree node with ID: ${id}.`
|
||||
);
|
||||
}
|
||||
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||
throw new Error(
|
||||
`Cloud sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
|
||||
);
|
||||
}
|
||||
try {
|
||||
await treeModel.sketchbookTree().pull({ node });
|
||||
return node;
|
||||
} catch (err) {
|
||||
if (isNotFound(err)) {
|
||||
await treeModel.refresh();
|
||||
this.messageService.error(sketchNotFound(sketch.name));
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private treeModelFrom(
|
||||
widget: SketchbookWidget
|
||||
): CloudSketchbookTreeModel | undefined {
|
||||
for (const treeWidget of widget.getTreeWidgets()) {
|
||||
if (treeWidget instanceof CloudSketchbookTreeWidget) {
|
||||
const model = treeWidget.model;
|
||||
if (model instanceof CloudSketchbookTreeModel) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ 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 { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||
|
||||
import {
|
||||
@@ -40,10 +41,9 @@ import { SettingsService } from '../dialogs/settings/settings';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../common/protocol/sketches-service-client-impl';
|
||||
} from '../sketches-service-client-impl';
|
||||
import {
|
||||
SketchesService,
|
||||
ConfigService,
|
||||
FileSystemExt,
|
||||
Sketch,
|
||||
CoreService,
|
||||
@@ -61,6 +61,8 @@ 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';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { ConfigServiceClient } from '../config/config-service-client';
|
||||
|
||||
export {
|
||||
Command,
|
||||
@@ -106,6 +108,9 @@ export abstract class Contribution
|
||||
@inject(FrontendApplicationStateService)
|
||||
protected readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.appStateService.reachedState('ready').then(() => this.onReady());
|
||||
@@ -138,11 +143,11 @@ export abstract class SketchContribution extends Contribution {
|
||||
@inject(FileSystemExt)
|
||||
protected readonly fileSystemExt: FileSystemExt;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
@inject(ConfigServiceClient)
|
||||
protected readonly configService: ConfigServiceClient;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
protected readonly sketchesService: SketchesService;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
@@ -156,6 +161,9 @@ export abstract class SketchContribution extends Contribution {
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
protected async sourceOverride(): Promise<Record<string, string>> {
|
||||
const override: Record<string, string> = {};
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
@@ -169,6 +177,25 @@ export abstract class SketchContribution extends Contribution {
|
||||
}
|
||||
return override;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults to `directories.user` if defined and not CLI config errors were detected.
|
||||
* Otherwise, the URI of the user home directory.
|
||||
*/
|
||||
protected async defaultUri(): Promise<URI> {
|
||||
const errors = this.configService.tryGetMessages();
|
||||
let defaultUri = this.configService.tryGetSketchDirUri();
|
||||
if (!defaultUri || errors?.length) {
|
||||
// Fall back to user home when the `directories.user` is not available or there are known CLI config errors
|
||||
defaultUri = new URI(await this.envVariableServer.getHomeDirUri());
|
||||
}
|
||||
return defaultUri;
|
||||
}
|
||||
|
||||
protected async defaultPath(): Promise<string> {
|
||||
const defaultUri = await this.defaultUri();
|
||||
return this.fileService.fsPath(defaultUri);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
|
@@ -3,7 +3,12 @@ import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board, BoardsService, ExecutableService } from '../../common/protocol';
|
||||
import {
|
||||
Board,
|
||||
BoardsService,
|
||||
ExecutableService,
|
||||
Sketch,
|
||||
} from '../../common/protocol';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import {
|
||||
URI,
|
||||
@@ -13,12 +18,11 @@ import {
|
||||
TabBarToolbarRegistry,
|
||||
} from './contribution';
|
||||
import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
|
||||
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
|
||||
|
||||
@injectable()
|
||||
export class Debug extends SketchContribution {
|
||||
@inject(HostedPluginSupport)
|
||||
@@ -36,9 +40,6 @@ export class Debug extends SketchContribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
private readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
/**
|
||||
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
|
||||
*/
|
||||
@@ -186,7 +187,7 @@ export class Debug extends SketchContribution {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
|
||||
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
|
||||
sketch
|
||||
);
|
||||
const [cliPath, sketchPath, configPath] = await Promise.all([
|
||||
@@ -203,7 +204,28 @@ export class Debug extends SketchContribution {
|
||||
sketchPath,
|
||||
configPath,
|
||||
};
|
||||
return this.commandService.executeCommand('arduino.debug.start', config);
|
||||
try {
|
||||
await this.commandService.executeCommand('arduino.debug.start', config);
|
||||
} catch (err) {
|
||||
if (await this.isSketchNotVerifiedError(err, sketch)) {
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
const answer = await this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/debug/sketchIsNotCompiled',
|
||||
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
|
||||
sketch.name
|
||||
),
|
||||
yes
|
||||
);
|
||||
if (answer === yes) {
|
||||
this.commandService.executeCommand('arduino-verify-sketch');
|
||||
}
|
||||
} else {
|
||||
this.messageService.error(
|
||||
err instanceof Error ? err.message : String(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get compileForDebug(): boolean {
|
||||
@@ -215,7 +237,24 @@ export class Debug extends SketchContribution {
|
||||
const oldState = this.compileForDebug;
|
||||
const newState = !oldState;
|
||||
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
|
||||
this.mainMenuManager.update();
|
||||
this.menuManager.update();
|
||||
}
|
||||
|
||||
private async isSketchNotVerifiedError(
|
||||
err: unknown,
|
||||
sketch: Sketch
|
||||
): Promise<boolean> {
|
||||
if (err instanceof Error) {
|
||||
try {
|
||||
const tempBuildPaths = await this.sketchesService.tempBuildPath(sketch);
|
||||
return tempBuildPaths.some((tempBuildPath) =>
|
||||
err.message.includes(tempBuildPath)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export namespace Debug {
|
||||
|
@@ -1,32 +1,131 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { ipcRenderer } from '@theia/core/electron-shared/electron';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import type { MaybeArray } from '@theia/core/lib/common/types';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import type { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
SketchContribution,
|
||||
Sketch,
|
||||
} from './contribution';
|
||||
import { SCHEDULE_DELETION_SIGNAL } from '../../electron-common/electron-messages';
|
||||
import { Sketch } from '../contributions/contribution';
|
||||
import { isNotFound } from '../create/typings';
|
||||
import { Command, CommandRegistry } from './contribution';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
|
||||
export interface DeleteSketchParams {
|
||||
/**
|
||||
* Either the URI of the sketch folder or the sketch to delete.
|
||||
*/
|
||||
readonly toDelete: string | Sketch;
|
||||
/**
|
||||
* If `true`, the currently opened sketch is expected to be deleted.
|
||||
* Hence, the editors must be closed, the sketch will be scheduled
|
||||
* for deletion, and the browser window will close or navigate away.
|
||||
* If `false`, the sketch will be scheduled for deletion,
|
||||
* but the current window remains open. If `force`, the window will
|
||||
* navigate away, but IDE2 won't open any confirmation dialogs.
|
||||
*/
|
||||
readonly willNavigateAway?: boolean | 'force';
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DeleteSketch extends SketchContribution {
|
||||
export class DeleteSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
|
||||
execute: (uri: string) => this.deleteSketch(uri),
|
||||
execute: (params: DeleteSketchParams) => this.deleteSketch(params),
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteSketch(uri: string): Promise<void> {
|
||||
const sketch = await this.loadSketch(uri);
|
||||
if (!sketch) {
|
||||
console.info(`Sketch not found at ${uri}. Skipping deletion.`);
|
||||
private async deleteSketch(params: DeleteSketchParams): Promise<void> {
|
||||
const { toDelete, willNavigateAway } = params;
|
||||
let sketch: Sketch;
|
||||
if (typeof toDelete === 'string') {
|
||||
const resolvedSketch = await this.loadSketch(toDelete);
|
||||
if (!resolvedSketch) {
|
||||
console.info(
|
||||
`Failed to load the sketch. It was not found at '${toDelete}'. Skipping deletion.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
sketch = resolvedSketch;
|
||||
} else {
|
||||
sketch = toDelete;
|
||||
}
|
||||
if (!willNavigateAway) {
|
||||
this.scheduleDeletion(sketch);
|
||||
return;
|
||||
}
|
||||
return this.sketchService.deleteSketch(sketch);
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (willNavigateAway !== 'force') {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localizeByDefault('Delete'),
|
||||
type: 'question',
|
||||
buttons: [Dialog.CANCEL, Dialog.OK],
|
||||
message: cloudUri
|
||||
? nls.localize(
|
||||
'theia/workspace/deleteCloudSketch',
|
||||
"The cloud sketch '{0}' will be permanently deleted from the Arduino servers and the local caches. This action is irreversible. Do you want to delete the current sketch?",
|
||||
sketch.name
|
||||
)
|
||||
: nls.localize(
|
||||
'theia/workspace/deleteCurrentSketch',
|
||||
"The sketch '{0}' will be permanently deleted. This action is irreversible. Do you want to delete the current sketch?",
|
||||
sketch.name
|
||||
),
|
||||
});
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cloudUri) {
|
||||
const posixPath = cloudUri.path.toString();
|
||||
const cloudSketch = this.createApi.sketchCache.getSketch(posixPath);
|
||||
if (!cloudSketch) {
|
||||
throw new Error(
|
||||
`Cloud sketch with path '${posixPath}' was not cached. Cache: ${this.createApi.sketchCache.toString()}`
|
||||
);
|
||||
}
|
||||
try {
|
||||
// IDE2 cannot use DELETE directory as the server responses with HTTP 500 if it's missing.
|
||||
// https://github.com/arduino/arduino-ide/issues/1825#issuecomment-1406301406
|
||||
await this.createApi.deleteSketch(cloudSketch.path);
|
||||
} catch (err) {
|
||||
if (!isNotFound(err)) {
|
||||
throw err;
|
||||
} else {
|
||||
console.info(
|
||||
`Could not delete the cloud sketch with path '${posixPath}'. It does not exist.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all([
|
||||
...Sketch.uris(sketch).map((uri) =>
|
||||
this.closeWithoutSaving(new URI(uri))
|
||||
),
|
||||
]);
|
||||
this.windowService.setSafeToShutDown();
|
||||
this.scheduleDeletion(sketch);
|
||||
return window.close();
|
||||
}
|
||||
|
||||
private scheduleDeletion(sketch: Sketch): void {
|
||||
ipcRenderer.send(SCHEDULE_DELETION_SIGNAL, sketch);
|
||||
}
|
||||
|
||||
private async loadSketch(uri: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchService.loadSketch(uri);
|
||||
const sketch = await this.sketchesService.loadSketch(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
@@ -35,6 +134,13 @@ export class DeleteSketch extends SketchContribution {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// fix: https://github.com/eclipse-theia/theia/issues/12107
|
||||
private async closeWithoutSaving(uri: URI): Promise<void> {
|
||||
const affected = getAffected(this.shell.widgets, uri);
|
||||
const toClose = [...affected].map(([, widget]) => widget);
|
||||
await this.shell.closeMany(toClose, { save: false });
|
||||
}
|
||||
}
|
||||
export namespace DeleteSketch {
|
||||
export namespace Commands {
|
||||
@@ -43,3 +149,20 @@ export namespace DeleteSketch {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getAffected<T extends Widget>(
|
||||
widgets: Iterable<T>,
|
||||
context: MaybeArray<URI>
|
||||
): [URI, T & NavigatableWidget][] {
|
||||
const uris = Array.isArray(context) ? context : [context];
|
||||
const result: [URI, T & NavigatableWidget][] = [];
|
||||
for (const widget of widgets) {
|
||||
if (NavigatableWidget.is(widget)) {
|
||||
const resourceUri = widget.getResourceUri();
|
||||
if (resourceUri && uris.some((uri) => uri.isEqualOrParent(resourceUri))) {
|
||||
result.push([resourceUri, widget]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@@ -57,9 +57,11 @@ export class EditContributions extends Contribution {
|
||||
execute: async () => {
|
||||
const value = await this.currentValue();
|
||||
if (value !== undefined) {
|
||||
this.clipboardService.writeText(`\`\`\`cpp
|
||||
this.clipboardService.writeText(`
|
||||
\`\`\`cpp
|
||||
${value}
|
||||
\`\`\``);
|
||||
\`\`\`
|
||||
`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@@ -12,7 +12,6 @@ import {
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import {
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
CoreService,
|
||||
} from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { unregisterSubmenu } from '../menu/arduino-menus';
|
||||
|
||||
@injectable()
|
||||
export abstract class Examples extends SketchContribution {
|
||||
@@ -37,10 +37,7 @@ export abstract class Examples extends SketchContribution {
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
@@ -51,6 +48,9 @@ export abstract class Examples extends SketchContribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
protected override init(): void {
|
||||
@@ -58,6 +58,12 @@ export abstract class Examples extends SketchContribution {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||
this.handleBoardChanged(selectedBoard)
|
||||
);
|
||||
this.notificationCenter.onDidReinitialize(() =>
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
// No force refresh. The core client was already refreshed.
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
||||
@@ -124,6 +130,11 @@ export abstract class Examples extends SketchContribution {
|
||||
const { label } = sketchContainerOrPlaceholder;
|
||||
submenuPath = [...menuPath, label];
|
||||
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
|
||||
this.toDispose.push(
|
||||
Disposable.create(() =>
|
||||
unregisterSubmenu(submenuPath, this.menuRegistry)
|
||||
)
|
||||
);
|
||||
sketches.push(...sketchContainerOrPlaceholder.sketches);
|
||||
children.push(...sketchContainerOrPlaceholder.children);
|
||||
} else {
|
||||
@@ -190,7 +201,7 @@ export abstract class Examples extends SketchContribution {
|
||||
|
||||
private async clone(uri: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchService.cloneExample(uri);
|
||||
const sketch = await this.sketchesService.cloneExample(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
@@ -243,9 +254,6 @@ export class BuiltInExamples extends Examples {
|
||||
|
||||
@injectable()
|
||||
export class LibraryExamples extends Examples {
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
override onStart(): void {
|
||||
|
@@ -17,7 +17,7 @@ import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class IncludeLibrary extends SketchContribution {
|
||||
@@ -53,6 +53,7 @@ export class IncludeLibrary extends SketchContribution {
|
||||
this.notificationCenter.onLibraryDidUninstall(() =>
|
||||
this.updateMenuActions()
|
||||
);
|
||||
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
|
@@ -1,15 +1,20 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import {
|
||||
ArduinoDaemon,
|
||||
assertSanitizedFqbn,
|
||||
BoardsService,
|
||||
ExecutableService,
|
||||
sanitizeFqbn,
|
||||
} from '../../common/protocol';
|
||||
import { HostedPluginEvents } from '../hosted-plugin-events';
|
||||
import { SketchContribution, URI } from './contribution';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { HostedPluginEvents } from '../hosted-plugin-events';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { SketchContribution, URI } from './contribution';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
|
||||
@injectable()
|
||||
export class InoLanguage extends SketchContribution {
|
||||
@@ -28,8 +33,15 @@ export class InoLanguage extends SketchContribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
private readonly boardDataStore: BoardsDataStore;
|
||||
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private readonly languageServerStartMutex = new Mutex();
|
||||
private languageServerFqbn?: string;
|
||||
private languageServerStartMutex = new Mutex();
|
||||
|
||||
override onReady(): void {
|
||||
const start = (
|
||||
@@ -43,27 +55,61 @@ export class InoLanguage extends SketchContribution {
|
||||
}
|
||||
}
|
||||
};
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(start);
|
||||
this.hostedPluginEvents.onPluginsDidStart(() =>
|
||||
start(this.boardsServiceProvider.boardsConfig)
|
||||
);
|
||||
this.hostedPluginEvents.onPluginsWillUnload(
|
||||
() => (this.languageServerFqbn = undefined)
|
||||
);
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, oldValue, newValue }) => {
|
||||
if (oldValue !== newValue) {
|
||||
switch (preferenceName) {
|
||||
case 'arduino.language.log':
|
||||
case 'arduino.language.realTimeDiagnostics':
|
||||
start(this.boardsServiceProvider.boardsConfig, true);
|
||||
const forceRestart = () => {
|
||||
start(this.boardsServiceProvider.boardsConfig, true);
|
||||
};
|
||||
this.toDispose.pushAll([
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(start),
|
||||
this.hostedPluginEvents.onPluginsDidStart(() =>
|
||||
start(this.boardsServiceProvider.boardsConfig)
|
||||
),
|
||||
this.hostedPluginEvents.onPluginsWillUnload(
|
||||
() => (this.languageServerFqbn = undefined)
|
||||
),
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, oldValue, newValue }) => {
|
||||
if (oldValue !== newValue) {
|
||||
switch (preferenceName) {
|
||||
case 'arduino.language.log':
|
||||
case 'arduino.language.realTimeDiagnostics':
|
||||
forceRestart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
),
|
||||
this.notificationCenter.onLibraryDidInstall(() => forceRestart()),
|
||||
this.notificationCenter.onLibraryDidUninstall(() => forceRestart()),
|
||||
this.notificationCenter.onPlatformDidInstall(() => forceRestart()),
|
||||
this.notificationCenter.onPlatformDidUninstall(() => forceRestart()),
|
||||
this.notificationCenter.onDidReinitialize(() => forceRestart()),
|
||||
this.boardDataStore.onChanged((dataChangePerFqbn) => {
|
||||
if (this.languageServerFqbn) {
|
||||
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
|
||||
if (!sanitizeFqbn) {
|
||||
throw new Error(
|
||||
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
|
||||
);
|
||||
}
|
||||
const matchingFqbn = dataChangePerFqbn.find(
|
||||
(fqbn) => sanitizedFqbn === fqbn
|
||||
);
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
if (
|
||||
matchingFqbn &&
|
||||
boardsConfig.selectedBoard?.fqbn === matchingFqbn
|
||||
) {
|
||||
start(boardsConfig);
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
start(this.boardsServiceProvider.boardsConfig);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private async startLanguageServer(
|
||||
fqbn: string,
|
||||
name: string | undefined,
|
||||
@@ -101,11 +147,18 @@ export class InoLanguage extends SketchContribution {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!forceStart && fqbn === this.languageServerFqbn) {
|
||||
assertSanitizedFqbn(fqbn);
|
||||
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
|
||||
if (!fqbnWithConfig) {
|
||||
throw new Error(
|
||||
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
|
||||
);
|
||||
}
|
||||
if (!forceStart && fqbnWithConfig === this.languageServerFqbn) {
|
||||
// NOOP
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Starting language server: ${fqbn}`);
|
||||
this.logger.info(`Starting language server: ${fqbnWithConfig}`);
|
||||
const log = this.preferences.get('arduino.language.log');
|
||||
const realTimeDiagnostics = this.preferences.get(
|
||||
'arduino.language.realTimeDiagnostics'
|
||||
@@ -141,7 +194,7 @@ export class InoLanguage extends SketchContribution {
|
||||
log: currentSketchPath ? currentSketchPath : log,
|
||||
cliDaemonInstance: '1',
|
||||
board: {
|
||||
fqbn,
|
||||
fqbn: fqbnWithConfig,
|
||||
name: name ? `"${name}"` : undefined,
|
||||
},
|
||||
realTimeDiagnostics,
|
||||
@@ -150,7 +203,7 @@ export class InoLanguage extends SketchContribution {
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(`Failed to start language server for ${fqbn}`, e);
|
||||
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
|
||||
this.languageServerFqbn = undefined;
|
||||
} finally {
|
||||
release();
|
||||
|
@@ -1,31 +1,17 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { 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 { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CommandRegistry, 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,
|
||||
@@ -62,63 +48,22 @@ export class InterfaceScale extends Contribution {
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
const increaseFontSizeMenuAction = {
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/increaseFontSize',
|
||||
'Increase Font Size'
|
||||
),
|
||||
order: '0',
|
||||
};
|
||||
const decreaseFontSizeMenuAction = {
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
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 {
|
||||
@@ -153,7 +98,7 @@ export class InterfaceScale extends Contribution {
|
||||
);
|
||||
if (isChanged) {
|
||||
this.fontScalingEnabled = fontScalingEnabled;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,76 +1,39 @@
|
||||
import { DialogError } from '@theia/core/lib/browser/dialogs';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { Widget } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import {
|
||||
Progress,
|
||||
ProgressUpdate,
|
||||
} from '@theia/core/lib/common/message-service-protocol';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog';
|
||||
import { v4 } from 'uuid';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import type { AuthenticationSession } from '../../node/auth/types';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { CreateApi } from '../create/create-api';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { Create } from '../create/typings';
|
||||
import { isConflict } from '../create/typings';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog';
|
||||
import {
|
||||
TaskFactoryImpl,
|
||||
WorkspaceInputDialogWithProgress,
|
||||
} from '../theia/workspace/workspace-input-dialog';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
|
||||
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
|
||||
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
|
||||
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
|
||||
import { Command, CommandRegistry, Contribution, URI } from './contribution';
|
||||
import { Command, CommandRegistry, Sketch } from './contribution';
|
||||
import {
|
||||
CloudSketchContribution,
|
||||
pullingSketch,
|
||||
sketchAlreadyExists,
|
||||
synchronizingSketchbook,
|
||||
} from './cloud-contribution';
|
||||
|
||||
@injectable()
|
||||
export class NewCloudSketch extends Contribution {
|
||||
@inject(CreateApi)
|
||||
private readonly createApi: CreateApi;
|
||||
@inject(SketchbookWidgetContribution)
|
||||
private readonly widgetContribution: SketchbookWidgetContribution;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(MainMenuManager)
|
||||
private readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
export class NewCloudSketch extends CloudSketchContribution {
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private _session: AuthenticationSession | undefined;
|
||||
private _enabled: boolean;
|
||||
|
||||
override onReady(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.authenticationService.onSessionDidChange((session) => {
|
||||
const oldSession = this._session;
|
||||
this._session = session;
|
||||
if (!!oldSession !== !!this._session) {
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}),
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (preferenceName === 'arduino.cloud.enabled') {
|
||||
const oldEnabled = this._enabled;
|
||||
this._enabled = Boolean(newValue);
|
||||
if (this._enabled !== oldEnabled) {
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
}),
|
||||
this.createFeatures.onDidChangeEnabled(() => this.menuManager.update()),
|
||||
this.createFeatures.onDidChangeSession(() => this.menuManager.update()),
|
||||
]);
|
||||
this._enabled = this.preferences['arduino.cloud.enabled'];
|
||||
this._session = this.authenticationService.session;
|
||||
if (this._session) {
|
||||
this.mainMenuManager.update();
|
||||
if (this.createFeatures.session) {
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,16 +43,16 @@ export class NewCloudSketch extends Contribution {
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
|
||||
execute: () => this.createNewSketch(),
|
||||
isEnabled: () => !!this._session,
|
||||
isVisible: () => this._enabled,
|
||||
execute: () => this.createNewSketch(true),
|
||||
isEnabled: () => Boolean(this.createFeatures.session),
|
||||
isVisible: () => this.createFeatures.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
label: nls.localize('arduino/cloudSketch/new', 'New Remote Sketch'),
|
||||
label: nls.localize('arduino/cloudSketch/new', 'New Cloud Sketch'),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
@@ -102,154 +65,95 @@ export class NewCloudSketch extends Contribution {
|
||||
}
|
||||
|
||||
private async createNewSketch(
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined
|
||||
): Promise<unknown> {
|
||||
const widget = await this.widgetContribution.widget;
|
||||
const treeModel = this.treeModelFrom(widget);
|
||||
if (!treeModel) {
|
||||
return undefined;
|
||||
}
|
||||
const rootNode = CompositeTreeNode.is(treeModel.root)
|
||||
? treeModel.root
|
||||
: undefined;
|
||||
if (!rootNode) {
|
||||
return undefined;
|
||||
}
|
||||
return this.openWizard(rootNode, treeModel, initialValue);
|
||||
}
|
||||
|
||||
private withProgress(
|
||||
value: string,
|
||||
treeModel: CloudSketchbookTreeModel
|
||||
): (progress: Progress) => Promise<unknown> {
|
||||
return async (progress: Progress) => {
|
||||
let result: Create.Sketch | undefined | 'conflict';
|
||||
try {
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/creating',
|
||||
"Creating remote sketch '{0}'...",
|
||||
value
|
||||
),
|
||||
});
|
||||
result = await this.createApi.createSketch(value);
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
result = 'conflict';
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (result) {
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/synchronizing',
|
||||
"Synchronizing sketchbook, pulling '{0}'...",
|
||||
value
|
||||
),
|
||||
});
|
||||
await treeModel.refresh();
|
||||
}
|
||||
}
|
||||
if (result === 'conflict') {
|
||||
return this.createNewSketch(value);
|
||||
}
|
||||
if (result) {
|
||||
return this.open(treeModel, result);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
private async open(
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
newSketch: Create.Sketch
|
||||
): Promise<URI | undefined> {
|
||||
const id = CreateUri.toUri(newSketch).path.toString();
|
||||
const node = treeModel.getNode(id);
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
`Could not find remote sketchbook tree node with Tree node ID: ${id}.`
|
||||
): Promise<void> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (treeModel) {
|
||||
const rootNode = treeModel.root;
|
||||
return this.openWizard(
|
||||
rootNode,
|
||||
treeModel,
|
||||
skipShowErrorMessageOnOpen,
|
||||
initialValue
|
||||
);
|
||||
}
|
||||
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||
throw new Error(
|
||||
`Remote sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
|
||||
);
|
||||
}
|
||||
try {
|
||||
await treeModel.sketchbookTree().pull({ node });
|
||||
} catch (err) {
|
||||
if (isNotFound(err)) {
|
||||
await treeModel.refresh();
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/newCloudSketch/notFound',
|
||||
"Could not pull the remote sketch '{0}'. It does not exist.",
|
||||
newSketch.name
|
||||
)
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return this.commandService.executeCommand(
|
||||
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||
{ node }
|
||||
);
|
||||
}
|
||||
|
||||
private treeModelFrom(
|
||||
widget: SketchbookWidget
|
||||
): CloudSketchbookTreeModel | undefined {
|
||||
for (const treeWidget of widget.getTreeWidgets()) {
|
||||
if (treeWidget instanceof CloudSketchbookTreeWidget) {
|
||||
const model = treeWidget.model;
|
||||
if (model instanceof CloudSketchbookTreeModel) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async openWizard(
|
||||
rootNode: CompositeTreeNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined
|
||||
): Promise<unknown> {
|
||||
): Promise<void> {
|
||||
const existingNames = rootNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
return new NewCloudSketchDialog(
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/newCloudSketch/newSketchTitle',
|
||||
'Name of a new Remote Sketch'
|
||||
),
|
||||
parentUri: CreateUri.root,
|
||||
initialValue,
|
||||
validate: (input) => {
|
||||
if (existingNames.includes(input)) {
|
||||
return nls.localize(
|
||||
'arduino/newCloudSketch/sketchAlreadyExists',
|
||||
"Remote sketch '{0}' already exists.",
|
||||
input
|
||||
);
|
||||
}
|
||||
// This is how https://create.arduino.cc/editor/ works when renaming a sketch.
|
||||
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
|
||||
return '';
|
||||
}
|
||||
return nls.localize(
|
||||
'arduino/newCloudSketch/invalidSketchName',
|
||||
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
|
||||
);
|
||||
const taskFactory = new TaskFactoryImpl((value) =>
|
||||
this.createNewSketchWithProgress(treeModel, value)
|
||||
);
|
||||
try {
|
||||
const dialog = new WorkspaceInputDialogWithProgress(
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/newCloudSketch/newSketchTitle',
|
||||
'Name of the new Cloud Sketch'
|
||||
),
|
||||
parentUri: CreateUri.root,
|
||||
initialValue,
|
||||
validate: (input) => {
|
||||
if (existingNames.includes(input)) {
|
||||
return sketchAlreadyExists(input);
|
||||
}
|
||||
return Sketch.validateCloudSketchFolderName(input) ?? '';
|
||||
},
|
||||
},
|
||||
},
|
||||
this.labelProvider,
|
||||
(value) => this.withProgress(value, treeModel)
|
||||
).open();
|
||||
this.labelProvider,
|
||||
taskFactory
|
||||
);
|
||||
await dialog.open(skipShowErrorMessageOnOpen);
|
||||
if (dialog.taskResult) {
|
||||
this.openInNewWindow(dialog.taskResult);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
await treeModel.refresh();
|
||||
return this.createNewSketch(false, taskFactory.value ?? initialValue);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private createNewSketchWithProgress(
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
value: string
|
||||
): (
|
||||
progress: Progress
|
||||
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
|
||||
return async (progress: Progress) => {
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/creating',
|
||||
"Creating cloud sketch '{0}'...",
|
||||
value
|
||||
),
|
||||
});
|
||||
const sketch = await this.createApi.createSketch(value);
|
||||
progress.report({ message: synchronizingSketchbook });
|
||||
await treeModel.refresh();
|
||||
progress.report({ message: pullingSketch(sketch.name) });
|
||||
const node = await this.pull(sketch);
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
private openInNewWindow(
|
||||
node: CloudSketchbookTree.CloudSketchDirNode
|
||||
): Promise<void> {
|
||||
return this.commandService.executeCommand(
|
||||
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||
{ node }
|
||||
);
|
||||
}
|
||||
}
|
||||
export namespace NewCloudSketch {
|
||||
@@ -259,115 +163,3 @@ export namespace NewCloudSketch {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isConflict(err: unknown): boolean {
|
||||
return isErrorWithStatusOf(err, 409);
|
||||
}
|
||||
function isNotFound(err: unknown): boolean {
|
||||
return isErrorWithStatusOf(err, 404);
|
||||
}
|
||||
function isErrorWithStatusOf(
|
||||
err: unknown,
|
||||
status: number
|
||||
): err is Error & { status: number } {
|
||||
if (err instanceof Error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const object = err as any;
|
||||
return 'status' in object && object.status === status;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
class NewCloudSketchDialog extends WorkspaceInputDialog {
|
||||
constructor(
|
||||
@inject(WorkspaceInputDialogProps)
|
||||
protected override readonly props: WorkspaceInputDialogProps,
|
||||
@inject(LabelProvider)
|
||||
protected override readonly labelProvider: LabelProvider,
|
||||
private readonly withProgress: (
|
||||
value: string
|
||||
) => (progress: Progress) => Promise<unknown>
|
||||
) {
|
||||
super(props, labelProvider);
|
||||
}
|
||||
protected override async accept(): Promise<void> {
|
||||
if (!this.resolve) {
|
||||
return;
|
||||
}
|
||||
this.acceptCancellationSource.cancel();
|
||||
this.acceptCancellationSource = new CancellationTokenSource();
|
||||
const token = this.acceptCancellationSource.token;
|
||||
const value = this.value;
|
||||
const error = await this.isValid(value, 'open');
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
if (!DialogError.getResult(error)) {
|
||||
this.setErrorMessage(error);
|
||||
} else {
|
||||
const spinner = document.createElement('div');
|
||||
spinner.classList.add('spinner');
|
||||
const disposables = new DisposableCollection();
|
||||
try {
|
||||
this.toggleButtons(true);
|
||||
disposables.push(Disposable.create(() => this.toggleButtons(false)));
|
||||
|
||||
const closeParent = this.closeCrossNode.parentNode;
|
||||
closeParent?.removeChild(this.closeCrossNode);
|
||||
disposables.push(
|
||||
Disposable.create(() => {
|
||||
closeParent?.appendChild(this.closeCrossNode);
|
||||
})
|
||||
);
|
||||
|
||||
this.errorMessageNode.classList.add('progress');
|
||||
disposables.push(
|
||||
Disposable.create(() =>
|
||||
this.errorMessageNode.classList.remove('progress')
|
||||
)
|
||||
);
|
||||
|
||||
const errorParent = this.errorMessageNode.parentNode;
|
||||
errorParent?.insertBefore(spinner, this.errorMessageNode);
|
||||
disposables.push(
|
||||
Disposable.create(() => errorParent?.removeChild(spinner))
|
||||
);
|
||||
|
||||
const cancellationSource = new CancellationTokenSource();
|
||||
const progress: Progress = {
|
||||
id: v4(),
|
||||
cancel: () => cancellationSource.cancel(),
|
||||
report: (update: ProgressUpdate) => {
|
||||
this.setProgressMessage(update);
|
||||
},
|
||||
result: Promise.resolve(value),
|
||||
};
|
||||
await this.withProgress(value)(progress);
|
||||
} finally {
|
||||
disposables.dispose();
|
||||
}
|
||||
this.resolve(value);
|
||||
Widget.detach(this);
|
||||
}
|
||||
}
|
||||
|
||||
private toggleButtons(disabled: boolean): void {
|
||||
if (this.acceptButton) {
|
||||
this.acceptButton.disabled = disabled;
|
||||
}
|
||||
if (this.closeButton) {
|
||||
this.closeButton.disabled = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
private setProgressMessage(update: ProgressUpdate): void {
|
||||
if (update.work && update.work.done === update.work.total) {
|
||||
this.errorMessageNode.innerText = '';
|
||||
} else {
|
||||
if (update.message) {
|
||||
this.errorMessageNode.innerText = update.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ export class NewSketch extends SketchContribution {
|
||||
|
||||
async newSketch(): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
const sketch = await this.sketchesService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
|
@@ -47,7 +47,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
private update(forceUpdate?: boolean): void {
|
||||
this.sketchService
|
||||
this.sketchesService
|
||||
.recentlyOpenedSketches(forceUpdate)
|
||||
.then((sketches) => this.refreshMenu(sketches));
|
||||
}
|
||||
|
@@ -1,32 +1,34 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import type { Settings } from '../dialogs/settings/settings';
|
||||
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
Command,
|
||||
MenuModelRegistry,
|
||||
CommandRegistry,
|
||||
SketchContribution,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
SketchContribution,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { Settings as Preferences } from '../dialogs/settings/settings';
|
||||
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class Settings extends SketchContribution {
|
||||
export class OpenSettings extends SketchContribution {
|
||||
@inject(SettingsDialog)
|
||||
protected readonly settingsDialog: SettingsDialog;
|
||||
private readonly settingsDialog: SettingsDialog;
|
||||
|
||||
protected settingsOpened = false;
|
||||
private settingsOpened = false;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Settings.Commands.OPEN, {
|
||||
registry.registerCommand(OpenSettings.Commands.OPEN, {
|
||||
execute: async () => {
|
||||
let settings: Preferences | undefined = undefined;
|
||||
let settings: Settings | undefined = undefined;
|
||||
try {
|
||||
this.settingsOpened = true;
|
||||
this.menuManager.update();
|
||||
settings = await this.settingsDialog.open();
|
||||
} finally {
|
||||
this.settingsOpened = false;
|
||||
this.menuManager.update();
|
||||
}
|
||||
if (settings) {
|
||||
await this.settingsService.update(settings);
|
||||
@@ -41,7 +43,7 @@ export class Settings extends SketchContribution {
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
|
||||
commandId: Settings.Commands.OPEN.id,
|
||||
commandId: OpenSettings.Commands.OPEN.id,
|
||||
label:
|
||||
nls.localize(
|
||||
'vscode/preferences.contribution/preferences',
|
||||
@@ -57,13 +59,13 @@ export class Settings extends SketchContribution {
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: Settings.Commands.OPEN.id,
|
||||
command: OpenSettings.Commands.OPEN.id,
|
||||
keybinding: 'CtrlCmd+,',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Settings {
|
||||
export namespace OpenSettings {
|
||||
export namespace Commands {
|
||||
export const OPEN: Command = {
|
||||
id: 'arduino-settings-open',
|
@@ -39,7 +39,7 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
focusMainSketchFile = false
|
||||
): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchService.loadSketch(uri.toString());
|
||||
const sketch = await this.sketchesService.loadSketch(uri.toString());
|
||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
|
||||
await this.ensureOpened(uri);
|
||||
@@ -112,7 +112,7 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
|
||||
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
|
||||
fileService: this.fileService,
|
||||
sketchService: this.sketchService,
|
||||
sketchesService: this.sketchesService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
if (movedSketch) {
|
||||
@@ -125,7 +125,7 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
}
|
||||
|
||||
private async openFallbackSketch(): Promise<void> {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
const sketch = await this.sketchesService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
||||
}
|
||||
|
||||
|
@@ -71,7 +71,7 @@ export class OpenSketch extends SketchContribution {
|
||||
}
|
||||
const uri = SketchLocation.toUri(toOpen);
|
||||
try {
|
||||
await this.sketchService.loadSketch(uri.toString());
|
||||
await this.sketchesService.loadSketch(uri.toString());
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
@@ -82,10 +82,7 @@ export class OpenSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
private async selectSketch(): Promise<Sketch | undefined> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri)
|
||||
);
|
||||
const defaultPath = await this.defaultPath();
|
||||
const { filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
@@ -109,14 +106,14 @@ export class OpenSketch extends SketchContribution {
|
||||
}
|
||||
const sketchFilePath = filePaths[0];
|
||||
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
|
||||
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
|
||||
const sketch = await this.sketchesService.getSketchFolder(sketchFileUri);
|
||||
if (sketch) {
|
||||
return sketch;
|
||||
}
|
||||
if (Sketch.isSketchFile(sketchFileUri)) {
|
||||
return promptMoveSketch(sketchFileUri, {
|
||||
fileService: this.fileService,
|
||||
sketchService: this.sketchService,
|
||||
sketchesService: this.sketchesService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
}
|
||||
@@ -135,11 +132,11 @@ export async function promptMoveSketch(
|
||||
sketchFileUri: string | URI,
|
||||
options: {
|
||||
fileService: FileService;
|
||||
sketchService: SketchesService;
|
||||
sketchesService: SketchesService;
|
||||
labelProvider: LabelProvider;
|
||||
}
|
||||
): Promise<Sketch | undefined> {
|
||||
const { fileService, sketchService, labelProvider } = options;
|
||||
const { fileService, sketchesService, labelProvider } = options;
|
||||
const uri =
|
||||
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
|
||||
const name = uri.path.name;
|
||||
@@ -179,6 +176,6 @@ export async function promptMoveSketch(
|
||||
uri,
|
||||
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||
);
|
||||
return sketchService.getSketchFolder(newSketchUri.toString());
|
||||
return sketchesService.getSketchFolder(newSketchUri.toString());
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,166 @@
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { isConflict } from '../create/typings';
|
||||
import {
|
||||
TaskFactoryImpl,
|
||||
WorkspaceInputDialogWithProgress,
|
||||
} from '../theia/workspace/workspace-input-dialog';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import {
|
||||
CloudSketchContribution,
|
||||
pullingSketch,
|
||||
pushingSketch,
|
||||
sketchAlreadyExists,
|
||||
synchronizingSketchbook,
|
||||
} from './cloud-contribution';
|
||||
import { Command, CommandRegistry, Sketch, URI } from './contribution';
|
||||
|
||||
export interface RenameCloudSketchParams {
|
||||
readonly cloudUri: URI;
|
||||
readonly sketch: Sketch;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class RenameCloudSketch extends CloudSketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH, {
|
||||
execute: (params: RenameCloudSketchParams) =>
|
||||
this.renameSketch(params, true),
|
||||
});
|
||||
}
|
||||
|
||||
private async renameSketch(
|
||||
params: RenameCloudSketchParams,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initValue: string = params.sketch.name
|
||||
): Promise<string | undefined> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (treeModel) {
|
||||
const posixPath = params.cloudUri.path.toString();
|
||||
const node = treeModel.getNode(posixPath);
|
||||
const parentNode = node?.parent;
|
||||
if (
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(node) &&
|
||||
CompositeTreeNode.is(parentNode)
|
||||
) {
|
||||
return this.openWizard(
|
||||
params,
|
||||
node,
|
||||
parentNode,
|
||||
treeModel,
|
||||
skipShowErrorMessageOnOpen,
|
||||
initValue
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async openWizard(
|
||||
params: RenameCloudSketchParams,
|
||||
node: CloudSketchbookTree.CloudSketchDirNode,
|
||||
parentNode: CompositeTreeNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined
|
||||
): Promise<string | undefined> {
|
||||
const parentUri = CloudSketchbookTree.CloudSketchDirNode.is(parentNode)
|
||||
? parentNode.uri
|
||||
: CreateUri.root;
|
||||
const existingNames = parentNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
const taskFactory = new TaskFactoryImpl((value) =>
|
||||
this.renameSketchWithProgress(params, node, treeModel, value)
|
||||
);
|
||||
try {
|
||||
const dialog = new WorkspaceInputDialogWithProgress(
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/renameCloudSketch/renameSketchTitle',
|
||||
'New name of the Cloud Sketch'
|
||||
),
|
||||
parentUri,
|
||||
initialValue,
|
||||
validate: (input) => {
|
||||
if (existingNames.includes(input)) {
|
||||
return sketchAlreadyExists(input);
|
||||
}
|
||||
return Sketch.validateCloudSketchFolderName(input) ?? '';
|
||||
},
|
||||
},
|
||||
this.labelProvider,
|
||||
taskFactory
|
||||
);
|
||||
await dialog.open(skipShowErrorMessageOnOpen);
|
||||
return dialog.taskResult;
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
await treeModel.refresh();
|
||||
return this.renameSketch(
|
||||
params,
|
||||
false,
|
||||
taskFactory.value ?? initialValue
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private renameSketchWithProgress(
|
||||
params: RenameCloudSketchParams,
|
||||
node: CloudSketchbookTree.CloudSketchDirNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
value: string
|
||||
): (progress: Progress) => Promise<string | undefined> {
|
||||
return async (progress: Progress) => {
|
||||
const fromName = params.cloudUri.path.name;
|
||||
const fromPosixPath = params.cloudUri.path.toString();
|
||||
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
|
||||
// push
|
||||
progress.report({ message: pushingSketch(params.sketch.name) });
|
||||
await treeModel.sketchbookTree().push(node);
|
||||
|
||||
// rename
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/renaming',
|
||||
"Renaming cloud sketch from '{0}' to '{1}'...",
|
||||
fromName,
|
||||
value
|
||||
),
|
||||
});
|
||||
await this.createApi.rename(fromPosixPath, toPosixPath);
|
||||
|
||||
// sync
|
||||
progress.report({
|
||||
message: synchronizingSketchbook,
|
||||
});
|
||||
this.createApi.sketchCache.init(); // invalidate the cache
|
||||
await this.createApi.sketches(); // IDE2 must pull all sketches to find the new one
|
||||
const sketch = this.createApi.sketchCache.getSketch(toPosixPath);
|
||||
if (!sketch) {
|
||||
return undefined;
|
||||
}
|
||||
await treeModel.refresh();
|
||||
|
||||
// pull
|
||||
progress.report({ message: pullingSketch(sketch.name) });
|
||||
const pulledNode = await this.pull(sketch);
|
||||
return pulledNode
|
||||
? node.uri.parent.resolve(sketch.name).toString()
|
||||
: undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
export namespace RenameCloudSketch {
|
||||
export namespace Commands {
|
||||
export const RENAME_CLOUD_SKETCH: Command = {
|
||||
id: 'arduino-rename-cloud-sketch',
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,28 +1,34 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
|
||||
import { Saveable } from '@theia/core/lib/browser/saveable';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
import {
|
||||
SketchContribution,
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { DeleteSketch } from './delete-sketch';
|
||||
import {
|
||||
RenameCloudSketch,
|
||||
RenameCloudSketchParams,
|
||||
} from './rename-cloud-sketch';
|
||||
|
||||
@injectable()
|
||||
export class SaveAsSketch extends SketchContribution {
|
||||
export class SaveAsSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly applicationShell: ApplicationShell;
|
||||
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
@@ -35,7 +41,7 @@ export class SaveAsSketch extends SketchContribution {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
|
||||
label: nls.localizeByDefault('Save As...'),
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
@@ -58,21 +64,70 @@ export class SaveAsSketch extends SketchContribution {
|
||||
markAsRecentlyOpened,
|
||||
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||
): Promise<boolean> {
|
||||
const [sketch, configuration] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
let destinationUri: string | undefined;
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (cloudUri) {
|
||||
destinationUri = await this.createCloudCopy({ cloudUri, sketch });
|
||||
} else {
|
||||
destinationUri = await this.createLocalCopy(sketch, execOnlyIfTemp);
|
||||
}
|
||||
if (!destinationUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newWorkspaceUri = await this.sketchesService.copy(sketch, {
|
||||
destinationUri,
|
||||
});
|
||||
if (!newWorkspaceUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
|
||||
}
|
||||
const options: WorkspaceInput & StartupTask.Owner = {
|
||||
preserveWindow: true,
|
||||
tasks: [],
|
||||
};
|
||||
if (openAfterMove) {
|
||||
this.windowService.setSafeToShutDown();
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
options.tasks.push({
|
||||
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
args: [{ toDelete: sketch.uri }],
|
||||
});
|
||||
}
|
||||
this.workspaceService.open(new URI(newWorkspaceUri), options);
|
||||
}
|
||||
return !!newWorkspaceUri;
|
||||
}
|
||||
|
||||
private async createCloudCopy(
|
||||
params: RenameCloudSketchParams
|
||||
): Promise<string | undefined> {
|
||||
return this.commandService.executeCommand<string>(
|
||||
RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH.id,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
private async createLocalCopy(
|
||||
sketch: Sketch,
|
||||
execOnlyIfTemp?: boolean
|
||||
): Promise<string | undefined> {
|
||||
const isTemp = await this.sketchesService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const sketchbookDirUri = new URI(configuration.sketchDirUri);
|
||||
const sketchbookDirUri = await this.defaultUri();
|
||||
// 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.
|
||||
@@ -87,91 +142,157 @@ export class SaveAsSketch extends SketchContribution {
|
||||
|
||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||
// IDE2 must never prompt an invalid sketch folder name (https://github.com/arduino/arduino-ide/pull/1833#issuecomment-1412569252)
|
||||
const defaultUri = containerDirUri.resolve(
|
||||
exists
|
||||
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
|
||||
: sketch.name
|
||||
Sketch.toValidSketchFolderName(sketch.name, exists)
|
||||
);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
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;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
if (!destinationUri) {
|
||||
return false;
|
||||
}
|
||||
const workspaceUri = await this.sketchService.copy(sketch, {
|
||||
destinationUri,
|
||||
});
|
||||
if (workspaceUri) {
|
||||
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchService.markAsRecentlyOpened(workspaceUri);
|
||||
}
|
||||
}
|
||||
const options: WorkspaceInput & StartupTask.Owner = {
|
||||
preserveWindow: true,
|
||||
tasks: [],
|
||||
};
|
||||
if (workspaceUri && openAfterMove) {
|
||||
this.windowService.setSafeToShutDown();
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
options.tasks.push({
|
||||
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
args: [sketch.uri],
|
||||
});
|
||||
}
|
||||
this.workspaceService.open(new URI(workspaceUri), options);
|
||||
}
|
||||
return !!workspaceUri;
|
||||
return await this.promptLocalSketchFolderDestination(sketch, defaultPath);
|
||||
}
|
||||
|
||||
private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
|
||||
/**
|
||||
* Prompts for the new sketch folder name until a valid one is give,
|
||||
* then resolves with the destination sketch folder URI string,
|
||||
* or `undefined` if the operation was canceled.
|
||||
*/
|
||||
private async promptLocalSketchFolderDestination(
|
||||
sketch: Sketch,
|
||||
defaultPath: string
|
||||
): Promise<string | undefined> {
|
||||
let sketchFolderDestinationUri: string | undefined;
|
||||
while (!sketchFolderDestinationUri) {
|
||||
const { filePath } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveFolderAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
);
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
// The new location of the sketch cannot be inside the location of current sketch.
|
||||
// https://github.com/arduino/arduino-ide/issues/1882
|
||||
let dialogContent: InvalidSketchFolderDialogContent | undefined;
|
||||
if (new URI(sketch.uri).isEqualOrParent(new URI(destinationUri))) {
|
||||
dialogContent = {
|
||||
message: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderLocationMessage',
|
||||
"Invalid sketch folder location: '{0}'",
|
||||
filePath
|
||||
),
|
||||
details: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderLocationDetails',
|
||||
'You cannot save a sketch into a folder inside itself.'
|
||||
),
|
||||
question: nls.localize(
|
||||
'arduino/sketch/editInvalidSketchFolderLocationQuestion',
|
||||
'Do you want to try saving the sketch to a different location?'
|
||||
),
|
||||
};
|
||||
}
|
||||
if (!dialogContent) {
|
||||
const sketchFolderName = new URI(destinationUri).path.base;
|
||||
const errorMessage = Sketch.validateSketchFolderName(sketchFolderName);
|
||||
if (errorMessage) {
|
||||
dialogContent = {
|
||||
message: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderNameMessage',
|
||||
"Invalid sketch folder name: '{0}'",
|
||||
sketchFolderName
|
||||
),
|
||||
details: errorMessage,
|
||||
question: nls.localize(
|
||||
'arduino/sketch/editInvalidSketchFolderQuestion',
|
||||
'Do you want to try saving the sketch with a different name?'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (dialogContent) {
|
||||
const message = `
|
||||
${dialogContent.message}
|
||||
|
||||
${dialogContent.details}
|
||||
|
||||
${dialogContent.question}`.trim();
|
||||
defaultPath = filePath;
|
||||
const { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
message,
|
||||
buttons: [Dialog.CANCEL, Dialog.YES],
|
||||
}
|
||||
);
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
sketchFolderDestinationUri = destinationUri;
|
||||
}
|
||||
}
|
||||
return sketchFolderDestinationUri;
|
||||
}
|
||||
|
||||
private async saveOntoCopiedSketch(
|
||||
sketch: Sketch,
|
||||
newSketchFolderUri: string
|
||||
): Promise<void> {
|
||||
const widgets = this.applicationShell.widgets;
|
||||
const snapshots = new Map<string, object>();
|
||||
const snapshots = new Map<string, Saveable.Snapshot>();
|
||||
for (const widget of widgets) {
|
||||
const saveable = Saveable.getDirty(widget);
|
||||
const uri = NavigatableWidget.getUri(widget);
|
||||
const uriString = uri?.toString();
|
||||
if (!uri) {
|
||||
continue;
|
||||
}
|
||||
const uriString = uri.toString();
|
||||
let relativePath: string;
|
||||
if (uri && uriString!.includes(sketchUri) && saveable && saveable.createSnapshot) {
|
||||
if (
|
||||
uriString.includes(sketch.uri) &&
|
||||
saveable &&
|
||||
saveable.createSnapshot
|
||||
) {
|
||||
// The main file will change its name during the copy process
|
||||
// We need to store the new name in the map
|
||||
if (mainFileUri === uriString) {
|
||||
const lastPart = new URI(newSketchUri).path.base + uri.path.ext;
|
||||
if (sketch.mainFileUri === uriString) {
|
||||
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
|
||||
relativePath = '/' + lastPart;
|
||||
} else {
|
||||
relativePath = uri.toString().substring(sketchUri.length);
|
||||
relativePath = uri.toString().substring(sketch.uri.length);
|
||||
}
|
||||
snapshots.set(relativePath, saveable.createSnapshot());
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
|
||||
const widgetUri = new URI(newSketchUri + path);
|
||||
try {
|
||||
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
await Promise.all(
|
||||
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
|
||||
const widgetUri = new URI(newSketchFolderUri + path);
|
||||
try {
|
||||
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface InvalidSketchFolderDialogContent {
|
||||
readonly message: string;
|
||||
readonly details: string;
|
||||
readonly question: string;
|
||||
}
|
||||
|
||||
export namespace SaveAsSketch {
|
||||
export namespace Commands {
|
||||
export const SAVE_AS_SKETCH: Command = {
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
KeybindingRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class SaveSketch extends SketchContribution {
|
||||
@@ -40,7 +40,7 @@ export class SaveSketch extends SketchContribution {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
const isTemp = await this.sketchesService.isTemp(sketch);
|
||||
if (isTemp) {
|
||||
return this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
|
@@ -1,50 +1,34 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
|
||||
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import {
|
||||
URI,
|
||||
SketchContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
MenuModelRegistry,
|
||||
open,
|
||||
SketchContribution,
|
||||
TabBarToolbarRegistry,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../common/protocol/sketches-service-client-impl';
|
||||
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class SketchControl extends SketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected override readonly editorManager: EditorManager;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(LocalCacheFsProvider)
|
||||
protected readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
private readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
protected readonly toDisposeBeforeCreateNewContextMenu =
|
||||
new DisposableCollection();
|
||||
@@ -57,107 +41,57 @@ export class SketchControl extends SketchContribution {
|
||||
this.shell.getWidgets('main').indexOf(widget) !== -1,
|
||||
execute: async () => {
|
||||
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
||||
|
||||
let parentElement: HTMLElement | undefined = undefined;
|
||||
const target = document.getElementById(
|
||||
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
||||
);
|
||||
if (target instanceof HTMLElement) {
|
||||
parentElement = target.parentElement ?? undefined;
|
||||
}
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = document.getElementById(
|
||||
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_RENAME.id,
|
||||
label: nls.localize('vscode/fileActions/rename', 'Rename'),
|
||||
order: '1',
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
WorkspaceCommands.FILE_RENAME
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_DELETE.id,
|
||||
label: nls.localize('vscode/fileActions/delete', 'Delete'),
|
||||
order: '2',
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
WorkspaceCommands.FILE_DELETE
|
||||
)
|
||||
)
|
||||
);
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||
const uris = [mainFileUri, ...rootFolderFileUris];
|
||||
|
||||
const parentSketchUri = this.editorManager.currentEditor
|
||||
?.getResourceUri()
|
||||
?.toString();
|
||||
const parentSketch = await this.sketchService.getSketchFolder(
|
||||
parentSketchUri || ''
|
||||
);
|
||||
|
||||
// if the current file is in the current opened sketch, show extra menus
|
||||
if (
|
||||
sketch &&
|
||||
parentSketch &&
|
||||
parentSketch.uri === sketch.uri &&
|
||||
this.allowRename(parentSketch.uri)
|
||||
) {
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_RENAME.id,
|
||||
label: nls.localize('vscode/fileActions/rename', 'Rename'),
|
||||
order: '1',
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
WorkspaceCommands.FILE_RENAME
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const renamePlaceholder = new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
nls.localize('vscode/fileActions/rename', 'Rename')
|
||||
);
|
||||
this.menuRegistry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
renamePlaceholder
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuNode(renamePlaceholder.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
sketch &&
|
||||
parentSketch &&
|
||||
parentSketch.uri === sketch.uri &&
|
||||
this.allowDelete(parentSketch.uri)
|
||||
) {
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
|
||||
label: nls.localize('vscode/fileActions/delete', 'Delete'),
|
||||
order: '2',
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
WorkspaceCommands.FILE_DELETE
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const deletePlaceholder = new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
nls.localize('vscode/fileActions/delete', 'Delete')
|
||||
);
|
||||
this.menuRegistry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
deletePlaceholder
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuNode(deletePlaceholder.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < uris.length; i++) {
|
||||
const uri = new URI(uris[i]);
|
||||
|
||||
@@ -193,6 +127,7 @@ export class SketchControl extends SketchContribution {
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
showDisabled: true,
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
},
|
||||
@@ -249,27 +184,6 @@ export class SketchControl extends SketchContribution {
|
||||
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
||||
});
|
||||
}
|
||||
|
||||
protected isCloudSketch(uri: string): boolean {
|
||||
try {
|
||||
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
|
||||
|
||||
if (cloudCacheLocation) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected allowRename(uri: string): boolean {
|
||||
return !this.isCloudSketch(uri);
|
||||
}
|
||||
|
||||
protected allowDelete(uri: string): boolean {
|
||||
return !this.isCloudSketch(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SketchControl {
|
||||
|
@@ -3,7 +3,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/common/files';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { Sketch, SketchContribution } from './contribution';
|
||||
import { OpenSketchFiles } from './open-sketch-files';
|
||||
|
||||
@@ -38,7 +38,7 @@ export class SketchFilesTracker extends SketchContribution {
|
||||
type === FileChangeType.ADDED &&
|
||||
resource.parent.toString() === sketch.uri
|
||||
) {
|
||||
const reloadedSketch = await this.sketchService.loadSketch(
|
||||
const reloadedSketch = await this.sketchesService.loadSketch(
|
||||
sketch.uri
|
||||
);
|
||||
if (Sketch.isInSketch(resource, reloadedSketch)) {
|
||||
|
@@ -11,6 +11,7 @@ import { nls } from '@theia/core/lib/common/nls';
|
||||
export class Sketchbook extends Examples {
|
||||
override onStart(): void {
|
||||
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||
this.configService.onDidChangeSketchDirUri(() => this.update());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
@@ -18,7 +19,7 @@ export class Sketchbook extends Examples {
|
||||
}
|
||||
|
||||
protected override update(): void {
|
||||
this.sketchService.getSketches({}).then((container) => {
|
||||
this.sketchesService.getSketches({}).then((container) => {
|
||||
this.register(container);
|
||||
this.menuManager.update();
|
||||
});
|
||||
|
@@ -12,7 +12,6 @@ import {
|
||||
PreferenceScope,
|
||||
PreferenceService,
|
||||
} from '@theia/core/lib/browser/preferences/preference-service';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import {
|
||||
arduinoCert,
|
||||
certificateList,
|
||||
@@ -31,22 +30,29 @@ export class UploadCertificate extends Contribution {
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@inject(ArduinoFirmwareUploader)
|
||||
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||
|
||||
protected dialogOpened = false;
|
||||
|
||||
override onStart(): void {
|
||||
this.preferences.onPreferenceChanged(({ preferenceName }) => {
|
||||
if (preferenceName === 'arduino.board.certificates') {
|
||||
this.menuManager.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UploadCertificate.Commands.OPEN, {
|
||||
execute: async () => {
|
||||
try {
|
||||
this.dialogOpened = true;
|
||||
this.menuManager.update();
|
||||
await this.dialog.open();
|
||||
} finally {
|
||||
this.dialogOpened = false;
|
||||
this.menuManager.update();
|
||||
}
|
||||
},
|
||||
isEnabled: () => !this.dialogOpened,
|
||||
@@ -54,7 +60,7 @@ export class UploadCertificate extends Contribution {
|
||||
|
||||
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
|
||||
execute: async (certToRemove) => {
|
||||
const certs = this.arduinoPreferences.get('arduino.board.certificates');
|
||||
const certs = this.preferences.get('arduino.board.certificates');
|
||||
|
||||
this.preferenceService.set(
|
||||
'arduino.board.certificates',
|
||||
@@ -75,7 +81,6 @@ export class UploadCertificate extends Contribution {
|
||||
.join(' ')}`
|
||||
);
|
||||
},
|
||||
isEnabled: () => true,
|
||||
});
|
||||
|
||||
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
|
||||
@@ -89,7 +94,6 @@ export class UploadCertificate extends Contribution {
|
||||
args: [args.cert],
|
||||
});
|
||||
},
|
||||
isEnabled: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -21,9 +21,11 @@ export class UploadFirmware extends Contribution {
|
||||
execute: async () => {
|
||||
try {
|
||||
this.dialogOpened = true;
|
||||
this.menuManager.update();
|
||||
await this.dialog.open();
|
||||
} finally {
|
||||
this.dialogOpened = false;
|
||||
this.menuManager.update();
|
||||
}
|
||||
},
|
||||
isEnabled: () => !this.dialogOpened,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { CoreService, Port } from '../../common/protocol';
|
||||
import { CoreService, Port, sanitizeFqbn } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CoreServiceContribution,
|
||||
} from './contribution';
|
||||
import { deepClone, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import type { VerifySketchParams } from './verify-sketch';
|
||||
import { UserFields } from './user-fields';
|
||||
|
||||
@@ -106,6 +106,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
// toggle the toolbar button and menu item state.
|
||||
// uploadInProgress will be set to false whether the upload fails or not
|
||||
this.uploadInProgress = true;
|
||||
this.menuManager.update();
|
||||
this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
|
||||
this.onDidChangeEmitter.fire();
|
||||
this.clearVisibleNotification();
|
||||
@@ -150,6 +151,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
this.handleError(e);
|
||||
} finally {
|
||||
this.uploadInProgress = false;
|
||||
this.menuManager.update();
|
||||
this.boardsServiceProvider.attemptPostUploadAutoSelect();
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
@@ -168,7 +170,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
|
||||
await Promise.all([
|
||||
verifyOptions.fqbn, // already decorated FQBN
|
||||
this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)),
|
||||
this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)),
|
||||
this.preferences.get('arduino.upload.verify'),
|
||||
this.preferences.get('arduino.upload.verbose'),
|
||||
]);
|
||||
@@ -205,19 +207,6 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
|
||||
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
|
||||
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
|
||||
*/
|
||||
private sanitizeFqbn(fqbn: string | undefined): string | undefined {
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const [vendor, arch, id] = fqbn.split(':');
|
||||
return `${vendor}:${arch}:${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UploadSketch {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
||||
import { 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 { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MenuModelRegistry, Contribution } from './contribution';
|
||||
import { UploadSketch } from './upload-sketch';
|
||||
|
||||
@@ -12,7 +12,6 @@ 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;
|
||||
@@ -20,42 +19,22 @@ export class UserFields extends Contribution {
|
||||
@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);
|
||||
this.menuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
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' }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
order: '2',
|
||||
});
|
||||
}
|
||||
|
||||
private selectedFqbnAddress(): string | undefined {
|
||||
|
@@ -0,0 +1,202 @@
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
import { Sketch, URI } from './contribution';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
|
||||
@injectable()
|
||||
export class ValidateSketch extends CloudSketchContribution {
|
||||
override onReady(): void {
|
||||
this.validate();
|
||||
}
|
||||
|
||||
private async validate(): Promise<void> {
|
||||
const result = await this.promptFixActions();
|
||||
if (!result) {
|
||||
const yes = await this.prompt(
|
||||
nls.localize('arduino/validateSketch/abortFixTitle', 'Invalid sketch'),
|
||||
nls.localize(
|
||||
'arduino/validateSketch/abortFixMessage',
|
||||
"The sketch is still invalid. Do you want to fix the remaining problems? By clicking '{0}', a new sketch will open.",
|
||||
Dialog.NO
|
||||
),
|
||||
[Dialog.NO, Dialog.YES]
|
||||
);
|
||||
if (yes) {
|
||||
return this.validate();
|
||||
}
|
||||
const sketch = await this.sketchesService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri), {
|
||||
preserveWindow: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns with an array of actions the user has to perform to fix the invalid sketch.
|
||||
*/
|
||||
private validateSketch(
|
||||
sketch: Sketch,
|
||||
dataDirUri: URI | undefined
|
||||
): FixAction[] {
|
||||
// sketch code file validation errors first as they do not require window reload
|
||||
const actions = Sketch.uris(sketch)
|
||||
.filter((uri) => uri !== sketch.mainFileUri)
|
||||
.map((uri) => new URI(uri))
|
||||
.filter((uri) => Sketch.Extensions.CODE_FILES.includes(uri.path.ext))
|
||||
.map((uri) => ({
|
||||
uri,
|
||||
error: this.doValidate(sketch, dataDirUri, uri.path.name),
|
||||
}))
|
||||
.filter(({ error }) => Boolean(error))
|
||||
.map((object) => <{ uri: URI; error: string }>object)
|
||||
.map(({ uri, error }) => ({
|
||||
execute: async () => {
|
||||
const unknown =
|
||||
(await this.promptRenameSketchFile(uri, error)) &&
|
||||
(await this.commandService.executeCommand(
|
||||
WorkspaceCommands.FILE_RENAME.id,
|
||||
uri
|
||||
));
|
||||
return !!unknown;
|
||||
},
|
||||
}));
|
||||
|
||||
// sketch folder + main sketch file last as it requires a `Save as...` and the window reload
|
||||
const sketchFolderName = new URI(sketch.uri).path.base;
|
||||
const sketchFolderNameError = this.doValidate(
|
||||
sketch,
|
||||
dataDirUri,
|
||||
sketchFolderName
|
||||
);
|
||||
if (sketchFolderNameError) {
|
||||
actions.push({
|
||||
execute: async () => {
|
||||
const unknown =
|
||||
(await this.promptRenameSketch(sketch, sketchFolderNameError)) &&
|
||||
(await this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
<SaveAsSketch.Options>{
|
||||
markAsRecentlyOpened: true,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: true,
|
||||
}
|
||||
));
|
||||
return !!unknown;
|
||||
},
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
private doValidate(
|
||||
sketch: Sketch,
|
||||
dataDirUri: URI | undefined,
|
||||
toValidate: string
|
||||
): string | undefined {
|
||||
const cloudUri = this.createFeatures.isCloud(sketch, dataDirUri);
|
||||
return cloudUri
|
||||
? Sketch.validateCloudSketchFolderName(toValidate)
|
||||
: Sketch.validateSketchFolderName(toValidate);
|
||||
}
|
||||
|
||||
private async currentSketch(): Promise<Sketch> {
|
||||
const sketch = this.sketchServiceClient.tryGetCurrentSketch();
|
||||
if (CurrentSketch.isValid(sketch)) {
|
||||
return sketch;
|
||||
}
|
||||
const deferred = new Deferred<Sketch>();
|
||||
const disposable = this.sketchServiceClient.onCurrentSketchDidChange(
|
||||
(sketch) => {
|
||||
if (CurrentSketch.isValid(sketch)) {
|
||||
disposable.dispose();
|
||||
deferred.resolve(sketch);
|
||||
}
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private async promptFixActions(): Promise<boolean> {
|
||||
const maybeDataDirUri = this.configService.tryGetDataDirUri();
|
||||
const [sketch, dataDirUri] = await Promise.all([
|
||||
this.currentSketch(),
|
||||
maybeDataDirUri ??
|
||||
waitForEvent(this.configService.onDidChangeDataDirUri, 5_000),
|
||||
]);
|
||||
const fixActions = this.validateSketch(sketch, dataDirUri);
|
||||
for (const fixAction of fixActions) {
|
||||
const result = await fixAction.execute();
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async promptRenameSketch(
|
||||
sketch: Sketch,
|
||||
error: string
|
||||
): Promise<boolean> {
|
||||
return this.prompt(
|
||||
nls.localize(
|
||||
'arduino/validateSketch/renameSketchFolderTitle',
|
||||
'Invalid sketch name'
|
||||
),
|
||||
nls.localize(
|
||||
'arduino/validateSketch/renameSketchFolderMessage',
|
||||
"The sketch '{0}' cannot be used. {1} To get rid of this message, rename the sketch. Do you want to rename the sketch now?",
|
||||
sketch.name,
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async promptRenameSketchFile(
|
||||
uri: URI,
|
||||
error: string
|
||||
): Promise<boolean> {
|
||||
return this.prompt(
|
||||
nls.localize(
|
||||
'arduino/validateSketch/renameSketchFileTitle',
|
||||
'Invalid sketch filename'
|
||||
),
|
||||
nls.localize(
|
||||
'arduino/validateSketch/renameSketchFileMessage',
|
||||
"The sketch file '{0}' cannot be used. {1} Do you want to rename the sketch file now?",
|
||||
uri.path.base,
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async prompt(
|
||||
title: string,
|
||||
message: string,
|
||||
buttons: string[] = [Dialog.CANCEL, Dialog.OK]
|
||||
): Promise<boolean> {
|
||||
const { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title,
|
||||
message,
|
||||
type: 'warning',
|
||||
buttons,
|
||||
}
|
||||
);
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
interface FixAction {
|
||||
execute(): Promise<boolean>;
|
||||
}
|
@@ -11,7 +11,7 @@ import {
|
||||
TabBarToolbarRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
import { CoreErrorHandler } from './core-error-handler';
|
||||
|
||||
@@ -21,11 +21,18 @@ export interface VerifySketchParams {
|
||||
*/
|
||||
readonly exportBinaries?: boolean;
|
||||
/**
|
||||
* If `true`, there won't be any UI indication of the verify command. It's `false` by default.
|
||||
* If `true`, there won't be any UI indication of the verify command in the toolbar. It's `false` by default.
|
||||
*/
|
||||
readonly silent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* - `"idle"` when neither verify, nor upload is running,
|
||||
* - `"explicit-verify"` when only verify is running triggered by the user, and
|
||||
* - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
|
||||
*/
|
||||
type VerifyProgress = 'idle' | 'explicit-verify' | 'automatic-verify';
|
||||
|
||||
@injectable()
|
||||
export class VerifySketch extends CoreServiceContribution {
|
||||
@inject(CoreErrorHandler)
|
||||
@@ -33,22 +40,24 @@ export class VerifySketch extends CoreServiceContribution {
|
||||
|
||||
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
private verifyInProgress = false;
|
||||
private verifyProgress: VerifyProgress = 'idle';
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
|
||||
execute: (params?: VerifySketchParams) => this.verifySketch(params),
|
||||
isEnabled: () => !this.verifyInProgress,
|
||||
isEnabled: () => this.verifyProgress === 'idle',
|
||||
});
|
||||
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
|
||||
execute: () => this.verifySketch({ exportBinaries: true }),
|
||||
isEnabled: () => !this.verifyInProgress,
|
||||
isEnabled: () => this.verifyProgress === 'idle',
|
||||
});
|
||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: () => !this.verifyInProgress,
|
||||
isToggled: () => this.verifyInProgress,
|
||||
isEnabled: () => this.verifyProgress !== 'explicit-verify',
|
||||
// toggled only when verify is running, but not toggled when automatic verify is running before the upload
|
||||
// https://github.com/arduino/arduino-ide/pull/1750#pullrequestreview-1214762975
|
||||
isToggled: () => this.verifyProgress === 'explicit-verify',
|
||||
execute: () =>
|
||||
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
|
||||
});
|
||||
@@ -99,15 +108,16 @@ export class VerifySketch extends CoreServiceContribution {
|
||||
private async verifySketch(
|
||||
params?: VerifySketchParams
|
||||
): Promise<CoreService.Options.Compile | undefined> {
|
||||
if (this.verifyInProgress) {
|
||||
if (this.verifyProgress !== 'idle') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!params?.silent) {
|
||||
this.verifyInProgress = true;
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
this.verifyProgress = params?.silent
|
||||
? 'automatic-verify'
|
||||
: 'explicit-verify';
|
||||
this.onDidChangeEmitter.fire();
|
||||
this.menuManager.update();
|
||||
this.clearVisibleNotification();
|
||||
this.coreErrorHandler.reset();
|
||||
|
||||
@@ -139,10 +149,9 @@ export class VerifySketch extends CoreServiceContribution {
|
||||
this.handleError(e);
|
||||
return undefined;
|
||||
} finally {
|
||||
this.verifyInProgress = false;
|
||||
if (!params?.silent) {
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
this.verifyProgress = 'idle';
|
||||
this.onDidChangeEmitter.fire();
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,12 +1,16 @@
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { SketchesService } from '../../common/protocol';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
import * as createPaths from './create-paths';
|
||||
import { posix } from './create-paths';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
import { Create, CreateError } from './typings';
|
||||
|
||||
export interface ResponseResultProvider {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(response: Response): Promise<any>;
|
||||
}
|
||||
export namespace ResponseResultProvider {
|
||||
@@ -15,6 +19,8 @@ export namespace ResponseResultProvider {
|
||||
export const JSON: ResponseResultProvider = (response) => response.json();
|
||||
}
|
||||
|
||||
// TODO: check if this is still needed: https://github.com/electron/electron/issues/18733
|
||||
// The original issue was reported for Electron 5.x and 6.x. Theia uses 15.x
|
||||
export function Utf8ArrayToStr(array: Uint8Array): string {
|
||||
let out, i, c;
|
||||
let char2, char3;
|
||||
@@ -61,20 +67,13 @@ type ResourceType = 'f' | 'd';
|
||||
@injectable()
|
||||
export class CreateApi {
|
||||
@inject(SketchCache)
|
||||
protected sketchCache: SketchCache;
|
||||
|
||||
protected authenticationService: AuthenticationClientService;
|
||||
protected arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
public init(
|
||||
authenticationService: AuthenticationClientService,
|
||||
arduinoPreferences: ArduinoPreferences
|
||||
): CreateApi {
|
||||
this.authenticationService = authenticationService;
|
||||
this.arduinoPreferences = arduinoPreferences;
|
||||
|
||||
return this;
|
||||
}
|
||||
readonly sketchCache: SketchCache;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly arduinoPreferences: ArduinoPreferences;
|
||||
@inject(SketchesService)
|
||||
private readonly sketchesService: SketchesService;
|
||||
|
||||
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
|
||||
return {
|
||||
@@ -129,10 +128,13 @@ export class CreateApi {
|
||||
|
||||
async createSketch(
|
||||
posixPath: string,
|
||||
content: string = CreateApi.defaultInoContent
|
||||
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent()
|
||||
): Promise<Create.Sketch> {
|
||||
const url = new URL(`${this.domain()}/sketches`);
|
||||
const headers = await this.headers();
|
||||
const [headers, content] = await Promise.all([
|
||||
this.headers(),
|
||||
contentProvider,
|
||||
]);
|
||||
const payload = {
|
||||
ino: btoa(content),
|
||||
path: posixPath,
|
||||
@@ -291,7 +293,7 @@ export class CreateApi {
|
||||
this.sketchCache.addSketch(sketch);
|
||||
|
||||
let file = '';
|
||||
if (sketch && sketch.secrets) {
|
||||
if (sketch.secrets) {
|
||||
for (const item of sketch.secrets) {
|
||||
file += `#define ${item.name} "${item.value}"\r\n`;
|
||||
}
|
||||
@@ -381,7 +383,7 @@ export class CreateApi {
|
||||
return;
|
||||
}
|
||||
|
||||
// do not upload "do_not_sync" files/directoris and their descendants
|
||||
// do not upload "do_not_sync" files/directories and their descendants
|
||||
const segments = posixPath.split(posix.sep) || [];
|
||||
if (
|
||||
segments.some((segment) => Create.do_not_sync_files.includes(segment))
|
||||
@@ -415,6 +417,21 @@ export class CreateApi {
|
||||
await this.delete(posixPath, 'd');
|
||||
}
|
||||
|
||||
/**
|
||||
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
|
||||
* See [Create.Resource#path](./typings.ts). Unlike other endpoints, it does not support the `$HOME`
|
||||
* variable substitution. The DELETE directory endpoint is bogus and responses with HTTP 500
|
||||
* instead of 404 when deleting a non-existing resource.
|
||||
*/
|
||||
async deleteSketch(sketchPath: string): Promise<void> {
|
||||
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
|
||||
const headers = await this.headers();
|
||||
await this.run(url, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
private async delete(posixPath: string, type: ResourceType): Promise<void> {
|
||||
const url = new URL(
|
||||
`${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}`
|
||||
@@ -475,14 +492,12 @@ export class CreateApi {
|
||||
}
|
||||
|
||||
private async run<T>(
|
||||
requestInfo: RequestInfo | URL,
|
||||
requestInfo: URL,
|
||||
init: RequestInit | undefined,
|
||||
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
|
||||
): Promise<T> {
|
||||
const response = await fetch(
|
||||
requestInfo instanceof URL ? requestInfo.toString() : requestInfo,
|
||||
init
|
||||
);
|
||||
console.debug(`HTTP ${init?.method}: ${requestInfo.toString()}`);
|
||||
const response = await fetch(requestInfo.toString(), init);
|
||||
if (!response.ok) {
|
||||
let details: string | undefined = undefined;
|
||||
try {
|
||||
@@ -516,19 +531,3 @@ export class CreateApi {
|
||||
return this.authenticationService.session?.accessToken || '';
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CreateApi {
|
||||
export const defaultInoContent = `/*
|
||||
|
||||
*/
|
||||
|
||||
void setup() {
|
||||
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
95
arduino-ide-extension/src/browser/create/create-features.ts
Normal file
95
arduino-ide-extension/src/browser/create/create-features.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Sketch } from '../../common/protocol';
|
||||
import { AuthenticationSession } from '../../node/auth/types';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
|
||||
|
||||
@injectable()
|
||||
export class CreateFeatures implements FrontendApplicationContribution {
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly preferences: ArduinoPreferences;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(LocalCacheFsProvider)
|
||||
private readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
|
||||
private readonly onDidChangeSessionEmitter = new Emitter<
|
||||
AuthenticationSession | undefined
|
||||
>();
|
||||
private readonly onDidChangeEnabledEmitter = new Emitter<boolean>();
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeSessionEmitter,
|
||||
this.onDidChangeEnabledEmitter
|
||||
);
|
||||
private _enabled: boolean;
|
||||
private _session: AuthenticationSession | undefined;
|
||||
|
||||
onStart(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.authenticationService.onSessionDidChange((session) => {
|
||||
const oldSession = this._session;
|
||||
this._session = session;
|
||||
if (!!oldSession !== !!this._session) {
|
||||
this.onDidChangeSessionEmitter.fire(this._session);
|
||||
}
|
||||
}),
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (preferenceName === 'arduino.cloud.enabled') {
|
||||
const oldEnabled = this._enabled;
|
||||
this._enabled = Boolean(newValue);
|
||||
if (this._enabled !== oldEnabled) {
|
||||
this.onDidChangeEnabledEmitter.fire(this._enabled);
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
this._enabled = this.preferences['arduino.cloud.enabled'];
|
||||
this._session = this.authenticationService.session;
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get onDidChangeSession(): Event<AuthenticationSession | undefined> {
|
||||
return this.onDidChangeSessionEmitter.event;
|
||||
}
|
||||
|
||||
get onDidChangeEnabled(): Event<boolean> {
|
||||
return this.onDidChangeEnabledEmitter.event;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
get session(): AuthenticationSession | undefined {
|
||||
return this._session;
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the sketch is under `directories.data/RemoteSketchbook`. Otherwise, `false`.
|
||||
* Returns with `undefined` if `dataDirUri` is `undefined`.
|
||||
*/
|
||||
isCloud(sketch: Sketch, dataDirUri: URI | undefined): boolean | undefined {
|
||||
if (!dataDirUri) {
|
||||
console.warn(
|
||||
`Could not decide whether the sketch ${sketch.uri} is cloud or local. The 'directories.data' location was not available from the CLI config.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return dataDirUri.isEqualOrParent(new URI(sketch.uri));
|
||||
}
|
||||
|
||||
cloudUri(sketch: Sketch): URI | undefined {
|
||||
if (!this.session) {
|
||||
return undefined;
|
||||
}
|
||||
return this.localCacheFsProvider.from(new URI(sketch.uri));
|
||||
}
|
||||
}
|
@@ -189,10 +189,6 @@ export class CreateFsProvider
|
||||
FileSystemProviderErrorCode.NoPermissions
|
||||
);
|
||||
}
|
||||
|
||||
return this.createApi.init(
|
||||
this.authenticationService,
|
||||
this.arduinoPreferences
|
||||
);
|
||||
return this.createApi;
|
||||
}
|
||||
}
|
||||
|
@@ -71,3 +71,23 @@ export class CreateError extends Error {
|
||||
Object.setPrototypeOf(this, CreateError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export type ConflictError = CreateError & { status: 409 };
|
||||
export function isConflict(err: unknown): err is ConflictError {
|
||||
return isErrorWithStatusOf(err, 409);
|
||||
}
|
||||
|
||||
export type NotFoundError = CreateError & { status: 404 };
|
||||
export function isNotFound(err: unknown): err is NotFoundError {
|
||||
return isErrorWithStatusOf(err, 404);
|
||||
}
|
||||
|
||||
function isErrorWithStatusOf(
|
||||
err: unknown,
|
||||
status: number
|
||||
): err is CreateError & { status: number } {
|
||||
if (err instanceof CreateError) {
|
||||
return err.status === status;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"editor.foreground": "#dae3e3",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#00818480",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editorCursor.foreground": "#dae3e3",
|
||||
"editorWhitespace.foreground": "#bfbfbf",
|
||||
"editorWidget.background": "#171e21",
|
||||
"editorWidget.foreground": "#dae3e3",
|
||||
@@ -67,7 +67,8 @@
|
||||
"tree.indentGuidesStroke": "#374146",
|
||||
"tab.unfocusedActiveForeground": "#dae3e3",
|
||||
"tab.inactiveBackground": "#171e21",
|
||||
"textLink.foreground": "#0ca1a6"
|
||||
"textLink.foreground": "#0ca1a6",
|
||||
"errorForeground": "#df7365"
|
||||
},
|
||||
"tokenColors": [
|
||||
{
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"editor.foreground": "#4e5b61",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#7fcbcdb3",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editorCursor.foreground": "#4e5b61",
|
||||
"editorWhitespace.foreground": "#bfbfbf",
|
||||
"editorWidget.background": "#f7f9f9",
|
||||
"editorWidget.foreground": "#4e5b61",
|
||||
@@ -67,7 +67,8 @@
|
||||
"tree.indentGuidesStroke": "#dae3e3",
|
||||
"tab.unfocusedActiveForeground": "#4e5b61",
|
||||
"tab.inactiveBackground": "#ecf1f1",
|
||||
"textLink.foreground": "#008184"
|
||||
"textLink.foreground": "#008184",
|
||||
"errorForeground": "#df7365"
|
||||
},
|
||||
"tokenColors": [
|
||||
{
|
||||
|
@@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { clipboard } from 'electron';
|
||||
import { clipboard } from '@theia/core/electron-shared/@electron/remote';
|
||||
import { ReactWidget, DialogProps } from '@theia/core/lib/browser';
|
||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||
import { CreateApi } from '../create/create-api';
|
||||
|
@@ -406,7 +406,7 @@ export class SettingsComponent extends React.Component<
|
||||
}
|
||||
onChange={this.socksProtocolDidChange}
|
||||
/>
|
||||
SOCKS
|
||||
SOCKS5
|
||||
</label>
|
||||
</form>
|
||||
<div className="flex-line proxy-settings">
|
||||
@@ -682,7 +682,7 @@ export class SettingsComponent extends React.Component<
|
||||
): void => {
|
||||
if (this.state.network !== 'none') {
|
||||
const network = this.cloneProxySettings;
|
||||
network.protocol = event.target.checked ? 'http' : 'socks';
|
||||
network.protocol = event.target.checked ? 'http' : 'socks5';
|
||||
this.setState({ network });
|
||||
}
|
||||
};
|
||||
@@ -692,7 +692,7 @@ export class SettingsComponent extends React.Component<
|
||||
): void => {
|
||||
if (this.state.network !== 'none') {
|
||||
const network = this.cloneProxySettings;
|
||||
network.protocol = event.target.checked ? 'socks' : 'http';
|
||||
network.protocol = event.target.checked ? 'socks5' : 'http';
|
||||
this.setState({ network });
|
||||
}
|
||||
};
|
||||
|
@@ -27,6 +27,7 @@ import {
|
||||
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
|
||||
import { DefaultTheme } from '@theia/application-package/lib/application-props';
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import type { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
|
||||
export const WINDOW_SETTING = 'window';
|
||||
export const EDITOR_SETTING = 'editor';
|
||||
@@ -171,7 +172,15 @@ export class SettingsService {
|
||||
this.preferenceService.get<boolean>(SHOW_ALL_FILES_SETTING, false),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
const { additionalUrls, sketchDirUri, network } = cliConfig;
|
||||
const {
|
||||
config = {
|
||||
additionalUrls: [],
|
||||
sketchDirUri: '',
|
||||
network: Network.Default(),
|
||||
},
|
||||
} = cliConfig;
|
||||
const { additionalUrls, sketchDirUri, network } = config;
|
||||
|
||||
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
|
||||
return {
|
||||
editorFontSize,
|
||||
@@ -223,7 +232,11 @@ export class SettingsService {
|
||||
try {
|
||||
const { sketchbookPath, editorFontSize, themeId } = await settings;
|
||||
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
|
||||
if (!(await this.fileService.exists(new URI(sketchbookDir)))) {
|
||||
let sketchbookStat: FileStat | undefined = undefined;
|
||||
try {
|
||||
sketchbookStat = await this.fileService.resolve(new URI(sketchbookDir));
|
||||
} catch {}
|
||||
if (!sketchbookStat || !sketchbookStat.isDirectory) {
|
||||
return nls.localize(
|
||||
'arduino/preferences/invalid.sketchbook.location',
|
||||
'Invalid sketchbook location: {0}',
|
||||
@@ -274,10 +287,19 @@ export class SettingsService {
|
||||
network,
|
||||
sketchbookShowAllFiles,
|
||||
} = this._settings;
|
||||
const [config, sketchDirUri] = await Promise.all([
|
||||
const [cliConfig, sketchDirUri] = await Promise.all([
|
||||
this.configService.getConfiguration(),
|
||||
this.fileSystemExt.getUri(sketchbookPath),
|
||||
]);
|
||||
const { config } = cliConfig;
|
||||
if (!config) {
|
||||
// Do not check for any error messages. The config might has errors (such as invalid directories.user) right before saving the new values.
|
||||
return nls.localize(
|
||||
'arduino/preferences/noCliConfig',
|
||||
'Could not load the CLI configuration'
|
||||
);
|
||||
}
|
||||
|
||||
(config as any).additionalUrls = additionalUrls;
|
||||
(config as any).sketchDirUri = sketchDirUri;
|
||||
(config as any).network = network;
|
||||
|
@@ -41,7 +41,6 @@ export class LibraryListWidget extends ListWidget<
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: LibraryPackage) => item.name,
|
||||
itemDeprecated: (item: LibraryPackage) => item.deprecated,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All', topic: 'All' },
|
||||
|
@@ -88,8 +88,25 @@ export class LocalCacheFsProvider
|
||||
}
|
||||
|
||||
protected async init(fileService: FileService): Promise<void> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
this._localCacheRoot = new URI(config.dataDirUri);
|
||||
const { config } = await this.configService.getConfiguration();
|
||||
// Any possible CLI config errors are ignored here. IDE2 does not verify the `directories.data` folder.
|
||||
// If the data dir is accessible, IDE2 creates the cache folder for the cloud sketches. Otherwise, it does not.
|
||||
// The data folder can be configured outside of the IDE2, and the new data folder will be picked up with a
|
||||
// subsequent IDE2 start.
|
||||
if (!config?.dataDirUri) {
|
||||
return; // the deferred promise will never resolve
|
||||
}
|
||||
const localCacheUri = new URI(config.dataDirUri);
|
||||
try {
|
||||
await fileService.access(localCacheUri);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`'directories.data' location is inaccessible at ${config.dataDirUri}`,
|
||||
err
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._localCacheRoot = localCacheUri;
|
||||
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
|
||||
this._localCacheRoot = this._localCacheRoot.resolve(segment);
|
||||
await fileService.createFolder(this._localCacheRoot);
|
||||
|
@@ -97,6 +97,11 @@ export namespace ArduinoMenus {
|
||||
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
|
||||
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
|
||||
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
|
||||
// `Tool` > `Ports` (always visible https://github.com/arduino/arduino-ide/issues/655)
|
||||
export const TOOLS__PORTS_SUBMENU = [
|
||||
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||
'2_ports',
|
||||
];
|
||||
|
||||
// -- Help
|
||||
// `Getting Started`, `Environment`, `Troubleshooting`, etc.
|
||||
|
@@ -18,7 +18,7 @@ import {
|
||||
AttachedBoardsChangeEvent,
|
||||
BoardsPackage,
|
||||
LibraryPackage,
|
||||
Config,
|
||||
ConfigState,
|
||||
Sketch,
|
||||
ProgressMessage,
|
||||
} from '../common/protocol';
|
||||
@@ -37,6 +37,7 @@ export class NotificationCenter
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
private readonly didReinitializeEmitter = new Emitter<void>();
|
||||
private readonly indexUpdateDidCompleteEmitter =
|
||||
new Emitter<IndexUpdateDidCompleteParams>();
|
||||
private readonly indexUpdateWillStartEmitter =
|
||||
@@ -47,9 +48,7 @@ export class NotificationCenter
|
||||
new Emitter<IndexUpdateDidFailParams>();
|
||||
private readonly daemonDidStartEmitter = new Emitter<string>();
|
||||
private readonly daemonDidStopEmitter = new Emitter<void>();
|
||||
private readonly configDidChangeEmitter = new Emitter<{
|
||||
config: Config | undefined;
|
||||
}>();
|
||||
private readonly configDidChangeEmitter = new Emitter<ConfigState>();
|
||||
private readonly platformDidInstallEmitter = new Emitter<{
|
||||
item: BoardsPackage;
|
||||
}>();
|
||||
@@ -57,7 +56,7 @@ export class NotificationCenter
|
||||
item: BoardsPackage;
|
||||
}>();
|
||||
private readonly libraryDidInstallEmitter = new Emitter<{
|
||||
item: LibraryPackage;
|
||||
item: LibraryPackage | 'zip-install';
|
||||
}>();
|
||||
private readonly libraryDidUninstallEmitter = new Emitter<{
|
||||
item: LibraryPackage;
|
||||
@@ -71,6 +70,7 @@ export class NotificationCenter
|
||||
new Emitter<FrontendApplicationState>();
|
||||
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.didReinitializeEmitter,
|
||||
this.indexUpdateWillStartEmitter,
|
||||
this.indexUpdateDidProgressEmitter,
|
||||
this.indexUpdateDidCompleteEmitter,
|
||||
@@ -85,6 +85,7 @@ export class NotificationCenter
|
||||
this.attachedBoardsDidChangeEmitter
|
||||
);
|
||||
|
||||
readonly onDidReinitialize = this.didReinitializeEmitter.event;
|
||||
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event;
|
||||
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event;
|
||||
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
|
||||
@@ -115,6 +116,10 @@ export class NotificationCenter
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
notifyDidReinitialize(): void {
|
||||
this.didReinitializeEmitter.fire();
|
||||
}
|
||||
|
||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
|
||||
this.indexUpdateWillStartEmitter.fire(params);
|
||||
}
|
||||
@@ -139,7 +144,7 @@ export class NotificationCenter
|
||||
this.daemonDidStopEmitter.fire();
|
||||
}
|
||||
|
||||
notifyConfigDidChange(event: { config: Config | undefined }): void {
|
||||
notifyConfigDidChange(event: ConfigState): void {
|
||||
this.configDidChangeEmitter.fire(event);
|
||||
}
|
||||
|
||||
@@ -151,7 +156,9 @@ export class NotificationCenter
|
||||
this.platformDidUninstallEmitter.fire(event);
|
||||
}
|
||||
|
||||
notifyLibraryDidInstall(event: { item: LibraryPackage }): void {
|
||||
notifyLibraryDidInstall(event: {
|
||||
item: LibraryPackage | 'zip-install';
|
||||
}): void {
|
||||
this.libraryDidInstallEmitter.fire(event);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,293 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/common/files';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { Sketch, SketchesService } from '../common/protocol';
|
||||
import { ConfigServiceClient } from './config/config-service-client';
|
||||
import {
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
SketchRef,
|
||||
} from '../common/protocol/sketches-service';
|
||||
import {
|
||||
ARDUINO_CLOUD_FOLDER,
|
||||
REMOTE_SKETCHBOOK_FOLDER,
|
||||
} from './utils/constants';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
|
||||
const READ_ONLY_FILES = ['sketch.json'];
|
||||
const READ_ONLY_FILES_REMOTE = ['thingProperties.h', 'thingsProperties.h'];
|
||||
|
||||
export type CurrentSketch = Sketch | 'invalid';
|
||||
export namespace CurrentSketch {
|
||||
export function isValid(arg: CurrentSketch | undefined): arg is Sketch {
|
||||
return !!arg && arg !== 'invalid';
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class SketchesServiceClientImpl
|
||||
implements FrontendApplicationContribution
|
||||
{
|
||||
@inject(FileService)
|
||||
private readonly fileService: FileService;
|
||||
@inject(SketchesService)
|
||||
private readonly sketchesService: SketchesService;
|
||||
@inject(WorkspaceService)
|
||||
private readonly workspaceService: WorkspaceService;
|
||||
@inject(ConfigServiceClient)
|
||||
private readonly configService: ConfigServiceClient;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
private sketches = new Map<string, SketchRef>();
|
||||
private onSketchbookDidChangeEmitter = new Emitter<{
|
||||
created: SketchRef[];
|
||||
removed: SketchRef[];
|
||||
}>();
|
||||
readonly onSketchbookDidChange = this.onSketchbookDidChangeEmitter.event;
|
||||
private currentSketchDidChangeEmitter = new Emitter<CurrentSketch>();
|
||||
readonly onCurrentSketchDidChange = this.currentSketchDidChangeEmitter.event;
|
||||
|
||||
private toDisposeBeforeWatchSketchbookDir = new DisposableCollection();
|
||||
private toDispose = new DisposableCollection(
|
||||
this.onSketchbookDidChangeEmitter,
|
||||
this.currentSketchDidChangeEmitter,
|
||||
this.toDisposeBeforeWatchSketchbookDir
|
||||
);
|
||||
|
||||
private _currentSketch: CurrentSketch | undefined;
|
||||
private currentSketchLoaded = new Deferred<CurrentSketch>();
|
||||
|
||||
onStart(): void {
|
||||
const sketchDirUri = this.configService.tryGetSketchDirUri();
|
||||
this.watchSketchbookDir(sketchDirUri);
|
||||
const refreshCurrentSketch = async () => {
|
||||
const currentSketch = await this.loadCurrentSketch();
|
||||
this.useCurrentSketch(currentSketch);
|
||||
};
|
||||
this.toDispose.push(
|
||||
this.configService.onDidChangeSketchDirUri((sketchDirUri) => {
|
||||
this.watchSketchbookDir(sketchDirUri);
|
||||
refreshCurrentSketch();
|
||||
})
|
||||
);
|
||||
this.appStateService
|
||||
.reachedState('started_contributions')
|
||||
.then(refreshCurrentSketch);
|
||||
}
|
||||
|
||||
private async watchSketchbookDir(
|
||||
sketchDirUri: URI | undefined
|
||||
): Promise<void> {
|
||||
this.toDisposeBeforeWatchSketchbookDir.dispose();
|
||||
if (!sketchDirUri) {
|
||||
return;
|
||||
}
|
||||
const container = await this.sketchesService.getSketches({
|
||||
uri: sketchDirUri.toString(),
|
||||
});
|
||||
for (const sketch of SketchContainer.toArray(container)) {
|
||||
this.sketches.set(sketch.uri, sketch);
|
||||
}
|
||||
this.toDisposeBeforeWatchSketchbookDir.pushAll([
|
||||
Disposable.create(() => this.sketches.clear()),
|
||||
// Watch changes in the sketchbook to update `File` > `Sketchbook` menu items.
|
||||
this.fileService.watch(sketchDirUri, {
|
||||
recursive: true,
|
||||
excludes: [],
|
||||
}),
|
||||
this.fileService.onDidFilesChange(async (event) => {
|
||||
for (const { type, resource } of event.changes) {
|
||||
// The file change events have higher precedence in the current sketch over the sketchbook.
|
||||
if (
|
||||
CurrentSketch.isValid(this._currentSketch) &&
|
||||
new URI(this._currentSketch.uri).isEqualOrParent(resource)
|
||||
) {
|
||||
// https://github.com/arduino/arduino-ide/pull/1351#pullrequestreview-1086666656
|
||||
// On a sketch file rename, the FS watcher will contain two changes:
|
||||
// - Deletion of the original file,
|
||||
// - Update of the new file,
|
||||
// Hence, `UPDATE` events must be processed but only and if only there is a `DELETED` change in the same event.
|
||||
// Otherwise, IDE2 would ask CLI to reload the sketch content on every save event in IDE2.
|
||||
if (type === FileChangeType.UPDATED && event.changes.length === 1) {
|
||||
// If the event contains only one `UPDATE` change, it cannot be a rename.
|
||||
return;
|
||||
}
|
||||
|
||||
let reloadedSketch: Sketch | undefined = undefined;
|
||||
try {
|
||||
reloadedSketch = await this.sketchesService.loadSketch(
|
||||
this._currentSketch.uri
|
||||
);
|
||||
} catch (err) {
|
||||
if (!SketchesError.NotFound.is(err)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!reloadedSketch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Sketch.sameAs(this._currentSketch, reloadedSketch)) {
|
||||
this.useCurrentSketch(reloadedSketch, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// We track main sketch files changes only. // TODO: check sketch folder changes. One can rename the folder without renaming the `.ino` file.
|
||||
if (sketchDirUri.isEqualOrParent(resource)) {
|
||||
if (Sketch.isSketchFile(resource)) {
|
||||
if (type === FileChangeType.ADDED) {
|
||||
try {
|
||||
const toAdd = await this.sketchesService.loadSketch(
|
||||
resource.parent.toString()
|
||||
);
|
||||
if (!this.sketches.has(toAdd.uri)) {
|
||||
console.log(
|
||||
`New sketch '${toAdd.name}' was created in sketchbook '${sketchDirUri}'.`
|
||||
);
|
||||
this.sketches.set(toAdd.uri, toAdd);
|
||||
this.fireSoon(toAdd, 'created');
|
||||
}
|
||||
} catch {}
|
||||
} else if (type === FileChangeType.DELETED) {
|
||||
const uri = resource.parent.toString();
|
||||
const toDelete = this.sketches.get(uri);
|
||||
if (toDelete) {
|
||||
console.log(
|
||||
`Sketch '${toDelete.name}' was removed from sketchbook '${sketchDirUri}'.`
|
||||
);
|
||||
this.sketches.delete(uri);
|
||||
this.fireSoon(toDelete, 'removed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
private useCurrentSketch(
|
||||
currentSketch: CurrentSketch,
|
||||
reassignPromise = false
|
||||
) {
|
||||
this._currentSketch = currentSketch;
|
||||
if (reassignPromise) {
|
||||
this.currentSketchLoaded = new Deferred();
|
||||
}
|
||||
this.currentSketchLoaded.resolve(this._currentSketch);
|
||||
this.currentSketchDidChangeEmitter.fire(this._currentSketch);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private async loadCurrentSketch(): Promise<CurrentSketch> {
|
||||
const sketches = (
|
||||
await Promise.all(
|
||||
this.workspaceService
|
||||
.tryGetRoots()
|
||||
.map(({ resource }) =>
|
||||
this.sketchesService.getSketchFolder(resource.toString())
|
||||
)
|
||||
)
|
||||
).filter(notEmpty);
|
||||
if (!sketches.length) {
|
||||
return 'invalid';
|
||||
}
|
||||
if (sketches.length > 1) {
|
||||
console.log(
|
||||
`Multiple sketch folders were found in the workspace. Falling back to the first one. Sketch folders: ${JSON.stringify(
|
||||
sketches
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return sketches[0];
|
||||
}
|
||||
|
||||
async currentSketch(): Promise<CurrentSketch> {
|
||||
return this.currentSketchLoaded.promise;
|
||||
}
|
||||
|
||||
tryGetCurrentSketch(): CurrentSketch | undefined {
|
||||
return this._currentSketch;
|
||||
}
|
||||
|
||||
async currentSketchFile(): Promise<string | undefined> {
|
||||
const currentSketch = await this.currentSketch();
|
||||
if (CurrentSketch.isValid(currentSketch)) {
|
||||
return currentSketch.mainFileUri;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private fireSoonHandle?: number;
|
||||
private bufferedSketchbookEvents: {
|
||||
type: 'created' | 'removed';
|
||||
sketch: SketchRef;
|
||||
}[] = [];
|
||||
|
||||
private fireSoon(sketch: SketchRef, type: 'created' | 'removed'): void {
|
||||
this.bufferedSketchbookEvents.push({ type, sketch });
|
||||
|
||||
if (typeof this.fireSoonHandle === 'number') {
|
||||
window.clearTimeout(this.fireSoonHandle);
|
||||
}
|
||||
|
||||
this.fireSoonHandle = window.setTimeout(() => {
|
||||
const event: { created: SketchRef[]; removed: SketchRef[] } = {
|
||||
created: [],
|
||||
removed: [],
|
||||
};
|
||||
for (const { type, sketch } of this.bufferedSketchbookEvents) {
|
||||
if (type === 'created') {
|
||||
event.created.push(sketch);
|
||||
} else {
|
||||
event.removed.push(sketch);
|
||||
}
|
||||
}
|
||||
this.onSketchbookDidChangeEmitter.fire(event);
|
||||
this.bufferedSketchbookEvents.length = 0;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the `uri` is not contained in any of the opened workspaces. Otherwise, `false`.
|
||||
*/
|
||||
isReadOnly(uri: URI | monaco.Uri | string): boolean {
|
||||
const toCheck = uri instanceof URI ? uri : new URI(uri);
|
||||
if (toCheck.scheme === 'user-storage') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isCloudSketch = toCheck
|
||||
.toString()
|
||||
.includes(`${REMOTE_SKETCHBOOK_FOLDER}/${ARDUINO_CLOUD_FOLDER}`);
|
||||
|
||||
const filesToCheck = [
|
||||
...READ_ONLY_FILES,
|
||||
...(isCloudSketch ? READ_ONLY_FILES_REMOTE : []),
|
||||
];
|
||||
|
||||
if (filesToCheck.includes(toCheck?.path?.base)) {
|
||||
return true;
|
||||
}
|
||||
const readOnly = !this.workspaceService
|
||||
.tryGetRoots()
|
||||
.some(({ resource }) => resource.isEqualOrParent(toCheck));
|
||||
return readOnly;
|
||||
}
|
||||
}
|
@@ -100,6 +100,19 @@
|
||||
background-color: var(--theia-titleBar-activeBackground);
|
||||
}
|
||||
|
||||
#arduino-toolbar-panel {
|
||||
background: var(--theia-titleBar-activeBackground);
|
||||
color: var(--theia-titleBar-activeForeground);
|
||||
display: flex;
|
||||
min-height: var(--theia-private-menubar-height);
|
||||
border-bottom: 1px solid var(--theia-titleBar-border);
|
||||
}
|
||||
#arduino-toolbar-panel:window-inactive,
|
||||
#arduino-toolbar-panel:-moz-window-inactive {
|
||||
background: var(--theia-titleBar-inactiveBackground);
|
||||
color: var(--theia-titleBar-inactiveForeground);
|
||||
}
|
||||
|
||||
#arduino-toolbar-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
@@ -7,6 +7,8 @@ import {
|
||||
SHELL_TABBAR_CONTEXT_MENU,
|
||||
TabBar,
|
||||
Widget,
|
||||
Layout,
|
||||
SplitPanel,
|
||||
} from '@theia/core/lib/browser';
|
||||
import {
|
||||
ConnectionStatus,
|
||||
@@ -17,6 +19,11 @@ import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { ToolbarAwareTabBar } from './tab-bars';
|
||||
|
||||
interface WidgetOptions
|
||||
extends Omit<TheiaApplicationShell.WidgetOptions, 'area'> {
|
||||
area?: TheiaApplicationShell.Area | 'toolbar';
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ApplicationShell extends TheiaApplicationShell {
|
||||
@inject(MessageService)
|
||||
@@ -24,10 +31,11 @@ export class ApplicationShell extends TheiaApplicationShell {
|
||||
|
||||
@inject(ConnectionStatusService)
|
||||
private readonly connectionStatusService: ConnectionStatusService;
|
||||
private toolbarPanel: Panel;
|
||||
|
||||
override async addWidget(
|
||||
widget: Widget,
|
||||
options: Readonly<TheiaApplicationShell.WidgetOptions> = {}
|
||||
options: Readonly<WidgetOptions> = {}
|
||||
): Promise<void> {
|
||||
// By default, Theia open a widget **next** to the currently active in the target area.
|
||||
// Instead of this logic, we want to open the new widget after the last of the target area.
|
||||
@@ -37,8 +45,12 @@ export class ApplicationShell extends TheiaApplicationShell {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (options.area === 'toolbar') {
|
||||
this.toolbarPanel.addWidget(widget);
|
||||
return;
|
||||
}
|
||||
const area = options.area || 'main';
|
||||
let ref: Widget | undefined = options.ref;
|
||||
const area: TheiaApplicationShell.Area = options.area || 'main';
|
||||
if (!ref && (area === 'main' || area === 'bottom')) {
|
||||
const tabBar = this.getTabBarFor(area);
|
||||
if (tabBar) {
|
||||
@@ -48,7 +60,10 @@ export class ApplicationShell extends TheiaApplicationShell {
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.addWidget(widget, { ...options, ref });
|
||||
return super.addWidget(widget, {
|
||||
...(<TheiaApplicationShell.WidgetOptions>options),
|
||||
ref,
|
||||
});
|
||||
}
|
||||
|
||||
override handleEvent(): boolean {
|
||||
@@ -56,6 +71,46 @@ export class ApplicationShell extends TheiaApplicationShell {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override initializeShell(): void {
|
||||
this.toolbarPanel = this.createToolbarPanel();
|
||||
super.initializeShell();
|
||||
}
|
||||
|
||||
private createToolbarPanel(): Panel {
|
||||
const toolbarPanel = new Panel();
|
||||
toolbarPanel.id = 'arduino-toolbar-panel';
|
||||
toolbarPanel.show();
|
||||
return toolbarPanel;
|
||||
}
|
||||
|
||||
protected override createLayout(): Layout {
|
||||
const bottomSplitLayout = this.createSplitLayout(
|
||||
[this.mainPanel, this.bottomPanel],
|
||||
[1, 0],
|
||||
{ orientation: 'vertical', spacing: 0 }
|
||||
);
|
||||
const panelForBottomArea = new SplitPanel({ layout: bottomSplitLayout });
|
||||
panelForBottomArea.id = 'theia-bottom-split-panel';
|
||||
|
||||
const leftRightSplitLayout = this.createSplitLayout(
|
||||
[
|
||||
this.leftPanelHandler.container,
|
||||
panelForBottomArea,
|
||||
this.rightPanelHandler.container,
|
||||
],
|
||||
[0, 1, 0],
|
||||
{ orientation: 'horizontal', spacing: 0 }
|
||||
);
|
||||
const panelForSideAreas = new SplitPanel({ layout: leftRightSplitLayout });
|
||||
panelForSideAreas.id = 'theia-left-right-split-panel';
|
||||
|
||||
return this.createBoxLayout(
|
||||
[this.topPanel, this.toolbarPanel, panelForSideAreas, this.statusBar],
|
||||
[0, 0, 1, 0],
|
||||
{ direction: 'top-to-bottom', spacing: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
// Avoid hiding top panel as we use it for arduino toolbar
|
||||
protected override createTopPanel(): Panel {
|
||||
const topPanel = super.createTopPanel();
|
||||
|
@@ -46,7 +46,8 @@ export class CommonFrontendContribution extends TheiaCommonFrontendContribution
|
||||
CommonCommands.SELECT_ICON_THEME,
|
||||
CommonCommands.SELECT_COLOR_THEME,
|
||||
CommonCommands.ABOUT_COMMAND,
|
||||
CommonCommands.SAVE_WITHOUT_FORMATTING, // Patched for https://github.com/eclipse-theia/theia/pull/8877
|
||||
CommonCommands.SAVE_WITHOUT_FORMATTING, // Patched for https://github.com/eclipse-theia/theia/pull/8877,
|
||||
CommonCommands.NEW_UNTITLED_FILE,
|
||||
]) {
|
||||
registry.unregisterMenuAction(command);
|
||||
}
|
||||
|
@@ -1,30 +1,35 @@
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Title, Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
|
||||
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { ConfigService } from '../../../common/protocol/config-service';
|
||||
import { ConfigServiceClient } from '../../config/config-service-client';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
|
||||
@injectable()
|
||||
export class TabBarDecoratorService extends TheiaTabBarDecoratorService {
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
@inject(ConfigServiceClient)
|
||||
private readonly configService: ConfigServiceClient;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
protected dataDirUri: URI | undefined;
|
||||
private dataDirUri: URI | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.configService
|
||||
.getConfiguration()
|
||||
.then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri)))
|
||||
.catch((err) =>
|
||||
this.logger.error(`Failed to determine the data directory: ${err}`)
|
||||
);
|
||||
const fireDidChange = () =>
|
||||
this.appStateService
|
||||
.reachedState('ready')
|
||||
.then(() => this.fireDidChangeDecorations());
|
||||
this.dataDirUri = this.configService.tryGetDataDirUri();
|
||||
this.configService.onDidChangeDataDirUri((dataDirUri) => {
|
||||
this.dataDirUri = dataDirUri;
|
||||
fireDidChange();
|
||||
});
|
||||
if (this.dataDirUri) {
|
||||
fireDidChange();
|
||||
}
|
||||
}
|
||||
|
||||
override getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {
|
||||
|
@@ -23,11 +23,6 @@ export class TabBarRenderer extends TheiaTabBarRenderer {
|
||||
}
|
||||
|
||||
export class ToolbarAwareTabBar extends TheiaToolbarAwareTabBar {
|
||||
protected override async updateBreadcrumbs(): Promise<void> {
|
||||
// NOOP
|
||||
// IDE2 does not use breadcrumbs.
|
||||
}
|
||||
|
||||
private readonly doUpdateToolbar = debounce(() => super.updateToolbar(), 500);
|
||||
protected override updateToolbar(): void {
|
||||
// Unlike Theia, IDE2 debounces the toolbar updates with 500ms
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ThemeService as TheiaThemeService } from '@theia/core/lib/browser/theming';
|
||||
import type { Theme } from '@theia/core/lib/common/theme';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
|
||||
|
||||
export namespace ArduinoThemes {
|
||||
export const Light: Theme = {
|
||||
@@ -18,7 +18,7 @@ export namespace ArduinoThemes {
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ThemeService extends TheiaThemeService {
|
||||
export class ThemeServiceWithDB extends TheiaThemeServiceWithDB {
|
||||
protected override init(): void {
|
||||
this.register(ArduinoThemes.Light, ArduinoThemes.Dark);
|
||||
super.init();
|
||||
|
@@ -10,7 +10,7 @@ import { OutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
} from '../../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class WidgetManager extends TheiaWidgetManager {
|
||||
|
@@ -14,7 +14,7 @@ import { SketchesService } from '../../../common/protocol';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { DebugConfigurationModel } from './debug-configuration-model';
|
||||
import {
|
||||
FileOperationError,
|
||||
|
@@ -10,4 +10,12 @@ export class EditorContribution extends TheiaEditorContribution {
|
||||
): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
protected override updateEncodingStatus(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
||||
editor: TextEditor | undefined
|
||||
): void {
|
||||
// https://github.com/arduino/arduino-ide/issues/1393
|
||||
// NOOP
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/l
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { SketchesService, Sketch } from '../../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
|
@@ -5,31 +5,23 @@ import {
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Diagnostic } from '@theia/core/shared/vscode-languageserver-types';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { Marker } from '@theia/markers/lib/common/marker';
|
||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
|
||||
import { ConfigService } from '../../../common/protocol/config-service';
|
||||
import { ConfigServiceClient } from '../../config/config-service-client';
|
||||
import debounce = require('lodash.debounce');
|
||||
|
||||
@injectable()
|
||||
export class ProblemManager extends TheiaProblemManager {
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
@inject(ConfigServiceClient)
|
||||
private readonly configService: ConfigServiceClient;
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
protected dataDirUri: URI | undefined;
|
||||
private dataDirUri: URI | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.configService
|
||||
.getConfiguration()
|
||||
.then(({ dataDirUri }) => (this.dataDirUri = new URI(dataDirUri)))
|
||||
.catch((err) =>
|
||||
this.logger.error(`Failed to determine the data directory: ${err}`)
|
||||
);
|
||||
this.dataDirUri = this.configService.tryGetDataDirUri();
|
||||
this.configService.onDidChangeDataDirUri((uri) => (this.dataDirUri = uri));
|
||||
}
|
||||
|
||||
override setMarkers(
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { EditorServiceOverrides, MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
|
||||
import { SketchesServiceClientImpl } from '../../sketches-service-client-impl';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import type { ReferencesModel } from '@theia/monaco-editor-core/esm/vs/editor/contrib/gotoSymbol/browser/referencesModel';
|
||||
|
||||
|
@@ -6,14 +6,16 @@ import { EditorPreferences } from '@theia/editor/lib/browser/editor-preferences'
|
||||
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
|
||||
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
|
||||
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
|
||||
import { SketchesServiceClientImpl } from '../../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class MonacoTextModelService extends TheiaMonacoTextModelService {
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
protected override async createModel(resource: Resource): Promise<MonacoEditorModel> {
|
||||
protected override async createModel(
|
||||
resource: Resource
|
||||
): Promise<MonacoEditorModel> {
|
||||
const factory = this.factories
|
||||
.getContributions()
|
||||
.find(({ scheme }) => resource.uri.scheme === scheme);
|
||||
|
@@ -1,34 +1,53 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { open } from '@theia/core/lib/browser/opener-service';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import {
|
||||
CommandRegistry,
|
||||
CommandService,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Path } from '@theia/core/lib/common/path';
|
||||
import { waitForEvent } from '@theia/core/lib/common/promise-util';
|
||||
import { SelectionService } from '@theia/core/lib/common/selection-service';
|
||||
import { MaybeArray } from '@theia/core/lib/common/types';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import {
|
||||
UriAwareCommandHandler,
|
||||
UriCommandHandler,
|
||||
} from '@theia/core/lib/common/uri-command-handler';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import {
|
||||
WorkspaceCommandContribution as TheiaWorkspaceCommandContribution,
|
||||
WorkspaceCommands,
|
||||
} from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { Sketch, SketchesService } from '../../../common/protocol';
|
||||
import { WorkspaceInputDialog } from './workspace-input-dialog';
|
||||
import { Sketch } from '../../../common/protocol';
|
||||
import { ConfigServiceClient } from '../../config/config-service-client';
|
||||
import { CreateFeatures } from '../../create/create-features';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
import { SaveAsSketch } from '../../contributions/save-as-sketch';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { WorkspaceInputDialog } from './workspace-input-dialog';
|
||||
|
||||
interface ValidationContext {
|
||||
sketch: Sketch;
|
||||
isCloud: boolean | undefined;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribution {
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
private readonly commandService: CommandService;
|
||||
@inject(SketchesServiceClientImpl)
|
||||
private readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
@inject(CreateFeatures)
|
||||
private readonly createFeatures: CreateFeatures;
|
||||
@inject(ApplicationShell)
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(ConfigServiceClient)
|
||||
private readonly configServiceClient: ConfigServiceClient;
|
||||
private _validationContext: ValidationContext | undefined;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
super.registerCommands(registry);
|
||||
@@ -46,9 +65,14 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
|
||||
execute: (uri) => this.renameFile(uri),
|
||||
})
|
||||
);
|
||||
registry.unregisterCommand(WorkspaceCommands.FILE_DELETE);
|
||||
registry.registerCommand(
|
||||
WorkspaceCommands.FILE_DELETE,
|
||||
this.newMultiUriAwareCommandHandler(this.deleteHandler)
|
||||
);
|
||||
}
|
||||
|
||||
protected async newFile(uri: URI | undefined): Promise<void> {
|
||||
private async newFile(uri: URI | undefined): Promise<void> {
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
@@ -67,51 +91,72 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
|
||||
this.labelProvider
|
||||
);
|
||||
|
||||
const name = await dialog.open();
|
||||
const nameWithExt = this.maybeAppendInoExt(name);
|
||||
if (nameWithExt) {
|
||||
const fileUri = parentUri.resolve(nameWithExt);
|
||||
await this.fileService.createFile(fileUri);
|
||||
this.fireCreateNewFile({ parent: parentUri, uri: fileUri });
|
||||
open(this.openerService, fileUri);
|
||||
const name = await this.openDialog(dialog, parentUri);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const nameWithExt = this.maybeAppendInoExt(name);
|
||||
const fileUri = parentUri.resolve(nameWithExt);
|
||||
await this.fileService.createFile(fileUri);
|
||||
this.fireCreateNewFile({ parent: parentUri, uri: fileUri });
|
||||
open(this.openerService, fileUri);
|
||||
}
|
||||
|
||||
protected override async validateFileName(
|
||||
name: string,
|
||||
userInput: string,
|
||||
parent: FileStat,
|
||||
recursive = false
|
||||
): Promise<string> {
|
||||
// In the Java IDE the followings are the rules:
|
||||
// - `name` without an extension should default to `name.ino`.
|
||||
// - `name` with a single trailing `.` also defaults to `name.ino`.
|
||||
const nameWithExt = this.maybeAppendInoExt(name);
|
||||
const errorMessage = await super.validateFileName(
|
||||
nameWithExt,
|
||||
parent,
|
||||
recursive
|
||||
);
|
||||
// If name does not have extension or ends with trailing dot (from IDE 1.x), treat it as an .ino file.
|
||||
// If has extension,
|
||||
// - if unsupported extension -> error
|
||||
// - if has a code file extension -> apply folder name validation without the extension and use the Theia-based validation
|
||||
// - if has any additional file extension -> use the default Theia-based validation
|
||||
const fileInput = parseFileInput(userInput);
|
||||
const { name, extension } = fileInput;
|
||||
if (!Sketch.Extensions.ALL.includes(extension)) {
|
||||
return invalidExtension(extension);
|
||||
}
|
||||
let errorMessage: string | undefined = undefined;
|
||||
if (Sketch.Extensions.CODE_FILES.includes(extension)) {
|
||||
errorMessage = this._validationContext?.isCloud
|
||||
? Sketch.validateCloudSketchFolderName(name)
|
||||
: Sketch.validateSketchFolderName(name);
|
||||
}
|
||||
if (errorMessage) {
|
||||
return errorMessage;
|
||||
return this.maybeRemapAlreadyExistsMessage(errorMessage, userInput);
|
||||
}
|
||||
const extension = nameWithExt.split('.').pop();
|
||||
if (!extension) {
|
||||
return nls.localize(
|
||||
'theia/workspace/invalidFilename',
|
||||
'Invalid filename.'
|
||||
); // XXX: this should not happen as we forcefully append `.ino` if it's not there.
|
||||
errorMessage = await super.validateFileName(userInput, parent, recursive); // run the default Theia validation with the raw input.
|
||||
if (errorMessage) {
|
||||
return this.maybeRemapAlreadyExistsMessage(errorMessage, userInput);
|
||||
}
|
||||
if (Sketch.Extensions.ALL.indexOf(`.${extension}`) === -1) {
|
||||
return nls.localize(
|
||||
'theia/workspace/invalidExtension',
|
||||
'.{0} is not a valid extension',
|
||||
extension
|
||||
);
|
||||
// It's a legacy behavior from IDE 1.x. Validate the file as if it were an `.ino` file.
|
||||
// If user did not write the `.ino` extension or ended the user input with dot, run the default Theia validation with the inferred name.
|
||||
if (extension === '.ino' && !userInput.endsWith('.ino')) {
|
||||
userInput = `${name}${extension}`;
|
||||
errorMessage = await super.validateFileName(userInput, parent, recursive);
|
||||
}
|
||||
return '';
|
||||
return this.maybeRemapAlreadyExistsMessage(errorMessage ?? '', userInput);
|
||||
}
|
||||
|
||||
protected maybeAppendInoExt(name: string | undefined): string {
|
||||
// Remaps the Theia-based `A file or folder **$fileName** already exists at this location. Please choose a different name.` to a custom one.
|
||||
private maybeRemapAlreadyExistsMessage(
|
||||
errorMessage: string,
|
||||
userInput: string
|
||||
): string {
|
||||
if (
|
||||
errorMessage ===
|
||||
nls.localizeByDefault(
|
||||
'A file or folder **{0}** already exists at this location. Please choose a different name.',
|
||||
this['trimFileName'](userInput)
|
||||
)
|
||||
) {
|
||||
return fileAlreadyExists(userInput);
|
||||
}
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
private maybeAppendInoExt(name: string): string {
|
||||
if (!name) {
|
||||
return '';
|
||||
}
|
||||
@@ -126,7 +171,7 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
|
||||
return name;
|
||||
}
|
||||
|
||||
protected async renameFile(uri: URI | undefined): Promise<void> {
|
||||
protected async renameFile(uri: URI | undefined): Promise<unknown> {
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
@@ -136,10 +181,7 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
|
||||
}
|
||||
|
||||
// file belongs to another sketch, do not allow rename
|
||||
const parentSketch = await this.sketchService.getSketchFolder(
|
||||
uri.toString()
|
||||
);
|
||||
if (parentSketch && parentSketch.uri !== sketch.uri) {
|
||||
if (!Sketch.isInSketch(uri, sketch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -149,11 +191,10 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
|
||||
openAfterMove: true,
|
||||
wipeOriginal: true,
|
||||
};
|
||||
await this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
return await this.commandService.executeCommand<string>(
|
||||
'arduino-save-as-sketch',
|
||||
options
|
||||
);
|
||||
return;
|
||||
}
|
||||
const parent = await this.getParent(uri);
|
||||
if (!parent) {
|
||||
@@ -180,12 +221,243 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
|
||||
},
|
||||
this.labelProvider
|
||||
);
|
||||
const newName = await dialog.open();
|
||||
const newNameWithExt = this.maybeAppendInoExt(newName);
|
||||
if (newNameWithExt) {
|
||||
const oldUri = uri;
|
||||
const newUri = uri.parent.resolve(newNameWithExt);
|
||||
this.fileService.move(oldUri, newUri);
|
||||
const name = await this.openDialog(dialog, uri);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const nameWithExt = this.maybeAppendInoExt(name);
|
||||
const oldUri = uri;
|
||||
const newUri = uri.parent.resolve(nameWithExt);
|
||||
return this.fileService.move(oldUri, newUri);
|
||||
}
|
||||
|
||||
protected override newUriAwareCommandHandler(
|
||||
handler: UriCommandHandler<URI>
|
||||
): UriAwareCommandHandler<URI> {
|
||||
return this.createUriAwareCommandHandler(handler);
|
||||
}
|
||||
|
||||
protected override newMultiUriAwareCommandHandler(
|
||||
handler: UriCommandHandler<URI[]>
|
||||
): UriAwareCommandHandler<URI[]> {
|
||||
return this.createUriAwareCommandHandler(handler, true);
|
||||
}
|
||||
|
||||
private createUriAwareCommandHandler<T extends MaybeArray<URI>>(
|
||||
delegate: UriCommandHandler<T>,
|
||||
multi = false
|
||||
): UriAwareCommandHandler<T> {
|
||||
return new UriAwareCommandHandlerWithCurrentEditorFallback(
|
||||
delegate,
|
||||
this.selectionService,
|
||||
this.shell,
|
||||
this.sketchesServiceClient,
|
||||
this.configServiceClient,
|
||||
this.createFeatures,
|
||||
{ multi }
|
||||
);
|
||||
}
|
||||
|
||||
private async openDialog(
|
||||
dialog: WorkspaceInputDialog,
|
||||
uri: URI
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
let dataDirUri = this.configServiceClient.tryGetDataDirUri();
|
||||
if (!dataDirUri) {
|
||||
dataDirUri = await waitForEvent(
|
||||
this.configServiceClient.onDidChangeDataDirUri,
|
||||
2_000
|
||||
);
|
||||
}
|
||||
this.acquireValidationContext(uri, dataDirUri);
|
||||
const name = await dialog.open(true);
|
||||
return name;
|
||||
} finally {
|
||||
this._validationContext = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private acquireValidationContext(
|
||||
uri: URI,
|
||||
dataDirUri: URI | undefined
|
||||
): void {
|
||||
const sketch = this.sketchesServiceClient.tryGetCurrentSketch();
|
||||
if (
|
||||
CurrentSketch.isValid(sketch) &&
|
||||
new URI(sketch.uri).isEqualOrParent(uri)
|
||||
) {
|
||||
const isCloud = this.createFeatures.isCloud(sketch, dataDirUri);
|
||||
this._validationContext = { sketch, isCloud };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// (non-API) exported for tests
|
||||
export function fileAlreadyExists(userInput: string): string {
|
||||
return nls.localize(
|
||||
'arduino/workspace/alreadyExists',
|
||||
"'{0}' already exists.",
|
||||
userInput
|
||||
);
|
||||
}
|
||||
|
||||
// (non-API) exported for tests
|
||||
export function invalidExtension(extension: string): string {
|
||||
return nls.localize(
|
||||
'theia/workspace/invalidExtension',
|
||||
'.{0} is not a valid extension',
|
||||
extension.charAt(0) === '.' ? extension.slice(1) : extension
|
||||
);
|
||||
}
|
||||
|
||||
interface FileInput {
|
||||
/**
|
||||
* The raw text the user enters in the `<input>`.
|
||||
*/
|
||||
readonly raw: string;
|
||||
/**
|
||||
* This is the name without the extension. If raw is `'lib.cpp'`, then `name` will be `'lib'`. If raw is `'foo'` or `'foo.'` this value is `'foo'`.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* With the leading dot. For example `'.ino'` or `'.cpp'`.
|
||||
*/
|
||||
readonly extension: string;
|
||||
}
|
||||
export function parseFileInput(userInput: string): FileInput {
|
||||
if (!userInput) {
|
||||
return {
|
||||
raw: '',
|
||||
name: '',
|
||||
extension: Sketch.Extensions.DEFAULT,
|
||||
};
|
||||
}
|
||||
const path = new Path(userInput);
|
||||
let extension = path.ext;
|
||||
if (extension.trim() === '' || extension.trim() === '.') {
|
||||
extension = Sketch.Extensions.DEFAULT;
|
||||
}
|
||||
return {
|
||||
raw: userInput,
|
||||
name: path.name,
|
||||
extension,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, the Theia-based URI-aware command handler tries to retrieve the URI from the selection service.
|
||||
* Delete/Rename from the tab-bar toolbar (`...`) is not active if the selection was never inside an editor.
|
||||
* This implementation falls back to the current current title of the main panel if no URI can be retrieved from the parent classes.
|
||||
* - https://github.com/arduino/arduino-ide/issues/1847
|
||||
* - https://github.com/eclipse-theia/theia/issues/12139
|
||||
*/
|
||||
class UriAwareCommandHandlerWithCurrentEditorFallback<
|
||||
T extends MaybeArray<URI>
|
||||
> extends UriAwareCommandHandler<T> {
|
||||
constructor(
|
||||
delegate: UriCommandHandler<T>,
|
||||
selectionService: SelectionService,
|
||||
private readonly shell: ApplicationShell,
|
||||
private readonly sketchesServiceClient: SketchesServiceClientImpl,
|
||||
private readonly configServiceClient: ConfigServiceClient,
|
||||
private readonly createFeatures: CreateFeatures,
|
||||
options?: UriAwareCommandHandler.Options
|
||||
) {
|
||||
super(selectionService, delegate, options);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected override getUri(...args: any[]): T | undefined {
|
||||
const uri = super.getUri(...args);
|
||||
if (!uri || (Array.isArray(uri) && !uri.length)) {
|
||||
const fallbackUri = this.currentTitleOwnerUriFromMainPanel;
|
||||
if (fallbackUri) {
|
||||
return (this.isMulti() ? [fallbackUri] : fallbackUri) as T;
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override isEnabled(...args: any[]): boolean {
|
||||
const [uri, ...others] = this.getArgsWithUri(...args);
|
||||
if (uri) {
|
||||
if (!this.isInSketch(uri)) {
|
||||
return false;
|
||||
}
|
||||
if (this.affectsCloudSketchFolderWhenSignedOut(uri)) {
|
||||
return false;
|
||||
}
|
||||
if (this.handler.isEnabled) {
|
||||
return this.handler.isEnabled(uri, ...others);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The `currentEditor` is broken after a rename. (https://github.com/eclipse-theia/theia/issues/12139)
|
||||
// `ApplicationShell#currentWidget` might provide a wrong result just as the `getFocusedCodeEditor` and `getFocusedCodeEditor` of the `MonacoEditorService`
|
||||
// Try to extract the URI from the current title of the main panel if it's an editor widget.
|
||||
private get currentTitleOwnerUriFromMainPanel(): URI | undefined {
|
||||
const owner = this.shell.mainPanel.currentTitle?.owner;
|
||||
return owner instanceof EditorWidget
|
||||
? owner.editor.getResourceUri()
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private isInSketch(uri: T | undefined): boolean {
|
||||
if (!uri) {
|
||||
return false;
|
||||
}
|
||||
const sketch = this.sketchesServiceClient.tryGetCurrentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
if (this.isMulti() && Array.isArray(uri)) {
|
||||
return uri.every((u) => Sketch.isInSketch(u, sketch));
|
||||
}
|
||||
if (!this.isMulti() && uri instanceof URI) {
|
||||
return Sketch.isInSketch(uri, sketch);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user is not logged in, deleting/renaming the main sketch file or the sketch folder of a cloud sketch is disabled.
|
||||
*/
|
||||
private affectsCloudSketchFolderWhenSignedOut(uri: T | undefined): boolean {
|
||||
return (
|
||||
!Boolean(this.createFeatures.session) &&
|
||||
Boolean(this.isCurrentSketchCloud()) &&
|
||||
this.affectsSketchFolder(uri)
|
||||
);
|
||||
}
|
||||
|
||||
private affectsSketchFolder(uri: T | undefined): boolean {
|
||||
if (!uri) {
|
||||
return false;
|
||||
}
|
||||
const sketch = this.sketchesServiceClient.tryGetCurrentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
if (this.isMulti() && Array.isArray(uri)) {
|
||||
return uri.map((u) => u.toString()).includes(sketch.mainFileUri);
|
||||
}
|
||||
if (!this.isMulti()) {
|
||||
return sketch.mainFileUri === uri.toString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isCurrentSketchCloud(): boolean | undefined {
|
||||
const sketch = this.sketchesServiceClient.tryGetCurrentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
const dataDirUri = this.configServiceClient.tryGetDataDirUri();
|
||||
return this.createFeatures.isCloud(sketch, dataDirUri);
|
||||
}
|
||||
}
|
||||
|
@@ -1,55 +1,36 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
|
||||
import { DeleteSketch } from '../../contributions/delete-sketch';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
} from '../../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class WorkspaceDeleteHandler extends TheiaWorkspaceDeleteHandler {
|
||||
@inject(CommandService)
|
||||
private readonly commandService: CommandService;
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
private readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
override async execute(uris: URI[]): Promise<void> {
|
||||
const sketch = await this.sketchesServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
// Deleting the main sketch file.
|
||||
if (
|
||||
uris
|
||||
.map((uri) => uri.toString())
|
||||
.some((uri) => uri === sketch.mainFileUri)
|
||||
) {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localize('vscode/fileActions/delete', 'Delete'),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||
],
|
||||
message: nls.localize(
|
||||
'theia/workspace/deleteCurrentSketch',
|
||||
'Do you want to delete the current sketch?'
|
||||
),
|
||||
});
|
||||
if (response === 1) {
|
||||
// OK
|
||||
await Promise.all(
|
||||
[
|
||||
...sketch.additionalFileUris,
|
||||
...sketch.otherSketchFileUris,
|
||||
sketch.mainFileUri,
|
||||
].map((uri) => this.closeWithoutSaving(new URI(uri)))
|
||||
);
|
||||
await this.fileService.delete(new URI(sketch.uri));
|
||||
window.close();
|
||||
}
|
||||
return;
|
||||
// Deleting the main sketch file means deleting the sketch folder and all its content.
|
||||
if (uris.some((uri) => uri.toString() === sketch.mainFileUri)) {
|
||||
return this.commandService.executeCommand(
|
||||
DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
{
|
||||
toDelete: sketch,
|
||||
willNavigateAway: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
// Individual file deletion(s).
|
||||
return super.execute(uris);
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,25 @@
|
||||
import { inject } from '@theia/core/shared/inversify';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { MaybePromise } from '@theia/core';
|
||||
import { Dialog, DialogError } from '@theia/core/lib/browser/dialogs';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { DialogError, DialogMode } from '@theia/core/lib/browser/dialogs';
|
||||
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import type {
|
||||
Progress,
|
||||
ProgressUpdate,
|
||||
} from '@theia/core/lib/common/message-service-protocol';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { inject } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
WorkspaceInputDialog as TheiaWorkspaceInputDialog,
|
||||
WorkspaceInputDialogProps,
|
||||
} from '@theia/workspace/lib/browser/workspace-input-dialog';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
|
||||
protected wasTouched = false;
|
||||
private skipShowErrorMessageOnOpen: boolean;
|
||||
|
||||
constructor(
|
||||
@inject(WorkspaceInputDialogProps)
|
||||
@@ -19,27 +29,31 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
|
||||
) {
|
||||
super(props, labelProvider);
|
||||
this.node.classList.add('workspace-input-dialog');
|
||||
this.appendCloseButton(
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel')
|
||||
);
|
||||
this.appendCloseButton(Dialog.CANCEL);
|
||||
}
|
||||
|
||||
protected override appendParentPath(): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
override isValid(value: string, mode: DialogMode): MaybePromise<DialogError> {
|
||||
if (value !== '') {
|
||||
this.wasTouched = true;
|
||||
}
|
||||
return super.isValid(value, mode);
|
||||
override isValid(value: string): MaybePromise<DialogError> {
|
||||
return super.isValid(value, 'open');
|
||||
}
|
||||
|
||||
override open(
|
||||
skipShowErrorMessageOnOpen = false
|
||||
): Promise<string | undefined> {
|
||||
this.skipShowErrorMessageOnOpen = skipShowErrorMessageOnOpen;
|
||||
return super.open();
|
||||
}
|
||||
|
||||
protected override setErrorMessage(error: DialogError): void {
|
||||
if (this.acceptButton) {
|
||||
this.acceptButton.disabled = !DialogError.getResult(error);
|
||||
}
|
||||
if (this.wasTouched) {
|
||||
if (this.skipShowErrorMessageOnOpen) {
|
||||
this.skipShowErrorMessageOnOpen = false;
|
||||
} else {
|
||||
this.errorMessageNode.innerText = DialogError.getMessage(error);
|
||||
}
|
||||
}
|
||||
@@ -54,3 +68,133 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
|
||||
return this.closeButton;
|
||||
}
|
||||
}
|
||||
|
||||
interface TaskFactory<T> {
|
||||
createTask(value: string): (progress: Progress) => Promise<T>;
|
||||
}
|
||||
|
||||
export class TaskFactoryImpl<T> implements TaskFactory<T> {
|
||||
private _value: string | undefined;
|
||||
|
||||
constructor(private readonly task: TaskFactory<T>['createTask']) {}
|
||||
|
||||
get value(): string | undefined {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
createTask(value: string): (progress: Progress) => Promise<T> {
|
||||
this._value = value;
|
||||
return this.task(this._value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace input dialog executing a long running operation with indefinite progress.
|
||||
*/
|
||||
export class WorkspaceInputDialogWithProgress<
|
||||
T = unknown
|
||||
> extends WorkspaceInputDialog {
|
||||
private _taskResult: T | undefined;
|
||||
|
||||
constructor(
|
||||
protected override readonly props: WorkspaceInputDialogProps,
|
||||
protected override readonly labelProvider: LabelProvider,
|
||||
/**
|
||||
* The created task will provide the result. See `#taskResult`.
|
||||
*/
|
||||
private readonly taskFactory: TaskFactory<T>
|
||||
) {
|
||||
super(props, labelProvider);
|
||||
}
|
||||
|
||||
get taskResult(): T | undefined {
|
||||
return this._taskResult;
|
||||
}
|
||||
|
||||
protected override async accept(): Promise<void> {
|
||||
if (!this.resolve) {
|
||||
return;
|
||||
}
|
||||
this.acceptCancellationSource.cancel();
|
||||
this.acceptCancellationSource = new CancellationTokenSource();
|
||||
const token = this.acceptCancellationSource.token;
|
||||
const value = this.value;
|
||||
const error = await this.isValid(value);
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
if (!DialogError.getResult(error)) {
|
||||
this.setErrorMessage(error);
|
||||
} else {
|
||||
const spinner = document.createElement('div');
|
||||
spinner.classList.add('spinner');
|
||||
const disposables = new DisposableCollection();
|
||||
try {
|
||||
this.toggleButtons(true);
|
||||
disposables.push(Disposable.create(() => this.toggleButtons(false)));
|
||||
|
||||
const closeParent = this.closeCrossNode.parentNode;
|
||||
closeParent?.removeChild(this.closeCrossNode);
|
||||
disposables.push(
|
||||
Disposable.create(() => {
|
||||
closeParent?.appendChild(this.closeCrossNode);
|
||||
})
|
||||
);
|
||||
|
||||
this.errorMessageNode.classList.add('progress');
|
||||
disposables.push(
|
||||
Disposable.create(() =>
|
||||
this.errorMessageNode.classList.remove('progress')
|
||||
)
|
||||
);
|
||||
|
||||
const errorParent = this.errorMessageNode.parentNode;
|
||||
errorParent?.insertBefore(spinner, this.errorMessageNode);
|
||||
disposables.push(
|
||||
Disposable.create(() => errorParent?.removeChild(spinner))
|
||||
);
|
||||
|
||||
const cancellationSource = new CancellationTokenSource();
|
||||
const progress: Progress = {
|
||||
id: v4(),
|
||||
cancel: () => cancellationSource.cancel(),
|
||||
report: (update: ProgressUpdate) => {
|
||||
this.setProgressMessage(update);
|
||||
},
|
||||
result: Promise.resolve(value),
|
||||
};
|
||||
const task = this.taskFactory.createTask(value);
|
||||
this._taskResult = await task(progress);
|
||||
this.resolve(value);
|
||||
} catch (err) {
|
||||
if (this.reject) {
|
||||
this.reject(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
Widget.detach(this);
|
||||
disposables.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private toggleButtons(disabled: boolean): void {
|
||||
if (this.acceptButton) {
|
||||
this.acceptButton.disabled = disabled;
|
||||
}
|
||||
if (this.closeButton) {
|
||||
this.closeButton.disabled = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
private setProgressMessage(update: ProgressUpdate): void {
|
||||
if (update.work && update.work.done === update.work.total) {
|
||||
this.errorMessageNode.innerText = '';
|
||||
} else {
|
||||
if (update.message) {
|
||||
this.errorMessageNode.innerText = update.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ import { WindowServiceExt } from '../core/window-service-ext';
|
||||
@injectable()
|
||||
export class WorkspaceService extends TheiaWorkspaceService {
|
||||
@inject(SketchesService)
|
||||
private readonly sketchService: SketchesService;
|
||||
private readonly sketchesService: SketchesService;
|
||||
@inject(WindowServiceExt)
|
||||
private readonly windowServiceExt: WindowServiceExt;
|
||||
@inject(ContributionProvider)
|
||||
@@ -41,7 +41,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
): Promise<FileStat | undefined> {
|
||||
const stat = await super.toFileStat(uri);
|
||||
if (!stat) {
|
||||
const newSketchUri = await this.sketchService.createNewSketch();
|
||||
const newSketchUri = await this.sketchesService.createNewSketch();
|
||||
return this.toFileStat(newSketchUri.uri);
|
||||
}
|
||||
// When opening a file instead of a directory, IDE2 (and Theia) expects a workspace JSON file.
|
||||
@@ -52,18 +52,18 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
// 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(
|
||||
const sketch = await this.sketchesService.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();
|
||||
const newSketchUri = await this.sketchesService.createNewSketch();
|
||||
return this.toFileStat(newSketchUri.uri);
|
||||
} else if (SketchesError.NotFound.is(err)) {
|
||||
this._workspaceError = err;
|
||||
const newSketchUri = await this.sketchService.createNewSketch();
|
||||
const newSketchUri = await this.sketchesService.createNewSketch();
|
||||
return this.toFileStat(newSketchUri.uri);
|
||||
}
|
||||
throw err;
|
||||
|
@@ -9,7 +9,7 @@ import { Sketch } from '../../../common/protocol';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
|
||||
@injectable()
|
||||
|
@@ -19,7 +19,8 @@ export class ArduinoToolbarContainer extends Widget {
|
||||
this.toolbars = toolbars;
|
||||
}
|
||||
|
||||
override onAfterAttach(msg: Message) {
|
||||
override onAfterAttach(msg: Message): void {
|
||||
super.onAfterAttach(msg);
|
||||
for (const toolbar of this.toolbars) {
|
||||
Widget.attach(toolbar, this.node);
|
||||
}
|
||||
@@ -56,9 +57,11 @@ export class ArduinoToolbarContribution
|
||||
);
|
||||
}
|
||||
|
||||
onStart(app: FrontendApplication) {
|
||||
app.shell.addWidget(this.arduinoToolbarContainer, {
|
||||
area: 'top',
|
||||
});
|
||||
onStart(app: FrontendApplication): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const options = <any>{
|
||||
area: 'toolbar',
|
||||
};
|
||||
app.shell.addWidget(this.arduinoToolbarContainer, options);
|
||||
}
|
||||
}
|
||||
|
@@ -1,2 +1,2 @@
|
||||
export const REMOTE_SKETCHBOOK_FOLDER = 'RemoteSketchbook';
|
||||
export const ARDUINO_CLOUD_FOLDER = 'ArduinoCloud';
|
||||
export const ARDUINO_CLOUD_FOLDER = 'ArduinoCloud';
|
||||
|
@@ -5,11 +5,3 @@
|
||||
export function setURL(url: URL, data: any = {}): void {
|
||||
history.pushState(data, '', url);
|
||||
}
|
||||
|
||||
/**
|
||||
* If available from the `window` object, then it means, the IDE2 has successfully patched the `MonacoThemingService#init` static method,
|
||||
* and can wait the custom theme registration.
|
||||
*/
|
||||
export const MonacoThemeServiceIsReady = Symbol(
|
||||
'@arduino-ide#monaco-theme-service-is-ready'
|
||||
);
|
||||
|
@@ -39,4 +39,11 @@ export class SketchCache {
|
||||
getSketch(path: string): Create.Sketch | null {
|
||||
return this.sketches[path] || null;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return JSON.stringify({
|
||||
sketches: this.sketches,
|
||||
fileStats: this.fileStats,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -26,8 +26,8 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge
|
||||
super();
|
||||
this.id = 'cloud-sketchbook-composite-widget';
|
||||
this.title.caption = nls.localize(
|
||||
'arduino/cloud/remoteSketchbook',
|
||||
'Remote Sketchbook'
|
||||
'arduino/cloud/cloudSketchbook',
|
||||
'Cloud Sketchbook'
|
||||
);
|
||||
this.title.iconClass = 'cloud-sketchbook-tree-icon';
|
||||
}
|
||||
@@ -55,8 +55,8 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge
|
||||
{this._session && (
|
||||
<CreateNew
|
||||
label={nls.localize(
|
||||
'arduino/sketchbook/newRemoteSketch',
|
||||
'New Remote Sketch'
|
||||
'arduino/sketchbook/newCloudSketch',
|
||||
'New Cloud Sketch'
|
||||
)}
|
||||
onClick={this.onDidClickCreateNew}
|
||||
/>
|
||||
|
@@ -26,7 +26,7 @@ import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { Contribution } from '../../contributions/contribution';
|
||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
import { MainMenuManager } from '../../../common/main-menu-manager';
|
||||
@@ -67,9 +67,9 @@ export namespace CloudSketchbookCommands {
|
||||
export const TOGGLE_CLOUD_SKETCHBOOK = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--disable',
|
||||
label: 'Show/Hide Remote Sketchbook',
|
||||
label: 'Show/Hide Cloud Sketchbook',
|
||||
},
|
||||
'arduino/cloud/showHideRemoveSketchbook'
|
||||
'arduino/cloud/showHideSketchbook'
|
||||
);
|
||||
|
||||
export const PULL_SKETCH = Command.toLocalizedCommand(
|
||||
|
@@ -17,12 +17,11 @@ import {
|
||||
LocalCacheUri,
|
||||
} from '../../local-cache/local-cache-fs-provider';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { SketchCache } from './cloud-sketch-cache';
|
||||
import { Create } from '../../create/typings';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
|
||||
export function sketchBaseDir(sketch: Create.Sketch): FileStat {
|
||||
function sketchBaseDir(sketch: Create.Sketch): FileStat {
|
||||
// extract the sketch path
|
||||
const [, path] = splitSketchPath(sketch.path);
|
||||
const dirs = posixSegments(path);
|
||||
@@ -42,7 +41,7 @@ export function sketchBaseDir(sketch: Create.Sketch): FileStat {
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
export function sketchesToFileStats(sketches: Create.Sketch[]): FileStat[] {
|
||||
function sketchesToFileStats(sketches: Create.Sketch[]): FileStat[] {
|
||||
const sketchesBaseDirs: Record<string, FileStat> = {};
|
||||
|
||||
for (const sketch of sketches) {
|
||||
@@ -64,8 +63,6 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(LocalCacheFsProvider)
|
||||
private readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
@inject(SketchCache)
|
||||
private readonly sketchCache: SketchCache;
|
||||
|
||||
private _localCacheFsProviderReady: Deferred<void> | undefined;
|
||||
|
||||
@@ -127,8 +124,7 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
||||
this.tree.root = undefined;
|
||||
return;
|
||||
}
|
||||
this.createApi.init(this.authenticationService, this.arduinoPreferences);
|
||||
this.sketchCache.init();
|
||||
this.createApi.sketchCache.init();
|
||||
const [sketches] = await Promise.all([
|
||||
this.createApi.sketches(),
|
||||
this.ensureLocalFsProviderReady(),
|
||||
|
@@ -9,7 +9,7 @@ import { CloudUserCommands } from '../../auth/cloud-user-commands';
|
||||
import { NodeProps } from '@theia/core/lib/browser/tree/tree-widget';
|
||||
import { TreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser';
|
||||
import { shell } from 'electron';
|
||||
import { shell } from '@theia/core/electron-shared/@electron/remote';
|
||||
import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import { SketchCache } from './cloud-sketch-cache';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { FileStatNode } from '@theia/filesystem/lib/browser/file-tree';
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
|
||||
@@ -28,8 +26,6 @@ import { CloudSketchbookCommands } from './cloud-sketchbook-contributions';
|
||||
import { DoNotAskAgainConfirmDialog } from '../../dialogs/do-not-ask-again-dialog';
|
||||
import { SketchbookTree } from '../sketchbook/sketchbook-tree';
|
||||
import { firstToUpperCase } from '../../../common/utils';
|
||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
|
||||
import { posix, splitSketchPath } from '../../create/create-paths';
|
||||
@@ -46,29 +42,17 @@ type FilesToSync = {
|
||||
};
|
||||
@injectable()
|
||||
export class CloudSketchbookTree extends SketchbookTree {
|
||||
@inject(FileService)
|
||||
protected override readonly fileService: FileService;
|
||||
|
||||
@inject(LocalCacheFsProvider)
|
||||
protected readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
|
||||
@inject(SketchCache)
|
||||
protected readonly sketchCache: SketchCache;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected override readonly arduinoPreferences: ArduinoPreferences;
|
||||
private readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
private readonly preferenceService: PreferenceService;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
private readonly messageService: MessageService;
|
||||
|
||||
@inject(CreateApi)
|
||||
protected readonly createApi: CreateApi;
|
||||
private readonly createApi: CreateApi;
|
||||
|
||||
async pushPublicWarn(
|
||||
node: CloudSketchbookTree.CloudSketchDirNode
|
||||
@@ -93,15 +77,13 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
PreferenceScope.User
|
||||
),
|
||||
}).open();
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return Boolean(ok);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
||||
async pull(arg: any): Promise<void> {
|
||||
const {
|
||||
// model,
|
||||
@@ -145,7 +127,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
);
|
||||
await this.sync(node.remoteUri, localUri);
|
||||
|
||||
this.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||
|
||||
node.commands = commandsCopy;
|
||||
this.messageService.info(
|
||||
@@ -213,7 +195,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
);
|
||||
await this.sync(localUri, node.remoteUri);
|
||||
|
||||
this.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||
|
||||
node.commands = commandsCopy;
|
||||
this.messageService.info(
|
||||
@@ -229,7 +211,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
});
|
||||
}
|
||||
|
||||
async recursiveURIs(uri: URI): Promise<URI[]> {
|
||||
private async recursiveURIs(uri: URI): Promise<URI[]> {
|
||||
// remote resources can be fetched one-shot via api
|
||||
if (CreateUri.is(uri)) {
|
||||
const resources = await this.createApi.readDirectory(
|
||||
@@ -286,7 +268,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
}, {});
|
||||
}
|
||||
|
||||
async getUrisMap(uri: URI) {
|
||||
private async getUrisMap(uri: URI): Promise<Record<string, URI>> {
|
||||
const basepath = uri.toString();
|
||||
const exists = await this.fileService.exists(uri);
|
||||
const uris =
|
||||
@@ -294,7 +276,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
return uris;
|
||||
}
|
||||
|
||||
async treeDiff(source: URI, dest: URI): Promise<FilesToSync> {
|
||||
private async treeDiff(source: URI, dest: URI): Promise<FilesToSync> {
|
||||
const [sourceURIs, destURIs] = await Promise.all([
|
||||
this.getUrisMap(source),
|
||||
this.getUrisMap(dest),
|
||||
@@ -356,7 +338,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
}
|
||||
}
|
||||
|
||||
async sync(source: URI, dest: URI) {
|
||||
private async sync(source: URI, dest: URI): Promise<void> {
|
||||
const { filesToWrite, filesToDelete } = await this.treeDiff(source, dest);
|
||||
await Promise.all(
|
||||
filesToWrite.map(async ({ source, dest }) => {
|
||||
@@ -375,7 +357,9 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
);
|
||||
}
|
||||
|
||||
override async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
|
||||
override async resolveChildren(
|
||||
parent: CompositeTreeNode
|
||||
): Promise<TreeNode[]> {
|
||||
return (await super.resolveChildren(parent)).sort((a, b) => {
|
||||
if (
|
||||
WorkspaceNode.is(parent) &&
|
||||
@@ -416,14 +400,16 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
CreateUri.is(node.remoteUri)
|
||||
) {
|
||||
let remoteFileStat: FileStat;
|
||||
const cacheHit = this.sketchCache.getItem(node.remoteUri.path.toString());
|
||||
const cacheHit = this.createApi.sketchCache.getItem(
|
||||
node.remoteUri.path.toString()
|
||||
);
|
||||
if (cacheHit) {
|
||||
remoteFileStat = cacheHit;
|
||||
} else {
|
||||
// not found, fetch and add it for future calls
|
||||
remoteFileStat = await this.fileService.resolve(node.remoteUri);
|
||||
if (remoteFileStat) {
|
||||
this.sketchCache.addItem(remoteFileStat);
|
||||
this.createApi.sketchCache.addItem(remoteFileStat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,6 +439,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
if (!CreateUri.is(childFs.resource)) {
|
||||
let refUri = node.fileStat.resource;
|
||||
if (node.fileStat.hasOwnProperty('remoteUri')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
refUri = (node.fileStat as any).remoteUri;
|
||||
}
|
||||
remoteUri = refUri.resolve(childFs.name);
|
||||
@@ -471,6 +458,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
}
|
||||
|
||||
protected override toNode(
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
||||
fileStat: any,
|
||||
parent: CompositeTreeNode
|
||||
): FileNode | DirNode {
|
||||
@@ -530,7 +518,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
* @returns
|
||||
*/
|
||||
protected override async augmentSketchNode(node: DirNode): Promise<void> {
|
||||
const sketch = this.sketchCache.getSketch(
|
||||
const sketch = this.createApi.sketchCache.getSketch(
|
||||
node.fileStat.resource.path.toString()
|
||||
);
|
||||
|
||||
@@ -594,7 +582,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
|
||||
protected override async isSketchNode(node: DirNode): Promise<boolean> {
|
||||
if (DirNode.is(node)) {
|
||||
const sketch = this.sketchCache.getSketch(
|
||||
const sketch = this.createApi.sketchCache.getSketch(
|
||||
node.fileStat.resource.path.toString()
|
||||
);
|
||||
return !!sketch;
|
||||
@@ -621,6 +609,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
if (DecoratedTreeNode.is(node)) {
|
||||
for (const property of Object.keys(decorationData)) {
|
||||
if (node.decorationData.hasOwnProperty(property)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
delete (node.decorationData as any)[property];
|
||||
}
|
||||
}
|
||||
@@ -661,7 +650,7 @@ export namespace CloudSketchbookTree {
|
||||
commands?: Command[];
|
||||
}
|
||||
export namespace CloudSketchDirNode {
|
||||
export function is(node: TreeNode): node is CloudSketchDirNode {
|
||||
export function is(node: TreeNode | undefined): node is CloudSketchDirNode {
|
||||
return SketchbookTree.SketchDirNode.is(node);
|
||||
}
|
||||
|
||||
|
@@ -147,7 +147,6 @@ export namespace ComponentList {
|
||||
export interface Props<T extends ArduinoComponent> {
|
||||
readonly items: T[];
|
||||
readonly itemLabel: (item: T) => string;
|
||||
readonly itemDeprecated: (item: T) => boolean;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
|
@@ -32,7 +32,7 @@ export class FilterableListContainer<
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
this.search = debounce(this.search, 500);
|
||||
this.search = debounce(this.search, 500, { trailing: true });
|
||||
this.search(this.state.searchOptions);
|
||||
this.props.searchOptionsDidChange((newSearchOptions) => {
|
||||
const { searchOptions } = this.state;
|
||||
@@ -82,12 +82,11 @@ export class FilterableListContainer<
|
||||
}
|
||||
|
||||
protected renderComponentList(): React.ReactNode {
|
||||
const { itemLabel, itemDeprecated, itemRenderer } = this.props;
|
||||
const { itemLabel, itemRenderer } = this.props;
|
||||
return (
|
||||
<ComponentList<T>
|
||||
items={this.state.items}
|
||||
itemLabel={itemLabel}
|
||||
itemDeprecated={itemDeprecated}
|
||||
itemRenderer={itemRenderer}
|
||||
install={this.install.bind(this)}
|
||||
uninstall={this.uninstall.bind(this)}
|
||||
@@ -109,9 +108,7 @@ export class FilterableListContainer<
|
||||
|
||||
protected search(searchOptions: S): void {
|
||||
const { searchable } = this.props;
|
||||
searchable
|
||||
.search(searchOptions)
|
||||
.then((items) => this.setState({ items: this.props.sort(items) }));
|
||||
searchable.search(searchOptions).then((items) => this.setState({ items }));
|
||||
}
|
||||
|
||||
protected async install(
|
||||
@@ -127,7 +124,7 @@ export class FilterableListContainer<
|
||||
run: ({ progressId }) => install({ item, progressId, version }),
|
||||
});
|
||||
const items = await searchable.search(this.state.searchOptions);
|
||||
this.setState({ items: this.props.sort(items) });
|
||||
this.setState({ items });
|
||||
}
|
||||
|
||||
protected async uninstall(item: T): Promise<void> {
|
||||
@@ -155,7 +152,7 @@ export class FilterableListContainer<
|
||||
run: ({ progressId }) => uninstall({ item, progressId }),
|
||||
});
|
||||
const items = await searchable.search(this.state.searchOptions);
|
||||
this.setState({ items: this.props.sort(items) });
|
||||
this.setState({ items });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +165,6 @@ export namespace FilterableListContainer {
|
||||
readonly container: ListWidget<T, S>;
|
||||
readonly searchable: Searchable<T, S>;
|
||||
readonly itemLabel: (item: T) => string;
|
||||
readonly itemDeprecated: (item: T) => boolean;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly filterRenderer: FilterRenderer<S>;
|
||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||
@@ -192,7 +188,6 @@ export namespace FilterableListContainer {
|
||||
progressId: string;
|
||||
}) => Promise<void>;
|
||||
readonly commandService: CommandService;
|
||||
readonly sort: (items: T[]) => T[];
|
||||
}
|
||||
|
||||
export interface State<T, S extends Searchable.Options> {
|
||||
|
@@ -7,6 +7,7 @@ import {
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
@@ -42,6 +43,7 @@ export abstract class ListWidget<
|
||||
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
|
||||
*/
|
||||
protected focusNode: HTMLElement | undefined;
|
||||
private readonly didReceiveFirstFocus = new Deferred();
|
||||
protected readonly searchOptionsChangeEmitter = new Emitter<
|
||||
Partial<S> | undefined
|
||||
>();
|
||||
@@ -51,11 +53,9 @@ 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, itemDeprecated, itemLabel } = options;
|
||||
const { id, label, iconClass } = options;
|
||||
this.id = id;
|
||||
this.title.label = label;
|
||||
this.title.caption = label;
|
||||
@@ -65,15 +65,6 @@ 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()
|
||||
@@ -117,6 +108,7 @@ export abstract class ListWidget<
|
||||
|
||||
protected onFocusResolved = (element: HTMLElement | undefined): void => {
|
||||
this.focusNode = element;
|
||||
this.didReceiveFirstFocus.resolve();
|
||||
};
|
||||
|
||||
protected async install({
|
||||
@@ -141,30 +133,6 @@ 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>
|
||||
@@ -175,14 +143,12 @@ export abstract class ListWidget<
|
||||
install={this.install.bind(this)}
|
||||
uninstall={this.uninstall.bind(this)}
|
||||
itemLabel={this.options.itemLabel}
|
||||
itemDeprecated={this.options.itemDeprecated}
|
||||
itemRenderer={this.options.itemRenderer}
|
||||
filterRenderer={this.options.filterRenderer}
|
||||
searchOptionsDidChange={this.searchOptionsChangeEmitter.event}
|
||||
messageService={this.messageService}
|
||||
commandService={this.commandService}
|
||||
responseService={this.responseService}
|
||||
sort={this.filterableListSort}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -192,7 +158,9 @@ export abstract class ListWidget<
|
||||
* If it is `undefined`, updates the view state by re-running the search with the current `filterText` term.
|
||||
*/
|
||||
refresh(searchOptions: Partial<S> | undefined): void {
|
||||
this.searchOptionsChangeEmitter.fire(searchOptions);
|
||||
this.didReceiveFirstFocus.promise.then(() =>
|
||||
this.searchOptionsChangeEmitter.fire(searchOptions)
|
||||
);
|
||||
}
|
||||
|
||||
updateScrollBar(): void {
|
||||
@@ -213,7 +181,6 @@ export namespace ListWidget {
|
||||
readonly installable: Installable<T>;
|
||||
readonly searchable: Searchable<T, S>;
|
||||
readonly itemLabel: (item: T) => string;
|
||||
readonly itemDeprecated: (item: T) => boolean;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly filterRenderer: FilterRenderer<S>;
|
||||
readonly defaultSearchOptions: S;
|
||||
|
@@ -1,8 +1,12 @@
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { ConfigService } from '../../../common/protocol';
|
||||
import { ConfigServiceClient } from './../../config/config-service-client';
|
||||
import { SketchbookTree } from './sketchbook-tree';
|
||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
import {
|
||||
@@ -13,7 +17,10 @@ import {
|
||||
} from '@theia/core/lib/browser/tree';
|
||||
import { SketchbookCommands } from './sketchbook-commands';
|
||||
import { OpenerService, open } from '@theia/core/lib/browser';
|
||||
import { CurrentSketch, SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
@@ -36,8 +43,8 @@ export class SketchbookTreeModel extends FileTreeModel {
|
||||
@inject(CommandRegistry)
|
||||
public readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
@inject(ConfigServiceClient)
|
||||
protected readonly configService: ConfigServiceClient;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
@@ -59,6 +66,12 @@ export class SketchbookTreeModel extends FileTreeModel {
|
||||
super.init();
|
||||
this.reportBusyProgress();
|
||||
this.initializeRoot();
|
||||
this.toDispose.push(
|
||||
this.configService.onDidChangeSketchDirUri(async () => {
|
||||
await this.updateRoot();
|
||||
this.selectRoot(this.root);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected readonly pendingBusyProgress = new Map<string, Deferred<void>>();
|
||||
@@ -121,6 +134,10 @@ export class SketchbookTreeModel extends FileTreeModel {
|
||||
return;
|
||||
}
|
||||
const root = this.root;
|
||||
this.selectRoot(root);
|
||||
}
|
||||
|
||||
private selectRoot(root: TreeNode | undefined) {
|
||||
if (CompositeTreeNode.is(root) && root.children.length === 1) {
|
||||
const child = root.children[0];
|
||||
if (
|
||||
@@ -161,10 +178,12 @@ export class SketchbookTreeModel extends FileTreeModel {
|
||||
}
|
||||
|
||||
protected async createRoot(): Promise<TreeNode | undefined> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
const rootFileStats = await this.fileService.resolve(
|
||||
new URI(config.sketchDirUri)
|
||||
);
|
||||
const sketchDirUri = this.configService.tryGetSketchDirUri();
|
||||
const errors = this.configService.tryGetMessages();
|
||||
if (!sketchDirUri || errors?.length) {
|
||||
return undefined;
|
||||
}
|
||||
const rootFileStats = await this.fileService.resolve(sketchDirUri);
|
||||
|
||||
if (this.workspaceService.opened && rootFileStats.children) {
|
||||
// filter out libraries and hardware
|
||||
@@ -183,7 +202,10 @@ export class SketchbookTreeModel extends FileTreeModel {
|
||||
/**
|
||||
* Move the given source file or directory to the given target directory.
|
||||
*/
|
||||
override async move(source: TreeNode, target: TreeNode): Promise<URI | undefined> {
|
||||
override async move(
|
||||
source: TreeNode,
|
||||
target: TreeNode
|
||||
): Promise<URI | undefined> {
|
||||
if (source.parent && WorkspaceRootNode.is(source)) {
|
||||
// do not support moving a root folder
|
||||
return undefined;
|
||||
|
@@ -21,7 +21,7 @@ import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection';
|
||||
import { Sketch } from '../../contributions/contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
@@ -26,7 +26,7 @@ import {
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { URI } from '../../contributions/contribution';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser';
|
||||
|
@@ -2,7 +2,7 @@ import { Installable } from './installable';
|
||||
|
||||
export interface ArduinoComponent {
|
||||
readonly name: string;
|
||||
readonly deprecated: boolean;
|
||||
readonly deprecated?: boolean;
|
||||
readonly author: string;
|
||||
readonly summary: string;
|
||||
readonly description: string;
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
Updatable,
|
||||
} from '../nls';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
|
||||
export type AvailablePorts = Record<string, [Port, Array<Board>]>;
|
||||
export namespace AvailablePorts {
|
||||
@@ -141,6 +142,16 @@ export const BoardsService = Symbol('BoardsService');
|
||||
export interface BoardsService
|
||||
extends Installable<BoardsPackage>,
|
||||
Searchable<BoardsPackage, BoardSearch> {
|
||||
install(options: {
|
||||
item: BoardsPackage;
|
||||
progressId?: string;
|
||||
version?: Installable.Version;
|
||||
noOverwrite?: boolean;
|
||||
/**
|
||||
* Only for testing to avoid confirmation dialogs from Windows User Access Control when installing a platform.
|
||||
*/
|
||||
skipPostInstall?: boolean;
|
||||
}): Promise<void>;
|
||||
getState(): Promise<AvailablePorts>;
|
||||
getBoardDetails(options: { fqbn: string }): Promise<BoardDetails | undefined>;
|
||||
getBoardPackage(options: { id: string }): Promise<BoardsPackage | undefined>;
|
||||
@@ -623,3 +634,131 @@ export namespace Board {
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error if the `fqbn` argument is not sanitized. A sanitized FQBN has the `VENDOR:ARCHITECTURE:BOARD_ID` construct.
|
||||
*/
|
||||
export function assertSanitizedFqbn(fqbn: string): void {
|
||||
if (fqbn.split(':').length !== 3) {
|
||||
throw new Error(
|
||||
`Expected a sanitized FQBN with three segments in the following format: 'VENDOR:ARCHITECTURE:BOARD_ID'. Got ${fqbn} instead.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
|
||||
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
|
||||
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
|
||||
*/
|
||||
export function sanitizeFqbn(fqbn: string | undefined): string | undefined {
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const [vendor, arch, id] = fqbn.split(':');
|
||||
return `${vendor}:${arch}:${id}`;
|
||||
}
|
||||
|
||||
export interface BoardConfig {
|
||||
selectedBoard?: Board;
|
||||
selectedPort?: Port;
|
||||
}
|
||||
|
||||
export interface BoardInfo {
|
||||
/**
|
||||
* Board name. Could be `'Unknown board`'.
|
||||
*/
|
||||
BN: string;
|
||||
/**
|
||||
* Vendor ID.
|
||||
*/
|
||||
VID: string;
|
||||
/**
|
||||
* Product ID.
|
||||
*/
|
||||
PID: string;
|
||||
/**
|
||||
* Serial number.
|
||||
*/
|
||||
SN: string;
|
||||
}
|
||||
|
||||
export const selectPortForInfo = nls.localize(
|
||||
'arduino/board/selectPortForInfo',
|
||||
'Please select a port to obtain board info.'
|
||||
);
|
||||
export const nonSerialPort = nls.localize(
|
||||
'arduino/board/nonSerialPort',
|
||||
"Non-serial port, can't obtain info."
|
||||
);
|
||||
export const noNativeSerialPort = nls.localize(
|
||||
'arduino/board/noNativeSerialPort',
|
||||
"Native serial port, can't obtain info."
|
||||
);
|
||||
export const unknownBoard = nls.localize(
|
||||
'arduino/board/unknownBoard',
|
||||
'Unknown board'
|
||||
);
|
||||
|
||||
/**
|
||||
* The returned promise resolves to a `BoardInfo` if available to show in the UI or an info message explaining why showing the board info is not possible.
|
||||
*/
|
||||
export async function getBoardInfo(
|
||||
selectedPort: Port | undefined,
|
||||
availablePorts: MaybePromise<AvailablePorts>
|
||||
): Promise<BoardInfo | string> {
|
||||
if (!selectedPort) {
|
||||
return selectPortForInfo;
|
||||
}
|
||||
// IDE2 must show the board info based on the selected port.
|
||||
// https://github.com/arduino/arduino-ide/issues/1489
|
||||
// IDE 1.x supports only serial port protocol
|
||||
if (selectedPort.protocol !== 'serial') {
|
||||
return nonSerialPort;
|
||||
}
|
||||
const selectedPortKey = Port.keyOf(selectedPort);
|
||||
const state = await availablePorts;
|
||||
const boardListOnSelectedPort = Object.entries(state).filter(
|
||||
([portKey, [port]]) =>
|
||||
portKey === selectedPortKey && isNonNativeSerial(port)
|
||||
);
|
||||
|
||||
if (!boardListOnSelectedPort.length) {
|
||||
return noNativeSerialPort;
|
||||
}
|
||||
|
||||
const [, [port, boards]] = boardListOnSelectedPort[0];
|
||||
if (boardListOnSelectedPort.length > 1 || boards.length > 1) {
|
||||
console.warn(
|
||||
`Detected more than one available boards on the selected port : ${JSON.stringify(
|
||||
selectedPort
|
||||
)}. Detected boards were: ${JSON.stringify(
|
||||
boardListOnSelectedPort
|
||||
)}. Using the first one: ${JSON.stringify([port, boards])}`
|
||||
);
|
||||
}
|
||||
|
||||
const board = boards[0];
|
||||
const BN = board?.name ?? unknownBoard;
|
||||
const VID = readProperty('vid', port);
|
||||
const PID = readProperty('pid', port);
|
||||
const SN = readProperty('serialNumber', port);
|
||||
return { VID, PID, SN, BN };
|
||||
}
|
||||
|
||||
// serial protocol with one or many detected boards or available VID+PID properties from the port
|
||||
function isNonNativeSerial(port: Port): boolean {
|
||||
return !!(
|
||||
port.protocol === 'serial' &&
|
||||
port.properties?.['vid'] &&
|
||||
port.properties?.['pid']
|
||||
);
|
||||
}
|
||||
|
||||
function readProperty(property: string, port: Port): string {
|
||||
return falsyToNullString(port.properties?.[property]);
|
||||
}
|
||||
|
||||
function falsyToNullString(s: string | undefined): string {
|
||||
return !!s ? s : '(null)';
|
||||
}
|
||||
|
@@ -3,13 +3,13 @@ import { RecursivePartial } from '@theia/core/lib/common/types';
|
||||
export const ConfigServicePath = '/services/config-service';
|
||||
export const ConfigService = Symbol('ConfigService');
|
||||
export interface ConfigService {
|
||||
getVersion(): Promise<
|
||||
Readonly<{ version: string; commit: string; status?: string }>
|
||||
>;
|
||||
getCliConfigFileUri(): Promise<string>;
|
||||
getConfiguration(): Promise<Config>;
|
||||
getVersion(): Promise<Readonly<string>>;
|
||||
getConfiguration(): Promise<ConfigState>;
|
||||
setConfiguration(config: Config): Promise<void>;
|
||||
}
|
||||
export type ConfigState =
|
||||
| { config: undefined; messages: string[] }
|
||||
| { config: Config; messages?: string[] };
|
||||
|
||||
export interface Daemon {
|
||||
readonly port: string | number;
|
||||
@@ -60,8 +60,10 @@ export namespace Network {
|
||||
try {
|
||||
// Patter: PROTOCOL://USER:PASS@HOSTNAME:PORT/
|
||||
const { protocol, hostname, password, username, port } = new URL(raw);
|
||||
// protocol in URL object contains a trailing colon
|
||||
const newProtocol = protocol.replace(/:$/, '');
|
||||
return {
|
||||
protocol,
|
||||
protocol: newProtocol,
|
||||
hostname,
|
||||
password,
|
||||
username,
|
||||
@@ -117,7 +119,16 @@ export interface Config {
|
||||
readonly network: Network;
|
||||
}
|
||||
export namespace Config {
|
||||
export function sameAs(left: Config, right: Config): boolean {
|
||||
export function sameAs(
|
||||
left: Config | undefined,
|
||||
right: Config | undefined
|
||||
): boolean {
|
||||
if (!left) {
|
||||
return !right;
|
||||
}
|
||||
if (!right) {
|
||||
return false;
|
||||
}
|
||||
const leftUrls = left.additionalUrls.sort();
|
||||
const rightUrls = right.additionalUrls.sort();
|
||||
if (leftUrls.length !== rightUrls.length) {
|
||||
@@ -148,7 +159,16 @@ export namespace AdditionalUrls {
|
||||
export function stringify(additionalUrls: AdditionalUrls): string {
|
||||
return additionalUrls.join(',');
|
||||
}
|
||||
export function sameAs(left: AdditionalUrls, right: AdditionalUrls): boolean {
|
||||
export function sameAs(
|
||||
left: AdditionalUrls | undefined,
|
||||
right: AdditionalUrls | undefined
|
||||
): boolean {
|
||||
if (!left) {
|
||||
return !right;
|
||||
}
|
||||
if (!right) {
|
||||
return false;
|
||||
}
|
||||
if (left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import type { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-facto
|
||||
import type {
|
||||
AttachedBoardsChangeEvent,
|
||||
BoardsPackage,
|
||||
Config,
|
||||
ConfigState,
|
||||
ProgressMessage,
|
||||
Sketch,
|
||||
IndexType,
|
||||
@@ -39,6 +39,11 @@ export interface IndexUpdateDidFailParams extends IndexUpdateParams {
|
||||
}
|
||||
|
||||
export interface NotificationServiceClient {
|
||||
// The cached state of the core client. Libraries, examples, etc. has been updated.
|
||||
// This can happen without an index update. For example, changing the `directories.user` location.
|
||||
// An index update always implicitly involves a re-initialization without notifying via this method.
|
||||
notifyDidReinitialize(): void;
|
||||
|
||||
// Index
|
||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void;
|
||||
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void;
|
||||
@@ -50,14 +55,16 @@ export interface NotificationServiceClient {
|
||||
notifyDaemonDidStop(): void;
|
||||
|
||||
// CLI config
|
||||
notifyConfigDidChange(event: { config: Config | undefined }): void;
|
||||
notifyConfigDidChange(event: ConfigState): void;
|
||||
|
||||
// Platforms
|
||||
notifyPlatformDidInstall(event: { item: BoardsPackage }): void;
|
||||
notifyPlatformDidUninstall(event: { item: BoardsPackage }): void;
|
||||
|
||||
// Libraries
|
||||
notifyLibraryDidInstall(event: { item: LibraryPackage }): void;
|
||||
notifyLibraryDidInstall(event: {
|
||||
item: LibraryPackage | 'zip-install';
|
||||
}): void;
|
||||
notifyLibraryDidUninstall(event: { item: LibraryPackage }): void;
|
||||
|
||||
// Boards discovery
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import type { ArduinoComponent } from './arduino-component';
|
||||
|
||||
export interface Searchable<T, O extends Searchable.Options> {
|
||||
search(options: O): Promise<T[]>;
|
||||
@@ -31,3 +32,31 @@ export namespace Searchable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IDE2 must keep the library search order from the CLI but do additional boosting
|
||||
// https://github.com/arduino/arduino-ide/issues/1106
|
||||
// This additional search result boosting considers the following groups: 'Arduino', '', 'Arduino-Retired', and 'Retired'.
|
||||
// If two libraries fall into the same group, the original index is the tiebreaker.
|
||||
export type SortGroup = 'Arduino' | '' | 'Arduino-Retired' | 'Retired';
|
||||
const sortGroupOrder: Record<SortGroup, number> = {
|
||||
Arduino: 0,
|
||||
'': 1,
|
||||
'Arduino-Retired': 2,
|
||||
Retired: 3,
|
||||
};
|
||||
|
||||
export function sortComponents<T extends ArduinoComponent>(
|
||||
components: T[],
|
||||
group: (component: T) => SortGroup
|
||||
): T[] {
|
||||
return components
|
||||
.map((component, index) => ({ ...component, index }))
|
||||
.sort((left, right) => {
|
||||
const leftGroup = group(left);
|
||||
const rightGroup = group(right);
|
||||
if (leftGroup === rightGroup) {
|
||||
return left.index - right.index;
|
||||
}
|
||||
return sortGroupOrder[leftGroup] - sortGroupOrder[rightGroup];
|
||||
});
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user