mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-03 15:38:33 +00:00
Compare commits
287 Commits
2.0.0-beta
...
2.0.0-rc9.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d93c9ba654 | ||
|
|
8a0dc1be7e | ||
|
|
564862e173 | ||
|
|
d7f7010bb5 | ||
|
|
e156dcc213 | ||
|
|
27a2a6ca03 | ||
|
|
581379f86f | ||
|
|
b62f3dec84 | ||
|
|
90d2950bdd | ||
|
|
5b7d64c1c1 | ||
|
|
55927ac3dd | ||
|
|
40c93bc19a | ||
|
|
59b8a2d6bb | ||
|
|
124738d810 | ||
|
|
19c0334a91 | ||
|
|
f22be3c587 | ||
|
|
9373a0bcaf | ||
|
|
5087ff08f2 | ||
|
|
71d5a1520a | ||
|
|
ec160df25e | ||
|
|
7fbf3dc656 | ||
|
|
7680194feb | ||
|
|
2fdb19ea75 | ||
|
|
8610332afc | ||
|
|
1f7c2eb52c | ||
|
|
119dfa78d9 | ||
|
|
337d22efbd | ||
|
|
5ff9ce0028 | ||
|
|
d4833affc6 | ||
|
|
8ad10b5adf | ||
|
|
fe31d15b9f | ||
|
|
99664ee544 | ||
|
|
57841b3c0a | ||
|
|
ed41b25889 | ||
|
|
4f27725b35 | ||
|
|
73835eced3 | ||
|
|
46fcc71dd8 | ||
|
|
453a657172 | ||
|
|
1514d014a9 | ||
|
|
e4d9243486 | ||
|
|
fb690c97e8 | ||
|
|
a0038315da | ||
|
|
aea550fe33 | ||
|
|
813444408e | ||
|
|
d8be8888ef | ||
|
|
431c3bdf2b | ||
|
|
c51b201362 | ||
|
|
7fed8febf1 | ||
|
|
f4a68e793e | ||
|
|
7d961537eb | ||
|
|
d7a2d83990 | ||
|
|
a36524e02a | ||
|
|
1073c3fc7d | ||
|
|
69d7e8e96c | ||
|
|
7f2b849963 | ||
|
|
0ce065e496 | ||
|
|
0b0958c20e | ||
|
|
06acd7fcde | ||
|
|
b1e00e6ff2 | ||
|
|
ea42dc52fd | ||
|
|
6586cb37a8 | ||
|
|
9b7ab14253 | ||
|
|
d6899af5e7 | ||
|
|
087cab177b | ||
|
|
5da558dfd9 | ||
|
|
953859831c | ||
|
|
a13a8771d1 | ||
|
|
5499c25528 | ||
|
|
1e469627b4 | ||
|
|
34ef25c4e4 | ||
|
|
d1aa446c89 | ||
|
|
e454acba41 | ||
|
|
75abb70bcd | ||
|
|
7ba98a212c | ||
|
|
6ae6ba5b3d | ||
|
|
439cdfbbff | ||
|
|
672fd4e4b0 | ||
|
|
0f1d379e58 | ||
|
|
a79c9b4449 | ||
|
|
0f8a29a493 | ||
|
|
a54d7c8f45 | ||
|
|
84109e416a | ||
|
|
083337de1c | ||
|
|
bd6bc135fd | ||
|
|
4611381a38 | ||
|
|
d6f4096cd0 | ||
|
|
a715da3d18 | ||
|
|
94ceefd960 | ||
|
|
27dd120e5d | ||
|
|
f5cee97fef | ||
|
|
a9aac0dbb0 | ||
|
|
4c6243176c | ||
|
|
a8047660a6 | ||
|
|
7c2843f7fd | ||
|
|
fd5154ae93 | ||
|
|
726628e20c | ||
|
|
585a82b51a | ||
|
|
5edccb9c35 | ||
|
|
555da878f4 | ||
|
|
df8658eff9 | ||
|
|
4c55807392 | ||
|
|
cb50d3a70d | ||
|
|
eaf14aa1eb | ||
|
|
a59e0da2af | ||
|
|
3a3ac6da4e | ||
|
|
d7809616a4 | ||
|
|
5b486b1480 | ||
|
|
5fc30bd33e | ||
|
|
522a5c6e01 | ||
|
|
1ae60ec9bc | ||
|
|
b8c718ce9e | ||
|
|
b407d0aee0 | ||
|
|
289f9d7946 | ||
|
|
905b78008d | ||
|
|
11961bb7c7 | ||
|
|
2be1fac585 | ||
|
|
b35340caa9 | ||
|
|
e6b3e2ec23 | ||
|
|
c07232698c | ||
|
|
58e992af13 | ||
|
|
a44b84ffd0 | ||
|
|
a3640cf812 | ||
|
|
03a75273e3 | ||
|
|
6176e50acf | ||
|
|
46a3466bc5 | ||
|
|
aba9db6a6b | ||
|
|
e5b34624ac | ||
|
|
c430cf0d88 | ||
|
|
1969e292f0 | ||
|
|
0db119d7ba | ||
|
|
c9b498fb08 | ||
|
|
78004fa4ca | ||
|
|
4de7737d14 | ||
|
|
f36df02f5d | ||
|
|
753872ea2a | ||
|
|
ca1c24050d | ||
|
|
61c2b1a007 | ||
|
|
8cac0872a4 | ||
|
|
70f1c5f8ec | ||
|
|
b416e5f9e8 | ||
|
|
bfe6835cab | ||
|
|
9e89964df2 | ||
|
|
04c3d0c1d3 | ||
|
|
c9996df11c | ||
|
|
49971ada07 | ||
|
|
e6b9d4e2aa | ||
|
|
93a374d0c6 | ||
|
|
0fc7c78e11 | ||
|
|
96b5edf427 | ||
|
|
a5a6a0b611 | ||
|
|
2a27a14a68 | ||
|
|
f2d492b5dc | ||
|
|
5979e5aad2 | ||
|
|
baa9b5f7ab | ||
|
|
481497e384 | ||
|
|
0207778373 | ||
|
|
d79f32efd7 | ||
|
|
3ab03dd62f | ||
|
|
bc3cb0c230 | ||
|
|
473cb11053 | ||
|
|
0a87fd00f3 | ||
|
|
9b1f15def8 | ||
|
|
77b430675d | ||
|
|
f660058c75 | ||
|
|
9ecff86bbe | ||
|
|
5ab3a747a6 | ||
|
|
877c1a1559 | ||
|
|
2f9bf86d75 | ||
|
|
112153fb96 | ||
|
|
69ac1f4779 | ||
|
|
a20899ff43 | ||
|
|
ef2be1c086 | ||
|
|
af33dce0f6 | ||
|
|
b3b22795f8 | ||
|
|
8a0454db51 | ||
|
|
f1a5d87ab2 | ||
|
|
cf0a2161af | ||
|
|
dcebd863cc | ||
|
|
e8477b14f3 | ||
|
|
0230071b5f | ||
|
|
1d88263c85 | ||
|
|
a71ac4c44d | ||
|
|
66fc27e58c | ||
|
|
bc365f4a8d | ||
|
|
a5891f9884 | ||
|
|
fcdf16a937 | ||
|
|
e0b6dbbf2a | ||
|
|
9529e78647 | ||
|
|
51da3c0668 | ||
|
|
c00d3d33dd | ||
|
|
cfa9b8aea6 | ||
|
|
6106e9ff1a | ||
|
|
b1d9f65a0d | ||
|
|
f4008100e1 | ||
|
|
11a6959a24 | ||
|
|
3c6e11832b | ||
|
|
c064673ce1 | ||
|
|
cc5764e536 | ||
|
|
9131f2d09e | ||
|
|
0b6fc0b973 | ||
|
|
c91fe2d775 | ||
|
|
bbded57ae4 | ||
|
|
a8ae0bb4e0 | ||
|
|
49d12d99ff | ||
|
|
767b09d2f1 | ||
|
|
88397931c5 | ||
|
|
5ddab1ded7 | ||
|
|
f0d9894a16 | ||
|
|
59e4c57ecd | ||
|
|
dd76f9180c | ||
|
|
6e34a27b7e | ||
|
|
a090dfe99c | ||
|
|
74bfdc4c56 | ||
|
|
20f7712129 | ||
|
|
9863dc2f90 | ||
|
|
13734a642c | ||
|
|
7ac7ae9063 | ||
|
|
437caeb348 | ||
|
|
3b04d8df26 | ||
|
|
99d65531c4 | ||
|
|
4f4ccb8c66 | ||
|
|
7bc83eba1d | ||
|
|
72750f0be3 | ||
|
|
8cbf7f419c | ||
|
|
ea2aeec69b | ||
|
|
b83702fde3 | ||
|
|
5be3e9de2d | ||
|
|
e8bc7d7179 | ||
|
|
acbb164c3c | ||
|
|
99099b06aa | ||
|
|
5c958bc6c7 | ||
|
|
11b75bd610 | ||
|
|
61262c23ac | ||
|
|
7503739a9f | ||
|
|
060ab5bccb | ||
|
|
1c42b8cefc | ||
|
|
825f0b0f2a | ||
|
|
846c22cb03 | ||
|
|
fc0f67493b | ||
|
|
54a67fc67c | ||
|
|
7f8b227c39 | ||
|
|
ba177be41d | ||
|
|
0eb2d25570 | ||
|
|
e9db1c0482 | ||
|
|
79b075c961 | ||
|
|
a46f36acd1 | ||
|
|
bfb90a8b4f | ||
|
|
658c19f55b | ||
|
|
3f8a07654d | ||
|
|
a8ec7c2640 | ||
|
|
a7a1f95ced | ||
|
|
835e9913ae | ||
|
|
d3d6ba8176 | ||
|
|
0f82e91380 | ||
|
|
7d5381bbde | ||
|
|
302fb7b6af | ||
|
|
6233e1fa98 | ||
|
|
2cb9889fe4 | ||
|
|
bed6e0b741 | ||
|
|
302f0109dd | ||
|
|
735d3733e2 | ||
|
|
4b36852f57 | ||
|
|
b84b6c921d | ||
|
|
289f07f187 | ||
|
|
b9c777a5c3 | ||
|
|
92af4bef26 | ||
|
|
167f059163 | ||
|
|
93515fc906 | ||
|
|
20c2e1c67e | ||
|
|
65152731f9 | ||
|
|
57b9eb95bb | ||
|
|
64dc124a53 | ||
|
|
38d372e2d5 | ||
|
|
5897f379a4 | ||
|
|
d790266cc8 | ||
|
|
4da5d573e4 | ||
|
|
4e6f9ae75d | ||
|
|
e10f0f1683 | ||
|
|
40a73af82b | ||
|
|
461ca06445 | ||
|
|
773675e3c5 | ||
|
|
4c536ec8fc | ||
|
|
e6cbefb880 | ||
|
|
0592199858 | ||
|
|
2a3873a923 | ||
|
|
05c0505228 | ||
|
|
8c4e66f536 |
65
.eslintrc.js
Normal file
65
.eslintrc.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
|
||||||
|
sourceType: 'module', // Allows for the use of imports
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true, // Allows for the parsing of JSX
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
'node_modules/*',
|
||||||
|
'**/node_modules/*',
|
||||||
|
'.node_modules/*',
|
||||||
|
'.github/*',
|
||||||
|
'.browser_modules/*',
|
||||||
|
'docs/*',
|
||||||
|
'scripts/*',
|
||||||
|
'electron/*',
|
||||||
|
'electron-app/*',
|
||||||
|
'browser-app/*',
|
||||||
|
'plugins/*',
|
||||||
|
'arduino-ide-extension/src/node/cli-protocol',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||||
|
'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react
|
||||||
|
'plugin:react-hooks/recommended', // Uses recommended rules from react hooks
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
|
||||||
|
],
|
||||||
|
plugins: ['prettier', 'unused-imports'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-unused-expressions': 'off',
|
||||||
|
'@typescript-eslint/no-namespace': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'warn',
|
||||||
|
'@typescript-eslint/no-empty-interface': 'warn',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'unused-imports/no-unused-imports': 'error',
|
||||||
|
'unused-imports/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
vars: 'all',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
args: 'after-used',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'react/display-name': 'warn',
|
||||||
|
eqeqeq: ['error', 'smart'],
|
||||||
|
'guard-for-in': 'off',
|
||||||
|
'id-blacklist': 'off',
|
||||||
|
'id-match': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'no-unused-expressions': 'off',
|
||||||
|
'no-var': 'error',
|
||||||
|
radix: 'error',
|
||||||
|
'prettier/prettier': 'warn',
|
||||||
|
},
|
||||||
|
};
|
||||||
74
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
74
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Report a problem with the code or documentation in this repository.
|
||||||
|
labels:
|
||||||
|
- "type: imperfection"
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the problem
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce
|
||||||
|
attributes:
|
||||||
|
label: To reproduce
|
||||||
|
description: Provide the specific set of steps we can follow to reproduce the problem.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: What would you expect to happen after following those instructions?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: project-version
|
||||||
|
attributes:
|
||||||
|
label: Arduino IDE version
|
||||||
|
description: |
|
||||||
|
Which version of the Arduino IDE are you using?
|
||||||
|
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
|
||||||
|
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: Which operating system(s) are you using on your computer?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- N/A
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: os-version
|
||||||
|
attributes:
|
||||||
|
label: Operating system version
|
||||||
|
description: Which version of the operating system are you using on your computer?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any additional information here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Issue checklist
|
||||||
|
description: Please double-check that you have done each of the following things before submitting the issue.
|
||||||
|
options:
|
||||||
|
- label: I searched for previous reports in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
|
||||||
|
required: true
|
||||||
|
- label: I verified the problem still occurs when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
|
||||||
|
required: true
|
||||||
|
- label: My report contains all necessary details
|
||||||
|
required: true
|
||||||
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: 'type: bug'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
- OS: [e.g. Windows]
|
|
||||||
- Version: [e.g. 2.0.0]
|
|
||||||
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
13
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
13
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Source:
|
||||||
|
# https://github.com/arduino/tooling-project-assets/blob/main/issue-templates/template-choosers/general/config.yml
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Learn about using this project
|
||||||
|
url: https://github.com/arduino/arduino-ide#readme
|
||||||
|
about: Detailed usage documentation is available here.
|
||||||
|
- name: Support request
|
||||||
|
url: https://forum.arduino.cc/
|
||||||
|
about: We can help you out on the Arduino Forum!
|
||||||
|
- name: Discuss development work on the project
|
||||||
|
url: https://groups.google.com/a/arduino.cc/g/developers
|
||||||
|
about: Arduino Developers Mailing List
|
||||||
69
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
69
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Suggest an enhancement to this project.
|
||||||
|
labels:
|
||||||
|
- "type: enhancement"
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Describe the request
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: current
|
||||||
|
attributes:
|
||||||
|
label: Describe the current behavior
|
||||||
|
description: |
|
||||||
|
What is the current behavior of the Arduino IDE in relation to your request?
|
||||||
|
How can we reproduce that behavior?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: project-version
|
||||||
|
attributes:
|
||||||
|
label: Arduino IDE version
|
||||||
|
description: |
|
||||||
|
Which version of the Arduino IDE are you using?
|
||||||
|
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
|
||||||
|
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: Which operating system(s) are you using on your computer?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- N/A
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: os-version
|
||||||
|
attributes:
|
||||||
|
label: Operating system version
|
||||||
|
description: Which version of the operating system are you using on your computer?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: additional
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any additional information here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Issue checklist
|
||||||
|
description: Please double-check that you have done each of the following things before submitting the issue.
|
||||||
|
options:
|
||||||
|
- label: I searched for previous requests in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
|
||||||
|
required: true
|
||||||
|
- label: I verified the feature was still missing when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
|
||||||
|
required: true
|
||||||
|
- label: My request contains all necessary details
|
||||||
|
required: true
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: 'type: enhancement'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
### Motivation
|
||||||
|
<!-- Why this pull request? -->
|
||||||
|
|
||||||
|
### Change description
|
||||||
|
<!-- What does your code do? -->
|
||||||
|
|
||||||
|
### Other information
|
||||||
|
<!-- Any additional information that could help the review process -->
|
||||||
|
|
||||||
|
### Reviewer checklist
|
||||||
|
|
||||||
|
* [ ] PR addresses a single concern.
|
||||||
|
* [ ] The PR has no duplicates (please search among the [Pull Requests](https://github.com/arduino/arduino-ide/pulls) before creating one)
|
||||||
|
* [ ] PR title and description are properly filled.
|
||||||
|
* [ ] Docs have been added / updated (for bug fixes / features)
|
||||||
24
.github/label-configuration-files/labels.yml
vendored
Normal file
24
.github/label-configuration-files/labels.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Used by the "Sync Labels" workflow
|
||||||
|
# See: https://github.com/Financial-Times/github-label-sync#label-config-file
|
||||||
|
|
||||||
|
- name: "topic: accessibility"
|
||||||
|
color: "00ffff"
|
||||||
|
description: Enabling the use of the software by everyone
|
||||||
|
- name: "topic: CLI"
|
||||||
|
color: "00ffff"
|
||||||
|
description: Related to Arduino CLI
|
||||||
|
- name: "topic: debugger"
|
||||||
|
color: "00ffff"
|
||||||
|
description: Related to the integrated debugger
|
||||||
|
- name: "topic: language server"
|
||||||
|
color: "00ffff"
|
||||||
|
description: Related to the Arduino Language Server
|
||||||
|
- name: "topic: serial monitor"
|
||||||
|
color: "00ffff"
|
||||||
|
description: Related to the Serial Monitor
|
||||||
|
- name: "topic: theia"
|
||||||
|
color: "00ffff"
|
||||||
|
description: Related to the Theia IDE framework
|
||||||
|
- name: "topic: theme"
|
||||||
|
color: "00ffff"
|
||||||
|
description: Related to GUI theming
|
||||||
131
.github/tools/fetch_athena_stats.py
vendored
Normal file
131
.github/tools/fetch_athena_stats.py
vendored
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import boto3
|
||||||
|
import semver
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
||||||
|
log = logging.getLogger()
|
||||||
|
logging.getLogger("boto3").setLevel(logging.CRITICAL)
|
||||||
|
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
||||||
|
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
|
def execute(client, statement, dest_s3_output_location):
|
||||||
|
log.info("execute query: {} dumping in {}".format(statement, dest_s3_output_location))
|
||||||
|
result = client.start_query_execution(
|
||||||
|
QueryString=statement,
|
||||||
|
ClientRequestToken=str(uuid.uuid4()),
|
||||||
|
ResultConfiguration={
|
||||||
|
"OutputLocation": dest_s3_output_location,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
execution_id = result["QueryExecutionId"]
|
||||||
|
log.info("wait for query {} completion".format(execution_id))
|
||||||
|
wait_for_query_execution_completion(client, execution_id)
|
||||||
|
log.info("operation successful")
|
||||||
|
return execution_id
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_query_execution_completion(client, query_execution_id):
|
||||||
|
query_ended = False
|
||||||
|
while not query_ended:
|
||||||
|
query_execution = client.get_query_execution(QueryExecutionId=query_execution_id)
|
||||||
|
state = query_execution["QueryExecution"]["Status"]["State"]
|
||||||
|
if state == "SUCCEEDED":
|
||||||
|
query_ended = True
|
||||||
|
elif state in ["FAILED", "CANCELLED"]:
|
||||||
|
raise BaseException(
|
||||||
|
"query failed or canceled: {}".format(query_execution["QueryExecution"]["Status"]["StateChangeReason"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def valid(key):
|
||||||
|
split = key.split("_")
|
||||||
|
if len(split) < 1:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
semver.parse(split[0])
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_results(client, execution_id):
|
||||||
|
results_paginator = client.get_paginator("get_query_results")
|
||||||
|
results_iter = results_paginator.paginate(QueryExecutionId=execution_id, PaginationConfig={"PageSize": 1000})
|
||||||
|
res = {}
|
||||||
|
for results_page in results_iter:
|
||||||
|
for row in results_page["ResultSet"]["Rows"][1:]:
|
||||||
|
# Loop through the JSON objects
|
||||||
|
key = row["Data"][0]["VarCharValue"]
|
||||||
|
if valid(key):
|
||||||
|
res[key] = row["Data"][1]["VarCharValue"]
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def convert_data(data):
|
||||||
|
result = []
|
||||||
|
for key, value in data.items():
|
||||||
|
# 0.18.0_macOS_64bit.tar.gz
|
||||||
|
split_key = key.split("_")
|
||||||
|
if len(split_key) != 3:
|
||||||
|
continue
|
||||||
|
(version, os_version, arch) = split_key
|
||||||
|
arch_split = arch.split(".")
|
||||||
|
if len(arch_split) < 1:
|
||||||
|
continue
|
||||||
|
arch = arch_split[0]
|
||||||
|
if len(arch) > 10:
|
||||||
|
# This can't be an architecture really.
|
||||||
|
# It's an ugly solution but works for now so deal with it.
|
||||||
|
continue
|
||||||
|
repo = os.environ["GITHUB_REPOSITORY"].split("/")[1]
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"type": "gauge",
|
||||||
|
"name": "arduino.downloads.total",
|
||||||
|
"value": value,
|
||||||
|
"host": os.environ["GITHUB_REPOSITORY"],
|
||||||
|
"tags": [
|
||||||
|
f"version:{version}",
|
||||||
|
f"os:{os_version}",
|
||||||
|
f"arch:{arch}",
|
||||||
|
"cdn:downloads.arduino.cc",
|
||||||
|
f"project:{repo}",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
DEST_S3_OUTPUT = os.environ["AWS_ATHENA_OUTPUT_LOCATION"]
|
||||||
|
AWS_ATHENA_SOURCE_TABLE = os.environ["AWS_ATHENA_SOURCE_TABLE"]
|
||||||
|
|
||||||
|
session = boto3.session.Session(region_name="us-east-1")
|
||||||
|
athena_client = session.client("athena")
|
||||||
|
|
||||||
|
# Load all partitions before querying downloads
|
||||||
|
execute(athena_client, f"MSCK REPAIR TABLE {AWS_ATHENA_SOURCE_TABLE};", DEST_S3_OUTPUT)
|
||||||
|
|
||||||
|
query = f"""SELECT replace(json_extract_scalar(url_decode(url_decode(querystring)),
|
||||||
|
'$.data.url'), 'https://downloads.arduino.cc/arduino-ide/arduino-ide_', '')
|
||||||
|
AS flavor, count(json_extract(url_decode(url_decode(querystring)),'$')) AS gauge
|
||||||
|
FROM {AWS_ATHENA_SOURCE_TABLE}
|
||||||
|
WHERE json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
|
||||||
|
LIKE 'https://downloads.arduino.cc/arduino-ide/arduino-ide_%'
|
||||||
|
AND json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
|
||||||
|
NOT LIKE '%latest%' -- exclude latest redirect
|
||||||
|
group by 1 ;"""
|
||||||
|
exec_id = execute(athena_client, query, DEST_S3_OUTPUT)
|
||||||
|
results = get_results(athena_client, exec_id)
|
||||||
|
result_json = convert_data(results)
|
||||||
|
|
||||||
|
print(f"::set-output name=result::{result_json}")
|
||||||
57
.github/workflows/arduino-stats.yaml
vendored
Normal file
57
.github/workflows/arduino-stats.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: arduino-stats
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# run every day at 07:00 AM, 03:00 PM and 11:00 PM
|
||||||
|
- cron: "0 7,15,23 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
repository_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push-stats:
|
||||||
|
# This workflow is only of value to the arduino/arduino-ide repository and
|
||||||
|
# would always fail in forks
|
||||||
|
if: github.repository == 'arduino/arduino-ide'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Fetch downloads count form Arduino CDN using AWS Athena
|
||||||
|
id: fetch
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.STATS_AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.STATS_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_ATHENA_SOURCE_TABLE: ${{ secrets.STATS_AWS_ATHENA_SOURCE_TABLE }}
|
||||||
|
AWS_ATHENA_OUTPUT_LOCATION: ${{ secrets.STATS_AWS_ATHENA_OUTPUT_LOCATION }}
|
||||||
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
pip install boto3 semver
|
||||||
|
python .github/tools/fetch_athena_stats.py
|
||||||
|
|
||||||
|
- name: Send metrics
|
||||||
|
uses: masci/datadog@v1
|
||||||
|
with:
|
||||||
|
api-key: ${{ secrets.DD_API_KEY }}
|
||||||
|
# Metrics input expects YAML but JSON will work just right.
|
||||||
|
metrics: ${{steps.fetch.outputs.result}}
|
||||||
|
|
||||||
|
- name: Report failure
|
||||||
|
if: failure()
|
||||||
|
uses: masci/datadog@v1
|
||||||
|
with:
|
||||||
|
api-key: ${{ secrets.DD_API_KEY }}
|
||||||
|
events: |
|
||||||
|
- title: "Arduino IDE stats failing"
|
||||||
|
text: "Stats collection failed"
|
||||||
|
alert_type: "error"
|
||||||
|
host: ${{ github.repository }}
|
||||||
|
tags:
|
||||||
|
- "project:arduino-ide"
|
||||||
|
- "cdn:downloads.arduino.cc"
|
||||||
|
- "workflow:${{ github.workflow }}"
|
||||||
101
.github/workflows/build.yml
vendored
101
.github/workflows/build.yml
vendored
@@ -4,28 +4,50 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
- '!.github/workflows/build.yml'
|
||||||
|
- '.vscode/**'
|
||||||
|
- 'docs/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- 'static/**'
|
||||||
|
- '*.md'
|
||||||
tags:
|
tags:
|
||||||
- '[0-9]+.[0-9]+.[0-9]+*'
|
- '[0-9]+.[0-9]+.[0-9]+*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
paths-ignore:
|
||||||
- main
|
- '.github/**'
|
||||||
|
- '!.github/workflows/build.yml'
|
||||||
|
- '.vscode/**'
|
||||||
|
- 'docs/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- 'static/**'
|
||||||
|
- '*.md'
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
|
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
|
||||||
|
|
||||||
env:
|
env:
|
||||||
JOB_TRANSFER_ARTIFACT: build-artifacts
|
JOB_TRANSFER_ARTIFACT: build-artifacts
|
||||||
|
CHANGELOG_ARTIFACTS: changelog
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
if: github.repository == 'arduino/arduino-ide'
|
name: build (${{ matrix.config.os }})
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
config:
|
config:
|
||||||
- os: windows-latest
|
- os: windows-2019
|
||||||
|
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX # Name of the secret that contains the certificate.
|
||||||
|
certificate-password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD # Name of the secret that contains the certificate password.
|
||||||
|
certificate-extension: pfx # File extension for the certificate.
|
||||||
- os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259
|
- os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
|
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
|
||||||
|
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
|
||||||
|
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12
|
||||||
|
certificate-password-secret: KEYCHAIN_PASSWORD
|
||||||
|
certificate-extension: p12
|
||||||
runs-on: ${{ matrix.config.os }}
|
runs-on: ${{ matrix.config.os }}
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
|
|
||||||
@@ -33,16 +55,16 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install Node.js 12.x
|
- name: Install Node.js 14.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '12.14.1'
|
node-version: '14.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- name: Install Python 2.7
|
- name: Install Python 3.x
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '2.7'
|
python-version: '3.x'
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -50,32 +72,26 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
AC_USERNAME: ${{ secrets.AC_USERNAME }}
|
AC_USERNAME: ${{ secrets.AC_USERNAME }}
|
||||||
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
|
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
|
||||||
|
AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
|
IS_NIGHTLY: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
|
||||||
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
|
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }}
|
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
|
||||||
run: |
|
run: |
|
||||||
# See: https://www.electron.build/code-signing
|
# See: https://www.electron.build/code-signing
|
||||||
if [ $IS_FORK = true ]; then
|
if [ $CAN_SIGN = false ]; then
|
||||||
echo "Skipping the app signing: building from a fork."
|
echo "Skipping the app signing: certificate not provided."
|
||||||
else
|
else
|
||||||
if [ "${{ runner.OS }}" = "macOS" ]; then
|
export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}"
|
||||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.p12"
|
echo "${{ secrets[matrix.config.certificate-secret] }}" | base64 --decode > "$CSC_LINK"
|
||||||
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
|
export CSC_KEY_PASSWORD="${{ secrets[matrix.config.certificate-password-secret] }}"
|
||||||
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
|
|
||||||
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK"
|
|
||||||
|
|
||||||
export CSC_KEY_PASSWORD="${{ secrets.KEYCHAIN_PASSWORD }}"
|
|
||||||
|
|
||||||
elif [ "${{ runner.OS }}" = "Windows" ]; then
|
|
||||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.pfx"
|
|
||||||
echo "${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PFX }}" | base64 --decode > "$CSC_LINK"
|
|
||||||
|
|
||||||
export CSC_KEY_PASSWORD="${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "${{ runner.OS }}" = "Windows" ]; then
|
||||||
|
npm config set msvs_version 2017 --global
|
||||||
|
fi
|
||||||
|
npx node-gyp install
|
||||||
yarn --cwd ./electron/packager/
|
yarn --cwd ./electron/packager/
|
||||||
yarn --cwd ./electron/packager/ package
|
yarn --cwd ./electron/packager/ package
|
||||||
|
|
||||||
@@ -94,15 +110,19 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
artifact:
|
artifact:
|
||||||
- path: "*Linux_64bit.zip"
|
- path: '*Linux_64bit.zip'
|
||||||
name: Linux_X86-64
|
name: Linux_X86-64_zip
|
||||||
- path: "*macOS_64bit.dmg"
|
- path: '*Linux_64bit.AppImage'
|
||||||
name: macOS
|
name: Linux_X86-64_app_image
|
||||||
- path: "*Windows_64bit.exe"
|
- path: '*macOS_64bit.dmg'
|
||||||
|
name: macOS_dmg
|
||||||
|
- path: '*macOS_64bit.zip'
|
||||||
|
name: macOS_zip
|
||||||
|
- path: '*Windows_64bit.exe'
|
||||||
name: Windows_X86-64_interactive_installer
|
name: Windows_X86-64_interactive_installer
|
||||||
- path: "*Windows_64bit.msi"
|
- path: '*Windows_64bit.msi'
|
||||||
name: Windows_X86-64_MSI
|
name: Windows_X86-64_MSI
|
||||||
- path: "*Windows_64bit.zip"
|
- path: '*Windows_64bit.zip'
|
||||||
name: Windows_X86-64_zip
|
name: Windows_X86-64_zip
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -174,16 +194,16 @@ jobs:
|
|||||||
- name: Publish Nightly [S3]
|
- name: Publish Nightly [S3]
|
||||||
uses: docker://plugins/s3
|
uses: docker://plugins/s3
|
||||||
env:
|
env:
|
||||||
PLUGIN_SOURCE: "${{ env.JOB_TRANSFER_ARTIFACT }}/*"
|
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
|
||||||
PLUGIN_STRIP_PREFIX: "${{ env.JOB_TRANSFER_ARTIFACT }}/"
|
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
|
||||||
PLUGIN_TARGET: "/arduino-ide/nightly"
|
PLUGIN_TARGET: '/arduino-ide/nightly'
|
||||||
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
|
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: changelog
|
needs: changelog
|
||||||
if: github.repository == 'arduino/arduino-ide' && startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download [GitHub Actions]
|
- name: Download [GitHub Actions]
|
||||||
@@ -208,11 +228,12 @@ jobs:
|
|||||||
body: ${{ needs.changelog.outputs.BODY }}
|
body: ${{ needs.changelog.outputs.BODY }}
|
||||||
|
|
||||||
- name: Publish Release [S3]
|
- name: Publish Release [S3]
|
||||||
|
if: github.repository == 'arduino/arduino-ide'
|
||||||
uses: docker://plugins/s3
|
uses: docker://plugins/s3
|
||||||
env:
|
env:
|
||||||
PLUGIN_SOURCE: "${{ env.JOB_TRANSFER_ARTIFACT }}/*"
|
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
|
||||||
PLUGIN_STRIP_PREFIX: "${{ env.JOB_TRANSFER_ARTIFACT }}/"
|
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
|
||||||
PLUGIN_TARGET: "/arduino-ide"
|
PLUGIN_TARGET: '/arduino-ide'
|
||||||
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
|
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
|||||||
45
.github/workflows/check-certificates.yml
vendored
45
.github/workflows/check-certificates.yml
vendored
@@ -1,35 +1,41 @@
|
|||||||
name: Check for issues with signing certificates
|
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/check-certificates.md
|
||||||
|
name: Check Certificates
|
||||||
|
|
||||||
|
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/check-certificates.ya?ml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/check-certificates.ya?ml'
|
||||||
schedule:
|
schedule:
|
||||||
# run every 10 hours
|
# Run every 10 hours.
|
||||||
- cron: "0 */10 * * *"
|
- cron: '0 */10 * * *'
|
||||||
# workflow_dispatch event allows the workflow to be triggered manually.
|
|
||||||
# This could be used to run an immediate check after updating certificate secrets.
|
|
||||||
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
repository_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Begin notifications when there are less than this many days remaining before expiration
|
# Begin notifications when there are less than this many days remaining before expiration.
|
||||||
EXPIRATION_WARNING_PERIOD: 30
|
EXPIRATION_WARNING_PERIOD: 30
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-certificates:
|
check-certificates:
|
||||||
|
name: ${{ matrix.certificate.identifier }}
|
||||||
# Only run when the workflow will have access to the certificate secrets.
|
# Only run when the workflow will have access to the certificate secrets.
|
||||||
if: >
|
if: >
|
||||||
(github.event_name != 'pull_request' && github.repository == 'arduino/arduino-ide') ||
|
(github.event_name != 'pull_request' && github.repository == 'arduino/arduino-ide') ||
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'arduino/arduino-ide')
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'arduino/arduino-ide')
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
certificate:
|
certificate:
|
||||||
- identifier: macOS signing certificate # Text used to identify the certificate in notifications
|
# Additional certificate definitions can be added to this list.
|
||||||
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # The name of the secret that contains the certificate
|
- identifier: macOS signing certificate # Text used to identify certificate in notifications.
|
||||||
password-secret: KEYCHAIN_PASSWORD # The name of the secret that contains the certificate password
|
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # Name of the secret that contains the certificate.
|
||||||
|
password-secret: KEYCHAIN_PASSWORD # Name of the secret that contains the certificate password.
|
||||||
- identifier: Windows signing certificate
|
- identifier: Windows signing certificate
|
||||||
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
|
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
|
||||||
password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
|
password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
|
||||||
@@ -37,7 +43,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Set certificate path environment variable
|
- name: Set certificate path environment variable
|
||||||
run: |
|
run: |
|
||||||
# See: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
|
# See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
|
||||||
echo "CERTIFICATE_PATH=${{ runner.temp }}/certificate.p12" >> "$GITHUB_ENV"
|
echo "CERTIFICATE_PATH=${{ runner.temp }}/certificate.p12" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Decode certificate
|
- name: Decode certificate
|
||||||
@@ -59,18 +65,17 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
)
|
)
|
||||||
|
|
||||||
# See: https://github.com/rtCamp/action-slack-notify
|
|
||||||
- name: Slack notification of certificate verification failure
|
- name: Slack notification of certificate verification failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: rtCamp/action-slack-notify@v2.1.0
|
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
|
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||||
SLACK_MESSAGE: |
|
SLACK_MESSAGE: |
|
||||||
:warning::warning::warning::warning:
|
:warning::warning::warning::warning:
|
||||||
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!!
|
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!!
|
||||||
:warning::warning::warning::warning:
|
:warning::warning::warning::warning:
|
||||||
SLACK_COLOR: danger
|
SLACK_COLOR: danger
|
||||||
MSG_MINIMAL: true
|
MSG_MINIMAL: true
|
||||||
|
uses: rtCamp/action-slack-notify@v2
|
||||||
|
|
||||||
- name: Get days remaining before certificate expiration date
|
- name: Get days remaining before certificate expiration date
|
||||||
env:
|
env:
|
||||||
@@ -99,7 +104,7 @@ jobs:
|
|||||||
|
|
||||||
DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"
|
DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))"
|
||||||
|
|
||||||
# Display the expiration information in the log
|
# Display the expiration information in the log.
|
||||||
echo "Certificate expiration date: $EXPIRATION_DATE"
|
echo "Certificate expiration date: $EXPIRATION_DATE"
|
||||||
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
|
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
|
||||||
|
|
||||||
@@ -114,14 +119,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Slack notification of pending certificate expiration
|
- name: Slack notification of pending certificate expiration
|
||||||
# Don't send spurious expiration notification if verification fails
|
# Don't send spurious expiration notification if verification fails.
|
||||||
if: failure() && steps.check-expiration.outcome == 'failure'
|
if: failure() && steps.check-expiration.outcome == 'failure'
|
||||||
uses: rtCamp/action-slack-notify@v2.1.0
|
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
|
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||||
SLACK_MESSAGE: |
|
SLACK_MESSAGE: |
|
||||||
:warning::warning::warning::warning:
|
:warning::warning::warning::warning:
|
||||||
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!
|
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!
|
||||||
:warning::warning::warning::warning:
|
:warning::warning::warning::warning:
|
||||||
SLACK_COLOR: danger
|
SLACK_COLOR: danger
|
||||||
MSG_MINIMAL: true
|
MSG_MINIMAL: true
|
||||||
|
uses: rtCamp/action-slack-notify@v2
|
||||||
|
|||||||
38
.github/workflows/check-i18n-task.yml
vendored
Normal file
38
.github/workflows/check-i18n-task.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: Check Internationalization
|
||||||
|
|
||||||
|
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/check-i18n-task.ya?ml'
|
||||||
|
- '**/package.json'
|
||||||
|
- '**.ts'
|
||||||
|
- 'i18n/**'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/check-i18n-task.ya?ml'
|
||||||
|
- '**/package.json'
|
||||||
|
- '**.ts'
|
||||||
|
- 'i18n/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
repository_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Node.js 14.x
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '14.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
|
||||||
|
- name: Check for errors
|
||||||
|
run: yarn i18n:check
|
||||||
55
.github/workflows/compose-full-changelog.yaml
vendored
Normal file
55
.github/workflows/compose-full-changelog.yaml
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: Compose full changelog
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- edited
|
||||||
|
|
||||||
|
env:
|
||||||
|
CHANGELOG_ARTIFACTS: changelog
|
||||||
|
# See: https://github.com/actions/setup-node/#readme
|
||||||
|
NODE_VERSION: 14.x
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-changelog:
|
||||||
|
if: github.repository == 'arduino/arduino-ide'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Get Tag
|
||||||
|
id: tag_name
|
||||||
|
run: |
|
||||||
|
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
|
||||||
|
|
||||||
|
- name: Create full changelog
|
||||||
|
id: full-changelog
|
||||||
|
run: |
|
||||||
|
yarn add @octokit/rest --ignore-workspace-root-check
|
||||||
|
mkdir "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}"
|
||||||
|
|
||||||
|
# Get the changelog file name to build
|
||||||
|
CHANGELOG_FILE_NAME="${{ steps.tag_name.outputs.TAG_NAME }}-$(date +%s).md"
|
||||||
|
|
||||||
|
# Create manifest file pointing to latest changelog file name
|
||||||
|
echo "$CHANGELOG_FILE_NAME" >> "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/latest.txt"
|
||||||
|
|
||||||
|
# Compose changelog
|
||||||
|
yarn run compose-changelog "${{ github.workspace }}/${{ env.CHANGELOG_ARTIFACTS }}/$CHANGELOG_FILE_NAME"
|
||||||
|
|
||||||
|
- name: Publish Changelog [S3]
|
||||||
|
uses: docker://plugins/s3
|
||||||
|
env:
|
||||||
|
PLUGIN_SOURCE: '${{ env.CHANGELOG_ARTIFACTS }}/*'
|
||||||
|
PLUGIN_STRIP_PREFIX: '${{ env.CHANGELOG_ARTIFACTS }}/'
|
||||||
|
PLUGIN_TARGET: '/arduino-ide/changelog'
|
||||||
|
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
96
.github/workflows/github-stats.yaml
vendored
Normal file
96
.github/workflows/github-stats.yaml
vendored
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
name: github-stats
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# run every 30 minutes
|
||||||
|
- cron: "*/30 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
repository_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push-stats:
|
||||||
|
# This workflow is only of value to the arduino/arduino-ide repository and
|
||||||
|
# would always fail in forks
|
||||||
|
if: github.repository == 'arduino/arduino-ide'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Fetch downloads count
|
||||||
|
id: fetch
|
||||||
|
uses: actions/github-script@v4
|
||||||
|
with:
|
||||||
|
github-token: ${{github.token}}
|
||||||
|
script: |
|
||||||
|
let metrics = []
|
||||||
|
|
||||||
|
// Get a list of releases
|
||||||
|
const opts = github.repos.listReleases.endpoint.merge({
|
||||||
|
...context.repo
|
||||||
|
})
|
||||||
|
const releases = await github.paginate(opts)
|
||||||
|
|
||||||
|
// Get download stats for every release
|
||||||
|
for (const rel of releases) {
|
||||||
|
// Names for assets are like `arduino-ide_2.0.0-beta.12_Linux_64bit.zip`,
|
||||||
|
// we'll use this later to split the asset file name more easily
|
||||||
|
const baseName = `arduino-ide_${rel.name}_`
|
||||||
|
|
||||||
|
// Get a list of assets for this release
|
||||||
|
const opts = github.repos.listReleaseAssets.endpoint.merge({
|
||||||
|
...context.repo,
|
||||||
|
release_id: rel.id
|
||||||
|
})
|
||||||
|
const assets = await github.paginate(opts)
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
// Ignore files that are not arduino-ide packages
|
||||||
|
if (!asset.name.startsWith(baseName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the base and remove file extension to get `Linux_32bit`
|
||||||
|
systemArch = asset.name.replace(baseName, "").split(".")[0].split("_")
|
||||||
|
|
||||||
|
// Add a metric object to the list of gathered metrics
|
||||||
|
metrics.push({
|
||||||
|
"type": "gauge",
|
||||||
|
"name": "arduino.downloads.total",
|
||||||
|
"value": asset.download_count,
|
||||||
|
"host": "${{ github.repository }}",
|
||||||
|
"tags": [
|
||||||
|
`version:${rel.name}`,
|
||||||
|
`os:${systemArch[0]}`,
|
||||||
|
`arch:${systemArch[1]}`,
|
||||||
|
"cdn:github.com",
|
||||||
|
"project:arduino-ide"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The action will put whatever we return from this function in
|
||||||
|
// `outputs.result`, JSON encoded. So we just return the array
|
||||||
|
// of objects and GitHub will do the rest.
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
- name: Send metrics
|
||||||
|
uses: masci/datadog@v1
|
||||||
|
with:
|
||||||
|
api-key: ${{ secrets.DD_API_KEY }}
|
||||||
|
# Metrics input expects YAML but JSON will work just right.
|
||||||
|
metrics: ${{steps.fetch.outputs.result}}
|
||||||
|
|
||||||
|
- name: Report failure
|
||||||
|
if: failure()
|
||||||
|
uses: masci/datadog@v1
|
||||||
|
with:
|
||||||
|
api-key: ${{ secrets.DD_API_KEY }}
|
||||||
|
events: |
|
||||||
|
- title: "Arduino IDE stats failing"
|
||||||
|
text: "Stats collection failed"
|
||||||
|
alert_type: "error"
|
||||||
|
host: ${{ github.repository }}
|
||||||
|
tags:
|
||||||
|
- "project:arduino-ide"
|
||||||
|
- "cdn:github.com"
|
||||||
|
- "workflow:${{ github.workflow }}"
|
||||||
30
.github/workflows/i18n-nightly-push.yml
vendored
Normal file
30
.github/workflows/i18n-nightly-push.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: i18n-nightly-push
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# run every day at 1AM
|
||||||
|
- cron: '0 1 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push-to-transifex:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Node.js 14.x
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '14.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
|
||||||
|
- name: Run i18n:push script
|
||||||
|
run: yarn run i18n:push
|
||||||
|
env:
|
||||||
|
TRANSIFEX_ORGANIZATION: ${{ secrets.TRANSIFEX_ORGANIZATION }}
|
||||||
|
TRANSIFEX_PROJECT: ${{ secrets.TRANSIFEX_PROJECT }}
|
||||||
|
TRANSIFEX_RESOURCE: ${{ secrets.TRANSIFEX_RESOURCE }}
|
||||||
|
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
|
||||||
38
.github/workflows/i18n-weekly-pull.yml
vendored
Normal file
38
.github/workflows/i18n-weekly-pull.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
name: i18n-weekly-pull
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# run every monday at 2AM
|
||||||
|
- cron: '0 2 * * 1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pull-from-transifex:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Node.js 14.x
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '14.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
|
||||||
|
- name: Run i18n:pull script
|
||||||
|
run: yarn run i18n:pull
|
||||||
|
env:
|
||||||
|
TRANSIFEX_ORGANIZATION: ${{ secrets.TRANSIFEX_ORGANIZATION }}
|
||||||
|
TRANSIFEX_PROJECT: ${{ secrets.TRANSIFEX_PROJECT }}
|
||||||
|
TRANSIFEX_RESOURCE: ${{ secrets.TRANSIFEX_RESOURCE }}
|
||||||
|
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v3
|
||||||
|
with:
|
||||||
|
commit-message: Updated translation files
|
||||||
|
title: Update translation files
|
||||||
|
branch: i18n/translations-update
|
||||||
|
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||||
139
.github/workflows/sync-labels.yml
vendored
Normal file
139
.github/workflows/sync-labels.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/sync-labels.md
|
||||||
|
name: Sync Labels
|
||||||
|
|
||||||
|
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/sync-labels.ya?ml"
|
||||||
|
- ".github/label-configuration-files/*.ya?ml"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/sync-labels.ya?ml"
|
||||||
|
- ".github/label-configuration-files/*.ya?ml"
|
||||||
|
schedule:
|
||||||
|
# Run daily at 8 AM UTC to sync with changes to shared label configurations.
|
||||||
|
- cron: "0 8 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
repository_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CONFIGURATIONS_FOLDER: .github/label-configuration-files
|
||||||
|
CONFIGURATIONS_ARTIFACT: label-configuration-files
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Download JSON schema for labels configuration file
|
||||||
|
id: download-schema
|
||||||
|
uses: carlosperate/download-file-action@v1
|
||||||
|
with:
|
||||||
|
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/arduino-tooling-gh-label-configuration-schema.json
|
||||||
|
location: ${{ runner.temp }}/label-configuration-schema
|
||||||
|
|
||||||
|
- name: Install JSON schema validator
|
||||||
|
run: |
|
||||||
|
sudo npm install \
|
||||||
|
--global \
|
||||||
|
ajv-cli \
|
||||||
|
ajv-formats
|
||||||
|
|
||||||
|
- name: Validate local labels configuration
|
||||||
|
run: |
|
||||||
|
# See: https://github.com/ajv-validator/ajv-cli#readme
|
||||||
|
ajv validate \
|
||||||
|
--all-errors \
|
||||||
|
-c ajv-formats \
|
||||||
|
-s "${{ steps.download-schema.outputs.file-path }}" \
|
||||||
|
-d "${{ env.CONFIGURATIONS_FOLDER }}/*.{yml,yaml}"
|
||||||
|
|
||||||
|
download:
|
||||||
|
needs: check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
filename:
|
||||||
|
# Filenames of the shared configurations to apply to the repository in addition to the local configuration.
|
||||||
|
# https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/sync-labels
|
||||||
|
- universal.yml
|
||||||
|
- tooling.yml
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download
|
||||||
|
uses: carlosperate/download-file-action@v1
|
||||||
|
with:
|
||||||
|
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }}
|
||||||
|
|
||||||
|
- name: Pass configuration files to next job via workflow artifact
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
*.yaml
|
||||||
|
*.yml
|
||||||
|
if-no-files-found: error
|
||||||
|
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
|
||||||
|
|
||||||
|
sync:
|
||||||
|
needs: download
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set environment variables
|
||||||
|
run: |
|
||||||
|
# See: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable
|
||||||
|
echo "MERGED_CONFIGURATION_PATH=${{ runner.temp }}/labels.yml" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Determine whether to dry run
|
||||||
|
id: dry-run
|
||||||
|
if: >
|
||||||
|
github.event_name == 'pull_request' ||
|
||||||
|
(
|
||||||
|
(
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
) &&
|
||||||
|
github.ref != format('refs/heads/{0}', github.event.repository.default_branch)
|
||||||
|
)
|
||||||
|
run: |
|
||||||
|
# Use of this flag in the github-label-sync command will cause it to only check the validity of the
|
||||||
|
# configuration.
|
||||||
|
echo "::set-output name=flag::--dry-run"
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Download configuration files artifact
|
||||||
|
uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
|
||||||
|
path: ${{ env.CONFIGURATIONS_FOLDER }}
|
||||||
|
|
||||||
|
- name: Remove unneeded artifact
|
||||||
|
uses: geekyeggo/delete-artifact@v1
|
||||||
|
with:
|
||||||
|
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
|
||||||
|
|
||||||
|
- name: Merge label configuration files
|
||||||
|
run: |
|
||||||
|
# Merge all configuration files
|
||||||
|
shopt -s extglob
|
||||||
|
cat "${{ env.CONFIGURATIONS_FOLDER }}"/*.@(yml|yaml) > "${{ env.MERGED_CONFIGURATION_PATH }}"
|
||||||
|
|
||||||
|
- name: Install github-label-sync
|
||||||
|
run: sudo npm install --global github-label-sync
|
||||||
|
|
||||||
|
- name: Sync labels
|
||||||
|
env:
|
||||||
|
GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
# See: https://github.com/Financial-Times/github-label-sync
|
||||||
|
github-label-sync \
|
||||||
|
--labels "${{ env.MERGED_CONFIGURATION_PATH }}" \
|
||||||
|
${{ steps.dry-run.outputs.flag }} \
|
||||||
|
${{ github.repository }}
|
||||||
49
.github/workflows/themes-weekly-pull.yml
vendored
Normal file
49
.github/workflows/themes-weekly-pull.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name: themes-weekly-pull
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# run every friday at 5AM
|
||||||
|
- cron: '0 5 * * 5'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 14.x
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pull-from-jsonbin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
|
||||||
|
- name: Run themes:pull script
|
||||||
|
run: yarn run themes:pull
|
||||||
|
env:
|
||||||
|
JSONBIN_MASTER_KEY: ${{ secrets.JSONBIN_MASTER_KEY }}
|
||||||
|
JSONBIN_ID: ${{ secrets.JSONBIN_ID }}
|
||||||
|
|
||||||
|
- name: Generate dark tokens
|
||||||
|
run: npx token-transformer scripts/themes/tokens/arduino-tokens.json scripts/themes/tokens/dark.json core,ide-default,ide-dark,theia core,ide-default,ide-dark
|
||||||
|
|
||||||
|
- name: Generate default tokens
|
||||||
|
run: npx token-transformer scripts/themes/tokens/arduino-tokens.json scripts/themes/tokens/default.json core,ide-default,theia core,ide-default
|
||||||
|
|
||||||
|
- name: Run themes:generate script
|
||||||
|
run: yarn run themes:generate
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v4
|
||||||
|
with:
|
||||||
|
commit-message: Updated themes
|
||||||
|
title: Update themes
|
||||||
|
branch: themes/themes-update
|
||||||
|
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -7,7 +7,8 @@ build/
|
|||||||
Examples/
|
Examples/
|
||||||
!electron/build/
|
!electron/build/
|
||||||
src-gen/
|
src-gen/
|
||||||
*webpack.config.js
|
!webpack.config.js
|
||||||
|
gen-webpack.config.js
|
||||||
.DS_Store
|
.DS_Store
|
||||||
# switching from `electron` to `browser` in dev mode.
|
# switching from `electron` to `browser` in dev mode.
|
||||||
.browser_modules
|
.browser_modules
|
||||||
@@ -16,3 +17,9 @@ yarn*.log
|
|||||||
plugins
|
plugins
|
||||||
# the config files for the CLI
|
# the config files for the CLI
|
||||||
arduino-ide-extension/data/cli/config
|
arduino-ide-extension/data/cli/config
|
||||||
|
# the tokens folder for the themes
|
||||||
|
scripts/themes/tokens
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
# content trace files for electron
|
||||||
|
electron-app/traces
|
||||||
|
|||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 80,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
99
.vscode/launch.json
vendored
99
.vscode/launch.json
vendored
@@ -1,36 +1,15 @@
|
|||||||
{
|
{
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "attach",
|
|
||||||
"name": "Attach by Process ID",
|
|
||||||
"processId": "${command:PickProcess}"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Electron Packager",
|
"name": "App (Electron) [Dev]",
|
||||||
"program": "${workspaceRoot}/electron/packager/index.js",
|
|
||||||
"cwd": "${workspaceFolder}/electron/packager"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "App (Electron)",
|
|
||||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||||
"windows": {
|
"windows": {
|
||||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "development",
|
|
||||||
"NODE_PRESERVE_SYMLINKS": "1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"cwd": "${workspaceFolder}/electron-app",
|
"cwd": "${workspaceFolder}/electron-app",
|
||||||
"protocol": "inspector",
|
|
||||||
"args": [
|
"args": [
|
||||||
".",
|
".",
|
||||||
"--log-level=debug",
|
"--log-level=debug",
|
||||||
@@ -39,7 +18,11 @@
|
|||||||
"--app-project-path=${workspaceRoot}/electron-app",
|
"--app-project-path=${workspaceRoot}/electron-app",
|
||||||
"--remote-debugging-port=9222",
|
"--remote-debugging-port=9222",
|
||||||
"--no-app-auto-install",
|
"--no-app-auto-install",
|
||||||
"--plugins=local-dir:../plugins"
|
"--plugins=local-dir:../plugins",
|
||||||
|
"--hosted-plugin-inspect=9339",
|
||||||
|
"--nosplash",
|
||||||
|
"--content-trace",
|
||||||
|
"--open-devtools"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"NODE_ENV": "development"
|
"NODE_ENV": "development"
|
||||||
@@ -49,12 +32,55 @@
|
|||||||
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
|
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
|
||||||
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
|
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
|
||||||
"${workspaceRoot}/electron-app/lib/**/*.js",
|
"${workspaceRoot}/electron-app/lib/**/*.js",
|
||||||
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
|
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
|
||||||
|
"${workspaceRoot}/node_modules/@theia/**/*.js"
|
||||||
],
|
],
|
||||||
"smartStep": true,
|
"smartStep": true,
|
||||||
"internalConsoleOptions": "openOnSessionStart",
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
"outputCapture": "std"
|
"outputCapture": "std"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "App (Electron)",
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||||
|
"windows": {
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
|
||||||
|
},
|
||||||
|
"cwd": "${workspaceFolder}/electron-app",
|
||||||
|
"args": [
|
||||||
|
".",
|
||||||
|
"--log-level=debug",
|
||||||
|
"--hostname=localhost",
|
||||||
|
"--no-cluster",
|
||||||
|
"--app-project-path=${workspaceRoot}/electron-app",
|
||||||
|
"--remote-debugging-port=9222",
|
||||||
|
"--no-app-auto-install",
|
||||||
|
"--plugins=local-dir:../plugins",
|
||||||
|
"--hosted-plugin-inspect=9339"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
},
|
||||||
|
"sourceMaps": true,
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
|
||||||
|
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
|
||||||
|
"${workspaceRoot}/electron-app/lib/**/*.js",
|
||||||
|
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js",
|
||||||
|
"${workspaceRoot}/node_modules/@theia/**/*.js"
|
||||||
|
],
|
||||||
|
"smartStep": true,
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"outputCapture": "std"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Attach to Electron Frontend",
|
||||||
|
"port": 9222,
|
||||||
|
"webRoot": "${workspaceFolder}/electron-app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
@@ -89,12 +115,13 @@
|
|||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"protocol": "inspector",
|
|
||||||
"name": "Run Test [current]",
|
"name": "Run Test [current]",
|
||||||
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
|
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
|
||||||
"args": [
|
"args": [
|
||||||
"--require",
|
"--require",
|
||||||
"reflect-metadata/Reflect",
|
"reflect-metadata/Reflect",
|
||||||
|
"--require",
|
||||||
|
"ignore-styles",
|
||||||
"--no-timeouts",
|
"--no-timeouts",
|
||||||
"--colors",
|
"--colors",
|
||||||
"**/${fileBasenameNoExtension}.js"
|
"**/${fileBasenameNoExtension}.js"
|
||||||
@@ -106,6 +133,28 @@
|
|||||||
"smartStep": true,
|
"smartStep": true,
|
||||||
"internalConsoleOptions": "openOnSessionStart",
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
"outputCapture": "std"
|
"outputCapture": "std"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Attach by Process ID",
|
||||||
|
"processId": "${command:PickProcess}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Electron Packager",
|
||||||
|
"program": "${workspaceRoot}/electron/packager/index.js",
|
||||||
|
"cwd": "${workspaceFolder}/electron/packager"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Launch Electron Backend & Frontend",
|
||||||
|
"configurations": [
|
||||||
|
"App (Electron)",
|
||||||
|
"Attach to Electron Frontend"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
18
.vscode/settings.json
vendored
18
.vscode/settings.json
vendored
@@ -1,21 +1,9 @@
|
|||||||
{
|
{
|
||||||
"tslint.enable": true,
|
|
||||||
"tslint.configFile": "./tslint.json",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/lib": false
|
"**/lib": false
|
||||||
},
|
},
|
||||||
"editor.insertSpaces": true,
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.detectIndentation": false,
|
"editor.codeActionsOnSave": {
|
||||||
"[typescript]": {
|
"source.fixAll.eslint": true
|
||||||
"editor.tabSize": 4
|
|
||||||
},
|
},
|
||||||
"[json]": {
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[jsonc]": {
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"files.insertFinalNewline": true,
|
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
|
||||||
}
|
}
|
||||||
|
|||||||
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@@ -1,6 +1,4 @@
|
|||||||
{
|
{
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
// for the documentation about the tasks.json format
|
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
@@ -35,7 +33,7 @@
|
|||||||
"panel": "new",
|
"panel": "new",
|
||||||
"clear": false
|
"clear": false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
{
|
{
|
||||||
"label": "Arduino IDE - Watch Browser App",
|
"label": "Arduino IDE - Watch Browser App",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
|||||||
27
BUILDING.md
27
BUILDING.md
@@ -14,7 +14,7 @@ The _Electron main_ process is responsible for:
|
|||||||
- managing the application lifecycle via listeners, and
|
- managing the application lifecycle via listeners, and
|
||||||
- creating and managing the web pages for the app.
|
- creating and managing the web pages for the app.
|
||||||
|
|
||||||
In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly on main process.
|
In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly one main process.
|
||||||
|
|
||||||
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>
|
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>
|
||||||
|
|
||||||
@@ -40,23 +40,39 @@ The _frontend_ is running as an Electron renderer process and can invoke service
|
|||||||
## Build from source
|
## Build from source
|
||||||
|
|
||||||
If you’re familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the
|
If you’re familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the
|
||||||
project, you should be able to build the Arduino IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
|
project, you should be able to build the Arduino IDE locally.
|
||||||
|
Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
|
||||||
|
> **Note**: Node.js 14 must be used instead of the version 12 recommended at the link above.
|
||||||
|
|
||||||
### Build
|
Once you have all the tools installed, you can build the editor following these steps
|
||||||
|
|
||||||
|
1. Install the dependencies and build
|
||||||
```sh
|
```sh
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rebuild the native dependencies
|
2. Rebuild the dependencies
|
||||||
|
```sh
|
||||||
|
yarn rebuild:browser
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Rebuild the electron dependencies
|
||||||
```sh
|
```sh
|
||||||
yarn rebuild:electron
|
yarn rebuild:electron
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start
|
4. Start the application
|
||||||
```sh
|
```sh
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Notes for Windows contributors
|
||||||
|
Windows requires the Microsoft Visual C++ (MSVC) compiler toolset to be installed on your development machine.
|
||||||
|
|
||||||
|
In case it's not already present, it can be downloaded from the "**Tools for Visual Studio 20XX**" section of the Visual Studio [downloads page](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) via the "**Build Tools for Visual Studio 20XX**" (e.g., "**Build Tools for Visual Studio 2022**") download link.
|
||||||
|
|
||||||
|
Select "**Desktop development with C++**" from the "**Workloads**" tab during the installation procedure.
|
||||||
|
|
||||||
### CI
|
### CI
|
||||||
|
|
||||||
This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions).
|
This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions).
|
||||||
@@ -73,6 +89,7 @@ This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide
|
|||||||
git push origin 1.2.3
|
git push origin 1.2.3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Notes for macOS contributors
|
## Notes for macOS contributors
|
||||||
Beginning in macOS 10.14.5, the software [must be notarized to run](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution). The signing and notarization processes for the Arduino IDE are managed by our Continuous Integration (CI) workflows, implemented with GitHub Actions. On every push and pull request, the Arduino IDE is built and saved to a workflow artifact. These artifacts can be used by contributors and beta testers who don't want to set up a build system locally.
|
Beginning in macOS 10.14.5, the software [must be notarized to run](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution). The signing and notarization processes for the Arduino IDE are managed by our Continuous Integration (CI) workflows, implemented with GitHub Actions. On every push and pull request, the Arduino IDE is built and saved to a workflow artifact. These artifacts can be used by contributors and beta testers who don't want to set up a build system locally.
|
||||||
For security reasons, signing and notarization are disabled for workflow runs for pull requests from forks of this repository. This means that macOS will block you from running those artifacts.
|
For security reasons, signing and notarization are disabled for workflow runs for pull requests from forks of this repository. This means that macOS will block you from running those artifacts.
|
||||||
|
|||||||
19
Dockerfile
19
Dockerfile
@@ -1,19 +0,0 @@
|
|||||||
FROM gitpod/workspace-full-vnc
|
|
||||||
|
|
||||||
USER root
|
|
||||||
RUN apt-get update -q --fix-missing && \
|
|
||||||
apt-get install -y -q software-properties-common && \
|
|
||||||
apt-get install -y -q --no-install-recommends \
|
|
||||||
build-essential \
|
|
||||||
libssl-dev \
|
|
||||||
golang-go \
|
|
||||||
libxkbfile-dev \
|
|
||||||
libnss3-dev
|
|
||||||
|
|
||||||
RUN set -ex && \
|
|
||||||
tmpdir=$(mktemp -d) && \
|
|
||||||
curl -L -o $tmpdir/protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip && \
|
|
||||||
mkdir -p /usr/lib/protoc && cd /usr/lib/protoc && unzip $tmpdir/protoc.zip && \
|
|
||||||
chmod -R 755 /usr/lib/protoc/include/google && \
|
|
||||||
ln -s /usr/lib/protoc/bin/* /usr/bin && \
|
|
||||||
rm $tmpdir/protoc.zip
|
|
||||||
43
README.md
43
README.md
@@ -15,29 +15,31 @@ The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is
|
|||||||
## Download
|
## Download
|
||||||
|
|
||||||
You can download the latest version from the [software download page on the Arduino website](https://www.arduino.cc/en/software#experimental-software).
|
You can download the latest version from the [software download page on the Arduino website](https://www.arduino.cc/en/software#experimental-software).
|
||||||
|
|
||||||
### Nightly builds
|
### Nightly builds
|
||||||
|
|
||||||
These builds are generated every day at 03:00 GMT from the `main` branch and
|
These builds are generated every day at 03:00 GMT from the `main` branch and
|
||||||
should be considered unstable:
|
should be considered unstable:
|
||||||
|
|
||||||
Platform | 32 bit | 64 bit |
|
| Platform | 32 bit | 64 bit |
|
||||||
--------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
| --------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||||
Linux | | [Nightly Linux 64 bit] |
|
| Linux | | [Nightly Linux AppImage 64 bit]<br />[Nightly Linux ZIP file 64 bit] |
|
||||||
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
|
| Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
|
||||||
Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
|
| Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
|
||||||
macOS | | [Nightly macOS 64 bit] |
|
| macOS | | [Nightly macOS 64 bit] |
|
||||||
|
|
||||||
[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/107
|
[🚧 work in progress...]: https://github.com/arduino/arduino-ide/issues/107
|
||||||
[Nightly Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
|
[nightly linux appimage 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.AppImage
|
||||||
[Nightly Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
|
[nightly linux zip file 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
|
||||||
[Nightly Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
|
[nightly windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
|
||||||
[Nightly Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
|
[nightly windows 64 bit msi]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
|
||||||
[Nightly macOS 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
|
[nightly windows 64 bit zip]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
|
||||||
|
[nightly macos 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
|
||||||
|
|
||||||
> These links return an HTTP `302: Found` response, redirecting to latest
|
> These links return an HTTP `302: Found` response, redirecting to latest
|
||||||
generated builds by replacing `latest` with the latest available build
|
> generated builds by replacing `latest` with the latest available build
|
||||||
date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
|
> date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
|
||||||
replaced with `20190806`)
|
> replaced with `20190806`)
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
@@ -47,8 +49,8 @@ If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-u
|
|||||||
|
|
||||||
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository. A few rules apply:
|
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository. A few rules apply:
|
||||||
|
|
||||||
* Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
|
- Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
|
||||||
* Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board.
|
- Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
@@ -64,10 +66,13 @@ Contributions are very welcome! You can browse the list of open issues to see wh
|
|||||||
|
|
||||||
This repository contains the main code, but two more repositories are included during the build process:
|
This repository contains the main code, but two more repositories are included during the build process:
|
||||||
|
|
||||||
* [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
|
- [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
|
||||||
* [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
|
- [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
|
||||||
|
|
||||||
See the [BUILDING.md](BUILDING.md) for a technical overview of the application and instructions for building the code.
|
See the [BUILDING.md](BUILDING.md) for a technical overview of the application and instructions for building the code.
|
||||||
|
|
||||||
|
You can help with the translation of the Arduino IDE to your language here: [Arduino IDE on Transifex](https://www.transifex.com/arduino-1/ide2/dashboard/).
|
||||||
|
|
||||||
## Donations
|
## Donations
|
||||||
|
|
||||||
This open source code was written by the Arduino team and is maintained on a daily basis with the help of the community. We invest a considerable amount of time in development, testing and optimization. Please consider [donating](https://www.arduino.cc/en/donate/) or [sponsoring](https://github.com/sponsors/arduino) to support our work, as well as [buying original Arduino boards](https://store.arduino.cc/) which is the best way to make sure our effort can continue in the long term.
|
This open source code was written by the Arduino team and is maintained on a daily basis with the help of the community. We invest a considerable amount of time in development, testing and optimization. Please consider [donating](https://www.arduino.cc/en/donate/) or [sponsoring](https://github.com/sponsors/arduino) to support our work, as well as [buying original Arduino boards](https://store.arduino.cc/) which is the best way to make sure our effort can continue in the long term.
|
||||||
|
|||||||
@@ -30,17 +30,20 @@ The Core Service is responsible for building your sketches and uploading them to
|
|||||||
- compiling a sketch for a selected board type
|
- compiling a sketch for a selected board type
|
||||||
- uploading a sketch to a connected board
|
- uploading a sketch to a connected board
|
||||||
|
|
||||||
#### Monitor Service
|
#### Serial Service
|
||||||
|
|
||||||
The Monitor Service allows getting information back from sketches running on your Arduino boards.
|
The Serial Service allows getting information back from sketches running on your Arduino boards.
|
||||||
|
|
||||||
- [src/common/protocol/monitor-service.ts](./src/common/protocol/monitor-service.ts) implements the common classes and interfaces
|
- [src/common/protocol/serial-service.ts](./src/common/protocol/serial-service.ts) implements the common classes and interfaces
|
||||||
- [src/node/monitor-service-impl.ts](./src/node/monitor-service-impl.ts) implements the service backend:
|
- [src/node/serial/serial-service-impl.ts](./src/node/serial/serial-service-impl.ts) implements the service backend:
|
||||||
- connecting to / disconnecting from a board
|
- connecting to / disconnecting from a board
|
||||||
- receiving and sending data
|
- receiving and sending data
|
||||||
- [src/browser/monitor/monitor-widget.tsx](./src/browser/monitor/monitor-widget.tsx) implements the serial monitor front-end:
|
- [src/browser/serial/serial-connection-manager.ts](./src/browser/serial/serial-connection-manager.ts) handles the serial connection in the frontend
|
||||||
|
- [src/browser/serial/monitor/monitor-widget.tsx](./src/browser/serial/monitor/monitor-widget.tsx) implements the serial monitor front-end:
|
||||||
- viewing the output from a connected board
|
- viewing the output from a connected board
|
||||||
- entering data to send to the board
|
- entering data to send to the board
|
||||||
|
- [src/browser/serial/plotter/plotter-frontend-contribution.ts](./src/browser/serial/plotter/plotter-frontend-contribution.ts) implements the serial plotter front-end:
|
||||||
|
- opening a new window running the [Serial Plotter Web App](https://github.com/arduino/arduino-serial-plotter-webapp)
|
||||||
|
|
||||||
#### Config Service
|
#### Config Service
|
||||||
|
|
||||||
@@ -58,3 +61,13 @@ The Config Service knows about your system, like for example the default sketch
|
|||||||
#### Rebuild gRPC protocol interfaces
|
#### Rebuild gRPC protocol interfaces
|
||||||
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
|
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
|
||||||
`yarn --cwd arduino-ide-extension generate-protocol`
|
`yarn --cwd arduino-ide-extension generate-protocol`
|
||||||
|
|
||||||
|
### Customize Icons
|
||||||
|
ArduinoIde uses a customized version of FontAwesome.
|
||||||
|
In order to update/replace icons follow the following steps:
|
||||||
|
- import the file `arduino-icons.json` in [Icomoon](https://icomoon.io/app/#/projects)
|
||||||
|
- load it
|
||||||
|
- edit the icons as needed
|
||||||
|
- !! download the **new** `arduino-icons.json` file and put it in this repo
|
||||||
|
- Click on "Generate Font" in Icomoon, then download
|
||||||
|
- place the updated fonts in the `src/style/fonts` directory
|
||||||
|
|||||||
1
arduino-ide-extension/arduino-icons.json
Normal file
1
arduino-ide-extension/arduino-icons.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,78 +1,108 @@
|
|||||||
{
|
{
|
||||||
"name": "arduino-ide-extension",
|
"name": "arduino-ide-extension",
|
||||||
"version": "2.0.0-beta.6",
|
"version": "2.0.0-rc9.1",
|
||||||
"description": "An extension for Theia building the Arduino IDE",
|
"description": "An extension for Theia building the Arduino IDE",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "yarn download-cli && yarn download-ls && yarn clean && yarn download-examples && yarn build",
|
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-i18n && yarn clean && yarn download-examples && yarn build && yarn test",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
|
"compose-changelog": "node ./scripts/compose-changelog.js",
|
||||||
"download-cli": "node ./scripts/download-cli.js",
|
"download-cli": "node ./scripts/download-cli.js",
|
||||||
|
"download-fwuploader": "node ./scripts/download-fwuploader.js",
|
||||||
|
"copy-i18n": "npx ncp ../i18n ./build/i18n",
|
||||||
"download-ls": "node ./scripts/download-ls.js",
|
"download-ls": "node ./scripts/download-ls.js",
|
||||||
"download-examples": "node ./scripts/download-examples.js",
|
"download-examples": "node ./scripts/download-examples.js",
|
||||||
"generate-protocol": "node ./scripts/generate-protocol.js",
|
"generate-protocol": "node ./scripts/generate-protocol.js",
|
||||||
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
|
"lint": "eslint",
|
||||||
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
|
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
|
||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
"test": "mocha \"./lib/test/**/*.test.js\"",
|
"test": "mocha \"./lib/test/**/*.test.js\"",
|
||||||
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
|
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.1.1",
|
"@grpc/grpc-js": "^1.6.7",
|
||||||
"@theia/application-package": "next",
|
"@theia/application-package": "1.25.0",
|
||||||
"@theia/core": "next",
|
"@theia/core": "1.25.0",
|
||||||
"@theia/editor": "next",
|
"@theia/editor": "1.25.0",
|
||||||
"@theia/filesystem": "next",
|
"@theia/electron": "1.25.0",
|
||||||
"@theia/git": "next",
|
"@theia/filesystem": "1.25.0",
|
||||||
"@theia/keymaps": "next",
|
"@theia/keymaps": "1.25.0",
|
||||||
"@theia/markers": "next",
|
"@theia/markers": "1.25.0",
|
||||||
"@theia/monaco": "next",
|
"@theia/monaco": "1.25.0",
|
||||||
"@theia/navigator": "next",
|
"@theia/navigator": "1.25.0",
|
||||||
"@theia/outline-view": "next",
|
"@theia/outline-view": "1.25.0",
|
||||||
"@theia/preferences": "next",
|
"@theia/output": "1.25.0",
|
||||||
"@theia/output": "next",
|
"@theia/preferences": "1.25.0",
|
||||||
"@theia/search-in-workspace": "next",
|
"@theia/search-in-workspace": "1.25.0",
|
||||||
"@theia/terminal": "next",
|
"@theia/terminal": "1.25.0",
|
||||||
"@theia/workspace": "next",
|
"@theia/workspace": "1.25.0",
|
||||||
|
"@tippyjs/react": "^4.2.5",
|
||||||
|
"@types/atob": "^2.1.2",
|
||||||
|
"@types/auth0-js": "^9.14.0",
|
||||||
|
"@types/btoa": "^1.2.3",
|
||||||
"@types/dateformat": "^3.0.1",
|
"@types/dateformat": "^3.0.1",
|
||||||
|
"@types/deep-equal": "^1.0.1",
|
||||||
"@types/deepmerge": "^2.2.0",
|
"@types/deepmerge": "^2.2.0",
|
||||||
"@types/glob": "^5.0.35",
|
"@types/glob": "^7.2.0",
|
||||||
"@types/google-protobuf": "^3.7.2",
|
"@types/google-protobuf": "^3.7.2",
|
||||||
"@types/js-yaml": "^3.12.2",
|
"@types/js-yaml": "^3.12.2",
|
||||||
|
"@types/keytar": "^4.4.0",
|
||||||
"@types/lodash.debounce": "^4.0.6",
|
"@types/lodash.debounce": "^4.0.6",
|
||||||
"@types/ncp": "^2.0.4",
|
"@types/ncp": "^2.0.4",
|
||||||
|
"@types/node-fetch": "^2.5.7",
|
||||||
"@types/ps-tree": "^1.1.0",
|
"@types/ps-tree": "^1.1.0",
|
||||||
"@types/react-select": "^3.0.0",
|
"@types/react-select": "^3.0.0",
|
||||||
"@types/react-tabs": "^2.3.2",
|
"@types/react-tabs": "^2.3.2",
|
||||||
"@types/sinon": "^7.5.2",
|
|
||||||
"@types/temp": "^0.8.34",
|
"@types/temp": "^0.8.34",
|
||||||
"@types/which": "^1.3.1",
|
"@types/which": "^1.3.1",
|
||||||
"ajv": "^6.5.3",
|
"ajv": "^6.5.3",
|
||||||
|
"arduino-serial-plotter-webapp": "0.1.0",
|
||||||
"async-mutex": "^0.3.0",
|
"async-mutex": "^0.3.0",
|
||||||
"css-element-queries": "^1.2.0",
|
"atob": "^2.1.2",
|
||||||
|
"auth0-js": "^9.14.0",
|
||||||
|
"btoa": "^1.2.1",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
"dateformat": "^3.0.3",
|
"dateformat": "^3.0.3",
|
||||||
"deepmerge": "^4.2.2",
|
"deep-equal": "^2.0.5",
|
||||||
"fuzzy": "^0.1.3",
|
"deepmerge": "2.0.1",
|
||||||
|
"electron-updater": "^4.6.5",
|
||||||
|
"fast-safe-stringify": "^2.1.1",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"google-protobuf": "^3.11.4",
|
"google-protobuf": "^3.20.1",
|
||||||
"lodash.debounce": "^4.0.8",
|
"hash.js": "^1.1.7",
|
||||||
|
"is-valid-path": "^0.1.1",
|
||||||
"js-yaml": "^3.13.1",
|
"js-yaml": "^3.13.1",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"keytar": "7.2.0",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"open": "^8.0.6",
|
||||||
"p-queue": "^5.0.0",
|
"p-queue": "^5.0.0",
|
||||||
"ps-tree": "^1.2.0",
|
"ps-tree": "^1.2.0",
|
||||||
|
"query-string": "^7.0.1",
|
||||||
"react-disable": "^0.1.0",
|
"react-disable": "^0.1.0",
|
||||||
|
"react-markdown": "^8.0.0",
|
||||||
"react-select": "^3.0.4",
|
"react-select": "^3.0.4",
|
||||||
"react-tabs": "^3.1.2",
|
"react-tabs": "^3.1.2",
|
||||||
|
"react-window": "^1.8.6",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"string-natural-compare": "^2.0.3",
|
"string-natural-compare": "^2.0.3",
|
||||||
"temp": "^0.9.1",
|
"temp": "^0.9.1",
|
||||||
|
"temp-dir": "^2.0.0",
|
||||||
"tree-kill": "^1.2.1",
|
"tree-kill": "^1.2.1",
|
||||||
"upath": "^1.1.2",
|
"upath": "^1.1.2",
|
||||||
|
"url": "^0.11.0",
|
||||||
"which": "^1.3.1"
|
"which": "^1.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@octokit/rest": "^18.12.0",
|
||||||
"@types/chai": "^4.2.7",
|
"@types/chai": "^4.2.7",
|
||||||
"@types/chai-string": "^1.4.2",
|
"@types/chai-string": "^1.4.2",
|
||||||
"@types/mocha": "^5.2.7",
|
"@types/mocha": "^5.2.7",
|
||||||
|
"@types/react-window": "^1.8.5",
|
||||||
|
"@types/sinon": "^10.0.6",
|
||||||
|
"@types/sinon-chai": "^3.2.6",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"chai-string": "^1.5.0",
|
"chai-string": "^1.5.0",
|
||||||
"decompress": "^4.2.0",
|
"decompress": "^4.2.0",
|
||||||
@@ -81,10 +111,13 @@
|
|||||||
"download": "^7.1.0",
|
"download": "^7.1.0",
|
||||||
"grpc_tools_node_protoc_ts": "^4.1.0",
|
"grpc_tools_node_protoc_ts": "^4.1.0",
|
||||||
"mocha": "^7.0.0",
|
"mocha": "^7.0.0",
|
||||||
|
"mockdate": "^3.0.5",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"protoc": "^1.0.4",
|
"protoc": "^1.0.4",
|
||||||
"shelljs": "^0.8.3",
|
"shelljs": "^0.8.3",
|
||||||
"sinon": "^9.0.1",
|
"sinon": "^12.0.1",
|
||||||
|
"sinon-chai": "^3.7.0",
|
||||||
|
"typemoq": "^2.1.0",
|
||||||
"uuid": "^3.2.1",
|
"uuid": "^3.2.1",
|
||||||
"yargs": "^11.1.0"
|
"yargs": "^11.1.0"
|
||||||
},
|
},
|
||||||
@@ -93,7 +126,8 @@
|
|||||||
},
|
},
|
||||||
"mocha": {
|
"mocha": {
|
||||||
"require": [
|
"require": [
|
||||||
"reflect-metadata/Reflect"
|
"reflect-metadata/Reflect",
|
||||||
|
"ignore-styles"
|
||||||
],
|
],
|
||||||
"reporter": "spec",
|
"reporter": "spec",
|
||||||
"colors": true,
|
"colors": true,
|
||||||
@@ -122,7 +156,16 @@
|
|||||||
],
|
],
|
||||||
"arduino": {
|
"arduino": {
|
||||||
"cli": {
|
"cli": {
|
||||||
"version": "0.18.2"
|
"version": "0.25.1"
|
||||||
|
},
|
||||||
|
"fwuploader": {
|
||||||
|
"version": "2.2.0"
|
||||||
|
},
|
||||||
|
"clangd": {
|
||||||
|
"version": "14.0.0"
|
||||||
|
},
|
||||||
|
"languageServer": {
|
||||||
|
"version": "0.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
arduino-ide-extension/scripts/compose-changelog.js
Executable file
116
arduino-ide-extension/scripts/compose-changelog.js
Executable file
@@ -0,0 +1,116 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const { Octokit } = require('@octokit/rest');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
userAgent: 'Arduino IDE compose-changelog.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await octokit.rest.repos
|
||||||
|
.listReleases({
|
||||||
|
owner: 'arduino',
|
||||||
|
repo: 'arduino-ide',
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const releases = response.data;
|
||||||
|
|
||||||
|
let fullChangelog = releases.reduce((acc, item, index) => {
|
||||||
|
// Process each line separately
|
||||||
|
const body = item.body.split('\n').map(processLine).join('\n');
|
||||||
|
// item.name is the name of the release changelog
|
||||||
|
return (
|
||||||
|
acc +
|
||||||
|
`## ${item.name}\n\n${body}${
|
||||||
|
index !== releases.length - 1 ? '\n\n---\n\n' : '\n'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}, '');
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.length == 0) {
|
||||||
|
console.error('Missing argument to destination file');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const changelogFile = path.resolve(args[0]);
|
||||||
|
|
||||||
|
await fs.writeFile(
|
||||||
|
changelogFile,
|
||||||
|
fullChangelog,
|
||||||
|
{
|
||||||
|
flag: 'w+',
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('Changelog written to', changelogFile);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// processLine applies different substitutions to line string.
|
||||||
|
// We're assuming that there are no more than one substitution
|
||||||
|
// per line to be applied.
|
||||||
|
const processLine = (line) => {
|
||||||
|
// Check if a link with one of the following format exists:
|
||||||
|
// * [#123](https://github.com/arduino/arduino-ide/pull/123)
|
||||||
|
// * [#123](https://github.com/arduino/arduino-ide/issues/123)
|
||||||
|
// * [#123](https://github.com/arduino/arduino-ide/pull/123/)
|
||||||
|
// * [#123](https://github.com/arduino/arduino-ide/issues/123/)
|
||||||
|
// If it does return the line as is.
|
||||||
|
let r =
|
||||||
|
/(\(|\[)#\d+(\)|\])(\(|\[)https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?(\)|\])/gm;
|
||||||
|
if (r.test(line)) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a issue or PR link with the following format exists:
|
||||||
|
// * #123
|
||||||
|
// If it does it's changed to:
|
||||||
|
// * [#123](https://github.com/arduino/arduino-ide/pull/123)
|
||||||
|
r = /(?<![\w\d\/_]{1})#((\d)+)(?![\w\d\/_]{1})/gm;
|
||||||
|
if (r.test(line)) {
|
||||||
|
return line.replace(
|
||||||
|
r,
|
||||||
|
`[#$1](https://github.com/arduino/arduino-ide/pull/$1)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a link with one of the following format exists:
|
||||||
|
// * https://github.com/arduino/arduino-ide/pull/123
|
||||||
|
// * https://github.com/arduino/arduino-ide/issues/123
|
||||||
|
// * https://github.com/arduino/arduino-ide/pull/123/
|
||||||
|
// * https://github.com/arduino/arduino-ide/issues/123/
|
||||||
|
// If it does it's changed respectively to:
|
||||||
|
// * [#123](https://github.com/arduino/arduino-ide/pull/123)
|
||||||
|
// * [#123](https://github.com/arduino/arduino-ide/issues/123)
|
||||||
|
// * [#123](https://github.com/arduino/arduino-ide/pull/123/)
|
||||||
|
// * [#123](https://github.com/arduino/arduino-ide/issues/123/)
|
||||||
|
r =
|
||||||
|
/(https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?)/gm;
|
||||||
|
if (r.test(line)) {
|
||||||
|
return line.replace(r, `[#$3]($1)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a link with the following format exists:
|
||||||
|
// * https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3
|
||||||
|
// * https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3/
|
||||||
|
// If it does it's changed to:
|
||||||
|
// * [`2.0.0-rc2...2.0.0-rc3`](https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3)
|
||||||
|
r =
|
||||||
|
/(https:\/\/github\.com\/arduino\/arduino-ide\/compare\/([^\/]*))\/?\s?/gm;
|
||||||
|
if (r.test(line)) {
|
||||||
|
return line.replace(r, '[`$2`]($1)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing matches just return the line as is
|
||||||
|
return line;
|
||||||
|
};
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const temp = require('temp');
|
|
||||||
const shell = require('shelljs');
|
const shell = require('shelljs');
|
||||||
const semver = require('semver');
|
const semver = require('semver');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const downloader = require('./downloader');
|
const downloader = require('./downloader');
|
||||||
|
const { goBuildFromGit } = require('./utils');
|
||||||
|
|
||||||
const version = (() => {
|
const version = (() => {
|
||||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||||
@@ -43,17 +41,24 @@
|
|||||||
if (typeof version === 'string') {
|
if (typeof version === 'string') {
|
||||||
const suffix = (() => {
|
const suffix = (() => {
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case 'darwin': return 'macOS_64bit.tar.gz';
|
case 'darwin':
|
||||||
case 'win32': return 'Windows_64bit.zip';
|
return 'macOS_64bit.tar.gz';
|
||||||
|
case 'win32':
|
||||||
|
return 'Windows_64bit.zip';
|
||||||
case 'linux': {
|
case 'linux': {
|
||||||
switch (arch) {
|
switch (arch) {
|
||||||
case 'arm': return 'Linux_ARMv7.tar.gz';
|
case 'arm':
|
||||||
case 'arm64': return 'Linux_ARM64.tar.gz';
|
return 'Linux_ARMv7.tar.gz';
|
||||||
case 'x64': return 'Linux_64bit.tar.gz';
|
case 'arm64':
|
||||||
default: return undefined;
|
return 'Linux_ARM64.tar.gz';
|
||||||
|
case 'x64':
|
||||||
|
return 'Linux_64bit.tar.gz';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: return undefined;
|
default:
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
if (!suffix) {
|
if (!suffix) {
|
||||||
@@ -62,80 +67,21 @@
|
|||||||
}
|
}
|
||||||
if (semver.valid(version)) {
|
if (semver.valid(version)) {
|
||||||
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_${suffix}`;
|
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_${suffix}`;
|
||||||
shell.echo(`📦 Identified released version of the CLI. Downloading version ${version} from '${url}'`);
|
shell.echo(
|
||||||
|
`📦 Identified released version of the CLI. Downloading version ${version} from '${url}'`
|
||||||
|
);
|
||||||
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
||||||
} else if (moment(version, 'YYYYMMDD', true).isValid()) {
|
} else if (moment(version, 'YYYYMMDD', true).isValid()) {
|
||||||
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
|
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
|
||||||
shell.echo(`🌙 Identified nightly version of the CLI. Downloading version ${version} from '${url}'`);
|
shell.echo(
|
||||||
|
`🌙 Identified nightly version of the CLI. Downloading version ${version} from '${url}'`
|
||||||
|
);
|
||||||
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
||||||
} else {
|
} else {
|
||||||
shell.echo(`🔥 Could not interpret 'version': ${version}`);
|
shell.echo(`🔥 Could not interpret 'version': ${version}`);
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
goBuildFromGit(version, destinationPath, 'CLI');
|
||||||
// We assume an object with `owner`, `repo`, commitish?` properties.
|
|
||||||
const { owner, repo, commitish } = version;
|
|
||||||
if (!owner) {
|
|
||||||
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
|
|
||||||
shell.exit(1);
|
|
||||||
}
|
}
|
||||||
if (!repo) {
|
|
||||||
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
|
|
||||||
shell.exit(1);
|
|
||||||
}
|
|
||||||
const url = `https://github.com/${owner}/${repo}.git`;
|
|
||||||
shell.echo(`Building CLI from ${url}. Commitish: ${commitish ? commitish : 'HEAD'}`);
|
|
||||||
|
|
||||||
if (fs.existsSync(destinationPath)) {
|
|
||||||
shell.echo(`Skipping the CLI build because it already exists: ${destinationPath}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shell.mkdir('-p', buildFolder).code !== 0) {
|
|
||||||
shell.echo('Could not create build folder.');
|
|
||||||
shell.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempRepoPath = temp.mkdirSync();
|
|
||||||
shell.echo(`>>> Cloning CLI source to ${tempRepoPath}...`);
|
|
||||||
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
|
|
||||||
shell.exit(1);
|
|
||||||
}
|
|
||||||
shell.echo('<<< Cloned CLI repo.')
|
|
||||||
|
|
||||||
if (commitish) {
|
|
||||||
shell.echo(`>>> Checking out ${commitish}...`);
|
|
||||||
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
|
|
||||||
shell.exit(1);
|
|
||||||
}
|
|
||||||
shell.echo(`<<< Checked out ${commitish}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
shell.echo(`>>> Building the CLI...`);
|
|
||||||
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
|
|
||||||
shell.exit(1);
|
|
||||||
}
|
|
||||||
shell.echo('<<< CLI build done.')
|
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(tempRepoPath, cliName))) {
|
|
||||||
shell.echo(`Could not find the CLI at ${path.join(tempRepoPath, cliName)}.`);
|
|
||||||
shell.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const builtCliPath = path.join(tempRepoPath, cliName);
|
|
||||||
shell.echo(`>>> Copying CLI from ${builtCliPath} to ${destinationPath}...`);
|
|
||||||
if (shell.cp(builtCliPath, destinationPath).code !== 0) {
|
|
||||||
shell.exit(1);
|
|
||||||
}
|
|
||||||
shell.echo(`<<< Copied the CLI.`);
|
|
||||||
|
|
||||||
shell.echo('<<< Verifying CLI...');
|
|
||||||
if (!fs.existsSync(destinationPath)) {
|
|
||||||
shell.exit(1);
|
|
||||||
}
|
|
||||||
shell.echo('>>> Verified CLI.');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
// The version to use.
|
// The version to use.
|
||||||
const version = '1.9.0';
|
const version = '1.9.1';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const { promises: fs } = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const shell = require('shelljs');
|
const shell = require('shelljs');
|
||||||
const { v4 } = require('uuid');
|
const { v4 } = require('uuid');
|
||||||
@@ -13,21 +13,84 @@ const version = '1.9.0';
|
|||||||
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
|
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
|
||||||
if (shell.mkdir('-p', repository).code !== 0) {
|
if (shell.mkdir('-p', repository).code !== 0) {
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shell.exec(`git clone https://github.com/arduino/arduino-examples.git ${repository}`).code !== 0) {
|
if (
|
||||||
|
shell.exec(
|
||||||
|
`git clone https://github.com/arduino/arduino-examples.git ${repository}`
|
||||||
|
).code !== 0
|
||||||
|
) {
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
|
if (
|
||||||
|
shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`)
|
||||||
|
.code !== 0
|
||||||
|
) {
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const destination = path.join(__dirname, '..', 'Examples');
|
const destination = path.join(__dirname, '..', 'Examples');
|
||||||
shell.mkdir('-p', destination);
|
shell.mkdir('-p', destination);
|
||||||
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
|
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
|
||||||
|
|
||||||
|
const isSketch = async (pathLike) => {
|
||||||
|
try {
|
||||||
|
const names = await fs.readdir(pathLike);
|
||||||
|
const dirName = path.basename(pathLike);
|
||||||
|
return names.indexOf(`${dirName}.ino`) !== -1;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'ENOTDIR') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const examples = [];
|
||||||
|
const categories = await fs.readdir(destination);
|
||||||
|
const visit = async (pathLike, container) => {
|
||||||
|
const stat = await fs.lstat(pathLike);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
if (await isSketch(pathLike)) {
|
||||||
|
container.sketches.push({
|
||||||
|
name: path.basename(pathLike),
|
||||||
|
relativePath: path.relative(destination, pathLike),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const names = await fs.readdir(pathLike);
|
||||||
|
for (const name of names) {
|
||||||
|
const childPath = path.join(pathLike, name);
|
||||||
|
if (await isSketch(childPath)) {
|
||||||
|
container.sketches.push({
|
||||||
|
name,
|
||||||
|
relativePath: path.relative(destination, childPath),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const child = {
|
||||||
|
label: name,
|
||||||
|
children: [],
|
||||||
|
sketches: [],
|
||||||
|
};
|
||||||
|
container.children.push(child);
|
||||||
|
await visit(childPath, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const category of categories) {
|
||||||
|
const example = {
|
||||||
|
label: category,
|
||||||
|
children: [],
|
||||||
|
sketches: [],
|
||||||
|
};
|
||||||
|
await visit(path.join(destination, category), example);
|
||||||
|
examples.push(example);
|
||||||
|
}
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(destination, 'examples.json'),
|
||||||
|
JSON.stringify(examples, null, 2),
|
||||||
|
{ encoding: 'utf8' }
|
||||||
|
);
|
||||||
|
shell.echo(`Generated output to ${path.join(destination, 'examples.json')}`);
|
||||||
})();
|
})();
|
||||||
|
|||||||
166
arduino-ide-extension/scripts/download-fwuploader.js
Executable file
166
arduino-ide-extension/scripts/download-fwuploader.js
Executable file
@@ -0,0 +1,166 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const temp = require('temp');
|
||||||
|
const shell = require('shelljs');
|
||||||
|
const semver = require('semver');
|
||||||
|
const downloader = require('./downloader');
|
||||||
|
|
||||||
|
const version = (() => {
|
||||||
|
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||||
|
if (!pkg) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { arduino } = pkg;
|
||||||
|
if (!arduino) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fwuploader } = arduino;
|
||||||
|
if (!fwuploader) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { version } = fwuploader;
|
||||||
|
return version;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
shell.echo(
|
||||||
|
`Could not retrieve Firmware Uploader version info from the 'package.json'.`
|
||||||
|
);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { platform, arch } = process;
|
||||||
|
const buildFolder = path.join(__dirname, '..', 'build');
|
||||||
|
const fwuploderName = `arduino-fwuploader${
|
||||||
|
platform === 'win32' ? '.exe' : ''
|
||||||
|
}`;
|
||||||
|
const destinationPath = path.join(buildFolder, fwuploderName);
|
||||||
|
|
||||||
|
if (typeof version === 'string') {
|
||||||
|
const suffix = (() => {
|
||||||
|
switch (platform) {
|
||||||
|
case 'darwin':
|
||||||
|
return 'macOS_64bit.tar.gz';
|
||||||
|
case 'win32':
|
||||||
|
return 'Windows_64bit.zip';
|
||||||
|
case 'linux': {
|
||||||
|
switch (arch) {
|
||||||
|
case 'arm':
|
||||||
|
return 'Linux_ARMv7.tar.gz';
|
||||||
|
case 'arm64':
|
||||||
|
return 'Linux_ARM64.tar.gz';
|
||||||
|
case 'x64':
|
||||||
|
return 'Linux_64bit.tar.gz';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
if (!suffix) {
|
||||||
|
shell.echo(
|
||||||
|
`The Firmware Uploader is not available for ${platform} ${arch}.`
|
||||||
|
);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
if (semver.valid(version)) {
|
||||||
|
const url = `https://downloads.arduino.cc/arduino-fwuploader/arduino-fwuploader_${version}_${suffix}`;
|
||||||
|
shell.echo(
|
||||||
|
`📦 Identified released version of the Firmware Uploader. Downloading version ${version} from '${url}'`
|
||||||
|
);
|
||||||
|
await downloader.downloadUnzipFile(
|
||||||
|
url,
|
||||||
|
destinationPath,
|
||||||
|
'arduino-fwuploader'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
shell.echo(`🔥 Could not interpret 'version': ${version}`);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We assume an object with `owner`, `repo`, commitish?` properties.
|
||||||
|
const { owner, repo, commitish } = version;
|
||||||
|
if (!owner) {
|
||||||
|
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
if (!repo) {
|
||||||
|
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
const url = `https://github.com/${owner}/${repo}.git`;
|
||||||
|
shell.echo(
|
||||||
|
`Building Firmware Uploader from ${url}. Commitish: ${
|
||||||
|
commitish ? commitish : 'HEAD'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(destinationPath)) {
|
||||||
|
shell.echo(
|
||||||
|
`Skipping the Firmware Uploader build because it already exists: ${destinationPath}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shell.mkdir('-p', buildFolder).code !== 0) {
|
||||||
|
shell.echo('Could not create build folder.');
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempRepoPath = temp.mkdirSync();
|
||||||
|
shell.echo(`>>> Cloning Firmware Uploader source to ${tempRepoPath}...`);
|
||||||
|
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo('<<< Cloned Firmware Uploader repo.');
|
||||||
|
|
||||||
|
if (commitish) {
|
||||||
|
shell.echo(`>>> Checking out ${commitish}...`);
|
||||||
|
if (
|
||||||
|
shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0
|
||||||
|
) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo(`<<< Checked out ${commitish}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.echo(`>>> Building the Firmware Uploader...`);
|
||||||
|
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo('<<< Firmware Uploader build done.');
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(tempRepoPath, fwuploderName))) {
|
||||||
|
shell.echo(
|
||||||
|
`Could not find the Firmware Uploader at ${path.join(
|
||||||
|
tempRepoPath,
|
||||||
|
fwuploderName
|
||||||
|
)}.`
|
||||||
|
);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const builtFwUploaderPath = path.join(tempRepoPath, fwuploderName);
|
||||||
|
shell.echo(
|
||||||
|
`>>> Copying Firmware Uploader from ${builtFwUploaderPath} to ${destinationPath}...`
|
||||||
|
);
|
||||||
|
if (shell.cp(builtFwUploaderPath, destinationPath).code !== 0) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo(`<<< Copied the Firmware Uploader.`);
|
||||||
|
|
||||||
|
shell.echo('<<< Verifying Firmware Uploader...');
|
||||||
|
if (!fs.existsSync(destinationPath)) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo('>>> Verified Firmware Uploader.');
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -4,69 +4,124 @@
|
|||||||
// - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX}
|
// - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX}
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
|
|
||||||
const DEFAULT_ALS_VERSION = 'nightly';
|
|
||||||
const DEFAULT_CLANGD_VERSION = 'snapshot_20210124';
|
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const shell = require('shelljs');
|
const shell = require('shelljs');
|
||||||
const downloader = require('./downloader');
|
const downloader = require('./downloader');
|
||||||
|
const { goBuildFromGit } = require('./utils');
|
||||||
|
|
||||||
|
const [DEFAULT_LS_VERSION, DEFAULT_CLANGD_VERSION] = (() => {
|
||||||
|
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||||
|
if (!pkg) return [undefined, undefined];
|
||||||
|
|
||||||
|
const { arduino } = pkg;
|
||||||
|
if (!arduino) return [undefined, undefined];
|
||||||
|
|
||||||
|
const { languageServer, clangd } = arduino;
|
||||||
|
if (!languageServer) return [undefined, undefined];
|
||||||
|
if (!clangd) return [undefined, undefined];
|
||||||
|
|
||||||
|
return [languageServer.version, clangd.version];
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!DEFAULT_LS_VERSION) {
|
||||||
|
shell.echo(
|
||||||
|
`Could not retrieve Arduino Language Server version info from the 'package.json'.`
|
||||||
|
);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DEFAULT_CLANGD_VERSION) {
|
||||||
|
shell.echo(
|
||||||
|
`Could not retrieve clangd version info from the 'package.json'.`
|
||||||
|
);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const yargs = require('yargs')
|
const yargs = require('yargs')
|
||||||
.option('ls-version', {
|
.option('ls-version', {
|
||||||
alias: 'lv',
|
alias: 'lv',
|
||||||
default: DEFAULT_ALS_VERSION,
|
default: DEFAULT_LS_VERSION,
|
||||||
choices: ['nightly'],
|
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_LS_VERSION}.`,
|
||||||
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`
|
|
||||||
})
|
})
|
||||||
.option('clangd-version', {
|
.option('clangd-version', {
|
||||||
alias: 'cv',
|
alias: 'cv',
|
||||||
default: DEFAULT_CLANGD_VERSION,
|
default: DEFAULT_CLANGD_VERSION,
|
||||||
choices: ['snapshot_20210124'],
|
choices: [DEFAULT_CLANGD_VERSION, 'snapshot_20210124'],
|
||||||
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`
|
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`,
|
||||||
})
|
})
|
||||||
.option('force-download', {
|
.option('force-download', {
|
||||||
alias: 'fd',
|
alias: 'fd',
|
||||||
default: false,
|
default: false,
|
||||||
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`
|
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`,
|
||||||
})
|
})
|
||||||
.version(false).parse();
|
.version(false)
|
||||||
|
.parse();
|
||||||
|
|
||||||
const alsVersion = yargs['ls-version'];
|
const lsVersion = yargs['ls-version'];
|
||||||
const clangdVersion = yargs['clangd-version']
|
const clangdVersion = yargs['clangd-version'];
|
||||||
const force = yargs['force-download'];
|
const force = yargs['force-download'];
|
||||||
const { platform, arch } = process;
|
const { platform, arch } = process;
|
||||||
|
const platformArch = platform + '-' + arch;
|
||||||
const build = path.join(__dirname, '..', 'build');
|
const build = path.join(__dirname, '..', 'build');
|
||||||
const lsExecutablePath = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
|
const lsExecutablePath = path.join(
|
||||||
|
build,
|
||||||
|
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
|
||||||
|
);
|
||||||
|
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
|
||||||
|
|
||||||
let clangdExecutablePath, lsSuffix, clangdPrefix;
|
switch (platformArch) {
|
||||||
switch (platform) {
|
case 'darwin-x64':
|
||||||
case 'darwin':
|
clangdExecutablePath = path.join(build, 'clangd');
|
||||||
clangdExecutablePath = path.join(build, 'bin', 'clangd')
|
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||||
lsSuffix = 'macOS_amd64.zip';
|
lsSuffix = 'macOS_64bit.tar.gz';
|
||||||
clangdPrefix = 'mac';
|
clangdSuffix = 'macOS_64bit';
|
||||||
break;
|
break;
|
||||||
case 'linux':
|
case 'linux-x64':
|
||||||
clangdExecutablePath = path.join(build, 'bin', 'clangd')
|
clangdExecutablePath = path.join(build, 'clangd');
|
||||||
lsSuffix = 'Linux_amd64.zip';
|
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||||
clangdPrefix = 'linux'
|
lsSuffix = 'Linux_64bit.tar.gz';
|
||||||
|
clangdSuffix = 'Linux_64bit';
|
||||||
break;
|
break;
|
||||||
case 'win32':
|
case 'win32-x64':
|
||||||
clangdExecutablePath = path.join(build, 'bin', 'clangd.exe')
|
clangdExecutablePath = path.join(build, 'clangd.exe');
|
||||||
lsSuffix = 'Windows_amd64.zip';
|
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
|
||||||
clangdPrefix = 'windows';
|
lsSuffix = 'Windows_64bit.zip';
|
||||||
|
clangdSuffix = 'Windows_64bit';
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported platform/arch: ${platformArch}.`);
|
||||||
}
|
}
|
||||||
if (!lsSuffix) {
|
if (!lsSuffix || !clangdSuffix) {
|
||||||
shell.echo(`The arduino-language-server is not available for ${platform} ${arch}.`);
|
shell.echo(
|
||||||
|
`The arduino-language-server is not available for ${platform} ${arch}.`
|
||||||
|
);
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${alsVersion === 'nightly' ? 'nightly/arduino-language-server' : 'arduino-language-server_' + alsVersion}_${lsSuffix}`;
|
if (typeof lsVersion === 'string') {
|
||||||
downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force);
|
const lsUrl = `https://downloads.arduino.cc/arduino-language-server/${
|
||||||
|
lsVersion === 'nightly'
|
||||||
|
? 'nightly/arduino-language-server'
|
||||||
|
: 'arduino-language-server_' + lsVersion
|
||||||
|
}_${lsSuffix}`;
|
||||||
|
downloader.downloadUnzipAll(lsUrl, build, lsExecutablePath, force);
|
||||||
|
} else {
|
||||||
|
goBuildFromGit(lsVersion, lsExecutablePath, 'language-server');
|
||||||
|
}
|
||||||
|
|
||||||
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd-${clangdPrefix}-${clangdVersion}.zip`;
|
const clangdUrl = `https://downloads.arduino.cc/tools/clangd_${clangdVersion}_${clangdSuffix}.tar.bz2`;
|
||||||
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, { strip: 1 }); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
|
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {
|
||||||
|
strip: 1,
|
||||||
|
}); // `strip`: the new clangd (12.x) is zipped into a folder, so we have to strip the outmost folder.
|
||||||
|
|
||||||
|
const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
|
||||||
|
downloader.downloadUnzipAll(
|
||||||
|
clangdFormatUrl,
|
||||||
|
build,
|
||||||
|
clangFormatExecutablePath,
|
||||||
|
force,
|
||||||
|
{
|
||||||
|
strip: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ const download = require('download');
|
|||||||
const decompress = require('decompress');
|
const decompress = require('decompress');
|
||||||
const unzip = require('decompress-unzip');
|
const unzip = require('decompress-unzip');
|
||||||
const untargz = require('decompress-targz');
|
const untargz = require('decompress-targz');
|
||||||
|
const untarbz2 = require('decompress-tarbz2');
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, _) => {
|
process.on('unhandledRejection', (reason, _) => {
|
||||||
shell.echo(String(reason));
|
shell.echo(String(reason));
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
throw reason;
|
throw reason;
|
||||||
});
|
});
|
||||||
process.on('uncaughtException', error => {
|
process.on('uncaughtException', (error) => {
|
||||||
shell.echo(String(error));
|
shell.echo(String(error));
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -23,7 +24,12 @@ process.on('uncaughtException', error => {
|
|||||||
* @param filePrefix {string} Prefix of the file name found in the archive
|
* @param filePrefix {string} Prefix of the file name found in the archive
|
||||||
* @param force {boolean} Whether to download even if the target file exists. `false` by default.
|
* @param force {boolean} Whether to download even if the target file exists. `false` by default.
|
||||||
*/
|
*/
|
||||||
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) => {
|
exports.downloadUnzipFile = async (
|
||||||
|
url,
|
||||||
|
targetFile,
|
||||||
|
filePrefix,
|
||||||
|
force = false
|
||||||
|
) => {
|
||||||
if (fs.existsSync(targetFile) && !force) {
|
if (fs.existsSync(targetFile) && !force) {
|
||||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||||
return;
|
return;
|
||||||
@@ -46,23 +52,25 @@ exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) =
|
|||||||
|
|
||||||
shell.echo('>>> Decompressing...');
|
shell.echo('>>> Decompressing...');
|
||||||
const files = await decompress(data, downloads, {
|
const files = await decompress(data, downloads, {
|
||||||
plugins: [
|
plugins: [unzip(), untargz(), untarbz2()],
|
||||||
unzip(),
|
|
||||||
untargz()
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
shell.echo('Error ocurred while decompressing the archive.');
|
shell.echo('Error ocurred while decompressing the archive.');
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
}
|
}
|
||||||
const fileIndex = files.findIndex(f => f.path.startsWith(filePrefix));
|
const fileIndex = files.findIndex((f) => f.path.startsWith(filePrefix));
|
||||||
if (fileIndex === -1) {
|
if (fileIndex === -1) {
|
||||||
shell.echo(`The downloaded artifact does not contain any file with prefix ${filePrefix}.`);
|
shell.echo(
|
||||||
|
`The downloaded artifact does not contain any file with prefix ${filePrefix}.`
|
||||||
|
);
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
}
|
}
|
||||||
shell.echo('<<< Decompressing succeeded.');
|
shell.echo('<<< Decompressing succeeded.');
|
||||||
|
|
||||||
if (shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile).code !== 0) {
|
if (
|
||||||
|
shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile)
|
||||||
|
.code !== 0
|
||||||
|
) {
|
||||||
shell.echo(`Could not move file to target path: ${targetFile}`);
|
shell.echo(`Could not move file to target path: ${targetFile}`);
|
||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
}
|
}
|
||||||
@@ -71,15 +79,22 @@ exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) =
|
|||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
}
|
}
|
||||||
shell.echo(`Done: ${targetFile}`);
|
shell.echo(`Done: ${targetFile}`);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param url {string} Download URL
|
* @param url {string} Download URL
|
||||||
* @param targetDir {string} Directory into which to decompress the archive
|
* @param targetDir {string} Directory into which to decompress the archive
|
||||||
* @param targetFile {string} Path to the main file expected after decompressing
|
* @param targetFile {string} Path to the main file expected after decompressing
|
||||||
* @param force {boolean} Whether to download even if the target file exists
|
* @param force {boolean} Whether to download even if the target file exists
|
||||||
|
* @param decompressOptions {import('decompress').DecompressOptions}
|
||||||
*/
|
*/
|
||||||
exports.downloadUnzipAll = async (url, targetDir, targetFile, force, decompressOptions = undefined) => {
|
exports.downloadUnzipAll = async (
|
||||||
|
url,
|
||||||
|
targetDir,
|
||||||
|
targetFile,
|
||||||
|
force,
|
||||||
|
decompressOptions = undefined
|
||||||
|
) => {
|
||||||
if (fs.existsSync(targetFile) && !force) {
|
if (fs.existsSync(targetFile) && !force) {
|
||||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||||
return;
|
return;
|
||||||
@@ -97,13 +112,10 @@ exports.downloadUnzipAll = async (url, targetDir, targetFile, force, decompressO
|
|||||||
|
|
||||||
shell.echo('>>> Decompressing...');
|
shell.echo('>>> Decompressing...');
|
||||||
let options = {
|
let options = {
|
||||||
plugins: [
|
plugins: [unzip(), untargz(), untarbz2()],
|
||||||
unzip(),
|
|
||||||
untargz()
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
if (decompressOptions) {
|
if (decompressOptions) {
|
||||||
options = Object.assign(options, decompressOptions)
|
options = Object.assign(options, decompressOptions);
|
||||||
}
|
}
|
||||||
const files = await decompress(data, targetDir, options);
|
const files = await decompress(data, targetDir, options);
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
@@ -117,4 +129,4 @@ exports.downloadUnzipAll = async (url, targetDir, targetFile, force, decompressO
|
|||||||
shell.exit(1);
|
shell.exit(1);
|
||||||
}
|
}
|
||||||
shell.echo(`Done: ${targetFile}`);
|
shell.echo(`Done: ${targetFile}`);
|
||||||
}
|
};
|
||||||
|
|||||||
92
arduino-ide-extension/scripts/utils.js
Normal file
92
arduino-ide-extension/scripts/utils.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Clones something from GitHub and builds it with `Golang`.
|
||||||
|
*
|
||||||
|
* @param version {object} the version object.
|
||||||
|
* @param destinationPath {string} the absolute path of the output binary. For example, `C:\\folder\\arduino-cli.exe` or `/path/to/arduino-language-server`
|
||||||
|
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
|
||||||
|
*/
|
||||||
|
exports.goBuildFromGit = (version, destinationPath, taskName) => {
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const temp = require('temp');
|
||||||
|
const shell = require('shelljs');
|
||||||
|
|
||||||
|
// We assume an object with `owner`, `repo`, commitish?` properties.
|
||||||
|
if (typeof version !== 'object') {
|
||||||
|
shell.echo(
|
||||||
|
`Expected a \`{ owner, repo, commitish }\` object. Got <${version}> instead.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { owner, repo, commitish } = version;
|
||||||
|
if (!owner) {
|
||||||
|
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
if (!repo) {
|
||||||
|
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
const url = `https://github.com/${owner}/${repo}.git`;
|
||||||
|
shell.echo(
|
||||||
|
`Building ${taskName} from ${url}. Commitish: ${
|
||||||
|
commitish ? commitish : 'HEAD'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(destinationPath)) {
|
||||||
|
shell.echo(
|
||||||
|
`Skipping the ${taskName} build because it already exists: ${destinationPath}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildFolder = path.join(__dirname, '..', 'build');
|
||||||
|
if (shell.mkdir('-p', buildFolder).code !== 0) {
|
||||||
|
shell.echo('Could not create build folder.');
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempRepoPath = temp.mkdirSync();
|
||||||
|
shell.echo(`>>> Cloning ${taskName} source to ${tempRepoPath}...`);
|
||||||
|
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo(`<<< Cloned ${taskName} repo.`);
|
||||||
|
|
||||||
|
if (commitish) {
|
||||||
|
shell.echo(`>>> Checking out ${commitish}...`);
|
||||||
|
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo(`<<< Checked out ${commitish}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.echo(`>>> Building the ${taskName}...`);
|
||||||
|
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo(`<<< Done ${taskName} build.`);
|
||||||
|
|
||||||
|
const binName = path.basename(destinationPath);
|
||||||
|
if (!fs.existsSync(path.join(tempRepoPath, binName))) {
|
||||||
|
shell.echo(
|
||||||
|
`Could not find the ${taskName} at ${path.join(tempRepoPath, binName)}.`
|
||||||
|
);
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const binPath = path.join(tempRepoPath, binName);
|
||||||
|
shell.echo(
|
||||||
|
`>>> Copying ${taskName} from ${binPath} to ${destinationPath}...`
|
||||||
|
);
|
||||||
|
if (shell.cp(binPath, destinationPath).code !== 0) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo(`<<< Copied the ${taskName}.`);
|
||||||
|
|
||||||
|
shell.echo(`<<< Verifying ${taskName}...`);
|
||||||
|
if (!fs.existsSync(destinationPath)) {
|
||||||
|
shell.exit(1);
|
||||||
|
}
|
||||||
|
shell.echo(`>>> Verified ${taskName}.`);
|
||||||
|
};
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Command } from '@theia/core/lib/common/command';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated all these commands should go under contributions and have their command, menu, keybinding, and toolbar contributions.
|
|
||||||
*/
|
|
||||||
export namespace ArduinoCommands {
|
|
||||||
|
|
||||||
export const TOGGLE_COMPILE_FOR_DEBUG: Command = {
|
|
||||||
id: 'arduino-toggle-compile-for-debug'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unlike `OPEN_SKETCH`, it opens all files from a sketch folder. (ino, cpp, etc...)
|
|
||||||
*/
|
|
||||||
export const OPEN_SKETCH_FILES: Command = {
|
|
||||||
id: 'arduino-open-sketch-files'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OPEN_BOARDS_DIALOG: Command = {
|
|
||||||
id: 'arduino-open-boards-dialog'
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,481 +1,360 @@
|
|||||||
import { Mutex } from 'async-mutex';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger, DisposableCollection } from '@theia/core';
|
|
||||||
import {
|
import {
|
||||||
ContextMenuRenderer,
|
inject,
|
||||||
FrontendApplication, FrontendApplicationContribution,
|
injectable,
|
||||||
OpenerService, StatusBar, StatusBarAlignment
|
postConstruct,
|
||||||
|
} from '@theia/core/shared/inversify';
|
||||||
|
import * as React from '@theia/core/shared/react';
|
||||||
|
import { SketchesService } from '../common/protocol';
|
||||||
|
import {
|
||||||
|
MAIN_MENU_BAR,
|
||||||
|
MenuContribution,
|
||||||
|
MenuModelRegistry,
|
||||||
|
} from '@theia/core';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
FrontendApplication,
|
||||||
|
FrontendApplicationContribution,
|
||||||
|
OnWillStopAction,
|
||||||
} from '@theia/core/lib/browser';
|
} from '@theia/core/lib/browser';
|
||||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||||
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
||||||
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
import {
|
||||||
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
|
TabBarToolbarContribution,
|
||||||
|
TabBarToolbarRegistry,
|
||||||
|
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import {
|
||||||
|
CommandContribution,
|
||||||
|
CommandRegistry,
|
||||||
|
} from '@theia/core/lib/common/command';
|
||||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser';
|
||||||
import { EditorMainMenu, EditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser';
|
|
||||||
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
|
|
||||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
|
||||||
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
|
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
|
||||||
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
|
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||||
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
|
||||||
import { OutputContribution } from '@theia/output/lib/browser/output-contribution';
|
|
||||||
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
|
||||||
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
|
||||||
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
||||||
import { inject, injectable, postConstruct } from 'inversify';
|
import {
|
||||||
import * as React from 'react';
|
CurrentSketch,
|
||||||
import { remote } from 'electron';
|
SketchesServiceClientImpl,
|
||||||
import { MainMenuManager } from '../common/main-menu-manager';
|
} from '../common/protocol/sketches-service-client-impl';
|
||||||
import { BoardsService, CoreService, Port, SketchesService, ExecutableService, Sketch } from '../common/protocol';
|
import { ArduinoPreferences } from './arduino-preferences';
|
||||||
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
|
|
||||||
import { ConfigService } from '../common/protocol/config-service';
|
|
||||||
import { FileSystemExt } from '../common/protocol/filesystem-ext';
|
|
||||||
import { ArduinoCommands } from './arduino-commands';
|
|
||||||
import { BoardsConfig } from './boards/boards-config';
|
|
||||||
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
|
||||||
import { BoardsDataStore } from './boards/boards-data-store';
|
|
||||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||||
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
||||||
import { EditorMode } from './editor-mode';
|
|
||||||
import { ArduinoMenus } from './menu/arduino-menus';
|
|
||||||
import { MonitorConnection } from './monitor/monitor-connection';
|
|
||||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
|
||||||
import { WorkspaceService } from './theia/workspace/workspace-service';
|
|
||||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
|
||||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
|
||||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
||||||
import { ResponseService } from '../common/protocol/response-service';
|
|
||||||
import { ArduinoPreferences } from './arduino-preferences';
|
|
||||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
|
||||||
import { SaveAsSketch } from './contributions/save-as-sketch';
|
import { SaveAsSketch } from './contributions/save-as-sketch';
|
||||||
import { FileChangeType } from '@theia/filesystem/lib/browser';
|
import { ArduinoMenus } from './menu/arduino-menus';
|
||||||
|
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||||
|
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||||
|
import { SerialPlotterContribution } from './serial/plotter/plotter-frontend-contribution';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
|
export class ArduinoFrontendContribution
|
||||||
TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution {
|
implements
|
||||||
|
FrontendApplicationContribution,
|
||||||
@inject(ILogger)
|
TabBarToolbarContribution,
|
||||||
protected logger: ILogger;
|
CommandContribution,
|
||||||
|
MenuContribution,
|
||||||
|
ColorContribution
|
||||||
|
{
|
||||||
@inject(MessageService)
|
@inject(MessageService)
|
||||||
protected readonly messageService: MessageService;
|
private readonly messageService: MessageService;
|
||||||
|
|
||||||
@inject(BoardsService)
|
|
||||||
protected readonly boardsService: BoardsService;
|
|
||||||
|
|
||||||
@inject(CoreService)
|
|
||||||
protected readonly coreService: CoreService;
|
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
|
|
||||||
@inject(SelectionService)
|
|
||||||
protected readonly selectionService: SelectionService;
|
|
||||||
|
|
||||||
@inject(EditorManager)
|
|
||||||
protected readonly editorManager: EditorManager;
|
|
||||||
|
|
||||||
@inject(ContextMenuRenderer)
|
|
||||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
||||||
|
|
||||||
@inject(FileDialogService)
|
|
||||||
protected readonly fileDialogService: FileDialogService;
|
|
||||||
|
|
||||||
@inject(FileService)
|
|
||||||
protected readonly fileService: FileService;
|
|
||||||
|
|
||||||
@inject(SketchesService)
|
@inject(SketchesService)
|
||||||
protected readonly sketchService: SketchesService;
|
private readonly sketchService: SketchesService;
|
||||||
|
|
||||||
@inject(BoardsConfigDialog)
|
|
||||||
protected readonly boardsConfigDialog: BoardsConfigDialog;
|
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
|
||||||
|
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
private readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@inject(StatusBar)
|
|
||||||
protected readonly statusBar: StatusBar;
|
|
||||||
|
|
||||||
@inject(WorkspaceService)
|
|
||||||
protected readonly workspaceService: WorkspaceService;
|
|
||||||
|
|
||||||
@inject(MonitorConnection)
|
|
||||||
protected readonly monitorConnection: MonitorConnection;
|
|
||||||
|
|
||||||
@inject(FileNavigatorContribution)
|
|
||||||
protected readonly fileNavigatorContributions: FileNavigatorContribution;
|
|
||||||
|
|
||||||
@inject(OutputContribution)
|
|
||||||
protected readonly outputContribution: OutputContribution;
|
|
||||||
|
|
||||||
@inject(OutlineViewContribution)
|
|
||||||
protected readonly outlineContribution: OutlineViewContribution;
|
|
||||||
|
|
||||||
@inject(ProblemContribution)
|
|
||||||
protected readonly problemContribution: ProblemContribution;
|
|
||||||
|
|
||||||
@inject(ScmContribution)
|
|
||||||
protected readonly scmContribution: ScmContribution;
|
|
||||||
|
|
||||||
@inject(SearchInWorkspaceFrontendContribution)
|
|
||||||
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
|
|
||||||
|
|
||||||
@inject(EditorMode)
|
|
||||||
protected readonly editorMode: EditorMode;
|
|
||||||
|
|
||||||
@inject(ArduinoDaemon)
|
|
||||||
protected readonly daemon: ArduinoDaemon;
|
|
||||||
|
|
||||||
@inject(OpenerService)
|
|
||||||
protected readonly openerService: OpenerService;
|
|
||||||
|
|
||||||
@inject(ConfigService)
|
|
||||||
protected readonly configService: ConfigService;
|
|
||||||
|
|
||||||
@inject(BoardsDataStore)
|
|
||||||
protected readonly boardsDataStore: BoardsDataStore;
|
|
||||||
|
|
||||||
@inject(MainMenuManager)
|
|
||||||
protected readonly mainMenuManager: MainMenuManager;
|
|
||||||
|
|
||||||
@inject(FileSystemExt)
|
|
||||||
protected readonly fileSystemExt: FileSystemExt;
|
|
||||||
|
|
||||||
@inject(HostedPluginSupport)
|
|
||||||
protected hostedPluginSupport: HostedPluginSupport;
|
|
||||||
|
|
||||||
@inject(ExecutableService)
|
|
||||||
protected executableService: ExecutableService;
|
|
||||||
|
|
||||||
@inject(ResponseService)
|
|
||||||
protected readonly responseService: ResponseService;
|
|
||||||
|
|
||||||
@inject(ArduinoPreferences)
|
@inject(ArduinoPreferences)
|
||||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
private readonly arduinoPreferences: ArduinoPreferences;
|
||||||
|
|
||||||
@inject(SketchesServiceClientImpl)
|
@inject(SketchesServiceClientImpl)
|
||||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
private readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||||
|
|
||||||
@inject(FrontendApplicationStateService)
|
@inject(FrontendApplicationStateService)
|
||||||
protected readonly appStateService: FrontendApplicationStateService;
|
private readonly appStateService: FrontendApplicationStateService;
|
||||||
|
|
||||||
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined;
|
|
||||||
protected toDisposeOnStop = new DisposableCollection();
|
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
protected async init(): Promise<void> {
|
protected async init(): Promise<void> {
|
||||||
if (!window.navigator.onLine) {
|
if (!window.navigator.onLine) {
|
||||||
// tslint:disable-next-line:max-line-length
|
// tslint:disable-next-line:max-line-length
|
||||||
this.messageService.warn('You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.');
|
this.messageService.warn(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/common/offlineIndicator',
|
||||||
|
'You appear to be offline. Without an Internet connection, the Arduino CLI might not be able to download the required resources and could cause malfunction. Please connect to the Internet and restart the application.'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const updateStatusBar = ({ selectedBoard, selectedPort }: BoardsConfig.Config) => {
|
|
||||||
this.statusBar.setElement('arduino-selected-board', {
|
|
||||||
alignment: StatusBarAlignment.RIGHT,
|
|
||||||
text: selectedBoard ? `$(microchip) ${selectedBoard.name}` : '$(close) no board selected',
|
|
||||||
className: 'arduino-selected-board'
|
|
||||||
});
|
|
||||||
if (selectedBoard) {
|
|
||||||
this.statusBar.setElement('arduino-selected-port', {
|
|
||||||
alignment: StatusBarAlignment.RIGHT,
|
|
||||||
text: selectedPort ? `on ${Port.toString(selectedPort)}` : '[not connected]',
|
|
||||||
className: 'arduino-selected-port'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
|
|
||||||
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
|
|
||||||
this.appStateService.reachedState('ready').then(async () => {
|
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
|
||||||
if (sketch && (!await this.sketchService.isTemp(sketch))) {
|
|
||||||
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
|
|
||||||
this.toDisposeOnStop.push(this.fileService.onDidFilesChange(async event => {
|
|
||||||
for (const { type, resource } of event.changes) {
|
|
||||||
if (type === FileChangeType.ADDED && resource.parent.toString() === sketch.uri) {
|
|
||||||
const reloadedSketch = await this.sketchService.loadSketch(sketch.uri)
|
|
||||||
if (Sketch.isInSketch(resource, reloadedSketch)) {
|
|
||||||
this.ensureOpened(resource.toString(), true, { mode: 'open' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStart(app: FrontendApplication): void {
|
async onStart(app: FrontendApplication): Promise<void> {
|
||||||
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
|
this.arduinoPreferences.onPreferenceChanged((event) => {
|
||||||
for (const viewContribution of [
|
if (event.newValue !== event.oldValue) {
|
||||||
this.fileNavigatorContributions,
|
switch (event.preferenceName) {
|
||||||
this.outputContribution,
|
case 'arduino.window.zoomLevel':
|
||||||
this.outlineContribution,
|
if (typeof event.newValue === 'number') {
|
||||||
this.problemContribution,
|
|
||||||
this.scmContribution,
|
|
||||||
this.siwContribution] as Array<FrontendApplicationContribution>) {
|
|
||||||
if (viewContribution.initializeLayout) {
|
|
||||||
viewContribution.initializeLayout(app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const start = async ({ selectedBoard }: BoardsConfig.Config) => {
|
|
||||||
if (selectedBoard) {
|
|
||||||
const { name, fqbn } = selectedBoard;
|
|
||||||
if (fqbn) {
|
|
||||||
this.startLanguageServer(fqbn, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.boardsServiceClientImpl.onBoardsConfigChanged(start);
|
|
||||||
this.arduinoPreferences.onPreferenceChanged(event => {
|
|
||||||
if (event.preferenceName === 'arduino.language.log' && event.newValue !== event.oldValue) {
|
|
||||||
start(this.boardsServiceClientImpl.boardsConfig);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.arduinoPreferences.ready.then(() => {
|
|
||||||
const webContents = remote.getCurrentWebContents();
|
|
||||||
const zoomLevel = this.arduinoPreferences.get('arduino.window.zoomLevel');
|
|
||||||
webContents.setZoomLevel(zoomLevel);
|
|
||||||
});
|
|
||||||
this.arduinoPreferences.onPreferenceChanged(event => {
|
|
||||||
if (event.preferenceName === 'arduino.window.zoomLevel' && typeof event.newValue === 'number' && event.newValue !== event.oldValue) {
|
|
||||||
const webContents = remote.getCurrentWebContents();
|
const webContents = remote.getCurrentWebContents();
|
||||||
webContents.setZoomLevel(event.newValue || 0);
|
webContents.setZoomLevel(event.newValue || 0);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
app.shell.leftPanelHandler.removeMenu('settings-menu');
|
this.appStateService.reachedState('ready').then(() =>
|
||||||
}
|
this.arduinoPreferences.ready.then(() => {
|
||||||
|
const webContents = remote.getCurrentWebContents();
|
||||||
onStop(): void {
|
const zoomLevel = this.arduinoPreferences.get(
|
||||||
this.toDisposeOnStop.dispose();
|
'arduino.window.zoomLevel'
|
||||||
}
|
);
|
||||||
|
webContents.setZoomLevel(zoomLevel);
|
||||||
protected languageServerFqbn?: string;
|
|
||||||
protected languageServerStartMutex = new Mutex();
|
|
||||||
protected async startLanguageServer(fqbn: string, name: string | undefined): Promise<void> {
|
|
||||||
const release = await this.languageServerStartMutex.acquire();
|
|
||||||
try {
|
|
||||||
await this.hostedPluginSupport.didStart;
|
|
||||||
const details = await this.boardsService.getBoardDetails({ fqbn });
|
|
||||||
if (!details) {
|
|
||||||
// Core is not installed for the selected board.
|
|
||||||
console.info(`Could not start language server for ${fqbn}. The core is not installed for the board.`);
|
|
||||||
if (this.languageServerFqbn) {
|
|
||||||
try {
|
|
||||||
await this.commandRegistry.executeCommand('arduino.languageserver.stop');
|
|
||||||
console.info(`Stopped language server process for ${this.languageServerFqbn}.`);
|
|
||||||
this.languageServerFqbn = undefined;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed to start language server process for ${this.languageServerFqbn}`, e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fqbn === this.languageServerFqbn) {
|
|
||||||
// NOOP
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.logger.info(`Starting language server: ${fqbn}`);
|
|
||||||
const log = this.arduinoPreferences.get('arduino.language.log');
|
|
||||||
let currentSketchPath: string | undefined = undefined;
|
|
||||||
if (log) {
|
|
||||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
|
||||||
if (currentSketch) {
|
|
||||||
currentSketchPath = await this.fileService.fsPath(new URI(currentSketch.uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
|
|
||||||
const [clangdPath, cliPath, lsPath, cliConfigPath] = await Promise.all([
|
|
||||||
this.fileService.fsPath(new URI(clangdUri)),
|
|
||||||
this.fileService.fsPath(new URI(cliUri)),
|
|
||||||
this.fileService.fsPath(new URI(lsUri)),
|
|
||||||
this.fileService.fsPath(new URI(await this.configService.getCliConfigFileUri()))
|
|
||||||
]);
|
|
||||||
this.languageServerFqbn = await Promise.race([
|
|
||||||
new Promise<undefined>((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${20_000} ms.`)), 20_000)),
|
|
||||||
this.commandRegistry.executeCommand<string>('arduino.languageserver.start', {
|
|
||||||
lsPath,
|
|
||||||
cliPath,
|
|
||||||
clangdPath,
|
|
||||||
log: currentSketchPath ? currentSketchPath : log,
|
|
||||||
cliConfigPath,
|
|
||||||
board: {
|
|
||||||
fqbn,
|
|
||||||
name: name ? `"${name}"` : undefined
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
]);
|
);
|
||||||
} catch (e) {
|
// Removes the _Settings_ (cog) icon from the left sidebar
|
||||||
console.log(`Failed to start language server for ${fqbn}`, e);
|
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
|
||||||
this.languageServerFqbn = undefined;
|
|
||||||
} finally {
|
|
||||||
release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||||
registry.registerItem({
|
registry.registerItem({
|
||||||
id: BoardsToolBarItem.TOOLBAR_ID,
|
id: BoardsToolBarItem.TOOLBAR_ID,
|
||||||
render: () => <BoardsToolBarItem
|
render: () => (
|
||||||
key='boardsToolbarItem'
|
<BoardsToolBarItem
|
||||||
|
key="boardsToolbarItem"
|
||||||
commands={this.commandRegistry}
|
commands={this.commandRegistry}
|
||||||
boardsServiceClient={this.boardsServiceClientImpl} />,
|
boardsServiceProvider={this.boardsServiceProvider}
|
||||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
/>
|
||||||
priority: 7
|
),
|
||||||
|
isVisible: (widget) =>
|
||||||
|
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||||
|
priority: 7,
|
||||||
|
});
|
||||||
|
registry.registerItem({
|
||||||
|
id: 'toggle-serial-plotter',
|
||||||
|
command: SerialPlotterContribution.Commands.OPEN_TOOLBAR.id,
|
||||||
|
tooltip: nls.localize(
|
||||||
|
'arduino/serial/openSerialPlotter',
|
||||||
|
'Serial Plotter'
|
||||||
|
),
|
||||||
});
|
});
|
||||||
registry.registerItem({
|
registry.registerItem({
|
||||||
id: 'toggle-serial-monitor',
|
id: 'toggle-serial-monitor',
|
||||||
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
|
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
|
||||||
tooltip: 'Serial Monitor'
|
tooltip: nls.localize('arduino/common/serialMonitor', 'Serial Monitor'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
|
for (const command of [
|
||||||
execute: () => this.editorMode.toggleCompileForDebug(),
|
EditorCommands.SPLIT_EDITOR_DOWN,
|
||||||
isToggled: () => this.editorMode.compileForDebug
|
EditorCommands.SPLIT_EDITOR_LEFT,
|
||||||
});
|
EditorCommands.SPLIT_EDITOR_RIGHT,
|
||||||
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
|
EditorCommands.SPLIT_EDITOR_UP,
|
||||||
execute: async (uri: URI) => {
|
EditorCommands.SPLIT_EDITOR_VERTICAL,
|
||||||
this.openSketchFiles(uri);
|
EditorCommands.SPLIT_EDITOR_HORIZONTAL,
|
||||||
|
FileNavigatorCommands.REVEAL_IN_NAVIGATOR,
|
||||||
|
]) {
|
||||||
|
registry.unregisterCommand(command);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
|
|
||||||
execute: async (query?: string | undefined) => {
|
|
||||||
const boardsConfig = await this.boardsConfigDialog.open(query);
|
|
||||||
if (boardsConfig) {
|
|
||||||
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry) {
|
registerMenus(registry: MenuModelRegistry): void {
|
||||||
const menuId = (menuPath: string[]): string => {
|
const menuId = (menuPath: string[]): string => {
|
||||||
const index = menuPath.length - 1;
|
const index = menuPath.length - 1;
|
||||||
const menuId = menuPath[index];
|
const menuId = menuPath[index];
|
||||||
return menuId;
|
return menuId;
|
||||||
}
|
};
|
||||||
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
|
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(MonacoMenus.SELECTION));
|
||||||
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
|
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(EditorMainMenu.GO));
|
||||||
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
|
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(TerminalMenus.TERMINAL));
|
||||||
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
|
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
|
||||||
|
|
||||||
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
|
registry.registerSubmenu(
|
||||||
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
|
ArduinoMenus.SKETCH,
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
nls.localize('arduino/menu/sketch', 'Sketch')
|
||||||
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
|
);
|
||||||
label: 'Optimize for Debugging',
|
registry.registerSubmenu(
|
||||||
order: '4'
|
ArduinoMenus.TOOLS,
|
||||||
});
|
nls.localize('arduino/menu/tools', 'Tools')
|
||||||
}
|
);
|
||||||
|
|
||||||
protected async openSketchFiles(uri: URI): Promise<void> {
|
|
||||||
try {
|
|
||||||
const sketch = await this.sketchService.loadSketch(uri.toString());
|
|
||||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
|
||||||
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
|
|
||||||
await this.ensureOpened(uri);
|
|
||||||
}
|
|
||||||
await this.ensureOpened(mainFileUri, true);
|
|
||||||
if (mainFileUri.endsWith('.pde')) {
|
|
||||||
const message = `The '${sketch.name}' still uses the old \`.pde\` format. Do you want to switch to the new \`.ino\` extension?`;
|
|
||||||
this.messageService.info(message, 'Later', 'Yes').then(async answer => {
|
|
||||||
if (answer === 'Yes') {
|
|
||||||
this.commandRegistry.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { execOnlyIfTemp: false, openAfterMove: true, wipeOriginal: false });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
const message = e instanceof Error ? e.message : JSON.stringify(e);
|
|
||||||
this.messageService.error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async ensureOpened(uri: string, forceOpen: boolean = false, options?: EditorOpenerOptions | undefined): Promise<any> {
|
|
||||||
const widget = this.editorManager.all.find(widget => widget.editor.uri.toString() === uri);
|
|
||||||
if (!widget || forceOpen) {
|
|
||||||
return this.editorManager.open(new URI(uri), options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerColors(colors: ColorRegistry): void {
|
registerColors(colors: ColorRegistry): void {
|
||||||
colors.register(
|
colors.register(
|
||||||
{
|
{
|
||||||
id: 'arduino.branding.primary',
|
id: 'arduino.toolbar.button.background',
|
||||||
defaults: {
|
|
||||||
dark: 'statusBar.background',
|
|
||||||
light: 'statusBar.background'
|
|
||||||
},
|
|
||||||
description: 'The primary branding color, such as dialog titles, library, and board manager list labels.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'arduino.branding.secondary',
|
|
||||||
defaults: {
|
|
||||||
dark: 'statusBar.background',
|
|
||||||
light: 'statusBar.background'
|
|
||||||
},
|
|
||||||
description: 'Secondary branding color for list selections, dropdowns, and widget borders.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'arduino.foreground',
|
|
||||||
defaults: {
|
|
||||||
dark: 'editorWidget.background',
|
|
||||||
light: 'editorWidget.background',
|
|
||||||
hc: 'editorWidget.background'
|
|
||||||
},
|
|
||||||
description: 'Color of the Arduino IDE foreground which is used for dialogs, such as the Select Board dialog.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'arduino.toolbar.background',
|
|
||||||
defaults: {
|
defaults: {
|
||||||
dark: 'button.background',
|
dark: 'button.background',
|
||||||
light: 'button.background',
|
light: 'button.background',
|
||||||
hc: 'activityBar.inactiveForeground'
|
hc: 'activityBar.inactiveForeground',
|
||||||
},
|
},
|
||||||
description: 'Background color of the toolbar items. Such as Upload, Verify, etc.'
|
description:
|
||||||
|
'Background color of the toolbar items. Such as Upload, Verify, etc.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'arduino.toolbar.hoverBackground',
|
id: 'arduino.toolbar.button.hoverBackground',
|
||||||
defaults: {
|
defaults: {
|
||||||
dark: 'button.hoverBackground',
|
dark: 'button.hoverBackground',
|
||||||
light: 'button.foreground',
|
light: 'button.hoverBackground',
|
||||||
hc: 'textLink.foreground'
|
hc: 'button.background',
|
||||||
},
|
},
|
||||||
description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.'
|
description:
|
||||||
|
'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'arduino.toolbar.button.secondary.label',
|
||||||
|
defaults: {
|
||||||
|
dark: 'secondaryButton.foreground',
|
||||||
|
light: 'button.foreground',
|
||||||
|
hc: 'activityBar.inactiveForeground',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'Foreground color of the toolbar items. Such as Serial Monitor and Serial Plotter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'arduino.toolbar.button.secondary.hoverBackground',
|
||||||
|
defaults: {
|
||||||
|
dark: 'secondaryButton.hoverBackground',
|
||||||
|
light: 'button.hoverBackground',
|
||||||
|
hc: 'textLink.foreground',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'Background color of the toolbar items when hovering over them, such as "Serial Monitor" and "Serial Plotter"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'arduino.toolbar.toggleBackground',
|
id: 'arduino.toolbar.toggleBackground',
|
||||||
defaults: {
|
defaults: {
|
||||||
dark: 'editor.selectionBackground',
|
dark: 'editor.selectionBackground',
|
||||||
light: 'editor.selectionBackground',
|
light: 'editor.selectionBackground',
|
||||||
hc: 'textPreformat.foreground'
|
hc: 'textPreformat.foreground',
|
||||||
},
|
},
|
||||||
description: 'Toggle color of the toolbar items when they are currently toggled (the command is in progress)'
|
description:
|
||||||
|
'Toggle color of the toolbar items when they are currently toggled (the command is in progress)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'arduino.output.foreground',
|
id: 'arduino.toolbar.dropdown.border',
|
||||||
defaults: {
|
defaults: {
|
||||||
dark: 'editor.foreground',
|
dark: 'dropdown.border',
|
||||||
light: 'editor.foreground',
|
light: 'dropdown.border',
|
||||||
hc: 'editor.foreground'
|
hc: 'dropdown.border',
|
||||||
},
|
},
|
||||||
description: 'Color of the text in the Output view.'
|
description: 'Border color of the Board Selector.',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'arduino.toolbar.dropdown.borderActive',
|
||||||
|
defaults: {
|
||||||
|
dark: 'focusBorder',
|
||||||
|
light: 'focusBorder',
|
||||||
|
hc: 'focusBorder',
|
||||||
|
},
|
||||||
|
description: "Border color of the Board Selector when it's active",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'arduino.toolbar.dropdown.background',
|
||||||
|
defaults: {
|
||||||
|
dark: 'tab.unfocusedActiveBackground',
|
||||||
|
light: 'dropdown.background',
|
||||||
|
hc: 'dropdown.background',
|
||||||
|
},
|
||||||
|
description: 'Background color of the Board Selector.',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'arduino.toolbar.dropdown.label',
|
||||||
|
defaults: {
|
||||||
|
dark: 'dropdown.foreground',
|
||||||
|
light: 'dropdown.foreground',
|
||||||
|
hc: 'dropdown.foreground',
|
||||||
|
},
|
||||||
|
description: 'Font color of the Board Selector.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'arduino.output.background',
|
id: 'arduino.toolbar.dropdown.iconSelected',
|
||||||
defaults: {
|
defaults: {
|
||||||
dark: 'editor.background',
|
dark: 'list.activeSelectionIconForeground',
|
||||||
light: 'editor.background',
|
light: 'list.activeSelectionIconForeground',
|
||||||
hc: 'editor.background'
|
hc: 'list.activeSelectionIconForeground',
|
||||||
},
|
},
|
||||||
description: 'Background color of the Output view.'
|
description:
|
||||||
|
'Color of the selected protocol icon in the Board Selector.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'arduino.toolbar.dropdown.option.backgroundHover',
|
||||||
|
defaults: {
|
||||||
|
dark: 'list.hoverBackground',
|
||||||
|
light: 'list.hoverBackground',
|
||||||
|
hc: 'list.hoverBackground',
|
||||||
|
},
|
||||||
|
description: 'Background color on hover of the Board Selector options.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'arduino.toolbar.dropdown.option.backgroundSelected',
|
||||||
|
defaults: {
|
||||||
|
dark: 'list.activeSelectionBackground',
|
||||||
|
light: 'list.activeSelectionBackground',
|
||||||
|
hc: 'list.activeSelectionBackground',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'Background color of the selected board in the Board Selector.',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: should be handled by `Close` contribution. https://github.com/arduino/arduino-ide/issues/1016
|
||||||
|
onWillStop(): OnWillStopAction {
|
||||||
|
return {
|
||||||
|
reason: 'temp-sketch',
|
||||||
|
action: () => {
|
||||||
|
return this.showTempSketchDialog();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showTempSketchDialog(): Promise<boolean> {
|
||||||
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const isTemp = await this.sketchService.isTemp(sketch);
|
||||||
|
if (!isTemp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const messageBoxResult = await remote.dialog.showMessageBox(
|
||||||
|
remote.getCurrentWindow(),
|
||||||
|
{
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/sketch/saveTempSketch',
|
||||||
|
'Save your sketch to open it again later.'
|
||||||
|
),
|
||||||
|
title: nls.localize(
|
||||||
|
'theia/core/quitTitle',
|
||||||
|
'Are you sure you want to quit?'
|
||||||
|
),
|
||||||
|
type: 'question',
|
||||||
|
buttons: [
|
||||||
|
Dialog.CANCEL,
|
||||||
|
nls.localizeByDefault('Save As...'),
|
||||||
|
nls.localizeByDefault("Don't Save"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const result = messageBoxResult.response;
|
||||||
|
if (result === 2) {
|
||||||
|
return true;
|
||||||
|
} else if (result === 1) {
|
||||||
|
return !!(await this.commandRegistry.executeCommand(
|
||||||
|
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||||
|
{
|
||||||
|
execOnlyIfTemp: false,
|
||||||
|
openAfterMove: false,
|
||||||
|
wipeOriginal: true,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,31 @@
|
|||||||
import '../../src/browser/style/index.css';
|
import '../../src/browser/style/index.css';
|
||||||
import { ContainerModule } from 'inversify';
|
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||||
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
|
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
|
||||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||||
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||||
import { TabBarToolbarContribution, TabBarToolbarFactory } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
import {
|
||||||
|
TabBarToolbarContribution,
|
||||||
|
TabBarToolbarFactory,
|
||||||
|
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||||
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
|
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
|
||||||
import { FrontendApplicationContribution, FrontendApplication as TheiaFrontendApplication } from '@theia/core/lib/browser/frontend-application'
|
import {
|
||||||
|
FrontendApplicationContribution,
|
||||||
|
FrontendApplication as TheiaFrontendApplication,
|
||||||
|
} from '@theia/core/lib/browser/frontend-application';
|
||||||
import { LibraryListWidget } from './library/library-list-widget';
|
import { LibraryListWidget } from './library/library-list-widget';
|
||||||
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
|
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
|
||||||
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
|
import {
|
||||||
import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service';
|
LibraryService,
|
||||||
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
|
LibraryServicePath,
|
||||||
|
} from '../common/protocol/library-service';
|
||||||
|
import {
|
||||||
|
BoardsService,
|
||||||
|
BoardsServicePath,
|
||||||
|
} from '../common/protocol/boards-service';
|
||||||
|
import {
|
||||||
|
SketchesService,
|
||||||
|
SketchesServicePath,
|
||||||
|
} from '../common/protocol/sketches-service';
|
||||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
||||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
||||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||||
@@ -35,56 +50,75 @@ import {
|
|||||||
ApplicationShell as TheiaApplicationShell,
|
ApplicationShell as TheiaApplicationShell,
|
||||||
ShellLayoutRestorer as TheiaShellLayoutRestorer,
|
ShellLayoutRestorer as TheiaShellLayoutRestorer,
|
||||||
CommonFrontendContribution as TheiaCommonFrontendContribution,
|
CommonFrontendContribution as TheiaCommonFrontendContribution,
|
||||||
KeybindingRegistry as TheiaKeybindingRegistry,
|
DockPanelRenderer as TheiaDockPanelRenderer,
|
||||||
TabBarRendererFactory,
|
TabBarRendererFactory,
|
||||||
ContextMenuRenderer,
|
ContextMenuRenderer,
|
||||||
createTreeContainer,
|
createTreeContainer,
|
||||||
TreeWidget
|
TreeWidget,
|
||||||
} from '@theia/core/lib/browser';
|
} from '@theia/core/lib/browser';
|
||||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||||
import { ApplicationShell } from './theia/core/application-shell';
|
import {
|
||||||
|
ApplicationShell,
|
||||||
|
DockPanelRenderer,
|
||||||
|
} from './theia/core/application-shell';
|
||||||
import { FrontendApplication } from './theia/core/frontend-application';
|
import { FrontendApplication } from './theia/core/frontend-application';
|
||||||
import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog';
|
import {
|
||||||
|
BoardsConfigDialog,
|
||||||
|
BoardsConfigDialogProps,
|
||||||
|
} from './boards/boards-config-dialog';
|
||||||
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
|
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
|
||||||
import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
||||||
import { ScmContribution } from './theia/scm/scm-contribution';
|
import { ScmContribution } from './theia/scm/scm-contribution';
|
||||||
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||||
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
|
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
|
||||||
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
|
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
|
||||||
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
|
import {
|
||||||
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
|
ConfigService,
|
||||||
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
|
ConfigServicePath,
|
||||||
import { MonitorWidget } from './monitor/monitor-widget';
|
} from '../common/protocol/config-service';
|
||||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
import { MonitorWidget } from './serial/monitor/monitor-widget';
|
||||||
import { MonitorConnection } from './monitor/monitor-connection';
|
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||||
import { MonitorModel } from './monitor/monitor-model';
|
|
||||||
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||||
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
||||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
||||||
import { ProblemManager } from './theia/markers/problem-manager';
|
import { ProblemManager } from './theia/markers/problem-manager';
|
||||||
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
||||||
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
|
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
|
||||||
import { EditorMode } from './editor-mode';
|
|
||||||
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
|
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
|
||||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||||
import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
|
import {
|
||||||
import { ArduinoDaemonPath, ArduinoDaemon } from '../common/protocol/arduino-daemon';
|
MonacoThemeJson,
|
||||||
|
MonacoThemingService,
|
||||||
|
} from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||||
|
import {
|
||||||
|
ArduinoDaemonPath,
|
||||||
|
ArduinoDaemon,
|
||||||
|
} from '../common/protocol/arduino-daemon';
|
||||||
import { EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser';
|
import { EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser';
|
||||||
import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution } from './theia/core/connection-status-service';
|
import {
|
||||||
|
FrontendConnectionStatusService,
|
||||||
|
ApplicationConnectionStatusContribution,
|
||||||
|
} from './theia/core/connection-status-service';
|
||||||
import {
|
import {
|
||||||
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
||||||
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution
|
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
|
||||||
} from '@theia/core/lib/browser/connection-status-service';
|
} from '@theia/core/lib/browser/connection-status-service';
|
||||||
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
|
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
|
||||||
import { BoardsDataStore } from './boards/boards-data-store';
|
import { BoardsDataStore } from './boards/boards-data-store';
|
||||||
import { ILogger } from '@theia/core';
|
import { ILogger } from '@theia/core';
|
||||||
import { FileSystemExt, FileSystemExtPath } from '../common/protocol/filesystem-ext';
|
import {
|
||||||
|
FileSystemExt,
|
||||||
|
FileSystemExtPath,
|
||||||
|
} from '../common/protocol/filesystem-ext';
|
||||||
import {
|
import {
|
||||||
WorkspaceFrontendContribution as TheiaWorkspaceFrontendContribution,
|
WorkspaceFrontendContribution as TheiaWorkspaceFrontendContribution,
|
||||||
FileMenuContribution as TheiaFileMenuContribution,
|
FileMenuContribution as TheiaFileMenuContribution,
|
||||||
WorkspaceCommandContribution as TheiaWorkspaceCommandContribution
|
WorkspaceCommandContribution as TheiaWorkspaceCommandContribution,
|
||||||
} from '@theia/workspace/lib/browser';
|
} from '@theia/workspace/lib/browser';
|
||||||
import { WorkspaceFrontendContribution, ArduinoFileMenuContribution } from './theia/workspace/workspace-frontend-contribution';
|
import {
|
||||||
|
WorkspaceFrontendContribution,
|
||||||
|
ArduinoFileMenuContribution,
|
||||||
|
} from './theia/workspace/workspace-frontend-contribution';
|
||||||
import { Contribution } from './contributions/contribution';
|
import { Contribution } from './contributions/contribution';
|
||||||
import { NewSketch } from './contributions/new-sketch';
|
import { NewSketch } from './contributions/new-sketch';
|
||||||
import { OpenSketch } from './contributions/open-sketch';
|
import { OpenSketch } from './contributions/open-sketch';
|
||||||
@@ -101,7 +135,6 @@ import { PreferencesContribution } from './theia/preferences/preferences-contrib
|
|||||||
import { QuitApp } from './contributions/quit-app';
|
import { QuitApp } from './contributions/quit-app';
|
||||||
import { SketchControl } from './contributions/sketch-control';
|
import { SketchControl } from './contributions/sketch-control';
|
||||||
import { Settings } from './contributions/settings';
|
import { Settings } from './contributions/settings';
|
||||||
import { KeybindingRegistry } from './theia/core/keybindings';
|
|
||||||
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
|
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
|
||||||
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
|
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
|
||||||
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
|
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
|
||||||
@@ -111,19 +144,39 @@ import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
|
|||||||
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
|
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||||
import { OutputWidget } from './theia/output/output-widget';
|
import { OutputWidget } from './theia/output/output-widget';
|
||||||
import { BurnBootloader } from './contributions/burn-bootloader';
|
import { BurnBootloader } from './contributions/burn-bootloader';
|
||||||
import { ExamplesServicePath, ExamplesService } from '../common/protocol/examples-service';
|
import {
|
||||||
|
ExamplesServicePath,
|
||||||
|
ExamplesService,
|
||||||
|
} from '../common/protocol/examples-service';
|
||||||
import { BuiltInExamples, LibraryExamples } from './contributions/examples';
|
import { BuiltInExamples, LibraryExamples } from './contributions/examples';
|
||||||
import { IncludeLibrary } from './contributions/include-library';
|
import { IncludeLibrary } from './contributions/include-library';
|
||||||
import { OutputChannelManager as TheiaOutputChannelManager } from '@theia/output/lib/common/output-channel';
|
import { OutputChannelManager as TheiaOutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
||||||
import { OutputChannelManager } from './theia/output/output-channel';
|
import { OutputChannelManager } from './theia/output/output-channel';
|
||||||
import { OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl, OutputChannelRegistryMainImpl } from './theia/plugin-ext/output-channel-registry-main';
|
import {
|
||||||
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
|
OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl,
|
||||||
|
OutputChannelRegistryMainImpl,
|
||||||
|
} from './theia/plugin-ext/output-channel-registry-main';
|
||||||
|
import {
|
||||||
|
ExecutableService,
|
||||||
|
ExecutableServicePath,
|
||||||
|
MonitorManagerProxy,
|
||||||
|
MonitorManagerProxyClient,
|
||||||
|
MonitorManagerProxyFactory,
|
||||||
|
MonitorManagerProxyPath,
|
||||||
|
} from '../common/protocol';
|
||||||
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||||
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
|
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
|
||||||
import { ResponseServiceImpl } from './response-service-impl';
|
import { ResponseServiceImpl } from './response-service-impl';
|
||||||
import { ResponseServicePath, ResponseService } from '../common/protocol/response-service';
|
import {
|
||||||
|
ResponseService,
|
||||||
|
ResponseServiceClient,
|
||||||
|
ResponseServicePath,
|
||||||
|
} from '../common/protocol/response-service';
|
||||||
import { NotificationCenter } from './notification-center';
|
import { NotificationCenter } from './notification-center';
|
||||||
import { NotificationServicePath, NotificationServiceServer } from '../common/protocol';
|
import {
|
||||||
|
NotificationServicePath,
|
||||||
|
NotificationServiceServer,
|
||||||
|
} from '../common/protocol';
|
||||||
import { About } from './contributions/about';
|
import { About } from './contributions/about';
|
||||||
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
|
import { IconThemeService } from '@theia/core/lib/browser/icon-theme-service';
|
||||||
import { TabBarRenderer } from './theia/core/tab-bars';
|
import { TabBarRenderer } from './theia/core/tab-bars';
|
||||||
@@ -139,8 +192,13 @@ import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationCo
|
|||||||
import { BoardSelection } from './contributions/board-selection';
|
import { BoardSelection } from './contributions/board-selection';
|
||||||
import { OpenRecentSketch } from './contributions/open-recent-sketch';
|
import { OpenRecentSketch } from './contributions/open-recent-sketch';
|
||||||
import { Help } from './contributions/help';
|
import { Help } from './contributions/help';
|
||||||
import { bindArduinoPreferences } from './arduino-preferences'
|
import { bindArduinoPreferences } from './arduino-preferences';
|
||||||
import { SettingsService, SettingsDialog, SettingsWidget, SettingsDialogProps } from './settings';
|
import { SettingsService } from './dialogs/settings/settings';
|
||||||
|
import {
|
||||||
|
SettingsDialog,
|
||||||
|
SettingsWidget,
|
||||||
|
SettingsDialogProps,
|
||||||
|
} from './dialogs/settings/settings-dialog';
|
||||||
import { AddFile } from './contributions/add-file';
|
import { AddFile } from './contributions/add-file';
|
||||||
import { ArchiveSketch } from './contributions/archive-sketch';
|
import { ArchiveSketch } from './contributions/archive-sketch';
|
||||||
import { OutputToolbarContribution as TheiaOutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
|
import { OutputToolbarContribution as TheiaOutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
|
||||||
@@ -152,31 +210,145 @@ import { DebugConfigurationManager } from './theia/debug/debug-configuration-man
|
|||||||
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
|
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
|
||||||
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
|
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
|
||||||
import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget';
|
import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget';
|
||||||
|
import { SearchInWorkspaceFactory as TheiaSearchInWorkspaceFactory } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';
|
||||||
|
import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory';
|
||||||
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
|
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
|
||||||
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
|
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
|
||||||
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
|
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
|
||||||
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||||
import { DebugEditorModel } from './theia/debug/debug-editor-model';
|
|
||||||
import { DebugEditorModelFactory } from '@theia/debug/lib/browser/editor/debug-editor-model';
|
|
||||||
import { StorageWrapper } from './storage-wrapper';
|
import { StorageWrapper } from './storage-wrapper';
|
||||||
import { NotificationManager } from './theia/messages/notifications-manager';
|
import { NotificationManager } from './theia/messages/notifications-manager';
|
||||||
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||||
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
|
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
|
||||||
import { NotificationsRenderer } from './theia/messages/notifications-renderer';
|
import { NotificationsRenderer } from './theia/messages/notifications-renderer';
|
||||||
|
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
|
||||||
|
import { LocalCacheFsProvider } from './local-cache/local-cache-fs-provider';
|
||||||
|
import { CloudSketchbookWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-widget';
|
||||||
|
import { CloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
|
||||||
|
import { createCloudSketchbookTreeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-tree-container';
|
||||||
|
import { CreateApi } from './create/create-api';
|
||||||
|
import { ShareSketchDialog } from './dialogs/cloud-share-sketch-dialog';
|
||||||
|
import { AuthenticationClientService } from './auth/authentication-client-service';
|
||||||
|
import {
|
||||||
|
AuthenticationService,
|
||||||
|
AuthenticationServicePath,
|
||||||
|
} from '../common/protocol/authentication-service';
|
||||||
|
import { CreateFsProvider } from './create/create-fs-provider';
|
||||||
|
import { FileServiceContribution } from '@theia/filesystem/lib/browser/file-service';
|
||||||
|
import { CloudSketchbookContribution } from './widgets/cloud-sketchbook/cloud-sketchbook-contributions';
|
||||||
|
import { CloudSketchbookCompositeWidget } from './widgets/cloud-sketchbook/cloud-sketchbook-composite-widget';
|
||||||
|
import { SketchbookWidget } from './widgets/sketchbook/sketchbook-widget';
|
||||||
|
import { SketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-widget';
|
||||||
|
import { createSketchbookTreeWidget } from './widgets/sketchbook/sketchbook-tree-container';
|
||||||
|
import { SketchCache } from './widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||||
|
import { UploadFirmware } from './contributions/upload-firmware';
|
||||||
|
import {
|
||||||
|
UploadFirmwareDialog,
|
||||||
|
UploadFirmwareDialogProps,
|
||||||
|
UploadFirmwareDialogWidget,
|
||||||
|
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
|
||||||
|
|
||||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
import { UploadCertificate } from './contributions/upload-certificate';
|
||||||
|
import {
|
||||||
|
ArduinoFirmwareUploader,
|
||||||
|
ArduinoFirmwareUploaderPath,
|
||||||
|
} from '../common/protocol/arduino-firmware-uploader';
|
||||||
|
import {
|
||||||
|
UploadCertificateDialog,
|
||||||
|
UploadCertificateDialogProps,
|
||||||
|
UploadCertificateDialogWidget,
|
||||||
|
} from './dialogs/certificate-uploader/certificate-uploader-dialog';
|
||||||
|
import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-contribution';
|
||||||
|
import {
|
||||||
|
UserFieldsDialog,
|
||||||
|
UserFieldsDialogProps,
|
||||||
|
UserFieldsDialogWidget,
|
||||||
|
} from './dialogs/user-fields/user-fields-dialog';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands';
|
||||||
|
import {
|
||||||
|
IDEUpdater,
|
||||||
|
IDEUpdaterClient,
|
||||||
|
IDEUpdaterPath,
|
||||||
|
} from '../common/protocol/ide-updater';
|
||||||
|
import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl';
|
||||||
|
import {
|
||||||
|
IDEUpdaterDialog,
|
||||||
|
IDEUpdaterDialogProps,
|
||||||
|
IDEUpdaterDialogWidget,
|
||||||
|
} from './dialogs/ide-updater/ide-updater-dialog';
|
||||||
|
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
|
||||||
|
import { MonitorModel } from './monitor-model';
|
||||||
|
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
|
||||||
|
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||||
|
import { EditorManager } from './theia/editor/editor-manager';
|
||||||
|
import { HostedPluginEvents } from './hosted-plugin-events';
|
||||||
|
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
|
||||||
|
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||||
|
import { Formatter, FormatterPath } from '../common/protocol/formatter';
|
||||||
|
import { Format } from './contributions/format';
|
||||||
|
import { MonacoFormattingConflictsContribution } from './theia/monaco/monaco-formatting-conflicts';
|
||||||
|
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';
|
||||||
|
import { DefaultJsonSchemaContribution } from './theia/core/json-schema-store';
|
||||||
|
import { DefaultJsonSchemaContribution as TheiaDefaultJsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
|
||||||
|
import { EditorNavigationContribution } from './theia/editor/editor-navigation-contribution';
|
||||||
|
import { EditorNavigationContribution as TheiaEditorNavigationContribution } from '@theia/editor/lib/browser/editor-navigation-contribution';
|
||||||
|
import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-generator';
|
||||||
|
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
|
||||||
|
import { AboutDialog } from './theia/core/about-dialog';
|
||||||
|
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
|
||||||
|
import {
|
||||||
|
SurveyNotificationService,
|
||||||
|
SurveyNotificationServicePath,
|
||||||
|
} from '../common/protocol/survey-service';
|
||||||
|
import { WindowContribution } from './theia/core/window-contribution';
|
||||||
|
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
|
||||||
|
import { CoreErrorHandler } from './contributions/core-error-handler';
|
||||||
|
import { CompilerErrors } from './contributions/compiler-errors';
|
||||||
|
import { WidgetManager } from './theia/core/widget-manager';
|
||||||
|
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
|
||||||
|
import { StartupTasks } from './widgets/sketchbook/startup-task';
|
||||||
|
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
|
||||||
|
import { Daemon } from './contributions/daemon';
|
||||||
|
import { FirstStartupInstaller } from './contributions/first-startup-installer';
|
||||||
|
import { OpenSketchFiles } from './contributions/open-sketch-files';
|
||||||
|
import { InoLanguage } from './contributions/ino-language';
|
||||||
|
import { SelectedBoard } from './contributions/selected-board';
|
||||||
|
import { CheckForUpdates } from './contributions/check-for-updates';
|
||||||
|
import { OpenBoardsConfig } from './contributions/open-boards-config';
|
||||||
|
import { SketchFilesTracker } from './contributions/sketch-files-tracker';
|
||||||
|
import { MonacoThemeServiceIsReady } from './utils/window';
|
||||||
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
|
import { StatusBarImpl } from './theia/core/status-bar';
|
||||||
|
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
|
||||||
|
|
||||||
MonacoThemingService.register({
|
const registerArduinoThemes = () => {
|
||||||
|
const themes: MonacoThemeJson[] = [
|
||||||
|
{
|
||||||
id: 'arduino-theme',
|
id: 'arduino-theme',
|
||||||
label: 'Light (Arduino)',
|
label: 'Light (Arduino)',
|
||||||
uiTheme: 'vs',
|
uiTheme: 'vs',
|
||||||
json: require('../../src/browser/data/arduino.color-theme.json')
|
json: require('../../src/browser/data/default.color-theme.json'),
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
id: 'arduino-theme-dark',
|
||||||
|
label: 'Dark (Arduino)',
|
||||||
|
uiTheme: 'vs-dark',
|
||||||
|
json: require('../../src/browser/data/dark.color-theme.json'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
themes.forEach((theme) => MonacoThemingService.register(theme));
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const global = window as any;
|
||||||
|
const ready = global[MonacoThemeServiceIsReady] as Deferred;
|
||||||
|
if (ready) {
|
||||||
|
ready.promise.then(registerArduinoThemes);
|
||||||
|
} else {
|
||||||
|
registerArduinoThemes();
|
||||||
|
}
|
||||||
|
|
||||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||||
ElementQueries.listen();
|
|
||||||
ElementQueries.init();
|
|
||||||
|
|
||||||
// Commands and toolbar items
|
// Commands and toolbar items
|
||||||
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
|
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
|
||||||
bind(CommandContribution).toService(ArduinoFrontendContribution);
|
bind(CommandContribution).toService(ArduinoFrontendContribution);
|
||||||
@@ -192,40 +364,75 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||||
|
|
||||||
// Library service
|
// Library service
|
||||||
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
|
bind(LibraryService)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
LibraryServicePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
// Library list widget
|
// Library list widget
|
||||||
bind(LibraryListWidget).toSelf();
|
bind(LibraryListWidget).toSelf();
|
||||||
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
|
bindViewContribution(bind, LibraryListWidgetFrontendContribution);
|
||||||
bind(WidgetFactory).toDynamicValue(context => ({
|
bind(WidgetFactory).toDynamicValue((context) => ({
|
||||||
id: LibraryListWidget.WIDGET_ID,
|
id: LibraryListWidget.WIDGET_ID,
|
||||||
createWidget: () => context.container.get(LibraryListWidget)
|
createWidget: () => context.container.get(LibraryListWidget),
|
||||||
}));
|
}));
|
||||||
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
|
bind(FrontendApplicationContribution).toService(
|
||||||
|
LibraryListWidgetFrontendContribution
|
||||||
|
);
|
||||||
|
|
||||||
// Sketch list service
|
// Sketch list service
|
||||||
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
|
bind(SketchesService)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
SketchesServicePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
bind(SketchesServiceClientImpl).toSelf().inSingletonScope();
|
bind(SketchesServiceClientImpl).toSelf().inSingletonScope();
|
||||||
bind(FrontendApplicationContribution).toService(SketchesServiceClientImpl);
|
bind(FrontendApplicationContribution).toService(SketchesServiceClientImpl);
|
||||||
|
|
||||||
// Config service
|
// Config service
|
||||||
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
|
bind(ConfigService)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
ConfigServicePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
// Boards service
|
// Boards service
|
||||||
bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope();
|
bind(BoardsService)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
BoardsServicePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
// Boards service client to receive and delegate notifications from the backend.
|
// Boards service client to receive and delegate notifications from the backend.
|
||||||
bind(BoardsServiceProvider).toSelf().inSingletonScope();
|
bind(BoardsServiceProvider).toSelf().inSingletonScope();
|
||||||
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
|
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
|
||||||
|
|
||||||
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
|
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
|
||||||
bind(FrontendApplicationContribution).to(BoardsDataMenuUpdater).inSingletonScope();
|
bind(FrontendApplicationContribution)
|
||||||
|
.to(BoardsDataMenuUpdater)
|
||||||
|
.inSingletonScope();
|
||||||
bind(BoardsDataStore).toSelf().inSingletonScope();
|
bind(BoardsDataStore).toSelf().inSingletonScope();
|
||||||
bind(FrontendApplicationContribution).toService(BoardsDataStore);
|
bind(FrontendApplicationContribution).toService(BoardsDataStore);
|
||||||
// Logger for the Arduino daemon
|
// Logger for the Arduino daemon
|
||||||
bind(ILogger).toDynamicValue(ctx => {
|
bind(ILogger)
|
||||||
|
.toDynamicValue((ctx) => {
|
||||||
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
const parentLogger = ctx.container.get<ILogger>(ILogger);
|
||||||
return parentLogger.child('store');
|
return parentLogger.child('store');
|
||||||
}).inSingletonScope().whenTargetNamed('store');
|
})
|
||||||
|
.inSingletonScope()
|
||||||
|
.whenTargetNamed('store');
|
||||||
|
|
||||||
// Boards auto-installer
|
// Boards auto-installer
|
||||||
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
|
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
|
||||||
@@ -234,86 +441,144 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
// Boards list widget
|
// Boards list widget
|
||||||
bind(BoardsListWidget).toSelf();
|
bind(BoardsListWidget).toSelf();
|
||||||
bindViewContribution(bind, BoardsListWidgetFrontendContribution);
|
bindViewContribution(bind, BoardsListWidgetFrontendContribution);
|
||||||
bind(WidgetFactory).toDynamicValue(context => ({
|
bind(WidgetFactory).toDynamicValue((context) => ({
|
||||||
id: BoardsListWidget.WIDGET_ID,
|
id: BoardsListWidget.WIDGET_ID,
|
||||||
createWidget: () => context.container.get(BoardsListWidget)
|
createWidget: () => context.container.get(BoardsListWidget),
|
||||||
}));
|
}));
|
||||||
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
|
bind(FrontendApplicationContribution).toService(
|
||||||
|
BoardsListWidgetFrontendContribution
|
||||||
|
);
|
||||||
|
|
||||||
// Board select dialog
|
// Board select dialog
|
||||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||||
bind(BoardsConfigDialog).toSelf().inSingletonScope();
|
bind(BoardsConfigDialog).toSelf().inSingletonScope();
|
||||||
bind(BoardsConfigDialogProps).toConstantValue({
|
bind(BoardsConfigDialogProps).toConstantValue({
|
||||||
title: 'Select Board'
|
title: nls.localize('arduino/common/selectBoard', 'Select Board'),
|
||||||
})
|
});
|
||||||
|
|
||||||
// Core service
|
// Core service
|
||||||
bind(CoreService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, CoreServicePath)).inSingletonScope();
|
bind(CoreService)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
CoreServicePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
bind(CoreErrorHandler).toSelf().inSingletonScope();
|
||||||
|
|
||||||
// Serial monitor
|
// Serial monitor
|
||||||
bind(MonitorModel).toSelf().inSingletonScope();
|
|
||||||
bind(FrontendApplicationContribution).toService(MonitorModel);
|
|
||||||
bind(MonitorWidget).toSelf();
|
bind(MonitorWidget).toSelf();
|
||||||
|
bind(FrontendApplicationContribution).toService(MonitorModel);
|
||||||
|
bind(MonitorModel).toSelf().inSingletonScope();
|
||||||
bindViewContribution(bind, MonitorViewContribution);
|
bindViewContribution(bind, MonitorViewContribution);
|
||||||
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
||||||
bind(WidgetFactory).toDynamicValue(context => ({
|
bind(WidgetFactory).toDynamicValue((context) => ({
|
||||||
id: MonitorWidget.ID,
|
id: MonitorWidget.ID,
|
||||||
createWidget: () => context.container.get(MonitorWidget)
|
createWidget: () => {
|
||||||
|
return new MonitorWidget(
|
||||||
|
context.container.get<MonitorModel>(MonitorModel),
|
||||||
|
context.container.get<MonitorManagerProxyClient>(
|
||||||
|
MonitorManagerProxyClient
|
||||||
|
),
|
||||||
|
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
|
||||||
|
);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
// Frontend binding for the serial monitor service
|
|
||||||
bind(MonitorService).toDynamicValue(context => {
|
bind(MonitorManagerProxyFactory).toFactory(
|
||||||
const connection = context.container.get(WebSocketConnectionProvider);
|
(context) => () =>
|
||||||
const client = context.container.get(MonitorServiceClientImpl);
|
context.container.get<MonitorManagerProxy>(MonitorManagerProxy)
|
||||||
return connection.createProxy(MonitorServicePath, client);
|
);
|
||||||
}).inSingletonScope();
|
|
||||||
bind(MonitorConnection).toSelf().inSingletonScope();
|
bind(MonitorManagerProxy)
|
||||||
// Serial monitor service client to receive and delegate notifications from the backend.
|
.toDynamicValue((context) =>
|
||||||
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
|
WebSocketConnectionProvider.createProxy(
|
||||||
bind(MonitorServiceClient).toDynamicValue(context => {
|
context.container,
|
||||||
const client = context.container.get(MonitorServiceClientImpl);
|
MonitorManagerProxyPath,
|
||||||
WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client);
|
context.container.get(MonitorManagerProxyClient)
|
||||||
return client;
|
)
|
||||||
}).inSingletonScope();
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
|
// Monitor manager proxy client to receive and delegate pluggable monitors
|
||||||
|
// notifications from the backend
|
||||||
|
bind(MonitorManagerProxyClient)
|
||||||
|
.to(MonitorManagerProxyClientImpl)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
bind(WorkspaceService).toSelf().inSingletonScope();
|
bind(WorkspaceService).toSelf().inSingletonScope();
|
||||||
rebind(TheiaWorkspaceService).toService(WorkspaceService);
|
rebind(TheiaWorkspaceService).toService(WorkspaceService);
|
||||||
bind(WorkspaceVariableContribution).toSelf().inSingletonScope();
|
bind(WorkspaceVariableContribution).toSelf().inSingletonScope();
|
||||||
rebind(TheiaWorkspaceVariableContribution).toService(WorkspaceVariableContribution);
|
rebind(TheiaWorkspaceVariableContribution).toService(
|
||||||
|
WorkspaceVariableContribution
|
||||||
|
);
|
||||||
|
|
||||||
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
|
bind(SurveyNotificationService)
|
||||||
bind(EditorMode).toSelf().inSingletonScope();
|
.toDynamicValue((context) => {
|
||||||
bind(FrontendApplicationContribution).toService(EditorMode);
|
return ElectronIpcConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
SurveyNotificationServicePath
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
// Layout and shell customizations.
|
// Layout and shell customizations.
|
||||||
rebind(TheiaOutlineViewContribution).to(OutlineViewContribution).inSingletonScope();
|
rebind(TheiaOutlineViewContribution)
|
||||||
|
.to(OutlineViewContribution)
|
||||||
|
.inSingletonScope();
|
||||||
rebind(TheiaProblemContribution).to(ProblemContribution).inSingletonScope();
|
rebind(TheiaProblemContribution).to(ProblemContribution).inSingletonScope();
|
||||||
rebind(TheiaFileNavigatorContribution).to(FileNavigatorContribution).inSingletonScope();
|
rebind(TheiaFileNavigatorContribution)
|
||||||
rebind(TheiaKeymapsFrontendContribution).to(KeymapsFrontendContribution).inSingletonScope();
|
.to(FileNavigatorContribution)
|
||||||
|
.inSingletonScope();
|
||||||
|
rebind(TheiaKeymapsFrontendContribution)
|
||||||
|
.to(KeymapsFrontendContribution)
|
||||||
|
.inSingletonScope();
|
||||||
rebind(TheiaEditorContribution).to(EditorContribution).inSingletonScope();
|
rebind(TheiaEditorContribution).to(EditorContribution).inSingletonScope();
|
||||||
rebind(TheiaMonacoStatusBarContribution).to(MonacoStatusBarContribution).inSingletonScope();
|
rebind(TheiaMonacoStatusBarContribution)
|
||||||
|
.to(MonacoStatusBarContribution)
|
||||||
|
.inSingletonScope();
|
||||||
rebind(TheiaApplicationShell).to(ApplicationShell).inSingletonScope();
|
rebind(TheiaApplicationShell).to(ApplicationShell).inSingletonScope();
|
||||||
rebind(TheiaScmContribution).to(ScmContribution).inSingletonScope();
|
rebind(TheiaScmContribution).to(ScmContribution).inSingletonScope();
|
||||||
rebind(TheiaSearchInWorkspaceFrontendContribution).to(SearchInWorkspaceFrontendContribution).inSingletonScope();
|
rebind(TheiaSearchInWorkspaceFrontendContribution)
|
||||||
|
.to(SearchInWorkspaceFrontendContribution)
|
||||||
|
.inSingletonScope();
|
||||||
rebind(TheiaFrontendApplication).to(FrontendApplication).inSingletonScope();
|
rebind(TheiaFrontendApplication).to(FrontendApplication).inSingletonScope();
|
||||||
rebind(TheiaWorkspaceFrontendContribution).to(WorkspaceFrontendContribution).inSingletonScope();
|
rebind(TheiaWorkspaceFrontendContribution)
|
||||||
rebind(TheiaFileMenuContribution).to(ArduinoFileMenuContribution).inSingletonScope();
|
.to(WorkspaceFrontendContribution)
|
||||||
rebind(TheiaCommonFrontendContribution).to(CommonFrontendContribution).inSingletonScope();
|
.inSingletonScope();
|
||||||
rebind(TheiaPreferencesContribution).to(PreferencesContribution).inSingletonScope();
|
rebind(TheiaFileMenuContribution)
|
||||||
rebind(TheiaKeybindingRegistry).to(KeybindingRegistry).inSingletonScope();
|
.to(ArduinoFileMenuContribution)
|
||||||
rebind(TheiaWorkspaceCommandContribution).to(WorkspaceCommandContribution).inSingletonScope();
|
.inSingletonScope();
|
||||||
rebind(TheiaWorkspaceDeleteHandler).to(WorkspaceDeleteHandler).inSingletonScope();
|
rebind(TheiaCommonFrontendContribution)
|
||||||
|
.to(CommonFrontendContribution)
|
||||||
|
.inSingletonScope();
|
||||||
|
rebind(TheiaPreferencesContribution)
|
||||||
|
.to(PreferencesContribution)
|
||||||
|
.inSingletonScope();
|
||||||
|
rebind(TheiaWorkspaceCommandContribution)
|
||||||
|
.to(WorkspaceCommandContribution)
|
||||||
|
.inSingletonScope();
|
||||||
|
rebind(TheiaWorkspaceDeleteHandler)
|
||||||
|
.to(WorkspaceDeleteHandler)
|
||||||
|
.inSingletonScope();
|
||||||
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
|
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
|
||||||
rebind(TabBarToolbarFactory).toFactory(({ container: parentContainer }) => () => {
|
rebind(TabBarToolbarFactory).toFactory(
|
||||||
|
({ container: parentContainer }) =>
|
||||||
|
() => {
|
||||||
const container = parentContainer.createChild();
|
const container = parentContainer.createChild();
|
||||||
container.bind(TabBarToolbar).toSelf().inSingletonScope();
|
container.bind(TabBarToolbar).toSelf().inSingletonScope();
|
||||||
return container.get(TabBarToolbar);
|
return container.get(TabBarToolbar);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
bind(OutputWidget).toSelf().inSingletonScope();
|
bind(OutputWidget).toSelf().inSingletonScope();
|
||||||
rebind(TheiaOutputWidget).toService(OutputWidget);
|
rebind(TheiaOutputWidget).toService(OutputWidget);
|
||||||
bind(OutputChannelManager).toSelf().inSingletonScope();
|
bind(OutputChannelManager).toSelf().inSingletonScope();
|
||||||
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
|
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
|
||||||
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
|
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
|
||||||
rebind(TheiaOutputChannelRegistryMainImpl).toService(OutputChannelRegistryMainImpl);
|
rebind(TheiaOutputChannelRegistryMainImpl).toService(
|
||||||
|
OutputChannelRegistryMainImpl
|
||||||
|
);
|
||||||
bind(MonacoTextModelService).toSelf().inSingletonScope();
|
bind(MonacoTextModelService).toSelf().inSingletonScope();
|
||||||
rebind(TheiaMonacoTextModelService).toService(MonacoTextModelService);
|
rebind(TheiaMonacoTextModelService).toService(MonacoTextModelService);
|
||||||
bind(MonacoEditorProvider).toSelf().inSingletonScope();
|
bind(MonacoEditorProvider).toSelf().inSingletonScope();
|
||||||
@@ -321,18 +586,36 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
|
|
||||||
bind(SearchInWorkspaceWidget).toSelf();
|
bind(SearchInWorkspaceWidget).toSelf();
|
||||||
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
|
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
|
||||||
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(({ container }) => {
|
|
||||||
|
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
|
||||||
|
bind(EditorManager).toSelf().inSingletonScope();
|
||||||
|
rebind(TheiaEditorManager).toService(EditorManager);
|
||||||
|
|
||||||
|
// replace search icon
|
||||||
|
rebind(TheiaSearchInWorkspaceFactory)
|
||||||
|
.to(SearchInWorkspaceFactory)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
|
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(
|
||||||
|
({ container }) => {
|
||||||
const childContainer = createTreeContainer(container);
|
const childContainer = createTreeContainer(container);
|
||||||
childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf()
|
childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf();
|
||||||
childContainer.rebind(TreeWidget).toService(SearchInWorkspaceResultTreeWidget);
|
childContainer
|
||||||
|
.rebind(TreeWidget)
|
||||||
|
.toService(SearchInWorkspaceResultTreeWidget);
|
||||||
return childContainer.get(SearchInWorkspaceResultTreeWidget);
|
return childContainer.get(SearchInWorkspaceResultTreeWidget);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Show a disconnected status bar, when the daemon is not available
|
// Show a disconnected status bar, when the daemon is not available
|
||||||
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
|
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
|
||||||
rebind(TheiaApplicationConnectionStatusContribution).toService(ApplicationConnectionStatusContribution);
|
rebind(TheiaApplicationConnectionStatusContribution).toService(
|
||||||
|
ApplicationConnectionStatusContribution
|
||||||
|
);
|
||||||
bind(FrontendConnectionStatusService).toSelf().inSingletonScope();
|
bind(FrontendConnectionStatusService).toSelf().inSingletonScope();
|
||||||
rebind(TheiaFrontendConnectionStatusService).toService(FrontendConnectionStatusService);
|
rebind(TheiaFrontendConnectionStatusService).toService(
|
||||||
|
FrontendConnectionStatusService
|
||||||
|
);
|
||||||
|
|
||||||
// Decorator customizations
|
// Decorator customizations
|
||||||
bind(TabBarDecoratorService).toSelf().inSingletonScope();
|
bind(TabBarDecoratorService).toSelf().inSingletonScope();
|
||||||
@@ -350,16 +633,63 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
bind(OutputToolbarContribution).toSelf().inSingletonScope();
|
bind(OutputToolbarContribution).toSelf().inSingletonScope();
|
||||||
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
|
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
|
||||||
|
|
||||||
bind(ArduinoDaemon).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ArduinoDaemonPath)).inSingletonScope();
|
// To remove `New Window` from the `File` menu
|
||||||
|
bind(WindowContribution).toSelf().inSingletonScope();
|
||||||
|
rebind(TheiaWindowContribution).toService(WindowContribution);
|
||||||
|
|
||||||
|
bind(ArduinoDaemon)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
ArduinoDaemonPath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
|
bind(Formatter)
|
||||||
|
.toDynamicValue(({ container }) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(container, FormatterPath)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
|
bind(ArduinoFirmwareUploader)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
ArduinoFirmwareUploaderPath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
// File-system extension
|
// File-system extension
|
||||||
bind(FileSystemExt).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, FileSystemExtPath)).inSingletonScope();
|
bind(FileSystemExt)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
FileSystemExtPath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
// Examples service@
|
// Examples service@
|
||||||
bind(ExamplesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ExamplesServicePath)).inSingletonScope();
|
bind(ExamplesService)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
ExamplesServicePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
// Executable URIs known by the backend
|
// Executable URIs known by the backend
|
||||||
bind(ExecutableService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ExecutableServicePath)).inSingletonScope();
|
bind(ExecutableService)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
ExecutableServicePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
Contribution.configure(bind, NewSketch);
|
Contribution.configure(bind, NewSketch);
|
||||||
Contribution.configure(bind, OpenSketch);
|
Contribution.configure(bind, OpenSketch);
|
||||||
@@ -380,33 +710,81 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
Contribution.configure(bind, About);
|
Contribution.configure(bind, About);
|
||||||
Contribution.configure(bind, Debug);
|
Contribution.configure(bind, Debug);
|
||||||
Contribution.configure(bind, Sketchbook);
|
Contribution.configure(bind, Sketchbook);
|
||||||
|
Contribution.configure(bind, UploadFirmware);
|
||||||
|
Contribution.configure(bind, UploadCertificate);
|
||||||
Contribution.configure(bind, BoardSelection);
|
Contribution.configure(bind, BoardSelection);
|
||||||
Contribution.configure(bind, OpenRecentSketch);
|
Contribution.configure(bind, OpenRecentSketch);
|
||||||
Contribution.configure(bind, Help);
|
Contribution.configure(bind, Help);
|
||||||
Contribution.configure(bind, AddFile);
|
Contribution.configure(bind, AddFile);
|
||||||
Contribution.configure(bind, ArchiveSketch);
|
Contribution.configure(bind, ArchiveSketch);
|
||||||
Contribution.configure(bind, AddZipLibrary);
|
Contribution.configure(bind, AddZipLibrary);
|
||||||
|
Contribution.configure(bind, PlotterFrontendContribution);
|
||||||
|
Contribution.configure(bind, Format);
|
||||||
|
Contribution.configure(bind, CompilerErrors);
|
||||||
|
Contribution.configure(bind, StartupTasks);
|
||||||
|
Contribution.configure(bind, IndexesUpdateProgress);
|
||||||
|
Contribution.configure(bind, Daemon);
|
||||||
|
Contribution.configure(bind, FirstStartupInstaller);
|
||||||
|
Contribution.configure(bind, OpenSketchFiles);
|
||||||
|
Contribution.configure(bind, InoLanguage);
|
||||||
|
Contribution.configure(bind, SelectedBoard);
|
||||||
|
Contribution.configure(bind, CheckForUpdates);
|
||||||
|
Contribution.configure(bind, OpenBoardsConfig);
|
||||||
|
Contribution.configure(bind, SketchFilesTracker);
|
||||||
|
|
||||||
bind(ResponseServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, responseService) => {
|
// Disabled the quick-pick customization from Theia when multiple formatters are available.
|
||||||
WebSocketConnectionProvider.createProxy(container, ResponseServicePath, responseService);
|
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
|
||||||
|
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
|
||||||
|
rebind(TheiaMonacoFormattingConflictsContribution).toService(
|
||||||
|
MonacoFormattingConflictsContribution
|
||||||
|
);
|
||||||
|
|
||||||
|
bind(ResponseServiceImpl)
|
||||||
|
.toSelf()
|
||||||
|
.inSingletonScope()
|
||||||
|
.onActivation(({ container }, responseService) => {
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
container,
|
||||||
|
ResponseServicePath,
|
||||||
|
responseService
|
||||||
|
);
|
||||||
return responseService;
|
return responseService;
|
||||||
});
|
});
|
||||||
|
|
||||||
bind(ResponseService).toService(ResponseServiceImpl);
|
bind(ResponseService).toService(ResponseServiceImpl);
|
||||||
|
bind(ResponseServiceClient).toService(ResponseServiceImpl);
|
||||||
|
|
||||||
bind(NotificationCenter).toSelf().inSingletonScope();
|
bind(NotificationCenter).toSelf().inSingletonScope();
|
||||||
bind(FrontendApplicationContribution).toService(NotificationCenter);
|
bind(FrontendApplicationContribution).toService(NotificationCenter);
|
||||||
bind(NotificationServiceServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, NotificationServicePath)).inSingletonScope();
|
bind(NotificationServiceServer)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
NotificationServicePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
// Enable the dirty indicator on uncloseable widgets.
|
// Enable the dirty indicator on uncloseable widgets.
|
||||||
rebind(TabBarRendererFactory).toFactory(context => () => {
|
rebind(TabBarRendererFactory).toFactory((context) => () => {
|
||||||
const contextMenuRenderer = context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
|
const contextMenuRenderer =
|
||||||
const decoratorService = context.container.get<TabBarDecoratorService>(TabBarDecoratorService);
|
context.container.get<ContextMenuRenderer>(ContextMenuRenderer);
|
||||||
const iconThemeService = context.container.get<IconThemeService>(IconThemeService);
|
const decoratorService = context.container.get<TabBarDecoratorService>(
|
||||||
return new TabBarRenderer(contextMenuRenderer, decoratorService, iconThemeService);
|
TabBarDecoratorService
|
||||||
|
);
|
||||||
|
const iconThemeService =
|
||||||
|
context.container.get<IconThemeService>(IconThemeService);
|
||||||
|
return new TabBarRenderer(
|
||||||
|
contextMenuRenderer,
|
||||||
|
decoratorService,
|
||||||
|
iconThemeService
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
|
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
|
||||||
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
|
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
|
||||||
|
// Note: `"editor.autoSave" was renamed to `"files.autoSave" and `"on"` was replaced with three
|
||||||
|
// different cases, but we treat `!== 'off'` as auto save enabled. (https://github.com/eclipse-theia/theia/issues/10812)
|
||||||
bind(EditorCommandContribution).toSelf().inSingletonScope();
|
bind(EditorCommandContribution).toSelf().inSingletonScope();
|
||||||
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
|
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
|
||||||
|
|
||||||
@@ -414,20 +792,49 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
|
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
|
||||||
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
|
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
|
||||||
|
|
||||||
|
// Do not fetch the `catalog.json` from Azure on FE load.
|
||||||
|
bind(DefaultJsonSchemaContribution).toSelf().inSingletonScope();
|
||||||
|
rebind(TheiaDefaultJsonSchemaContribution).toService(
|
||||||
|
DefaultJsonSchemaContribution
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do not block the app startup when initializing the editor navigation history.
|
||||||
|
bind(EditorNavigationContribution).toSelf().inSingletonScope();
|
||||||
|
rebind(TheiaEditorNavigationContribution).toService(
|
||||||
|
EditorNavigationContribution
|
||||||
|
);
|
||||||
|
|
||||||
|
// IDE2 does not use the Theia preferences widget, no need to create and sync the underlying tree model.
|
||||||
|
bind(PreferenceTreeGenerator).toSelf().inSingletonScope();
|
||||||
|
rebind(TheiaPreferenceTreeGenerator).toService(PreferenceTreeGenerator);
|
||||||
|
|
||||||
|
// IDE2 has a custom about dialog, so there is no need to load the Theia extensions on FE load
|
||||||
|
bind(AboutDialog).toSelf().inSingletonScope();
|
||||||
|
rebind(TheiaAboutDialog).toService(AboutDialog);
|
||||||
|
|
||||||
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
|
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
|
||||||
bind(DebugSessionManager).toSelf().inSingletonScope();
|
bind(DebugSessionManager).toSelf().inSingletonScope();
|
||||||
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
|
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
|
||||||
// To remove the `Run` menu item from the application menu.
|
// To remove the `Run` menu item from the application menu.
|
||||||
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
|
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
|
||||||
rebind(TheiaDebugFrontendApplicationContribution).toService(DebugFrontendApplicationContribution);
|
rebind(TheiaDebugFrontendApplicationContribution).toService(
|
||||||
|
DebugFrontendApplicationContribution
|
||||||
|
);
|
||||||
// To be able to use a `launch.json` from outside of the workspace.
|
// To be able to use a `launch.json` from outside of the workspace.
|
||||||
bind(DebugConfigurationManager).toSelf().inSingletonScope();
|
bind(DebugConfigurationManager).toSelf().inSingletonScope();
|
||||||
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
|
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
|
||||||
|
|
||||||
// Patch for the debug hover: https://github.com/eclipse-theia/theia/pull/9256/
|
// To avoid duplicate tabs use deepEqual instead of string equal: https://github.com/eclipse-theia/theia/issues/11309
|
||||||
rebind(DebugEditorModelFactory).toDynamicValue(({ container }) => <DebugEditorModelFactory>(editor =>
|
bind(WidgetManager).toSelf().inSingletonScope();
|
||||||
DebugEditorModel.createModel(container, editor)
|
rebind(TheiaWidgetManager).toService(WidgetManager);
|
||||||
)).inSingletonScope();
|
|
||||||
|
// To avoid running a status bar update on every single `keypress` event from the editor.
|
||||||
|
bind(StatusBarImpl).toSelf().inSingletonScope();
|
||||||
|
rebind(TheiaStatusBarImpl).toService(StatusBarImpl);
|
||||||
|
|
||||||
|
// Debounced update for the tab-bar toolbar when typing in the editor.
|
||||||
|
bind(DockPanelRenderer).toSelf();
|
||||||
|
rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer);
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
bindArduinoPreferences(bind);
|
bindArduinoPreferences(bind);
|
||||||
@@ -438,7 +845,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
bind(SettingsWidget).toSelf().inSingletonScope();
|
bind(SettingsWidget).toSelf().inSingletonScope();
|
||||||
bind(SettingsDialog).toSelf().inSingletonScope();
|
bind(SettingsDialog).toSelf().inSingletonScope();
|
||||||
bind(SettingsDialogProps).toConstantValue({
|
bind(SettingsDialogProps).toConstantValue({
|
||||||
title: 'Preferences'
|
title: nls.localize(
|
||||||
|
'vscode/preferences.contribution/preferences',
|
||||||
|
'Preferences'
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
bind(StorageWrapper).toSelf().inSingletonScope();
|
bind(StorageWrapper).toSelf().inSingletonScope();
|
||||||
@@ -448,4 +858,94 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
rebind(TheiaNotificationManager).toService(NotificationManager);
|
rebind(TheiaNotificationManager).toService(NotificationManager);
|
||||||
bind(NotificationsRenderer).toSelf().inSingletonScope();
|
bind(NotificationsRenderer).toSelf().inSingletonScope();
|
||||||
rebind(TheiaNotificationsRenderer).toService(NotificationsRenderer);
|
rebind(TheiaNotificationsRenderer).toService(NotificationsRenderer);
|
||||||
|
|
||||||
|
// UI for the Sketchbook
|
||||||
|
bind(SketchbookWidget).toSelf();
|
||||||
|
bind(SketchbookTreeWidget).toDynamicValue(({ container }) =>
|
||||||
|
createSketchbookTreeWidget(container)
|
||||||
|
);
|
||||||
|
bindViewContribution(bind, SketchbookWidgetContribution);
|
||||||
|
bind(FrontendApplicationContribution).toService(SketchbookWidgetContribution);
|
||||||
|
bind(WidgetFactory).toDynamicValue(({ container }) => ({
|
||||||
|
id: 'arduino-sketchbook-widget',
|
||||||
|
createWidget: () => container.get(SketchbookWidget),
|
||||||
|
}));
|
||||||
|
|
||||||
|
bind(CloudSketchbookWidget).toSelf();
|
||||||
|
rebind(SketchbookWidget).toService(CloudSketchbookWidget);
|
||||||
|
bind(CloudSketchbookTreeWidget).toDynamicValue(({ container }) =>
|
||||||
|
createCloudSketchbookTreeWidget(container)
|
||||||
|
);
|
||||||
|
bind(CreateApi).toSelf().inSingletonScope();
|
||||||
|
bind(SketchCache).toSelf().inSingletonScope();
|
||||||
|
|
||||||
|
bind(ShareSketchDialog).toSelf().inSingletonScope();
|
||||||
|
bind(AuthenticationClientService).toSelf().inSingletonScope();
|
||||||
|
bind(CommandContribution).toService(AuthenticationClientService);
|
||||||
|
bind(FrontendApplicationContribution).toService(AuthenticationClientService);
|
||||||
|
bind(AuthenticationService)
|
||||||
|
.toDynamicValue((context) =>
|
||||||
|
WebSocketConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
AuthenticationServicePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.inSingletonScope();
|
||||||
|
bind(CreateFsProvider).toSelf().inSingletonScope();
|
||||||
|
bind(FrontendApplicationContribution).toService(CreateFsProvider);
|
||||||
|
bind(FileServiceContribution).toService(CreateFsProvider);
|
||||||
|
bind(CloudSketchbookContribution).toSelf().inSingletonScope();
|
||||||
|
bind(CommandContribution).toService(CloudSketchbookContribution);
|
||||||
|
bind(LocalCacheFsProvider).toSelf().inSingletonScope();
|
||||||
|
bind(FileServiceContribution).toService(LocalCacheFsProvider);
|
||||||
|
bind(CloudSketchbookCompositeWidget).toSelf();
|
||||||
|
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
|
||||||
|
id: 'cloud-sketchbook-composite-widget',
|
||||||
|
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
|
||||||
|
}));
|
||||||
|
|
||||||
|
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
|
||||||
|
bind(UploadFirmwareDialog).toSelf().inSingletonScope();
|
||||||
|
bind(UploadFirmwareDialogProps).toConstantValue({
|
||||||
|
title: 'UploadFirmware',
|
||||||
|
});
|
||||||
|
bind(UploadCertificateDialogWidget).toSelf().inSingletonScope();
|
||||||
|
bind(UploadCertificateDialog).toSelf().inSingletonScope();
|
||||||
|
bind(UploadCertificateDialogProps).toConstantValue({
|
||||||
|
title: 'UploadCertificate',
|
||||||
|
});
|
||||||
|
|
||||||
|
bind(IDEUpdaterDialogWidget).toSelf().inSingletonScope();
|
||||||
|
bind(IDEUpdaterDialog).toSelf().inSingletonScope();
|
||||||
|
bind(IDEUpdaterDialogProps).toConstantValue({
|
||||||
|
title: 'IDEUpdater',
|
||||||
|
});
|
||||||
|
|
||||||
|
bind(UserFieldsDialogWidget).toSelf().inSingletonScope();
|
||||||
|
bind(UserFieldsDialog).toSelf().inSingletonScope();
|
||||||
|
bind(UserFieldsDialogProps).toConstantValue({
|
||||||
|
title: 'UserFields',
|
||||||
|
});
|
||||||
|
|
||||||
|
bind(IDEUpdaterCommands).toSelf().inSingletonScope();
|
||||||
|
bind(CommandContribution).toService(IDEUpdaterCommands);
|
||||||
|
|
||||||
|
// Frontend binding for the IDE Updater service
|
||||||
|
bind(IDEUpdaterClientImpl).toSelf().inSingletonScope();
|
||||||
|
bind(IDEUpdaterClient).toService(IDEUpdaterClientImpl);
|
||||||
|
bind(IDEUpdater)
|
||||||
|
.toDynamicValue((context) => {
|
||||||
|
const client = context.container.get(IDEUpdaterClientImpl);
|
||||||
|
return ElectronIpcConnectionProvider.createProxy(
|
||||||
|
context.container,
|
||||||
|
IDEUpdaterPath,
|
||||||
|
client
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.inSingletonScope();
|
||||||
|
|
||||||
|
bind(HostedPluginSupport).toSelf().inSingletonScope();
|
||||||
|
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
|
||||||
|
bind(HostedPluginEvents).toSelf().inSingletonScope();
|
||||||
|
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,74 +1,286 @@
|
|||||||
import { interfaces } from 'inversify';
|
import { interfaces } from '@theia/core/shared/inversify';
|
||||||
import { createPreferenceProxy, PreferenceProxy, PreferenceService, PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser/preferences';
|
import {
|
||||||
|
createPreferenceProxy,
|
||||||
|
PreferenceProxy,
|
||||||
|
PreferenceService,
|
||||||
|
PreferenceContribution,
|
||||||
|
PreferenceSchema,
|
||||||
|
} from '@theia/core/lib/browser/preferences';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol';
|
import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol';
|
||||||
|
|
||||||
|
export enum UpdateChannel {
|
||||||
|
Stable = 'stable',
|
||||||
|
Nightly = 'nightly',
|
||||||
|
}
|
||||||
|
export const ErrorRevealStrategyLiterals = [
|
||||||
|
/**
|
||||||
|
* Scroll vertically as necessary and reveal a line.
|
||||||
|
*/
|
||||||
|
'auto',
|
||||||
|
/**
|
||||||
|
* Scroll vertically as necessary and reveal a line centered vertically.
|
||||||
|
*/
|
||||||
|
'center',
|
||||||
|
/**
|
||||||
|
* Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition.
|
||||||
|
*/
|
||||||
|
'top',
|
||||||
|
/**
|
||||||
|
* Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport.
|
||||||
|
*/
|
||||||
|
'centerIfOutsideViewport',
|
||||||
|
] as const;
|
||||||
|
export type ErrorRevealStrategy = typeof ErrorRevealStrategyLiterals[number];
|
||||||
|
export namespace ErrorRevealStrategy {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
||||||
|
export function is(arg: any): arg is ErrorRevealStrategy {
|
||||||
|
return !!arg && ErrorRevealStrategyLiterals.includes(arg);
|
||||||
|
}
|
||||||
|
export const Default: ErrorRevealStrategy = 'centerIfOutsideViewport';
|
||||||
|
}
|
||||||
|
|
||||||
export const ArduinoConfigSchema: PreferenceSchema = {
|
export const ArduinoConfigSchema: PreferenceSchema = {
|
||||||
'type': 'object',
|
type: 'object',
|
||||||
'properties': {
|
properties: {
|
||||||
'arduino.language.log': {
|
'arduino.language.log': {
|
||||||
'type': 'boolean',
|
type: 'boolean',
|
||||||
'description': "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
|
description: nls.localize(
|
||||||
'default': false
|
'arduino/preferences/language.log',
|
||||||
|
"True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default."
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
'arduino.language.realTimeDiagnostics': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/language.realTimeDiagnostics',
|
||||||
|
"If true, the language server provides real-time diagnostics when typing in the editor. It's false by default."
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
'arduino.compile.verbose': {
|
'arduino.compile.verbose': {
|
||||||
'type': 'boolean',
|
type: 'boolean',
|
||||||
'description': 'True for verbose compile output. False by default',
|
description: nls.localize(
|
||||||
'default': false
|
'arduino/preferences/compile.verbose',
|
||||||
|
'True for verbose compile output. False by default'
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
'arduino.compile.experimental': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/compile.experimental',
|
||||||
|
'True if the IDE should handle multiple compiler errors. False by default'
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
'arduino.compile.revealRange': {
|
||||||
|
enum: [...ErrorRevealStrategyLiterals],
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/compile.revealRange',
|
||||||
|
"Adjusts how compiler errors are revealed in the editor after a failed verify/upload. Possible values: 'auto': Scroll vertically as necessary and reveal a line. 'center': Scroll vertically as necessary and reveal a line centered vertically. 'top': Scroll vertically as necessary and reveal a line close to the top of the viewport, optimized for viewing a code definition. 'centerIfOutsideViewport': Scroll vertically as necessary and reveal a line centered vertically only if it lies outside the viewport. The default value is '{0}'.",
|
||||||
|
ErrorRevealStrategy.Default
|
||||||
|
),
|
||||||
|
default: ErrorRevealStrategy.Default,
|
||||||
},
|
},
|
||||||
'arduino.compile.warnings': {
|
'arduino.compile.warnings': {
|
||||||
'enum': [...CompilerWarningLiterals],
|
enum: [...CompilerWarningLiterals],
|
||||||
'description': "Tells gcc which warning level to use. It's 'None' by default",
|
description: nls.localize(
|
||||||
'default': 'None'
|
'arduino/preferences/compile.warnings',
|
||||||
|
"Tells gcc which warning level to use. It's 'None' by default"
|
||||||
|
),
|
||||||
|
default: 'None',
|
||||||
},
|
},
|
||||||
'arduino.upload.verbose': {
|
'arduino.upload.verbose': {
|
||||||
'type': 'boolean',
|
type: 'boolean',
|
||||||
'description': 'True for verbose upload output. False by default.',
|
description: nls.localize(
|
||||||
'default': false
|
'arduino/preferences/upload.verbose',
|
||||||
|
'True for verbose upload output. False by default.'
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
'arduino.upload.verify': {
|
'arduino.upload.verify': {
|
||||||
'type': 'boolean',
|
type: 'boolean',
|
||||||
'default': false
|
default: false,
|
||||||
},
|
},
|
||||||
'arduino.window.autoScale': {
|
'arduino.window.autoScale': {
|
||||||
'type': 'boolean',
|
type: 'boolean',
|
||||||
'description': 'True if the user interface automatically scales with the font size.',
|
description: nls.localize(
|
||||||
'default': true
|
'arduino/preferences/window.autoScale',
|
||||||
|
'True if the user interface automatically scales with the font size.'
|
||||||
|
),
|
||||||
|
default: true,
|
||||||
},
|
},
|
||||||
'arduino.window.zoomLevel': {
|
'arduino.window.zoomLevel': {
|
||||||
'type': 'number',
|
type: 'number',
|
||||||
'description': 'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.',
|
description: nls.localize(
|
||||||
'default': 0
|
'arduino/preferences/window.zoomLevel',
|
||||||
|
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.'
|
||||||
|
),
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
'arduino.ide.updateChannel': {
|
||||||
|
type: 'string',
|
||||||
|
enum: Object.values(UpdateChannel) as UpdateChannel[],
|
||||||
|
default: UpdateChannel.Stable,
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/ide.updateChannel',
|
||||||
|
"Release channel to get updated from. 'stable' is the stable release, 'nightly' is the latest development build."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'arduino.ide.updateBaseUrl': {
|
||||||
|
type: 'string',
|
||||||
|
default: 'https://downloads.arduino.cc/arduino-ide',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/ide.updateBaseUrl',
|
||||||
|
"The base URL where to download updates from. Defaults to 'https://downloads.arduino.cc/arduino-ide'"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'arduino.board.certificates': {
|
||||||
|
type: 'string',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/board.certificates',
|
||||||
|
'List of certificates that can be uploaded to boards'
|
||||||
|
),
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
'arduino.sketchbook.showAllFiles': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/sketchbook.showAllFiles',
|
||||||
|
'True to show all sketch files inside the sketch. It is false by default.'
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
'arduino.cloud.enabled': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/cloud.enabled',
|
||||||
|
'True if the sketch sync functions are enabled. Defaults to true.'
|
||||||
|
),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
'arduino.cloud.pull.warn': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/cloud.pull.warn',
|
||||||
|
'True if users should be warned before pulling a cloud sketch. Defaults to true.'
|
||||||
|
),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
'arduino.cloud.push.warn': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/cloud.push.warn',
|
||||||
|
'True if users should be warned before pushing a cloud sketch. Defaults to true.'
|
||||||
|
),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
'arduino.cloud.pushpublic.warn': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/cloud.pushpublic.warn',
|
||||||
|
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.'
|
||||||
|
),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
'arduino.cloud.sketchSyncEndpoint': {
|
||||||
|
type: 'string',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/cloud.sketchSyncEndpoint',
|
||||||
|
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.'
|
||||||
|
),
|
||||||
|
default: 'https://api2.arduino.cc/create',
|
||||||
|
},
|
||||||
|
'arduino.auth.clientID': {
|
||||||
|
type: 'string',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/auth.clientID',
|
||||||
|
'The OAuth2 client ID.'
|
||||||
|
),
|
||||||
|
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
|
||||||
|
},
|
||||||
|
'arduino.auth.domain': {
|
||||||
|
type: 'string',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/auth.domain',
|
||||||
|
'The OAuth2 domain.'
|
||||||
|
),
|
||||||
|
default: 'login.arduino.cc',
|
||||||
|
},
|
||||||
|
'arduino.auth.audience': {
|
||||||
|
type: 'string',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/auth.audience',
|
||||||
|
'The OAuth2 audience.'
|
||||||
|
),
|
||||||
|
default: 'https://api.arduino.cc',
|
||||||
|
},
|
||||||
|
'arduino.auth.registerUri': {
|
||||||
|
type: 'string',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/auth.registerUri',
|
||||||
|
'The URI used to register a new user.'
|
||||||
|
),
|
||||||
|
default: 'https://auth.arduino.cc/login#/register',
|
||||||
|
},
|
||||||
|
'arduino.survey.notification': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/survey.notification',
|
||||||
|
'True if users should be notified if a survey is available. True by default.'
|
||||||
|
),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
'arduino.cli.daemon.debug': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/cli.daemonDebug',
|
||||||
|
"Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default."
|
||||||
|
),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'arduino.ide.autoUpdate': {
|
|
||||||
'type': 'boolean',
|
|
||||||
'description': 'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
|
|
||||||
'default': true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ArduinoConfiguration {
|
export interface ArduinoConfiguration {
|
||||||
'arduino.language.log': boolean;
|
'arduino.language.log': boolean;
|
||||||
|
'arduino.language.realTimeDiagnostics': boolean;
|
||||||
'arduino.compile.verbose': boolean;
|
'arduino.compile.verbose': boolean;
|
||||||
|
'arduino.compile.experimental': boolean;
|
||||||
|
'arduino.compile.revealRange': ErrorRevealStrategy;
|
||||||
'arduino.compile.warnings': CompilerWarnings;
|
'arduino.compile.warnings': CompilerWarnings;
|
||||||
'arduino.upload.verbose': boolean;
|
'arduino.upload.verbose': boolean;
|
||||||
'arduino.upload.verify': boolean;
|
'arduino.upload.verify': boolean;
|
||||||
'arduino.window.autoScale': boolean;
|
'arduino.window.autoScale': boolean;
|
||||||
'arduino.window.zoomLevel': number;
|
'arduino.window.zoomLevel': number;
|
||||||
'arduino.ide.autoUpdate': boolean;
|
'arduino.ide.updateChannel': UpdateChannel;
|
||||||
|
'arduino.ide.updateBaseUrl': string;
|
||||||
|
'arduino.board.certificates': string;
|
||||||
|
'arduino.sketchbook.showAllFiles': boolean;
|
||||||
|
'arduino.cloud.enabled': boolean;
|
||||||
|
'arduino.cloud.pull.warn': boolean;
|
||||||
|
'arduino.cloud.push.warn': boolean;
|
||||||
|
'arduino.cloud.pushpublic.warn': boolean;
|
||||||
|
'arduino.cloud.sketchSyncEndpoint': string;
|
||||||
|
'arduino.auth.clientID': string;
|
||||||
|
'arduino.auth.domain': string;
|
||||||
|
'arduino.auth.audience': string;
|
||||||
|
'arduino.auth.registerUri': string;
|
||||||
|
'arduino.survey.notification': boolean;
|
||||||
|
'arduino.cli.daemon.debug': boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ArduinoPreferences = Symbol('ArduinoPreferences');
|
export const ArduinoPreferences = Symbol('ArduinoPreferences');
|
||||||
export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>;
|
export type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>;
|
||||||
|
|
||||||
export function createArduinoPreferences(preferences: PreferenceService): ArduinoPreferences {
|
|
||||||
return createPreferenceProxy(preferences, ArduinoConfigSchema);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bindArduinoPreferences(bind: interfaces.Bind): void {
|
export function bindArduinoPreferences(bind: interfaces.Bind): void {
|
||||||
bind(ArduinoPreferences).toDynamicValue(ctx => {
|
bind(ArduinoPreferences).toDynamicValue((ctx) => {
|
||||||
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
||||||
return createArduinoPreferences(preferences);
|
return createPreferenceProxy(preferences, ArduinoConfigSchema);
|
||||||
|
});
|
||||||
|
bind(PreferenceContribution).toConstantValue({
|
||||||
|
schema: ArduinoConfigSchema,
|
||||||
});
|
});
|
||||||
bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema });
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { toUnix } from 'upath';
|
import { URI } from '@theia/core/shared/vscode-uri';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
|
||||||
import { isWindows } from '@theia/core/lib/common/os';
|
import { isWindows } from '@theia/core/lib/common/os';
|
||||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||||
@@ -24,20 +23,24 @@ namespace ArduinoWorkspaceRootResolver {
|
|||||||
readonly isValid: (uri: string) => MaybePromise<boolean>;
|
readonly isValid: (uri: string) => MaybePromise<boolean>;
|
||||||
}
|
}
|
||||||
export interface ResolveOptions {
|
export interface ResolveOptions {
|
||||||
readonly hash?: string
|
readonly hash?: string;
|
||||||
readonly recentWorkspaces: string[];
|
readonly recentWorkspaces: string[];
|
||||||
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
|
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
|
||||||
readonly recentSketches: string[];
|
readonly recentSketches: string[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class ArduinoWorkspaceRootResolver {
|
export class ArduinoWorkspaceRootResolver {
|
||||||
|
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {}
|
||||||
|
|
||||||
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {
|
async resolve(
|
||||||
}
|
options: ArduinoWorkspaceRootResolver.ResolveOptions
|
||||||
|
): Promise<{ uri: string } | undefined> {
|
||||||
async resolve(options: ArduinoWorkspaceRootResolver.ResolveOptions): Promise<{ uri: string } | undefined> {
|
|
||||||
const { hash, recentWorkspaces, recentSketches } = options;
|
const { hash, recentWorkspaces, recentSketches } = options;
|
||||||
for (const uri of [this.hashToUri(hash), ...recentWorkspaces, ...recentSketches].filter(notEmpty)) {
|
for (const uri of [
|
||||||
|
this.hashToUri(hash),
|
||||||
|
...recentWorkspaces,
|
||||||
|
...recentSketches,
|
||||||
|
].filter(notEmpty)) {
|
||||||
const valid = await this.isValid(uri);
|
const valid = await this.isValid(uri);
|
||||||
if (valid) {
|
if (valid) {
|
||||||
return { uri };
|
return { uri };
|
||||||
@@ -56,13 +59,10 @@ export class ArduinoWorkspaceRootResolver {
|
|||||||
// - 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#L143 and
|
||||||
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
|
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
|
||||||
protected hashToUri(hash: string | undefined): string | undefined {
|
protected hashToUri(hash: string | undefined): string | undefined {
|
||||||
if (hash
|
if (hash && hash.length > 1 && hash.startsWith('#')) {
|
||||||
&& hash.length > 1
|
const path = decodeURI(hash.slice(1)).replace(/\\/g, '/'); // Trim the leading `#`, decode the URI and replace Windows separators
|
||||||
&& hash.startsWith('#')) {
|
return URI.file(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)).toString();
|
||||||
const path = hash.slice(1); // Trim the leading `#`.
|
|
||||||
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { Emitter } from '@theia/core/lib/common/event';
|
||||||
|
import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||||
|
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||||
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
|
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||||
|
import {
|
||||||
|
CommandRegistry,
|
||||||
|
CommandContribution,
|
||||||
|
} from '@theia/core/lib/common/command';
|
||||||
|
import {
|
||||||
|
AuthenticationService,
|
||||||
|
AuthenticationServiceClient,
|
||||||
|
AuthenticationSession,
|
||||||
|
} from '../../common/protocol/authentication-service';
|
||||||
|
import { CloudUserCommands } from './cloud-user-commands';
|
||||||
|
import { serverPort } from '../../node/auth/authentication-server';
|
||||||
|
import { AuthOptions } from '../../node/auth/types';
|
||||||
|
import { ArduinoPreferences } from '../arduino-preferences';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class AuthenticationClientService
|
||||||
|
implements
|
||||||
|
FrontendApplicationContribution,
|
||||||
|
CommandContribution,
|
||||||
|
AuthenticationServiceClient
|
||||||
|
{
|
||||||
|
@inject(AuthenticationService)
|
||||||
|
protected readonly service: JsonRpcProxy<AuthenticationService>;
|
||||||
|
|
||||||
|
@inject(WindowService)
|
||||||
|
protected readonly windowService: WindowService;
|
||||||
|
|
||||||
|
@inject(ArduinoPreferences)
|
||||||
|
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||||
|
|
||||||
|
protected authOptions: AuthOptions;
|
||||||
|
protected _session: AuthenticationSession | undefined;
|
||||||
|
protected readonly toDispose = new DisposableCollection();
|
||||||
|
protected readonly onSessionDidChangeEmitter = new Emitter<
|
||||||
|
AuthenticationSession | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
readonly onSessionDidChange = this.onSessionDidChangeEmitter.event;
|
||||||
|
|
||||||
|
async onStart(): Promise<void> {
|
||||||
|
this.toDispose.push(this.onSessionDidChangeEmitter);
|
||||||
|
this.service.setClient(this);
|
||||||
|
this.service
|
||||||
|
.session()
|
||||||
|
.then((session) => this.notifySessionDidChange(session));
|
||||||
|
|
||||||
|
this.setOptions().then(() => this.service.initAuthSession());
|
||||||
|
|
||||||
|
this.arduinoPreferences.onPreferenceChanged((event) => {
|
||||||
|
if (event.preferenceName.startsWith('arduino.auth.')) {
|
||||||
|
this.setOptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setOptions(): Promise<void> {
|
||||||
|
return this.service.setOptions({
|
||||||
|
redirectUri: `http://localhost:${serverPort}/callback`,
|
||||||
|
responseType: 'code',
|
||||||
|
clientID: this.arduinoPreferences['arduino.auth.clientID'],
|
||||||
|
domain: this.arduinoPreferences['arduino.auth.domain'],
|
||||||
|
audience: this.arduinoPreferences['arduino.auth.audience'],
|
||||||
|
registerUri: this.arduinoPreferences['arduino.auth.registerUri'],
|
||||||
|
scopes: ['openid', 'profile', 'email', 'offline_access'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateSession(session?: AuthenticationSession | undefined) {
|
||||||
|
this._session = session;
|
||||||
|
this.onSessionDidChangeEmitter.fire(this._session);
|
||||||
|
}
|
||||||
|
|
||||||
|
get session(): AuthenticationSession | undefined {
|
||||||
|
return this._session;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(CloudUserCommands.LOGIN, {
|
||||||
|
execute: () => this.service.login(),
|
||||||
|
});
|
||||||
|
registry.registerCommand(CloudUserCommands.LOGOUT, {
|
||||||
|
execute: () => this.service.logout(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
notifySessionDidChange(session: AuthenticationSession | undefined): void {
|
||||||
|
this.updateSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Command } from '@theia/core/lib/common/command';
|
||||||
|
|
||||||
|
export namespace CloudUserCommands {
|
||||||
|
export const LOGIN = Command.toLocalizedCommand(
|
||||||
|
{
|
||||||
|
id: 'arduino-cloud--login',
|
||||||
|
label: 'Sign in',
|
||||||
|
},
|
||||||
|
'arduino/cloud/signIn'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LOGOUT = Command.toLocalizedCommand(
|
||||||
|
{
|
||||||
|
id: 'arduino-cloud--logout',
|
||||||
|
label: 'Sign Out',
|
||||||
|
},
|
||||||
|
'arduino/cloud/signOut'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
|
||||||
|
id: 'arduino-cloud-sketchbook--open-profile-menu',
|
||||||
|
label: 'Contextual menu',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,19 +1,40 @@
|
|||||||
import { injectable, inject } from 'inversify';
|
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||||
import { BoardsService, BoardsPackage, Board } from '../../common/protocol/boards-service';
|
import {
|
||||||
|
BoardsService,
|
||||||
|
BoardsPackage,
|
||||||
|
Board,
|
||||||
|
Port,
|
||||||
|
} from '../../common/protocol/boards-service';
|
||||||
import { BoardsServiceProvider } from './boards-service-provider';
|
import { BoardsServiceProvider } from './boards-service-provider';
|
||||||
|
import { Installable, ResponseServiceClient } from '../../common/protocol';
|
||||||
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
|
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
|
||||||
import { BoardsConfig } from './boards-config';
|
import { nls } from '@theia/core/lib/common';
|
||||||
import { Installable } from '../../common/protocol';
|
import { NotificationCenter } from '../notification-center';
|
||||||
import { ResponseServiceImpl } from '../response-service-impl';
|
|
||||||
|
interface AutoInstallPromptAction {
|
||||||
|
// isAcceptance, whether or not the action indicates acceptance of auto-install proposal
|
||||||
|
isAcceptance?: boolean;
|
||||||
|
key: string;
|
||||||
|
handler: (...args: unknown[]) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AutoInstallPromptActions = AutoInstallPromptAction[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
|
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
|
||||||
* have the corresponding core installed, it proposes the user to install the core.
|
* have the corresponding core installed, it proposes the user to install the core.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// * Cases in which we do not show the auto-install prompt:
|
||||||
|
// 1. When a related platform is already installed
|
||||||
|
// 2. When a prompt is already showing in the UI
|
||||||
|
// 3. When a board is unplugged
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||||
|
@inject(NotificationCenter)
|
||||||
|
private readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
@inject(MessageService)
|
@inject(MessageService)
|
||||||
protected readonly messageService: MessageService;
|
protected readonly messageService: MessageService;
|
||||||
@@ -24,8 +45,8 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
|||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||||
|
|
||||||
@inject(ResponseServiceImpl)
|
@inject(ResponseServiceClient)
|
||||||
protected readonly responseService: ResponseServiceImpl;
|
protected readonly responseService: ResponseServiceClient;
|
||||||
|
|
||||||
@inject(BoardsListWidgetFrontendContribution)
|
@inject(BoardsListWidgetFrontendContribution)
|
||||||
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||||
@@ -33,58 +54,228 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
|||||||
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
|
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
|
||||||
protected notifications: Board[] = [];
|
protected notifications: Board[] = [];
|
||||||
|
|
||||||
|
// * "refusal" meaning a "prompt action" not accepting the auto-install offer ("X" or "install manually")
|
||||||
|
// we can use "portSelectedOnLastRefusal" to deduce when a board is unplugged after a user has "refused"
|
||||||
|
// an auto-install prompt. Important to know as we do not want "an unplug" to trigger a "refused" prompt
|
||||||
|
// showing again
|
||||||
|
private portSelectedOnLastRefusal: Port | undefined;
|
||||||
|
private lastRefusedPackageId: string | undefined;
|
||||||
|
|
||||||
onStart(): void {
|
onStart(): void {
|
||||||
this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this));
|
const setEventListeners = () => {
|
||||||
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
|
this.boardsServiceClient.onBoardsConfigChanged((config) => {
|
||||||
|
const { selectedBoard, selectedPort } = config;
|
||||||
|
|
||||||
|
const boardWasUnplugged =
|
||||||
|
!selectedPort && this.portSelectedOnLastRefusal;
|
||||||
|
|
||||||
|
this.clearLastRefusedPromptInfo();
|
||||||
|
|
||||||
|
if (
|
||||||
|
boardWasUnplugged ||
|
||||||
|
!selectedBoard ||
|
||||||
|
this.promptAlreadyShowingForBoard(selectedBoard)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ensureCoreExists(config: BoardsConfig.Config): void {
|
this.ensureCoreExists(selectedBoard, selectedPort);
|
||||||
const { selectedBoard } = config;
|
});
|
||||||
if (selectedBoard && !this.notifications.find(board => Board.sameAs(board, selectedBoard))) {
|
|
||||||
this.notifications.push(selectedBoard);
|
|
||||||
this.boardsService.search({}).then(packages => {
|
|
||||||
|
|
||||||
|
// we "clearRefusedPackageInfo" if a "refused" package is eventually
|
||||||
|
// installed, though this is not strictly necessary. It's more of a
|
||||||
|
// cleanup, to ensure the related variables are representative of
|
||||||
|
// current state.
|
||||||
|
this.notificationCenter.onPlatformDidInstall((installed) => {
|
||||||
|
if (this.lastRefusedPackageId === installed.item.id) {
|
||||||
|
this.clearLastRefusedPromptInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// we should invoke this.ensureCoreExists only once we're sure
|
||||||
|
// everything has been reconciled
|
||||||
|
this.boardsServiceClient.reconciled.then(() => {
|
||||||
|
const { selectedBoard, selectedPort } =
|
||||||
|
this.boardsServiceClient.boardsConfig;
|
||||||
|
|
||||||
|
if (selectedBoard) {
|
||||||
|
this.ensureCoreExists(selectedBoard, selectedPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeNotificationByBoard(selectedBoard: Board): void {
|
||||||
|
const index = this.notifications.findIndex((notification) =>
|
||||||
|
Board.sameAs(notification, selectedBoard)
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.notifications.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearLastRefusedPromptInfo(): void {
|
||||||
|
this.lastRefusedPackageId = undefined;
|
||||||
|
this.portSelectedOnLastRefusal = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setLastRefusedPromptInfo(
|
||||||
|
packageId: string,
|
||||||
|
selectedPort?: Port
|
||||||
|
): void {
|
||||||
|
this.lastRefusedPackageId = packageId;
|
||||||
|
this.portSelectedOnLastRefusal = selectedPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
private promptAlreadyShowingForBoard(board: Board): boolean {
|
||||||
|
return Boolean(
|
||||||
|
this.notifications.find((notification) =>
|
||||||
|
Board.sameAs(notification, board)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ensureCoreExists(selectedBoard: Board, selectedPort?: Port): void {
|
||||||
|
this.notifications.push(selectedBoard);
|
||||||
|
this.boardsService.search({}).then((packages) => {
|
||||||
|
const candidate = this.getInstallCandidate(packages, selectedBoard);
|
||||||
|
|
||||||
|
if (candidate) {
|
||||||
|
this.showAutoInstallPrompt(candidate, selectedBoard, selectedPort);
|
||||||
|
} else {
|
||||||
|
this.removeNotificationByBoard(selectedBoard);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getInstallCandidate(
|
||||||
|
packages: BoardsPackage[],
|
||||||
|
selectedBoard: Board
|
||||||
|
): BoardsPackage | undefined {
|
||||||
// filter packagesForBoard selecting matches from the cli (installed packages)
|
// filter packagesForBoard selecting matches from the cli (installed packages)
|
||||||
// and matches based on the board name
|
// and matches based on the board name
|
||||||
// NOTE: this ensures the Deprecated & new packages are all in the array
|
// NOTE: this ensures the Deprecated & new packages are all in the array
|
||||||
// so that we can check if any of the valid packages is already installed
|
// so that we can check if any of the valid packages is already installed
|
||||||
const packagesForBoard = packages.filter(pkg => BoardsPackage.contains(selectedBoard, pkg) || pkg.boards.some(board => board.name === selectedBoard.name));
|
const packagesForBoard = packages.filter(
|
||||||
|
(pkg) =>
|
||||||
|
BoardsPackage.contains(selectedBoard, pkg) ||
|
||||||
|
pkg.boards.some((board) => board.name === selectedBoard.name)
|
||||||
|
);
|
||||||
|
|
||||||
// check if one of the packages for the board is already installed. if so, no hint
|
// check if one of the packages for the board is already installed. if so, no hint
|
||||||
if (packagesForBoard.some(({ installedVersion }) => !!installedVersion)) { return; }
|
if (packagesForBoard.some(({ installedVersion }) => !!installedVersion)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// filter the installable (not installed) packages,
|
// filter the installable (not installed) packages,
|
||||||
// CLI returns the packages already sorted with the deprecated ones at the end of the list
|
// CLI returns the packages already sorted with the deprecated ones at the end of the list
|
||||||
// in order to ensure the new ones are preferred
|
// in order to ensure the new ones are preferred
|
||||||
const candidates = packagesForBoard
|
const candidates = packagesForBoard.filter(
|
||||||
.filter(({ installable, installedVersion }) => installable && !installedVersion);
|
({ installable, installedVersion }) => installable && !installedVersion
|
||||||
|
);
|
||||||
|
|
||||||
const candidate = candidates[0];
|
return candidates[0];
|
||||||
if (candidate) {
|
|
||||||
const version = candidate.availableVersions[0] ? `[v ${candidate.availableVersions[0]}]` : '';
|
|
||||||
// tslint:disable-next-line:max-line-length
|
|
||||||
this.messageService.info(`The \`"${candidate.name} ${version}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Install Manually', 'Yes').then(async answer => {
|
|
||||||
const index = this.notifications.findIndex(board => Board.sameAs(board, selectedBoard));
|
|
||||||
if (index !== -1) {
|
|
||||||
this.notifications.splice(index, 1);
|
|
||||||
}
|
}
|
||||||
if (answer === 'Yes') {
|
|
||||||
await Installable.installWithProgress({
|
private showAutoInstallPrompt(
|
||||||
|
candidate: BoardsPackage,
|
||||||
|
selectedBoard: Board,
|
||||||
|
selectedPort?: Port
|
||||||
|
): void {
|
||||||
|
const candidateName = candidate.name;
|
||||||
|
const version = candidate.availableVersions[0]
|
||||||
|
? `[v ${candidate.availableVersions[0]}]`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const info = this.generatePromptInfoText(
|
||||||
|
candidateName,
|
||||||
|
version,
|
||||||
|
selectedBoard.name
|
||||||
|
);
|
||||||
|
|
||||||
|
const actions = this.createPromptActions(candidate);
|
||||||
|
|
||||||
|
const onRefuse = () => {
|
||||||
|
this.setLastRefusedPromptInfo(candidate.id, selectedPort);
|
||||||
|
};
|
||||||
|
const handleAction = this.createOnAnswerHandler(actions, onRefuse);
|
||||||
|
|
||||||
|
const onAnswer = (answer: string) => {
|
||||||
|
this.removeNotificationByBoard(selectedBoard);
|
||||||
|
|
||||||
|
handleAction(answer);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.messageService
|
||||||
|
.info(info, ...actions.map((action) => action.key))
|
||||||
|
.then(onAnswer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generatePromptInfoText(
|
||||||
|
candidateName: string,
|
||||||
|
version: string,
|
||||||
|
boardName: string
|
||||||
|
): string {
|
||||||
|
return nls.localize(
|
||||||
|
'arduino/board/installNow',
|
||||||
|
'The "{0} {1}" core has to be installed for the currently selected "{2}" board. Do you want to install it now?',
|
||||||
|
candidateName,
|
||||||
|
version,
|
||||||
|
boardName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPromptActions(
|
||||||
|
candidate: BoardsPackage
|
||||||
|
): AutoInstallPromptActions {
|
||||||
|
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||||
|
const manualInstall = nls.localize(
|
||||||
|
'arduino/board/installManually',
|
||||||
|
'Install Manually'
|
||||||
|
);
|
||||||
|
|
||||||
|
const actions: AutoInstallPromptActions = [
|
||||||
|
{
|
||||||
|
key: manualInstall,
|
||||||
|
handler: () => {
|
||||||
|
this.boardsManagerFrontendContribution
|
||||||
|
.openView({ reveal: true })
|
||||||
|
.then((widget) =>
|
||||||
|
widget.refresh(candidate.name.toLocaleLowerCase())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isAcceptance: true,
|
||||||
|
key: yes,
|
||||||
|
handler: () => {
|
||||||
|
return Installable.installWithProgress({
|
||||||
installable: this.boardsService,
|
installable: this.boardsService,
|
||||||
item: candidate,
|
item: candidate,
|
||||||
messageService: this.messageService,
|
messageService: this.messageService,
|
||||||
responseService: this.responseService,
|
responseService: this.responseService,
|
||||||
version: candidate.availableVersions[0]
|
version: candidate.availableVersions[0],
|
||||||
});
|
});
|
||||||
return
|
},
|
||||||
}
|
},
|
||||||
if (answer) {
|
];
|
||||||
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
|
|
||||||
}
|
return actions;
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createOnAnswerHandler(
|
||||||
|
actions: AutoInstallPromptActions,
|
||||||
|
onRefuse?: () => void
|
||||||
|
): (answer: string) => void {
|
||||||
|
return (answer) => {
|
||||||
|
const actionToHandle = actions.find((action) => action.key === answer);
|
||||||
|
actionToHandle?.handler();
|
||||||
|
|
||||||
|
if (!actionToHandle?.isAcceptance && onRefuse) {
|
||||||
|
onRefuse();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from '@theia/core/shared/react';
|
||||||
import { injectable, inject } from 'inversify';
|
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||||
import { Emitter } from '@theia/core/lib/common/event';
|
import { Emitter } from '@theia/core/lib/common/event';
|
||||||
import { ReactWidget, Message } from '@theia/core/lib/browser';
|
import { ReactWidget, Message } from '@theia/core/lib/browser';
|
||||||
import { BoardsService } from '../../common/protocol/boards-service';
|
import { BoardsService } from '../../common/protocol/boards-service';
|
||||||
@@ -9,7 +9,6 @@ import { NotificationCenter } from '../notification-center';
|
|||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsConfigDialogWidget extends ReactWidget {
|
export class BoardsConfigDialogWidget extends ReactWidget {
|
||||||
|
|
||||||
@inject(BoardsService)
|
@inject(BoardsService)
|
||||||
protected readonly boardsService: BoardsService;
|
protected readonly boardsService: BoardsService;
|
||||||
|
|
||||||
@@ -20,7 +19,8 @@ export class BoardsConfigDialogWidget extends ReactWidget {
|
|||||||
protected readonly notificationCenter: NotificationCenter;
|
protected readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
|
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
|
||||||
protected readonly onBoardConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
protected readonly onBoardConfigChangedEmitter =
|
||||||
|
new Emitter<BoardsConfig.Config>();
|
||||||
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
|
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
|
||||||
|
|
||||||
protected focusNode: HTMLElement | undefined;
|
protected focusNode: HTMLElement | undefined;
|
||||||
@@ -30,7 +30,7 @@ export class BoardsConfigDialogWidget extends ReactWidget {
|
|||||||
this.id = 'select-board-dialog';
|
this.id = 'select-board-dialog';
|
||||||
this.toDispose.pushAll([
|
this.toDispose.pushAll([
|
||||||
this.onBoardConfigChangedEmitter,
|
this.onBoardConfigChangedEmitter,
|
||||||
this.onFilterTextDidChangeEmitter
|
this.onFilterTextDidChangeEmitter,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,29 +40,32 @@ export class BoardsConfigDialogWidget extends ReactWidget {
|
|||||||
|
|
||||||
protected fireConfigChanged = (config: BoardsConfig.Config) => {
|
protected fireConfigChanged = (config: BoardsConfig.Config) => {
|
||||||
this.onBoardConfigChangedEmitter.fire(config);
|
this.onBoardConfigChangedEmitter.fire(config);
|
||||||
}
|
};
|
||||||
|
|
||||||
protected setFocusNode = (element: HTMLElement | undefined) => {
|
protected setFocusNode = (element: HTMLElement | undefined) => {
|
||||||
this.focusNode = element;
|
this.focusNode = element;
|
||||||
}
|
};
|
||||||
|
|
||||||
protected render(): React.ReactNode {
|
protected render(): React.ReactNode {
|
||||||
return <div className='selectBoardContainer'>
|
return (
|
||||||
|
<div className="selectBoardContainer">
|
||||||
<BoardsConfig
|
<BoardsConfig
|
||||||
boardsServiceProvider={this.boardsServiceClient}
|
boardsServiceProvider={this.boardsServiceClient}
|
||||||
notificationCenter={this.notificationCenter}
|
notificationCenter={this.notificationCenter}
|
||||||
onConfigChange={this.fireConfigChanged}
|
onConfigChange={this.fireConfigChanged}
|
||||||
onFocusNodeSet={this.setFocusNode}
|
onFocusNodeSet={this.setFocusNode}
|
||||||
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event} />
|
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event}
|
||||||
</div>;
|
onAppStateDidChange={this.notificationCenter.onAppStateDidChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onActivateRequest(msg: Message): void {
|
protected override onActivateRequest(msg: Message): void {
|
||||||
super.onActivateRequest(msg);
|
super.onActivateRequest(msg);
|
||||||
if (this.focusNode instanceof HTMLInputElement) {
|
if (this.focusNode instanceof HTMLInputElement) {
|
||||||
this.focusNode.select();
|
this.focusNode.select();
|
||||||
}
|
}
|
||||||
(this.focusNode || this.node).focus();
|
(this.focusNode || this.node).focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { injectable, inject, postConstruct } from 'inversify';
|
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||||
import { Message } from '@phosphor/messaging';
|
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||||
import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
|
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
|
||||||
|
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||||
import { BoardsConfig } from './boards-config';
|
import { BoardsConfig } from './boards-config';
|
||||||
import { BoardsService } from '../../common/protocol/boards-service';
|
import { BoardsService } from '../../common/protocol/boards-service';
|
||||||
import { BoardsServiceProvider } from './boards-service-provider';
|
import { BoardsServiceProvider } from './boards-service-provider';
|
||||||
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
|
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsConfigDialogProps extends DialogProps {
|
export class BoardsConfigDialogProps extends DialogProps {}
|
||||||
}
|
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||||
|
|
||||||
@inject(BoardsConfigDialogWidget)
|
@inject(BoardsConfigDialogWidget)
|
||||||
protected readonly widget: BoardsConfigDialogWidget;
|
protected readonly widget: BoardsConfigDialogWidget;
|
||||||
|
|
||||||
@@ -24,28 +24,37 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
|||||||
|
|
||||||
protected config: BoardsConfig.Config = {};
|
protected config: BoardsConfig.Config = {};
|
||||||
|
|
||||||
constructor(@inject(BoardsConfigDialogProps) protected readonly props: BoardsConfigDialogProps) {
|
constructor(
|
||||||
|
@inject(BoardsConfigDialogProps)
|
||||||
|
protected override readonly props: BoardsConfigDialogProps
|
||||||
|
) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.contentNode.classList.add('select-board-dialog');
|
this.contentNode.classList.add('select-board-dialog');
|
||||||
this.contentNode.appendChild(this.createDescription());
|
this.contentNode.appendChild(this.createDescription());
|
||||||
|
|
||||||
this.appendCloseButton('CANCEL');
|
this.appendCloseButton(
|
||||||
this.appendAcceptButton('OK');
|
nls.localize('vscode/issueMainService/cancel', 'Cancel')
|
||||||
|
);
|
||||||
|
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
protected init(): void {
|
protected init(): void {
|
||||||
this.toDispose.push(this.boardsServiceClient.onBoardsConfigChanged(config => {
|
this.toDispose.push(
|
||||||
|
this.boardsServiceClient.onBoardsConfigChanged((config) => {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.update();
|
this.update();
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
|
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
|
||||||
*/
|
*/
|
||||||
async open(query: string | undefined = undefined): Promise<BoardsConfig.Config | undefined> {
|
override async open(
|
||||||
|
query: string | undefined = undefined
|
||||||
|
): Promise<BoardsConfig.Config | undefined> {
|
||||||
if (typeof query === 'string') {
|
if (typeof query === 'string') {
|
||||||
this.widget.search(query);
|
this.widget.search(query);
|
||||||
}
|
}
|
||||||
@@ -57,7 +66,10 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
|||||||
head.classList.add('head');
|
head.classList.add('head');
|
||||||
|
|
||||||
const title = document.createElement('div');
|
const title = document.createElement('div');
|
||||||
title.textContent = 'Select Other Board & Port';
|
title.textContent = nls.localize(
|
||||||
|
'arduino/board/configDialogTitle',
|
||||||
|
'Select Other Board & Port'
|
||||||
|
);
|
||||||
title.classList.add('title');
|
title.classList.add('title');
|
||||||
head.appendChild(title);
|
head.appendChild(title);
|
||||||
|
|
||||||
@@ -66,8 +78,14 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
|||||||
head.appendChild(text);
|
head.appendChild(text);
|
||||||
|
|
||||||
for (const paragraph of [
|
for (const paragraph of [
|
||||||
'Select both a Board and a Port if you want to upload a sketch.',
|
nls.localize(
|
||||||
'If you only select a Board you will be able just to compile, but not to upload your sketch.'
|
'arduino/board/configDialog1',
|
||||||
|
'Select both a Board and a Port if you want to upload a sketch.'
|
||||||
|
),
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/configDialog2',
|
||||||
|
'If you only select a Board you will be able to compile, but not to upload your sketch.'
|
||||||
|
),
|
||||||
]) {
|
]) {
|
||||||
const p = document.createElement('div');
|
const p = document.createElement('div');
|
||||||
p.textContent = paragraph;
|
p.textContent = paragraph;
|
||||||
@@ -77,39 +95,44 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
|||||||
return head;
|
return head;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onAfterAttach(msg: Message): void {
|
protected override onAfterAttach(msg: Message): void {
|
||||||
if (this.widget.isAttached) {
|
if (this.widget.isAttached) {
|
||||||
Widget.detach(this.widget);
|
Widget.detach(this.widget);
|
||||||
}
|
}
|
||||||
Widget.attach(this.widget, this.contentNode);
|
Widget.attach(this.widget, this.contentNode);
|
||||||
this.toDisposeOnDetach.push(this.widget.onBoardConfigChanged(config => {
|
this.toDisposeOnDetach.push(
|
||||||
|
this.widget.onBoardConfigChanged((config) => {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.update();
|
this.update();
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
super.onAfterAttach(msg);
|
super.onAfterAttach(msg);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onUpdateRequest(msg: Message) {
|
protected override onUpdateRequest(msg: Message): void {
|
||||||
super.onUpdateRequest(msg);
|
super.onUpdateRequest(msg);
|
||||||
this.widget.update();
|
this.widget.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onActivateRequest(msg: Message): void {
|
protected override onActivateRequest(msg: Message): void {
|
||||||
super.onActivateRequest(msg);
|
super.onActivateRequest(msg);
|
||||||
this.widget.activate();
|
this.widget.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleEnter(event: KeyboardEvent): boolean | void {
|
protected override handleEnter(event: KeyboardEvent): boolean | void {
|
||||||
if (event.target instanceof HTMLTextAreaElement) {
|
if (event.target instanceof HTMLTextAreaElement) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected isValid(value: BoardsConfig.Config): DialogError {
|
protected override isValid(value: BoardsConfig.Config): DialogError {
|
||||||
if (!value.selectedBoard) {
|
if (!value.selectedBoard) {
|
||||||
if (value.selectedPort) {
|
if (value.selectedPort) {
|
||||||
return 'Please pick a board connected to the port you have selected.';
|
return nls.localize(
|
||||||
|
'arduino/board/pleasePickBoard',
|
||||||
|
'Please pick a board connected to the port you have selected.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -119,5 +142,4 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
|||||||
get value(): BoardsConfig.Config {
|
get value(): BoardsConfig.Config {
|
||||||
return this.config;
|
return this.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
import * as React from 'react';
|
import * as React from '@theia/core/shared/react';
|
||||||
import { Event } from '@theia/core/lib/common/event';
|
import { Event } from '@theia/core/lib/common/event';
|
||||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
import { Board, Port, AttachedBoardsChangeEvent, BoardWithPackage } from '../../common/protocol/boards-service';
|
import {
|
||||||
|
Board,
|
||||||
|
Port,
|
||||||
|
AttachedBoardsChangeEvent,
|
||||||
|
BoardWithPackage,
|
||||||
|
} from '../../common/protocol/boards-service';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
import { BoardsServiceProvider } from './boards-service-provider';
|
import {
|
||||||
|
AvailableBoard,
|
||||||
|
BoardsServiceProvider,
|
||||||
|
} from './boards-service-provider';
|
||||||
|
import { naturalCompare } from '../../common/utils';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
|
||||||
|
|
||||||
export namespace BoardsConfig {
|
export namespace BoardsConfig {
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
selectedBoard?: Board;
|
selectedBoard?: Board;
|
||||||
selectedPort?: Port;
|
selectedPort?: Port;
|
||||||
@@ -20,6 +30,7 @@ export namespace BoardsConfig {
|
|||||||
readonly onConfigChange: (config: Config) => void;
|
readonly onConfigChange: (config: Config) => void;
|
||||||
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
|
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
|
||||||
readonly onFilteredTextDidChangeEvent: Event<string>;
|
readonly onFilteredTextDidChangeEvent: Event<string>;
|
||||||
|
readonly onAppStateDidChange: Event<FrontendApplicationState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State extends Config {
|
export interface State extends Config {
|
||||||
@@ -28,44 +39,53 @@ export namespace BoardsConfig {
|
|||||||
showAllPorts: boolean;
|
showAllPorts: boolean;
|
||||||
query: string;
|
query: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class Item<T> extends React.Component<{
|
export abstract class Item<T> extends React.Component<{
|
||||||
item: T,
|
item: T;
|
||||||
label: string,
|
label: string;
|
||||||
selected: boolean,
|
selected: boolean;
|
||||||
onClick: (item: T) => void,
|
onClick: (item: T) => void;
|
||||||
missing?: boolean,
|
missing?: boolean;
|
||||||
details?: string
|
details?: string;
|
||||||
}> {
|
}> {
|
||||||
|
override render(): React.ReactNode {
|
||||||
render(): React.ReactNode {
|
|
||||||
const { selected, label, missing, details } = this.props;
|
const { selected, label, missing, details } = this.props;
|
||||||
const classNames = ['item'];
|
const classNames = ['item'];
|
||||||
if (selected) {
|
if (selected) {
|
||||||
classNames.push('selected');
|
classNames.push('selected');
|
||||||
}
|
}
|
||||||
if (missing === true) {
|
if (missing === true) {
|
||||||
classNames.push('missing')
|
classNames.push('missing');
|
||||||
}
|
}
|
||||||
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!details ? '' : details}`}>
|
return (
|
||||||
<div className='label'>
|
<div
|
||||||
{label}
|
onClick={this.onClick}
|
||||||
|
className={classNames.join(' ')}
|
||||||
|
title={`${label}${!details ? '' : details}`}
|
||||||
|
>
|
||||||
|
<div className="label">{label}</div>
|
||||||
|
{!details ? '' : <div className="details">{details}</div>}
|
||||||
|
{!selected ? (
|
||||||
|
''
|
||||||
|
) : (
|
||||||
|
<div className="selected-icon">
|
||||||
|
<i className="fa fa-check" />
|
||||||
</div>
|
</div>
|
||||||
{!details ? '' : <div className='details'>{details}</div>}
|
)}
|
||||||
{!selected ? '' : <div className='selected-icon'><i className='fa fa-check' /></div>}
|
</div>
|
||||||
</div>;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onClick = () => {
|
protected onClick = () => {
|
||||||
this.props.onClick(this.props.item);
|
this.props.onClick(this.props.item);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
export class BoardsConfig extends React.Component<
|
||||||
|
BoardsConfig.Props,
|
||||||
export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConfig.State> {
|
BoardsConfig.State
|
||||||
|
> {
|
||||||
protected toDispose = new DisposableCollection();
|
protected toDispose = new DisposableCollection();
|
||||||
|
|
||||||
constructor(props: BoardsConfig.Props) {
|
constructor(props: BoardsConfig.Props) {
|
||||||
@@ -77,103 +97,192 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
|||||||
knownPorts: [],
|
knownPorts: [],
|
||||||
showAllPorts: false,
|
showAllPorts: false,
|
||||||
query: '',
|
query: '',
|
||||||
...boardsConfig
|
...boardsConfig,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
override componentDidMount(): void {
|
||||||
this.updateBoards();
|
|
||||||
this.updatePorts(this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty));
|
|
||||||
this.toDispose.pushAll([
|
this.toDispose.pushAll([
|
||||||
this.props.notificationCenter.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
|
this.props.onAppStateDidChange((state) => {
|
||||||
this.props.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
|
if (state === 'ready') {
|
||||||
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
|
this.updateBoards();
|
||||||
|
this.updatePorts(
|
||||||
|
this.props.boardsServiceProvider.availableBoards
|
||||||
|
.map(({ port }) => port)
|
||||||
|
.filter(notEmpty)
|
||||||
|
);
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
this.props.notificationCenter.onPlatformInstalled(() => this.updateBoards(this.state.query)),
|
this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
|
||||||
this.props.notificationCenter.onPlatformUninstalled(() => this.updateBoards(this.state.query)),
|
this.updatePorts(
|
||||||
this.props.notificationCenter.onIndexUpdated(() => this.updateBoards(this.state.query)),
|
event.newState.ports,
|
||||||
this.props.notificationCenter.onDaemonStarted(() => this.updateBoards(this.state.query)),
|
AttachedBoardsChangeEvent.diff(event).detached.ports
|
||||||
this.props.notificationCenter.onDaemonStopped(() => this.setState({ searchResults: [] })),
|
)
|
||||||
this.props.onFilteredTextDidChangeEvent(query => this.setState({ query }, () => this.updateBoards(this.state.query)))
|
),
|
||||||
|
this.props.boardsServiceProvider.onBoardsConfigChanged(
|
||||||
|
({ selectedBoard, selectedPort }) => {
|
||||||
|
this.setState({ selectedBoard, selectedPort }, () =>
|
||||||
|
this.fireConfigChanged()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
this.props.notificationCenter.onPlatformDidInstall(() =>
|
||||||
|
this.updateBoards(this.state.query)
|
||||||
|
),
|
||||||
|
this.props.notificationCenter.onPlatformDidUninstall(() =>
|
||||||
|
this.updateBoards(this.state.query)
|
||||||
|
),
|
||||||
|
this.props.notificationCenter.onIndexDidUpdate(() =>
|
||||||
|
this.updateBoards(this.state.query)
|
||||||
|
),
|
||||||
|
this.props.notificationCenter.onDaemonDidStart(() =>
|
||||||
|
this.updateBoards(this.state.query)
|
||||||
|
),
|
||||||
|
this.props.notificationCenter.onDaemonDidStop(() =>
|
||||||
|
this.setState({ searchResults: [] })
|
||||||
|
),
|
||||||
|
this.props.onFilteredTextDidChangeEvent((query) =>
|
||||||
|
this.setState({ query }, () => this.updateBoards(this.state.query))
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
override componentWillUnmount(): void {
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fireConfigChanged() {
|
protected fireConfigChanged(): void {
|
||||||
const { selectedBoard, selectedPort } = this.state;
|
const { selectedBoard, selectedPort } = this.state;
|
||||||
this.props.onConfigChange({ selectedBoard, selectedPort });
|
this.props.onConfigChange({ selectedBoard, selectedPort });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateBoards = (eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = '') => {
|
protected updateBoards = (
|
||||||
const query = typeof eventOrQuery === 'string'
|
eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
|
||||||
|
) => {
|
||||||
|
const query =
|
||||||
|
typeof eventOrQuery === 'string'
|
||||||
? eventOrQuery
|
? eventOrQuery
|
||||||
: eventOrQuery.target.value.toLowerCase();
|
: eventOrQuery.target.value.toLowerCase();
|
||||||
this.setState({ query });
|
this.setState({ query });
|
||||||
this.queryBoards({ query }).then(searchResults => this.setState({ searchResults }));
|
this.queryBoards({ query }).then((searchResults) =>
|
||||||
}
|
this.setState({ searchResults })
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
|
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
|
||||||
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
|
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
|
||||||
let { selectedPort } = this.state;
|
let { selectedPort } = this.state;
|
||||||
// If the currently selected port is not available anymore, unset the selected port.
|
// If the currently selected port is not available anymore, unset the selected port.
|
||||||
if (removedPorts.some(port => Port.equals(port, selectedPort))) {
|
if (removedPorts.some((port) => Port.sameAs(port, selectedPort))) {
|
||||||
selectedPort = undefined;
|
selectedPort = undefined;
|
||||||
}
|
}
|
||||||
this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged());
|
this.setState({ knownPorts, selectedPort }, () =>
|
||||||
|
this.fireConfigChanged()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
protected queryBoards = (options: { query?: string } = {}): Promise<Array<BoardWithPackage>> => {
|
protected queryBoards = (
|
||||||
|
options: { query?: string } = {}
|
||||||
|
): Promise<Array<BoardWithPackage>> => {
|
||||||
return this.props.boardsServiceProvider.searchBoards(options);
|
return this.props.boardsServiceProvider.searchBoards(options);
|
||||||
}
|
};
|
||||||
|
|
||||||
protected get availablePorts(): MaybePromise<Port[]> {
|
protected get availablePorts(): MaybePromise<Port[]> {
|
||||||
return this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty);
|
return this.props.boardsServiceProvider.availableBoards
|
||||||
|
.map(({ port }) => port)
|
||||||
|
.filter(notEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected queryPorts = async (availablePorts: MaybePromise<Port[]> = this.availablePorts) => {
|
protected get availableBoards(): AvailableBoard[] {
|
||||||
const ports = await availablePorts;
|
return this.props.boardsServiceProvider.availableBoards;
|
||||||
return { knownPorts: ports.sort(Port.compare) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected queryPorts = async (
|
||||||
|
availablePorts: MaybePromise<Port[]> = this.availablePorts
|
||||||
|
) => {
|
||||||
|
// Available ports must be sorted in this order:
|
||||||
|
// 1. Serial with recognized boards
|
||||||
|
// 2. Serial with guessed boards
|
||||||
|
// 3. Serial with incomplete boards
|
||||||
|
// 4. Network with recognized boards
|
||||||
|
// 5. Other protocols with recognized boards
|
||||||
|
const ports = (await availablePorts).sort((left: Port, right: Port) => {
|
||||||
|
if (left.protocol === 'serial' && right.protocol !== 'serial') {
|
||||||
|
return -1;
|
||||||
|
} else if (left.protocol !== 'serial' && right.protocol === 'serial') {
|
||||||
|
return 1;
|
||||||
|
} else if (left.protocol === 'network' && right.protocol !== 'network') {
|
||||||
|
return -1;
|
||||||
|
} else if (left.protocol !== 'network' && right.protocol === 'network') {
|
||||||
|
return 1;
|
||||||
|
} else if (left.protocol === right.protocol) {
|
||||||
|
// We show ports, including those that have guessed
|
||||||
|
// or unrecognized boards, so we must sort those too.
|
||||||
|
const leftBoard = this.availableBoards.find(
|
||||||
|
(board) => board.port === left
|
||||||
|
);
|
||||||
|
const rightBoard = this.availableBoards.find(
|
||||||
|
(board) => board.port === right
|
||||||
|
);
|
||||||
|
if (leftBoard && !rightBoard) {
|
||||||
|
return -1;
|
||||||
|
} else if (!leftBoard && rightBoard) {
|
||||||
|
return 1;
|
||||||
|
} else if (leftBoard?.state! < rightBoard?.state!) {
|
||||||
|
return -1;
|
||||||
|
} else if (leftBoard?.state! > rightBoard?.state!) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return naturalCompare(left.address, right.address);
|
||||||
|
});
|
||||||
|
return { knownPorts: ports };
|
||||||
|
};
|
||||||
|
|
||||||
protected toggleFilterPorts = () => {
|
protected toggleFilterPorts = () => {
|
||||||
this.setState({ showAllPorts: !this.state.showAllPorts });
|
this.setState({ showAllPorts: !this.state.showAllPorts });
|
||||||
}
|
};
|
||||||
|
|
||||||
protected selectPort = (selectedPort: Port | undefined) => {
|
protected selectPort = (selectedPort: Port | undefined) => {
|
||||||
this.setState({ selectedPort }, () => this.fireConfigChanged());
|
this.setState({ selectedPort }, () => this.fireConfigChanged());
|
||||||
}
|
};
|
||||||
|
|
||||||
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
|
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
|
||||||
this.setState({ selectedBoard }, () => this.fireConfigChanged());
|
this.setState({ selectedBoard }, () => this.fireConfigChanged());
|
||||||
}
|
};
|
||||||
|
|
||||||
protected focusNodeSet = (element: HTMLElement | null) => {
|
protected focusNodeSet = (element: HTMLElement | null) => {
|
||||||
this.props.onFocusNodeSet(element || undefined);
|
this.props.onFocusNodeSet(element || undefined);
|
||||||
}
|
};
|
||||||
|
|
||||||
render(): React.ReactNode {
|
override render(): React.ReactNode {
|
||||||
return <div className='body'>
|
return (
|
||||||
|
<div className="body">
|
||||||
{this.renderContainer('boards', this.renderBoards.bind(this))}
|
{this.renderContainer('boards', this.renderBoards.bind(this))}
|
||||||
{this.renderContainer('ports', this.renderPorts.bind(this), this.renderPortsFooter.bind(this))}
|
{this.renderContainer(
|
||||||
</div>;
|
'ports',
|
||||||
|
this.renderPorts.bind(this),
|
||||||
|
this.renderPortsFooter.bind(this)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderContainer(title: string, contentRenderer: () => React.ReactNode, footerRenderer?: () => React.ReactNode): React.ReactNode {
|
protected renderContainer(
|
||||||
return <div className='container'>
|
title: string,
|
||||||
<div className='content'>
|
contentRenderer: () => React.ReactNode,
|
||||||
<div className='title'>
|
footerRenderer?: () => React.ReactNode
|
||||||
{title}
|
): React.ReactNode {
|
||||||
</div>
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="content">
|
||||||
|
<div className="title">{title}</div>
|
||||||
{contentRenderer()}
|
{contentRenderer()}
|
||||||
<div className='footer'>
|
<div className="footer">{footerRenderer ? footerRenderer() : ''}</div>
|
||||||
{(footerRenderer ? footerRenderer() : '')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderBoards(): React.ReactNode {
|
protected renderBoards(): React.ReactNode {
|
||||||
@@ -181,7 +290,8 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
|||||||
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
|
// Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560
|
||||||
// It is tricky when the core is not yet installed, no FQBNs are available.
|
// It is tricky when the core is not yet installed, no FQBNs are available.
|
||||||
const distinctBoards = new Map<string, Board.Detailed>();
|
const distinctBoards = new Map<string, Board.Detailed>();
|
||||||
const toKey = ({ name, packageName, fqbn }: Board.Detailed) => !!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
|
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
|
||||||
|
!!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
|
||||||
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
|
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
|
||||||
const key = toKey(board);
|
const key = toKey(board);
|
||||||
if (!distinctBoards.has(key)) {
|
if (!distinctBoards.has(key)) {
|
||||||
@@ -189,107 +299,148 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>
|
return (
|
||||||
<div className='search'>
|
<React.Fragment>
|
||||||
|
<div className="search">
|
||||||
<input
|
<input
|
||||||
type='search'
|
type="search"
|
||||||
value={query}
|
value={query}
|
||||||
className='theia-input'
|
className="theia-input"
|
||||||
placeholder='SEARCH BOARD'
|
placeholder="SEARCH BOARD"
|
||||||
onChange={this.updateBoards}
|
onChange={this.updateBoards}
|
||||||
ref={this.focusNodeSet}
|
ref={this.focusNodeSet}
|
||||||
/>
|
/>
|
||||||
<i className='fa fa-search'></i>
|
<i className="fa fa-search"></i>
|
||||||
</div>
|
</div>
|
||||||
<div className='boards list'>
|
<div className="boards list">
|
||||||
{Array.from(distinctBoards.values()).map(board => <Item<BoardWithPackage>
|
{Array.from(distinctBoards.values()).map((board) => (
|
||||||
key={`${board.name}-${board.packageName}`}
|
<Item<BoardWithPackage>
|
||||||
|
key={toKey(board)}
|
||||||
item={board}
|
item={board}
|
||||||
label={board.name}
|
label={board.name}
|
||||||
details={board.details}
|
details={board.details}
|
||||||
selected={board.selected}
|
selected={board.selected}
|
||||||
onClick={this.selectBoard}
|
onClick={this.selectBoard}
|
||||||
missing={board.missing}
|
missing={board.missing}
|
||||||
/>)}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
</React.Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderPorts(): React.ReactNode {
|
protected renderPorts(): React.ReactNode {
|
||||||
const filter = this.state.showAllPorts ? () => true : Port.isBoardPort;
|
let ports = [] as Port[];
|
||||||
const ports = this.state.knownPorts.filter(filter);
|
if (this.state.showAllPorts) {
|
||||||
return !ports.length ?
|
ports = this.state.knownPorts;
|
||||||
(
|
} else {
|
||||||
<div className='loading noselect'>
|
ports = this.state.knownPorts.filter((port) => {
|
||||||
No ports discovered
|
if (port.protocol === 'serial') {
|
||||||
</div>
|
return true;
|
||||||
) :
|
}
|
||||||
(
|
// All other ports with different protocol are
|
||||||
<div className='ports list'>
|
// only shown if there is a recognized board
|
||||||
{ports.map(port => <Item<Port>
|
// connected
|
||||||
key={Port.toString(port)}
|
for (const board of this.availableBoards) {
|
||||||
|
if (board.port?.address === port.address) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return !ports.length ? (
|
||||||
|
<div className="loading noselect">No ports discovered</div>
|
||||||
|
) : (
|
||||||
|
<div className="ports list">
|
||||||
|
{ports.map((port) => (
|
||||||
|
<Item<Port>
|
||||||
|
key={`${port.id}`}
|
||||||
item={port}
|
item={port}
|
||||||
label={Port.toString(port)}
|
label={Port.toString(port)}
|
||||||
selected={Port.equals(this.state.selectedPort, port)}
|
selected={Port.sameAs(this.state.selectedPort, port)}
|
||||||
onClick={this.selectPort}
|
onClick={this.selectPort}
|
||||||
/>)}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderPortsFooter(): React.ReactNode {
|
protected renderPortsFooter(): React.ReactNode {
|
||||||
return <div className='noselect'>
|
return (
|
||||||
|
<div className="noselect">
|
||||||
<label
|
<label
|
||||||
title='Shows all available ports when enabled'>
|
title={nls.localize(
|
||||||
|
'arduino/board/showAllAvailablePorts',
|
||||||
|
'Shows all available ports when enabled'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type='checkbox'
|
type="checkbox"
|
||||||
defaultChecked={this.state.showAllPorts}
|
defaultChecked={this.state.showAllPorts}
|
||||||
onChange={this.toggleFilterPorts}
|
onChange={this.toggleFilterPorts}
|
||||||
/>
|
/>
|
||||||
<span>Show all ports</span>
|
<span>Show all ports</span>
|
||||||
</label>
|
</label>
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace BoardsConfig {
|
export namespace BoardsConfig {
|
||||||
|
|
||||||
export namespace Config {
|
export namespace Config {
|
||||||
|
|
||||||
export function sameAs(config: Config, other: Config | Board): boolean {
|
export function sameAs(config: Config, other: Config | Board): boolean {
|
||||||
const { selectedBoard, selectedPort } = config;
|
const { selectedBoard, selectedPort } = config;
|
||||||
if (Board.is(other)) {
|
if (Board.is(other)) {
|
||||||
return !!selectedBoard
|
return (
|
||||||
&& Board.equals(other, selectedBoard)
|
!!selectedBoard &&
|
||||||
&& Port.sameAs(selectedPort, other.port);
|
Board.equals(other, selectedBoard) &&
|
||||||
|
Port.sameAs(selectedPort, other.port)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return sameAs(config, other);
|
return sameAs(config, other);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function equals(left: Config, right: Config): boolean {
|
export function equals(left: Config, right: Config): boolean {
|
||||||
return left.selectedBoard === right.selectedBoard
|
return (
|
||||||
&& left.selectedPort === right.selectedPort;
|
left.selectedBoard === right.selectedBoard &&
|
||||||
|
left.selectedPort === right.selectedPort
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toString(config: Config, options: { default: string } = { default: '' }): string {
|
export function toString(
|
||||||
|
config: Config,
|
||||||
|
options: { default: string } = { default: '' }
|
||||||
|
): string {
|
||||||
const { selectedBoard, selectedPort: port } = config;
|
const { selectedBoard, selectedPort: port } = config;
|
||||||
if (!selectedBoard) {
|
if (!selectedBoard) {
|
||||||
return options.default;
|
return options.default;
|
||||||
}
|
}
|
||||||
const { name } = selectedBoard;
|
const { name } = selectedBoard;
|
||||||
return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
|
return `${name}${port ? ` at ${port.address}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setConfig(config: Config | undefined, urlToAttachTo: URL): URL {
|
export function setConfig(
|
||||||
|
config: Config | undefined,
|
||||||
|
urlToAttachTo: URL
|
||||||
|
): URL {
|
||||||
const copy = new URL(urlToAttachTo.toString());
|
const copy = new URL(urlToAttachTo.toString());
|
||||||
if (!config) {
|
if (!config) {
|
||||||
copy.searchParams.delete('boards-config');
|
copy.searchParams.delete('boards-config');
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedBoard = config.selectedBoard ? { name: config.selectedBoard.name, fqbn: config.selectedBoard.fqbn } : undefined;
|
const selectedBoard = config.selectedBoard
|
||||||
const selectedPort = config.selectedPort ? { protocol: config.selectedPort.protocol, address: config.selectedPort.address } : undefined;
|
? {
|
||||||
|
name: config.selectedBoard.name,
|
||||||
|
fqbn: config.selectedBoard.fqbn,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
const selectedPort = config.selectedPort
|
||||||
|
? {
|
||||||
|
protocol: config.selectedPort.protocol,
|
||||||
|
address: config.selectedPort.address,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
|
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
|
||||||
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
|
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
|
||||||
return copy;
|
return copy;
|
||||||
@@ -306,14 +457,14 @@ export namespace BoardsConfig {
|
|||||||
if (typeof candidate === 'object') {
|
if (typeof candidate === 'object') {
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
console.warn(`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`);
|
console.warn(
|
||||||
|
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
|
||||||
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`Could not get board config from URL: ${url}.`, e);
|
console.log(`Could not get board config from URL: ${url}.`, e);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
import * as PQueue from 'p-queue';
|
import * as PQueue from 'p-queue';
|
||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
import {
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
import { BoardsServiceProvider } from './boards-service-provider';
|
import { BoardsServiceProvider } from './boards-service-provider';
|
||||||
import { Board, ConfigOption, Programmer } from '../../common/protocol';
|
import { Board, ConfigOption, Programmer } from '../../common/protocol';
|
||||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||||
import { BoardsDataStore } from './boards-data-store';
|
import { BoardsDataStore } from './boards-data-store';
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
|
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||||
|
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
protected readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@@ -28,67 +32,137 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
|||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||||
|
|
||||||
|
@inject(FrontendApplicationStateService)
|
||||||
|
private readonly appStateService: FrontendApplicationStateService;
|
||||||
|
|
||||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||||
protected readonly toDisposeOnBoardChange = new DisposableCollection();
|
protected readonly toDisposeOnBoardChange = new DisposableCollection();
|
||||||
|
|
||||||
async onStart(): Promise<void> {
|
async onStart(): Promise<void> {
|
||||||
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
|
this.appStateService
|
||||||
this.boardsDataStore.onChanged(() => this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
|
.reachedState('ready')
|
||||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.updateMenuActions(selectedBoard));
|
.then(() =>
|
||||||
|
this.updateMenuActions(
|
||||||
|
this.boardsServiceClient.boardsConfig.selectedBoard
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.boardsDataStore.onChanged(() =>
|
||||||
|
this.updateMenuActions(
|
||||||
|
this.boardsServiceClient.boardsConfig.selectedBoard
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||||
|
this.updateMenuActions(selectedBoard)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async updateMenuActions(selectedBoard: Board | undefined): Promise<void> {
|
protected async updateMenuActions(
|
||||||
|
selectedBoard: Board | undefined
|
||||||
|
): Promise<void> {
|
||||||
return this.queue.add(async () => {
|
return this.queue.add(async () => {
|
||||||
this.toDisposeOnBoardChange.dispose();
|
this.toDisposeOnBoardChange.dispose();
|
||||||
this.mainMenuManager.update();
|
this.mainMenuManager.update();
|
||||||
if (selectedBoard) {
|
if (selectedBoard) {
|
||||||
const { fqbn } = selectedBoard;
|
const { fqbn } = selectedBoard;
|
||||||
if (fqbn) {
|
if (fqbn) {
|
||||||
const { configOptions, programmers, selectedProgrammer } = await this.boardsDataStore.getData(fqbn);
|
const { configOptions, programmers, selectedProgrammer } =
|
||||||
|
await this.boardsDataStore.getData(fqbn);
|
||||||
if (configOptions.length) {
|
if (configOptions.length) {
|
||||||
const boardsConfigMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z01_boardsConfig']; // `z_` is for ordering.
|
const boardsConfigMenuPath = [
|
||||||
for (const { label, option, values } of configOptions.sort(ConfigOption.LABEL_COMPARATOR)) {
|
...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP,
|
||||||
|
'z01_boardsConfig',
|
||||||
|
]; // `z_` is for ordering.
|
||||||
|
for (const { label, option, values } of configOptions.sort(
|
||||||
|
ConfigOption.LABEL_COMPARATOR
|
||||||
|
)) {
|
||||||
const menuPath = [...boardsConfigMenuPath, `${option}`];
|
const menuPath = [...boardsConfigMenuPath, `${option}`];
|
||||||
const commands = new Map<string, Disposable & { label: string }>()
|
const commands = new Map<
|
||||||
|
string,
|
||||||
|
Disposable & { label: string }
|
||||||
|
>();
|
||||||
for (const value of values) {
|
for (const value of values) {
|
||||||
const id = `${fqbn}-${option}--${value.value}`;
|
const id = `${fqbn}-${option}--${value.value}`;
|
||||||
const command = { id };
|
const command = { id };
|
||||||
const selectedValue = value.value;
|
const selectedValue = value.value;
|
||||||
const handler = {
|
const handler = {
|
||||||
execute: () => this.boardsDataStore.selectConfigOption({ fqbn, option, selectedValue }),
|
execute: () =>
|
||||||
isToggled: () => value.selected
|
this.boardsDataStore.selectConfigOption({
|
||||||
|
fqbn,
|
||||||
|
option,
|
||||||
|
selectedValue,
|
||||||
|
}),
|
||||||
|
isToggled: () => value.selected,
|
||||||
};
|
};
|
||||||
commands.set(id, Object.assign(this.commandRegistry.registerCommand(command, handler), { label: value.label }));
|
commands.set(
|
||||||
|
id,
|
||||||
|
Object.assign(
|
||||||
|
this.commandRegistry.registerCommand(command, handler),
|
||||||
|
{ label: value.label }
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.menuRegistry.registerSubmenu(menuPath, label);
|
this.menuRegistry.registerSubmenu(menuPath, label);
|
||||||
this.toDisposeOnBoardChange.pushAll([
|
this.toDisposeOnBoardChange.pushAll([
|
||||||
...commands.values(),
|
...commands.values(),
|
||||||
Disposable.create(() => unregisterSubmenu(menuPath, this.menuRegistry)),
|
Disposable.create(() =>
|
||||||
|
unregisterSubmenu(menuPath, this.menuRegistry)
|
||||||
|
),
|
||||||
...Array.from(commands.keys()).map((commandId, i) => {
|
...Array.from(commands.keys()).map((commandId, i) => {
|
||||||
const { label } = commands.get(commandId)!;
|
const { label } = commands.get(commandId)!;
|
||||||
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: `${i}`, label });
|
this.menuRegistry.registerMenuAction(menuPath, {
|
||||||
return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId));
|
commandId,
|
||||||
})
|
order: `${i}`,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
return Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuAction(commandId)
|
||||||
|
);
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (programmers.length) {
|
if (programmers.length) {
|
||||||
const programmersMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z02_programmers'];
|
const programmersMenuPath = [
|
||||||
const label = selectedProgrammer ? `Programmer: "${selectedProgrammer.name}"` : 'Programmer'
|
...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP,
|
||||||
|
'z02_programmers',
|
||||||
|
];
|
||||||
|
const programmerNls = nls.localize(
|
||||||
|
'arduino/board/programmer',
|
||||||
|
'Programmer'
|
||||||
|
);
|
||||||
|
const label = selectedProgrammer
|
||||||
|
? `${programmerNls}: "${selectedProgrammer.name}"`
|
||||||
|
: programmerNls;
|
||||||
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
|
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
|
||||||
this.toDisposeOnBoardChange.push(Disposable.create(() => unregisterSubmenu(programmersMenuPath, this.menuRegistry)));
|
this.toDisposeOnBoardChange.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
unregisterSubmenu(programmersMenuPath, this.menuRegistry)
|
||||||
|
)
|
||||||
|
);
|
||||||
for (const programmer of programmers) {
|
for (const programmer of programmers) {
|
||||||
const { id, name } = programmer;
|
const { id, name } = programmer;
|
||||||
const command = { id: `${fqbn}-programmer--${id}` };
|
const command = { id: `${fqbn}-programmer--${id}` };
|
||||||
const handler = {
|
const handler = {
|
||||||
execute: () => this.boardsDataStore.selectProgrammer({ fqbn, selectedProgrammer: programmer }),
|
execute: () =>
|
||||||
isToggled: () => Programmer.equals(programmer, selectedProgrammer)
|
this.boardsDataStore.selectProgrammer({
|
||||||
|
fqbn,
|
||||||
|
selectedProgrammer: programmer,
|
||||||
|
}),
|
||||||
|
isToggled: () =>
|
||||||
|
Programmer.equals(programmer, selectedProgrammer),
|
||||||
};
|
};
|
||||||
this.menuRegistry.registerMenuAction(programmersMenuPath, { commandId: command.id, label: name });
|
this.menuRegistry.registerMenuAction(programmersMenuPath, {
|
||||||
|
commandId: command.id,
|
||||||
|
label: name,
|
||||||
|
});
|
||||||
this.commandRegistry.registerCommand(command, handler);
|
this.commandRegistry.registerCommand(command, handler);
|
||||||
this.toDisposeOnBoardChange.pushAll([
|
this.toDisposeOnBoardChange.pushAll([
|
||||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
|
Disposable.create(() =>
|
||||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command.id))
|
this.commandRegistry.unregisterCommand(command)
|
||||||
|
),
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuAction(command.id)
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,5 +171,4 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import { injectable, inject, named } from 'inversify';
|
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||||
import { ILogger } from '@theia/core/lib/common/logger';
|
import { ILogger } from '@theia/core/lib/common/logger';
|
||||||
import { deepClone } from '@theia/core/lib/common/objects';
|
import { deepClone } from '@theia/core/lib/common/objects';
|
||||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
|
||||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||||
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
|
import {
|
||||||
|
FrontendApplicationContribution,
|
||||||
|
LocalStorageService,
|
||||||
|
} from '@theia/core/lib/browser';
|
||||||
import { notEmpty } from '../../common/utils';
|
import { notEmpty } from '../../common/utils';
|
||||||
import { BoardsService, ConfigOption, Installable, BoardDetails, Programmer } from '../../common/protocol';
|
import {
|
||||||
|
BoardsService,
|
||||||
|
ConfigOption,
|
||||||
|
BoardDetails,
|
||||||
|
Programmer,
|
||||||
|
} from '../../common/protocol';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsDataStore implements FrontendApplicationContribution {
|
export class BoardsDataStore implements FrontendApplicationContribution {
|
||||||
|
|
||||||
@inject(ILogger)
|
@inject(ILogger)
|
||||||
@named('store')
|
@named('store')
|
||||||
protected readonly logger: ILogger;
|
protected readonly logger: ILogger;
|
||||||
@@ -27,15 +33,16 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
|||||||
protected readonly onChangedEmitter = new Emitter<void>();
|
protected readonly onChangedEmitter = new Emitter<void>();
|
||||||
|
|
||||||
onStart(): void {
|
onStart(): void {
|
||||||
this.notificationCenter.onPlatformInstalled(async ({ item }) => {
|
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
|
||||||
const { installedVersion: version } = item;
|
|
||||||
if (!version) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let shouldFireChanged = false;
|
let shouldFireChanged = false;
|
||||||
for (const fqbn of item.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) {
|
for (const fqbn of item.boards
|
||||||
const key = this.getStorageKey(fqbn, version);
|
.map(({ fqbn }) => fqbn)
|
||||||
let data = await this.storageService.getData<ConfigOption[] | undefined>(key);
|
.filter(notEmpty)
|
||||||
|
.filter((fqbn) => !!fqbn)) {
|
||||||
|
const key = this.getStorageKey(fqbn);
|
||||||
|
let data = await this.storageService.getData<
|
||||||
|
ConfigOption[] | undefined
|
||||||
|
>(key);
|
||||||
if (!data || !data.length) {
|
if (!data || !data.length) {
|
||||||
const details = await this.getBoardDetailsSafe(fqbn);
|
const details = await this.getBoardDetailsSafe(fqbn);
|
||||||
if (details) {
|
if (details) {
|
||||||
@@ -59,30 +66,23 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
|||||||
|
|
||||||
async appendConfigToFqbn(
|
async appendConfigToFqbn(
|
||||||
fqbn: string | undefined,
|
fqbn: string | undefined,
|
||||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
|
|
||||||
if (!fqbn) {
|
if (!fqbn) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const { configOptions } = await this.getData(fqbn);
|
||||||
const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
|
|
||||||
return ConfigOption.decorate(fqbn, configOptions);
|
return ConfigOption.decorate(fqbn, configOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData(
|
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
|
||||||
fqbn: string | undefined,
|
|
||||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<BoardsDataStore.Data> {
|
|
||||||
|
|
||||||
if (!fqbn) {
|
if (!fqbn) {
|
||||||
return BoardsDataStore.Data.EMPTY;
|
return BoardsDataStore.Data.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = await boardsPackageVersion;
|
const key = this.getStorageKey(fqbn);
|
||||||
if (!version) {
|
let data = await this.storageService.getData<
|
||||||
return BoardsDataStore.Data.EMPTY;
|
BoardsDataStore.Data | undefined
|
||||||
}
|
>(key, undefined);
|
||||||
const key = this.getStorageKey(fqbn, version);
|
|
||||||
let data = await this.storageService.getData<BoardsDataStore.Data | undefined>(key, undefined);
|
|
||||||
if (BoardsDataStore.Data.is(data)) {
|
if (BoardsDataStore.Data.is(data)) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -92,38 +92,44 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
|||||||
return BoardsDataStore.Data.EMPTY;
|
return BoardsDataStore.Data.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
data = { configOptions: boardDetails.configOptions, programmers: boardDetails.programmers };
|
data = {
|
||||||
|
configOptions: boardDetails.configOptions,
|
||||||
|
programmers: boardDetails.programmers,
|
||||||
|
};
|
||||||
await this.storageService.setData(key, data);
|
await this.storageService.setData(key, data);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectProgrammer(
|
async selectProgrammer(
|
||||||
{ fqbn, selectedProgrammer }: { fqbn: string, selectedProgrammer: Programmer },
|
{
|
||||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
|
fqbn,
|
||||||
|
selectedProgrammer,
|
||||||
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
|
}: { fqbn: string; selectedProgrammer: Programmer },
|
||||||
|
): Promise<boolean> {
|
||||||
|
const data = deepClone(await this.getData(fqbn));
|
||||||
const { programmers } = data;
|
const { programmers } = data;
|
||||||
if (!programmers.find(p => Programmer.equals(selectedProgrammer, p))) {
|
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = await boardsPackageVersion;
|
await this.setData({
|
||||||
if (!version) {
|
fqbn,
|
||||||
return false;
|
data: { ...data, selectedProgrammer },
|
||||||
}
|
});
|
||||||
|
|
||||||
await this.setData({ fqbn, data: { ...data, selectedProgrammer }, version });
|
|
||||||
this.fireChanged();
|
this.fireChanged();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectConfigOption(
|
async selectConfigOption(
|
||||||
{ fqbn, option, selectedValue }: { fqbn: string, option: string, selectedValue: string },
|
{
|
||||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
|
fqbn,
|
||||||
|
option,
|
||||||
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
|
selectedValue,
|
||||||
|
}: { fqbn: string; option: string; selectedValue: string }
|
||||||
|
): Promise<boolean> {
|
||||||
|
const data = deepClone(await this.getData(fqbn));
|
||||||
const { configOptions } = data;
|
const { configOptions } = data;
|
||||||
const configOption = configOptions.find(c => c.option === option);
|
const configOption = configOptions.find((c) => c.option === option);
|
||||||
if (!configOption) {
|
if (!configOption) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -139,36 +145,46 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
|||||||
if (!updated) {
|
if (!updated) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const version = await boardsPackageVersion;
|
await this.setData({ fqbn, data });
|
||||||
if (!version) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.setData({ fqbn, data, version });
|
|
||||||
this.fireChanged();
|
this.fireChanged();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async setData(
|
protected async setData({
|
||||||
{ fqbn, data, version }: { fqbn: string, data: BoardsDataStore.Data, version: Installable.Version }): Promise<void> {
|
fqbn,
|
||||||
|
data,
|
||||||
const key = this.getStorageKey(fqbn, version);
|
}: {
|
||||||
|
fqbn: string;
|
||||||
|
data: BoardsDataStore.Data;
|
||||||
|
}): Promise<void> {
|
||||||
|
const key = this.getStorageKey(fqbn);
|
||||||
return this.storageService.setData(key, data);
|
return this.storageService.setData(key, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getStorageKey(fqbn: string, version: Installable.Version): string {
|
protected getStorageKey(fqbn: string): string {
|
||||||
return `.arduinoIDE-configOptions-${version}-${fqbn}`;
|
return `.arduinoIDE-configOptions-${fqbn}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getBoardDetailsSafe(fqbn: string): Promise<BoardDetails | undefined> {
|
protected async getBoardDetailsSafe(
|
||||||
|
fqbn: string
|
||||||
|
): Promise<BoardDetails | undefined> {
|
||||||
try {
|
try {
|
||||||
const details = this.boardsService.getBoardDetails({ fqbn });
|
const details = this.boardsService.getBoardDetails({ fqbn });
|
||||||
return details;
|
return details;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error && err.message.includes('loading board data') && err.message.includes('is not installed')) {
|
if (
|
||||||
this.logger.warn(`The boards package is not installed for board with FQBN: ${fqbn}`);
|
err instanceof Error &&
|
||||||
|
err.message.includes('loading board data') &&
|
||||||
|
err.message.includes('is not installed')
|
||||||
|
) {
|
||||||
|
this.logger.warn(
|
||||||
|
`The boards package is not installed for board with FQBN: ${fqbn}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.logger.error(`An unexpected error occurred while retrieving the board details for ${fqbn}.`, err);
|
this.logger.error(
|
||||||
|
`An unexpected error occurred while retrieving the board details for ${fqbn}.`,
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -177,18 +193,6 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
|||||||
protected fireChanged(): void {
|
protected fireChanged(): void {
|
||||||
this.onChangedEmitter.fire();
|
this.onChangedEmitter.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getBoardsPackageVersion(fqbn: string | undefined): Promise<Installable.Version | undefined> {
|
|
||||||
if (!fqbn) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const boardsPackage = await this.boardsService.getContainerBoardPackage({ fqbn });
|
|
||||||
if (!boardsPackage) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return boardsPackage.installedVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace BoardsDataStore {
|
export namespace BoardsDataStore {
|
||||||
@@ -200,12 +204,16 @@ export namespace BoardsDataStore {
|
|||||||
export namespace Data {
|
export namespace Data {
|
||||||
export const EMPTY: Data = {
|
export const EMPTY: Data = {
|
||||||
configOptions: [],
|
configOptions: [],
|
||||||
programmers: []
|
programmers: [],
|
||||||
};
|
};
|
||||||
export function is(arg: any): arg is Data {
|
export function is(arg: any): arg is Data {
|
||||||
return !!arg
|
return (
|
||||||
&& 'configOptions' in arg && Array.isArray(arg['configOptions'])
|
!!arg &&
|
||||||
&& 'programmers' in arg && Array.isArray(arg['programmers'])
|
'configOptions' in arg &&
|
||||||
|
Array.isArray(arg['configOptions']) &&
|
||||||
|
'programmers' in arg &&
|
||||||
|
Array.isArray(arg['programmers'])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,88 @@
|
|||||||
import { inject, injectable, postConstruct } from 'inversify';
|
import {
|
||||||
import { BoardsPackage, BoardsService } from '../../common/protocol/boards-service';
|
inject,
|
||||||
|
injectable,
|
||||||
|
postConstruct,
|
||||||
|
} from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
BoardsPackage,
|
||||||
|
BoardsService,
|
||||||
|
} from '../../common/protocol/boards-service';
|
||||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||||
|
|
||||||
static WIDGET_ID = 'boards-list-widget';
|
static WIDGET_ID = 'boards-list-widget';
|
||||||
static WIDGET_LABEL = 'Boards Manager';
|
static WIDGET_LABEL = nls.localize('arduino/boardsManager', 'Boards Manager');
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@inject(BoardsService) protected service: BoardsService,
|
@inject(BoardsService) protected service: BoardsService,
|
||||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardsPackage>) {
|
@inject(ListItemRenderer)
|
||||||
|
protected itemRenderer: ListItemRenderer<BoardsPackage>
|
||||||
|
) {
|
||||||
super({
|
super({
|
||||||
id: BoardsListWidget.WIDGET_ID,
|
id: BoardsListWidget.WIDGET_ID,
|
||||||
label: BoardsListWidget.WIDGET_LABEL,
|
label: BoardsListWidget.WIDGET_LABEL,
|
||||||
iconClass: 'fa fa-microchip',
|
iconClass: 'fa fa-arduino-boards',
|
||||||
searchable: service,
|
searchable: service,
|
||||||
installable: service,
|
installable: service,
|
||||||
itemLabel: (item: BoardsPackage) => item.name,
|
itemLabel: (item: BoardsPackage) => item.name,
|
||||||
itemDeprecated: (item: BoardsPackage) => item.deprecated,
|
itemDeprecated: (item: BoardsPackage) => item.deprecated,
|
||||||
itemRenderer
|
itemRenderer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
protected init(): void {
|
protected override init(): void {
|
||||||
super.init();
|
super.init();
|
||||||
this.toDispose.pushAll([
|
this.toDispose.pushAll([
|
||||||
this.notificationCenter.onPlatformInstalled(() => this.refresh(undefined)),
|
this.notificationCenter.onPlatformDidInstall(() =>
|
||||||
this.notificationCenter.onPlatformUninstalled(() => this.refresh(undefined)),
|
this.refresh(undefined)
|
||||||
|
),
|
||||||
|
this.notificationCenter.onPlatformDidUninstall(() =>
|
||||||
|
this.refresh(undefined)
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async install({ item, progressId, version }: { item: BoardsPackage, progressId: string, version: string; }): Promise<void> {
|
protected override async install({
|
||||||
|
item,
|
||||||
|
progressId,
|
||||||
|
version,
|
||||||
|
}: {
|
||||||
|
item: BoardsPackage;
|
||||||
|
progressId: string;
|
||||||
|
version: string;
|
||||||
|
}): Promise<void> {
|
||||||
await super.install({ item, progressId, version });
|
await super.install({ item, progressId, version });
|
||||||
this.messageService.info(`Successfully installed platform ${item.name}:${version}`, { timeout: 3000 });
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/succesfullyInstalledPlatform',
|
||||||
|
'Successfully installed platform {0}:{1}',
|
||||||
|
item.name,
|
||||||
|
version
|
||||||
|
),
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async uninstall({ item, progressId }: { item: BoardsPackage, progressId: string }): Promise<void> {
|
protected override async uninstall({
|
||||||
|
item,
|
||||||
|
progressId,
|
||||||
|
}: {
|
||||||
|
item: BoardsPackage;
|
||||||
|
progressId: string;
|
||||||
|
}): Promise<void> {
|
||||||
await super.uninstall({ item, progressId });
|
await super.uninstall({ item, progressId });
|
||||||
this.messageService.info(`Successfully uninstalled platform ${item.name}:${item.installedVersion}`, { timeout: 3000 });
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/succesfullyUninstalledPlatform',
|
||||||
|
'Successfully uninstalled platform {0}:{1}',
|
||||||
|
item.name,
|
||||||
|
item.installedVersion!
|
||||||
|
),
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { injectable, inject } from 'inversify';
|
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||||
import { Emitter } from '@theia/core/lib/common/event';
|
import { Emitter } from '@theia/core/lib/common/event';
|
||||||
import { ILogger } from '@theia/core/lib/common/logger';
|
import { ILogger } from '@theia/core/lib/common/logger';
|
||||||
import { CommandService } from '@theia/core/lib/common/command';
|
import { CommandService } from '@theia/core/lib/common/command';
|
||||||
@@ -11,24 +11,25 @@ import {
|
|||||||
BoardsService,
|
BoardsService,
|
||||||
BoardsPackage,
|
BoardsPackage,
|
||||||
AttachedBoardsChangeEvent,
|
AttachedBoardsChangeEvent,
|
||||||
BoardWithPackage
|
BoardWithPackage,
|
||||||
|
BoardUserField,
|
||||||
} from '../../common/protocol';
|
} from '../../common/protocol';
|
||||||
import { BoardsConfig } from './boards-config';
|
import { BoardsConfig } from './boards-config';
|
||||||
import { naturalCompare } from '../../common/utils';
|
import { naturalCompare } from '../../common/utils';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
import { ArduinoCommands } from '../arduino-commands';
|
|
||||||
import { StorageWrapper } from '../storage-wrapper';
|
import { StorageWrapper } from '../storage-wrapper';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
|
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsServiceProvider implements FrontendApplicationContribution {
|
export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||||
|
|
||||||
@inject(ILogger)
|
@inject(ILogger)
|
||||||
protected logger: ILogger;
|
protected logger: ILogger;
|
||||||
|
|
||||||
@inject(MessageService)
|
@inject(MessageService)
|
||||||
protected messageService: MessageService;
|
protected messageService: MessageService;
|
||||||
|
|
||||||
|
|
||||||
@inject(BoardsService)
|
@inject(BoardsService)
|
||||||
protected boardsService: BoardsService;
|
protected boardsService: BoardsService;
|
||||||
|
|
||||||
@@ -38,8 +39,15 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected notificationCenter: NotificationCenter;
|
protected notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
protected readonly onBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
@inject(FrontendApplicationStateService)
|
||||||
protected readonly onAvailableBoardsChangedEmitter = new Emitter<AvailableBoard[]>();
|
private readonly appStateService: FrontendApplicationStateService;
|
||||||
|
|
||||||
|
protected readonly onBoardsConfigChangedEmitter =
|
||||||
|
new Emitter<BoardsConfig.Config>();
|
||||||
|
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
|
||||||
|
AvailableBoard[]
|
||||||
|
>();
|
||||||
|
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
|
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
|
||||||
@@ -48,7 +56,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
* We have to listen on such changes and auto-reconnect the same board on another port.
|
* We have to listen on such changes and auto-reconnect the same board on another port.
|
||||||
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
|
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
|
||||||
*/
|
*/
|
||||||
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
|
protected latestValidBoardsConfig:
|
||||||
|
| RecursiveRequired<BoardsConfig.Config>
|
||||||
|
| undefined = undefined;
|
||||||
protected latestBoardsConfig: BoardsConfig.Config | undefined = undefined;
|
protected latestBoardsConfig: BoardsConfig.Config | undefined = undefined;
|
||||||
protected _boardsConfig: BoardsConfig.Config = {};
|
protected _boardsConfig: BoardsConfig.Config = {};
|
||||||
protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only.
|
protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only.
|
||||||
@@ -60,28 +70,50 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
* This even also fires, when the boards package was not available for the currently selected board,
|
* This even also fires, when the boards package was not available for the currently selected board,
|
||||||
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
|
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
|
||||||
* currently selected board.\
|
* currently selected board.\
|
||||||
* This even also emitted when the board package for the currently selected board was uninstalled.
|
* This event is also emitted when the board package for the currently selected board was uninstalled.
|
||||||
*/
|
*/
|
||||||
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
|
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
|
||||||
readonly onAvailableBoardsChanged = this.onAvailableBoardsChangedEmitter.event;
|
readonly onAvailableBoardsChanged =
|
||||||
|
this.onAvailableBoardsChangedEmitter.event;
|
||||||
|
readonly onAvailablePortsChanged = this.onAvailablePortsChangedEmitter.event;
|
||||||
|
|
||||||
|
private readonly _reconciled = new Deferred<void>();
|
||||||
|
|
||||||
onStart(): void {
|
onStart(): void {
|
||||||
this.notificationCenter.onAttachedBoardsChanged(this.notifyAttachedBoardsChanged.bind(this));
|
this.notificationCenter.onAttachedBoardsDidChange(
|
||||||
this.notificationCenter.onPlatformInstalled(this.notifyPlatformInstalled.bind(this));
|
this.notifyAttachedBoardsChanged.bind(this)
|
||||||
this.notificationCenter.onPlatformUninstalled(this.notifyPlatformUninstalled.bind(this));
|
);
|
||||||
|
this.notificationCenter.onPlatformDidInstall(
|
||||||
|
this.notifyPlatformInstalled.bind(this)
|
||||||
|
);
|
||||||
|
this.notificationCenter.onPlatformDidUninstall(
|
||||||
|
this.notifyPlatformUninstalled.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
Promise.all([
|
this.appStateService.reachedState('ready').then(async () => {
|
||||||
|
const [attachedBoards, availablePorts] = await Promise.all([
|
||||||
this.boardsService.getAttachedBoards(),
|
this.boardsService.getAttachedBoards(),
|
||||||
this.boardsService.getAvailablePorts(),
|
this.boardsService.getAvailablePorts(),
|
||||||
this.loadState()
|
this.loadState(),
|
||||||
]).then(([attachedBoards, availablePorts]) => {
|
]);
|
||||||
this._attachedBoards = attachedBoards;
|
this._attachedBoards = attachedBoards;
|
||||||
this._availablePorts = availablePorts;
|
this._availablePorts = availablePorts;
|
||||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||||
|
|
||||||
|
await this.reconcileAvailableBoards();
|
||||||
|
|
||||||
|
this.tryReconnect();
|
||||||
|
this._reconciled.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
get reconciled(): Promise<void> {
|
||||||
|
return this._reconciled.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected notifyAttachedBoardsChanged(
|
||||||
|
event: AttachedBoardsChangeEvent
|
||||||
|
): void {
|
||||||
if (!AttachedBoardsChangeEvent.isEmpty(event)) {
|
if (!AttachedBoardsChangeEvent.isEmpty(event)) {
|
||||||
this.logger.info('Attached boards and available ports changed:');
|
this.logger.info('Attached boards and available ports changed:');
|
||||||
this.logger.info(AttachedBoardsChangeEvent.toString(event));
|
this.logger.info(AttachedBoardsChangeEvent.toString(event));
|
||||||
@@ -89,6 +121,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
}
|
}
|
||||||
this._attachedBoards = event.newState.boards;
|
this._attachedBoards = event.newState.boards;
|
||||||
this._availablePorts = event.newState.ports;
|
this._availablePorts = event.newState.ports;
|
||||||
|
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,12 +130,19 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
const { selectedBoard } = this.boardsConfig;
|
const { selectedBoard } = this.boardsConfig;
|
||||||
const { installedVersion, id } = event.item;
|
const { installedVersion, id } = event.item;
|
||||||
if (selectedBoard) {
|
if (selectedBoard) {
|
||||||
const installedBoard = event.item.boards.find(({ name }) => name === selectedBoard.name);
|
const installedBoard = event.item.boards.find(
|
||||||
if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) {
|
({ name }) => name === selectedBoard.name
|
||||||
this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`);
|
);
|
||||||
|
if (
|
||||||
|
installedBoard &&
|
||||||
|
(!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)
|
||||||
|
) {
|
||||||
|
this.logger.info(
|
||||||
|
`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`
|
||||||
|
);
|
||||||
this.boardsConfig = {
|
this.boardsConfig = {
|
||||||
...this.boardsConfig,
|
...this.boardsConfig,
|
||||||
selectedBoard: installedBoard
|
selectedBoard: installedBoard,
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -110,13 +150,32 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
// This logic handles it "gracefully" by unselecting the board, so that we can avoid no FQBN is set error.
|
// This logic handles it "gracefully" by unselecting the board, so that we can avoid no FQBN is set error.
|
||||||
// https://github.com/arduino/arduino-cli/issues/620
|
// https://github.com/arduino/arduino-cli/issues/620
|
||||||
// https://github.com/arduino/arduino-pro-ide/issues/374
|
// https://github.com/arduino/arduino-pro-ide/issues/374
|
||||||
if (BoardWithPackage.is(selectedBoard) && selectedBoard.packageId === event.item.id && !installedBoard) {
|
if (
|
||||||
this.messageService.warn(`Could not find previously selected board '${selectedBoard.name}' in installed platform '${event.item.name}'. Please manually reselect the board you want to use. Do you want to reselect it now?`, 'Reselect later', 'Yes').then(async answer => {
|
BoardWithPackage.is(selectedBoard) &&
|
||||||
if (answer === 'Yes') {
|
selectedBoard.packageId === event.item.id &&
|
||||||
this.commandService.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id, selectedBoard.name);
|
!installedBoard
|
||||||
|
) {
|
||||||
|
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||||
|
this.messageService
|
||||||
|
.warn(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/couldNotFindPreviouslySelected',
|
||||||
|
"Could not find previously selected board '{0}' in installed platform '{1}'. Please manually reselect the board you want to use. Do you want to reselect it now?",
|
||||||
|
selectedBoard.name,
|
||||||
|
event.item.name
|
||||||
|
),
|
||||||
|
nls.localize('arduino/board/reselectLater', 'Reselect later'),
|
||||||
|
yes
|
||||||
|
)
|
||||||
|
.then(async (answer) => {
|
||||||
|
if (answer === yes) {
|
||||||
|
this.commandService.executeCommand(
|
||||||
|
'arduino-open-boards-dialog',
|
||||||
|
selectedBoard.name
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.boardsConfig = {}
|
this.boardsConfig = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Trigger a board re-set. See: https://github.com/arduino/arduino-cli/issues/954
|
// Trigger a board re-set. See: https://github.com/arduino/arduino-cli/issues/954
|
||||||
@@ -129,7 +188,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
this.logger.info('Boards package uninstalled: ', JSON.stringify(event));
|
this.logger.info('Boards package uninstalled: ', JSON.stringify(event));
|
||||||
const { selectedBoard } = this.boardsConfig;
|
const { selectedBoard } = this.boardsConfig;
|
||||||
if (selectedBoard && selectedBoard.fqbn) {
|
if (selectedBoard && selectedBoard.fqbn) {
|
||||||
const uninstalledBoard = event.item.boards.find(({ name }) => name === selectedBoard.name);
|
const uninstalledBoard = event.item.boards.find(
|
||||||
|
({ name }) => name === selectedBoard.name
|
||||||
|
);
|
||||||
if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) {
|
if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) {
|
||||||
// We should not unset the FQBN, if the selected board is an attached, recognized board.
|
// We should not unset the FQBN, if the selected board is an attached, recognized board.
|
||||||
// Attach Uno and install AVR, select Uno. Uninstall the AVR core while Uno is selected. We do not want to discard the FQBN of the Uno board.
|
// Attach Uno and install AVR, select Uno. Uninstall the AVR core while Uno is selected. We do not want to discard the FQBN of the Uno board.
|
||||||
@@ -138,43 +199,59 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
// it is just a FQBN, so we need to find the `selected` board among the `AvailableBoards`
|
// it is just a FQBN, so we need to find the `selected` board among the `AvailableBoards`
|
||||||
const selectedAvailableBoard = AvailableBoard.is(selectedBoard)
|
const selectedAvailableBoard = AvailableBoard.is(selectedBoard)
|
||||||
? selectedBoard
|
? selectedBoard
|
||||||
: this._availableBoards.find(availableBoard => Board.sameAs(availableBoard, selectedBoard));
|
: this._availableBoards.find((availableBoard) =>
|
||||||
if (selectedAvailableBoard && selectedAvailableBoard.selected && selectedAvailableBoard.state === AvailableBoard.State.recognized) {
|
Board.sameAs(availableBoard, selectedBoard)
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
selectedAvailableBoard &&
|
||||||
|
selectedAvailableBoard.selected &&
|
||||||
|
selectedAvailableBoard.state === AvailableBoard.State.recognized
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.logger.info(`Board package ${event.item.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`);
|
this.logger.info(
|
||||||
|
`Board package ${event.item.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`
|
||||||
|
);
|
||||||
const selectedBoardWithoutFqbn = {
|
const selectedBoardWithoutFqbn = {
|
||||||
name: selectedBoard.name
|
name: selectedBoard.name,
|
||||||
// No FQBN
|
// No FQBN
|
||||||
};
|
};
|
||||||
this.boardsConfig = {
|
this.boardsConfig = {
|
||||||
...this.boardsConfig,
|
...this.boardsConfig,
|
||||||
selectedBoard: selectedBoardWithoutFqbn
|
selectedBoard: selectedBoardWithoutFqbn,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async tryReconnect(): Promise<boolean> {
|
protected tryReconnect(): boolean {
|
||||||
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
|
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
|
||||||
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
|
for (const board of this.availableBoards.filter(
|
||||||
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
|
({ state }) => state !== AvailableBoard.State.incomplete
|
||||||
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
|
)) {
|
||||||
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
|
if (
|
||||||
|
this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn &&
|
||||||
|
this.latestValidBoardsConfig.selectedBoard.name === board.name &&
|
||||||
|
Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)
|
||||||
|
) {
|
||||||
this.boardsConfig = this.latestValidBoardsConfig;
|
this.boardsConfig = this.latestValidBoardsConfig;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
|
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
|
||||||
// See documentation on `latestValidBoardsConfig`.
|
// See documentation on `latestValidBoardsConfig`.
|
||||||
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
|
for (const board of this.availableBoards.filter(
|
||||||
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
|
({ state }) => state !== AvailableBoard.State.incomplete
|
||||||
&& this.latestValidBoardsConfig.selectedBoard.name === board.name) {
|
)) {
|
||||||
|
if (
|
||||||
|
this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn &&
|
||||||
|
this.latestValidBoardsConfig.selectedBoard.name === board.name &&
|
||||||
|
this.latestValidBoardsConfig.selectedPort.protocol ===
|
||||||
|
board.port?.protocol
|
||||||
|
) {
|
||||||
this.boardsConfig = {
|
this.boardsConfig = {
|
||||||
...this.latestValidBoardsConfig,
|
...this.latestValidBoardsConfig,
|
||||||
selectedPort: board.port
|
selectedPort: board.port,
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -184,12 +261,20 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set boardsConfig(config: BoardsConfig.Config) {
|
set boardsConfig(config: BoardsConfig.Config) {
|
||||||
this.doSetBoardsConfig(config);
|
this.setBoardsConfig(config);
|
||||||
this.saveState().finally(() => this.reconcileAvailableBoards().finally(() => this.onBoardsConfigChangedEmitter.fire(this._boardsConfig)));
|
this.saveState().finally(() =>
|
||||||
|
this.reconcileAvailableBoards().finally(() =>
|
||||||
|
this.onBoardsConfigChangedEmitter.fire(this._boardsConfig)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected doSetBoardsConfig(config: BoardsConfig.Config): void {
|
get boardsConfig(): BoardsConfig.Config {
|
||||||
this.logger.info('Board config changed: ', JSON.stringify(config));
|
return this._boardsConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setBoardsConfig(config: BoardsConfig.Config): void {
|
||||||
|
this.logger.debug('Board config changed: ', JSON.stringify(config));
|
||||||
this._boardsConfig = config;
|
this._boardsConfig = config;
|
||||||
this.latestBoardsConfig = this._boardsConfig;
|
this.latestBoardsConfig = this._boardsConfig;
|
||||||
if (this.canUploadTo(this._boardsConfig)) {
|
if (this.canUploadTo(this._boardsConfig)) {
|
||||||
@@ -197,13 +282,27 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchBoards({ query, cores }: { query?: string, cores?: string[] }): Promise<BoardWithPackage[]> {
|
async searchBoards({
|
||||||
|
query,
|
||||||
|
cores,
|
||||||
|
}: {
|
||||||
|
query?: string;
|
||||||
|
cores?: string[];
|
||||||
|
}): Promise<BoardWithPackage[]> {
|
||||||
const boards = await this.boardsService.searchBoards({ query });
|
const boards = await this.boardsService.searchBoards({ query });
|
||||||
return boards;
|
return boards;
|
||||||
}
|
}
|
||||||
|
|
||||||
get boardsConfig(): BoardsConfig.Config {
|
async selectedBoardUserFields(): Promise<BoardUserField[]> {
|
||||||
return this._boardsConfig;
|
if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const fqbn = this._boardsConfig.selectedBoard.fqbn;
|
||||||
|
if (!fqbn) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const protocol = this._boardsConfig.selectedPort.protocol;
|
||||||
|
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -211,15 +310,20 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
*/
|
*/
|
||||||
canVerify(
|
canVerify(
|
||||||
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
||||||
options: { silent: boolean } = { silent: true }): config is BoardsConfig.Config & { selectedBoard: Board } {
|
options: { silent: boolean } = { silent: true }
|
||||||
|
): config is BoardsConfig.Config & { selectedBoard: Board } {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.selectedBoard) {
|
if (!config.selectedBoard) {
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
this.messageService.warn('No boards selected.', { timeout: 3000 });
|
this.messageService.warn(
|
||||||
|
nls.localize('arduino/board/noneSelected', 'No boards selected.'),
|
||||||
|
{
|
||||||
|
timeout: 3000,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -232,8 +336,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
*/
|
*/
|
||||||
canUploadTo(
|
canUploadTo(
|
||||||
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
config: BoardsConfig.Config | undefined = this.boardsConfig,
|
||||||
options: { silent: boolean } = { silent: true }): config is RecursiveRequired<BoardsConfig.Config> {
|
options: { silent: boolean } = { silent: true }
|
||||||
|
): config is RecursiveRequired<BoardsConfig.Config> {
|
||||||
if (!this.canVerify(config, options)) {
|
if (!this.canVerify(config, options)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -241,14 +345,30 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
const { name } = config.selectedBoard;
|
const { name } = config.selectedBoard;
|
||||||
if (!config.selectedPort) {
|
if (!config.selectedPort) {
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
|
this.messageService.warn(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/noPortsSelected',
|
||||||
|
"No ports selected for board: '{0}'.",
|
||||||
|
name
|
||||||
|
),
|
||||||
|
{
|
||||||
|
timeout: 3000,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.selectedBoard.fqbn) {
|
if (!config.selectedBoard.fqbn) {
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
|
this.messageService.warn(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/noFQBN',
|
||||||
|
'The FQBN is not available for the selected board "{0}". Do you have the corresponding core installed?',
|
||||||
|
name
|
||||||
|
),
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -260,19 +380,33 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
return this._availableBoards;
|
return this._availableBoards;
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitUntilAvailable(what: Board & { port: Port }, timeout?: number): Promise<void> {
|
async waitUntilAvailable(
|
||||||
|
what: Board & { port: Port },
|
||||||
|
timeout?: number
|
||||||
|
): Promise<void> {
|
||||||
const find = (needle: Board & { port: Port }, haystack: AvailableBoard[]) =>
|
const find = (needle: Board & { port: Port }, haystack: AvailableBoard[]) =>
|
||||||
haystack.find(board => Board.equals(needle, board) && Port.equals(needle.port, board.port));
|
haystack.find(
|
||||||
const timeoutTask = !!timeout && timeout > 0
|
(board) =>
|
||||||
? new Promise<void>((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout} ms.`)), timeout))
|
Board.equals(needle, board) && Port.sameAs(needle.port, board.port)
|
||||||
: new Promise<void>(() => { /* never */ });
|
);
|
||||||
const waitUntilTask = new Promise<void>(resolve => {
|
const timeoutTask =
|
||||||
|
!!timeout && timeout > 0
|
||||||
|
? new Promise<void>((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error(`Timeout after ${timeout} ms.`)),
|
||||||
|
timeout
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: new Promise<void>(() => {
|
||||||
|
/* never */
|
||||||
|
});
|
||||||
|
const waitUntilTask = new Promise<void>((resolve) => {
|
||||||
let candidate = find(what, this.availableBoards);
|
let candidate = find(what, this.availableBoards);
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const disposable = this.onAvailableBoardsChanged(availableBoards => {
|
const disposable = this.onAvailableBoardsChanged((availableBoards) => {
|
||||||
candidate = find(what, availableBoards);
|
candidate = find(what, availableBoards);
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
disposable.dispose();
|
disposable.dispose();
|
||||||
@@ -284,68 +418,117 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async reconcileAvailableBoards(): Promise<void> {
|
protected async reconcileAvailableBoards(): Promise<void> {
|
||||||
const attachedBoards = this._attachedBoards;
|
|
||||||
const availablePorts = this._availablePorts;
|
const availablePorts = this._availablePorts;
|
||||||
// Unset the port on the user's config, if it is not available anymore.
|
// Unset the port on the user's config, if it is not available anymore.
|
||||||
if (this.boardsConfig.selectedPort && !availablePorts.some(port => Port.sameAs(port, this.boardsConfig.selectedPort))) {
|
if (
|
||||||
this.doSetBoardsConfig({ selectedBoard: this.boardsConfig.selectedBoard, selectedPort: undefined });
|
this.boardsConfig.selectedPort &&
|
||||||
|
!availablePorts.some((port) =>
|
||||||
|
Port.sameAs(port, this.boardsConfig.selectedPort)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.setBoardsConfig({
|
||||||
|
selectedBoard: this.boardsConfig.selectedBoard,
|
||||||
|
selectedPort: undefined,
|
||||||
|
});
|
||||||
this.onBoardsConfigChangedEmitter.fire(this._boardsConfig);
|
this.onBoardsConfigChangedEmitter.fire(this._boardsConfig);
|
||||||
}
|
}
|
||||||
const boardsConfig = this.boardsConfig;
|
const boardsConfig = this.boardsConfig;
|
||||||
const currentAvailableBoards = this._availableBoards;
|
const currentAvailableBoards = this._availableBoards;
|
||||||
const availableBoards: AvailableBoard[] = [];
|
const availableBoards: AvailableBoard[] = [];
|
||||||
const availableBoardPorts = availablePorts.filter(Port.isBoardPort);
|
const attachedBoards = this._attachedBoards.filter(({ port }) => !!port);
|
||||||
const attachedSerialBoards = attachedBoards.filter(({ port }) => !!port);
|
const availableBoardPorts = availablePorts.filter((port) => {
|
||||||
|
if (port.protocol === 'serial') {
|
||||||
|
// We always show all serial ports, even if there
|
||||||
|
// is no recognized board connected to it
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other ports with different protocol are
|
||||||
|
// only shown if there is a recognized board
|
||||||
|
// connected
|
||||||
|
for (const board of attachedBoards) {
|
||||||
|
if (board.port?.address === port.address) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
for (const boardPort of availableBoardPorts) {
|
for (const boardPort of availableBoardPorts) {
|
||||||
let state = AvailableBoard.State.incomplete; // Initial pessimism.
|
const board = attachedBoards.find(({ port }) =>
|
||||||
let board = attachedSerialBoards.find(({ port }) => Port.sameAs(boardPort, port));
|
Port.sameAs(boardPort, port)
|
||||||
|
);
|
||||||
|
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(
|
||||||
|
boardPort
|
||||||
|
);
|
||||||
|
|
||||||
|
let availableBoard = {} as AvailableBoard;
|
||||||
if (board) {
|
if (board) {
|
||||||
state = AvailableBoard.State.recognized;
|
availableBoard = {
|
||||||
} else {
|
...board,
|
||||||
|
state: AvailableBoard.State.recognized,
|
||||||
|
selected: BoardsConfig.Config.sameAs(boardsConfig, board),
|
||||||
|
port: boardPort,
|
||||||
|
};
|
||||||
|
} else if (lastSelectedBoard) {
|
||||||
// If the selected board is not recognized because it is a 3rd party board: https://github.com/arduino/arduino-cli/issues/623
|
// If the selected board is not recognized because it is a 3rd party board: https://github.com/arduino/arduino-cli/issues/623
|
||||||
// We still want to show it without the red X in the boards toolbar: https://github.com/arduino/arduino-pro-ide/issues/198#issuecomment-599355836
|
// We still want to show it without the red X in the boards toolbar: https://github.com/arduino/arduino-pro-ide/issues/198#issuecomment-599355836
|
||||||
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(boardPort);
|
availableBoard = {
|
||||||
if (lastSelectedBoard) {
|
|
||||||
board = {
|
|
||||||
...lastSelectedBoard,
|
...lastSelectedBoard,
|
||||||
port: boardPort
|
state: AvailableBoard.State.guessed,
|
||||||
|
selected: BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard),
|
||||||
|
port: boardPort,
|
||||||
};
|
};
|
||||||
state = AvailableBoard.State.guessed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!board) {
|
|
||||||
availableBoards.push({ name: 'Unknown', port: boardPort, state });
|
|
||||||
} else {
|
} else {
|
||||||
const selected = BoardsConfig.Config.sameAs(boardsConfig, board);
|
availableBoard = {
|
||||||
availableBoards.push({ ...board, state, selected, port: boardPort });
|
name: nls.localize('arduino/common/unknown', 'Unknown'),
|
||||||
|
port: boardPort,
|
||||||
|
state: AvailableBoard.State.incomplete,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
availableBoards.push(availableBoard);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boardsConfig.selectedBoard && !availableBoards.some(({ selected }) => selected)) {
|
if (
|
||||||
|
boardsConfig.selectedBoard &&
|
||||||
|
!availableBoards.some(({ selected }) => selected)
|
||||||
|
) {
|
||||||
|
// If the selected board has the same port of an unknown board
|
||||||
|
// that is already in availableBoards we might get a duplicate port.
|
||||||
|
// So we remove the one already in the array and add the selected one.
|
||||||
|
const found = availableBoards.findIndex(
|
||||||
|
(board) => board.port?.address === boardsConfig.selectedPort?.address
|
||||||
|
);
|
||||||
|
if (found >= 0) {
|
||||||
|
availableBoards.splice(found, 1);
|
||||||
|
}
|
||||||
availableBoards.push({
|
availableBoards.push({
|
||||||
...boardsConfig.selectedBoard,
|
...boardsConfig.selectedBoard,
|
||||||
port: boardsConfig.selectedPort,
|
port: boardsConfig.selectedPort,
|
||||||
selected: true,
|
selected: true,
|
||||||
state: AvailableBoard.State.incomplete
|
state: AvailableBoard.State.incomplete,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedAvailableBoards = availableBoards.sort(AvailableBoard.compare);
|
availableBoards.sort(AvailableBoard.compare);
|
||||||
let hasChanged = sortedAvailableBoards.length !== currentAvailableBoards.length;
|
|
||||||
for (let i = 0; !hasChanged && i < sortedAvailableBoards.length; i++) {
|
let hasChanged = availableBoards.length !== currentAvailableBoards.length;
|
||||||
hasChanged = AvailableBoard.compare(sortedAvailableBoards[i], currentAvailableBoards[i]) !== 0;
|
for (let i = 0; !hasChanged && i < availableBoards.length; i++) {
|
||||||
|
const [left, right] = [availableBoards[i], currentAvailableBoards[i]];
|
||||||
|
hasChanged =
|
||||||
|
left.fqbn !== right.fqbn ||
|
||||||
|
!!AvailableBoard.compare(left, right) ||
|
||||||
|
left.selected !== right.selected;
|
||||||
}
|
}
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
this._availableBoards = sortedAvailableBoards;
|
this._availableBoards = availableBoards;
|
||||||
this.onAvailableBoardsChangedEmitter.fire(this._availableBoards);
|
this.onAvailableBoardsChangedEmitter.fire(this._availableBoards);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getLastSelectedBoardOnPort(port: Port | string | undefined): Promise<Board | undefined> {
|
protected async getLastSelectedBoardOnPort(
|
||||||
if (!port) {
|
port: Port
|
||||||
return undefined;
|
): Promise<Board | undefined> {
|
||||||
}
|
|
||||||
const key = this.getLastSelectedBoardOnPortKey(port);
|
const key = this.getLastSelectedBoardOnPortKey(port);
|
||||||
return this.getData<Board>(key);
|
return this.getData<Board>(key);
|
||||||
}
|
}
|
||||||
@@ -362,17 +545,21 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
}
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.setData('latest-valid-boards-config', this.latestValidBoardsConfig),
|
this.setData('latest-valid-boards-config', this.latestValidBoardsConfig),
|
||||||
this.setData('latest-boards-config', this.latestBoardsConfig)
|
this.setData('latest-boards-config', this.latestBoardsConfig),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
|
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
|
||||||
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
|
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
|
||||||
return `last-selected-board-on-port:${typeof port === 'string' ? port : Port.toString(port)}`;
|
return `last-selected-board-on-port:${
|
||||||
|
typeof port === 'string' ? port : port.address
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadState(): Promise<void> {
|
protected async loadState(): Promise<void> {
|
||||||
const storedLatestValidBoardsConfig = await this.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
|
const storedLatestValidBoardsConfig = await this.getData<
|
||||||
|
RecursiveRequired<BoardsConfig.Config>
|
||||||
|
>('latest-valid-boards-config');
|
||||||
if (storedLatestValidBoardsConfig) {
|
if (storedLatestValidBoardsConfig) {
|
||||||
this.latestValidBoardsConfig = storedLatestValidBoardsConfig;
|
this.latestValidBoardsConfig = storedLatestValidBoardsConfig;
|
||||||
if (this.canUploadTo(this.latestValidBoardsConfig)) {
|
if (this.canUploadTo(this.latestValidBoardsConfig)) {
|
||||||
@@ -380,10 +567,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If we could not restore the latest valid config, try to restore something, the board at least.
|
// If we could not restore the latest valid config, try to restore something, the board at least.
|
||||||
let storedLatestBoardsConfig = await this.getData<BoardsConfig.Config | undefined>('latest-boards-config');
|
let storedLatestBoardsConfig = await this.getData<
|
||||||
|
BoardsConfig.Config | undefined
|
||||||
|
>('latest-boards-config');
|
||||||
// Try to get from the URL if it was not persisted.
|
// Try to get from the URL if it was not persisted.
|
||||||
if (!storedLatestBoardsConfig) {
|
if (!storedLatestBoardsConfig) {
|
||||||
storedLatestBoardsConfig = BoardsConfig.Config.getConfig(new URL(window.location.href));
|
storedLatestBoardsConfig = BoardsConfig.Config.getConfig(
|
||||||
|
new URL(window.location.href)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (storedLatestBoardsConfig) {
|
if (storedLatestBoardsConfig) {
|
||||||
this.latestBoardsConfig = storedLatestBoardsConfig;
|
this.latestBoardsConfig = storedLatestBoardsConfig;
|
||||||
@@ -393,11 +584,18 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setData<T>(key: string, value: T): Promise<void> {
|
private setData<T>(key: string, value: T): Promise<void> {
|
||||||
return this.commandService.executeCommand(StorageWrapper.Commands.SET_DATA.id, key, value);
|
return this.commandService.executeCommand(
|
||||||
|
StorageWrapper.Commands.SET_DATA.id,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getData<T>(key: string): Promise<T | undefined> {
|
private getData<T>(key: string): Promise<T | undefined> {
|
||||||
return this.commandService.executeCommand<T>(StorageWrapper.Commands.GET_DATA.id, key);
|
return this.commandService.executeCommand<T>(
|
||||||
|
StorageWrapper.Commands.GET_DATA.id,
|
||||||
|
key
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +612,6 @@ export interface AvailableBoard extends Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace AvailableBoard {
|
export namespace AvailableBoard {
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
/**
|
/**
|
||||||
* Retrieved from the CLI via the `board list` command.
|
* Retrieved from the CLI via the `board list` command.
|
||||||
@@ -427,47 +624,52 @@ export namespace AvailableBoard {
|
|||||||
/**
|
/**
|
||||||
* We do not know anything about this board, probably a 3rd party. The user has not selected a board for this port yet.
|
* We do not know anything about this board, probably a 3rd party. The user has not selected a board for this port yet.
|
||||||
*/
|
*/
|
||||||
'incomplete'
|
'incomplete',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function is(board: any): board is AvailableBoard {
|
export function is(board: any): board is AvailableBoard {
|
||||||
return Board.is(board) && 'state' in board;
|
return Board.is(board) && 'state' in board;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasPort(board: AvailableBoard): board is AvailableBoard & { port: Port } {
|
export function hasPort(
|
||||||
|
board: AvailableBoard
|
||||||
|
): board is AvailableBoard & { port: Port } {
|
||||||
return !!board.port;
|
return !!board.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Available boards must be sorted in this order:
|
||||||
|
// 1. Serial with recognized boards
|
||||||
|
// 2. Serial with guessed boards
|
||||||
|
// 3. Serial with incomplete boards
|
||||||
|
// 4. Network with recognized boards
|
||||||
|
// 5. Other protocols with recognized boards
|
||||||
export const compare = (left: AvailableBoard, right: AvailableBoard) => {
|
export const compare = (left: AvailableBoard, right: AvailableBoard) => {
|
||||||
if (left.selected && !right.selected) {
|
if (left.port?.protocol === 'serial' && right.port?.protocol !== 'serial') {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
} else if (
|
||||||
if (right.selected && !left.selected) {
|
left.port?.protocol !== 'serial' &&
|
||||||
|
right.port?.protocol === 'serial'
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
|
} else if (
|
||||||
|
left.port?.protocol === 'network' &&
|
||||||
|
right.port?.protocol !== 'network'
|
||||||
|
) {
|
||||||
|
return -1;
|
||||||
|
} else if (
|
||||||
|
left.port?.protocol !== 'network' &&
|
||||||
|
right.port?.protocol === 'network'
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
|
} else if (left.port?.protocol === right.port?.protocol) {
|
||||||
|
// We show all ports, including those that have guessed
|
||||||
|
// or unrecognized boards, so we must sort those too.
|
||||||
|
if (left.state < right.state) {
|
||||||
|
return -1;
|
||||||
|
} else if (left.state > right.state) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
let result = naturalCompare(left.name, right.name);
|
|
||||||
if (result !== 0) {
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
if (left.fqbn && right.fqbn) {
|
return naturalCompare(left.port?.address!, right.port?.address!);
|
||||||
result = naturalCompare(left.fqbn, right.fqbn);
|
};
|
||||||
if (result !== 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (left.port && right.port) {
|
|
||||||
result = Port.compare(left.port, right.port);
|
|
||||||
if (result !== 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!!left.selected && !right.selected) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (!!right.selected && !left.selected) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return left.state - right.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import * as React from 'react';
|
import * as React from '@theia/core/shared/react';
|
||||||
import * as ReactDOM from 'react-dom';
|
import * as ReactDOM from '@theia/core/shared/react-dom';
|
||||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
import { Port } from '../../common/protocol';
|
import { Port } from '../../common/protocol';
|
||||||
|
import { OpenBoardsConfig } from '../contributions/open-boards-config';
|
||||||
|
import {
|
||||||
|
BoardsServiceProvider,
|
||||||
|
AvailableBoard,
|
||||||
|
} from './boards-service-provider';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { BoardsConfig } from './boards-config';
|
import { BoardsConfig } from './boards-config';
|
||||||
import { ArduinoCommands } from '../arduino-commands';
|
|
||||||
import { BoardsServiceProvider, AvailableBoard } from './boards-service-provider';
|
|
||||||
|
|
||||||
export interface BoardsDropDownListCoords {
|
export interface BoardsDropDownListCoords {
|
||||||
readonly top: number;
|
readonly top: number;
|
||||||
@@ -17,18 +22,19 @@ export interface BoardsDropDownListCoords {
|
|||||||
export namespace BoardsDropDown {
|
export namespace BoardsDropDown {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly coords: BoardsDropDownListCoords | 'hidden';
|
readonly coords: BoardsDropDownListCoords | 'hidden';
|
||||||
readonly items: Array<AvailableBoard & { onClick: () => void, port: Port }>;
|
readonly items: Array<AvailableBoard & { onClick: () => void; port: Port }>;
|
||||||
readonly openBoardsConfig: () => void;
|
readonly openBoardsConfig: () => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||||
|
|
||||||
protected dropdownElement: HTMLElement;
|
protected dropdownElement: HTMLElement;
|
||||||
|
private listRef: React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
constructor(props: BoardsDropDown.Props) {
|
constructor(props: BoardsDropDown.Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.listRef = React.createRef();
|
||||||
let list = document.getElementById('boards-dropdown-container');
|
let list = document.getElementById('boards-dropdown-container');
|
||||||
if (!list) {
|
if (!list) {
|
||||||
list = document.createElement('div');
|
list = document.createElement('div');
|
||||||
@@ -38,7 +44,13 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): React.ReactNode {
|
override componentDidUpdate(prevProps: BoardsDropDown.Props): void {
|
||||||
|
if (prevProps.coords === 'hidden' && this.listRef.current) {
|
||||||
|
this.listRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override render(): React.ReactNode {
|
||||||
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
|
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,43 +59,110 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
|||||||
if (coords === 'hidden') {
|
if (coords === 'hidden') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return <div className='arduino-boards-dropdown-list'
|
const footerLabel = nls.localize(
|
||||||
|
'arduino/board/openBoardsConfig',
|
||||||
|
'Select other board and port…'
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="arduino-boards-dropdown-list"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
...coords
|
...coords,
|
||||||
}}>
|
}}
|
||||||
{this.renderItem({
|
ref={this.listRef}
|
||||||
label: 'Select Other Board & Port',
|
tabIndex={0}
|
||||||
onClick: () => this.props.openBoardsConfig()
|
>
|
||||||
|
<div className="arduino-boards-dropdown-list--items-container">
|
||||||
|
{items
|
||||||
|
.map(({ name, port, selected, onClick }) => ({
|
||||||
|
boardLabel: name,
|
||||||
|
port,
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
}))
|
||||||
|
.map(this.renderItem)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key={footerLabel}
|
||||||
|
tabIndex={0}
|
||||||
|
className="arduino-boards-dropdown-item arduino-board-dropdown-footer"
|
||||||
|
onClick={() => this.props.openBoardsConfig()}
|
||||||
|
>
|
||||||
|
<div>{footerLabel}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderItem({
|
||||||
|
boardLabel,
|
||||||
|
port,
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
boardLabel: string;
|
||||||
|
port: Port;
|
||||||
|
selected?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}): React.ReactNode {
|
||||||
|
const protocolIcon = iconNameFromProtocol(port.protocol);
|
||||||
|
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`board-item--${boardLabel}-${port.address}`}
|
||||||
|
className={classNames('arduino-boards-dropdown-item', {
|
||||||
|
'arduino-boards-dropdown-item--selected': selected,
|
||||||
})}
|
})}
|
||||||
{items.map(({ name, port, selected, onClick }) => ({ label: `${name} at ${Port.toString(port)}`, selected, onClick })).map(this.renderItem)}
|
onClick={onClick}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'arduino-boards-dropdown-item--protocol',
|
||||||
|
'fa',
|
||||||
|
protocolIcon
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="arduino-boards-dropdown-item--label"
|
||||||
|
title={`${boardLabel}\n${port.address}`}
|
||||||
|
>
|
||||||
|
<div className="arduino-boards-dropdown-item--board-label noWrapInfo noselect">
|
||||||
|
{boardLabel}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="arduino-boards-dropdown-item--port-label noWrapInfo noselect">
|
||||||
|
{port.address}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selected ? <div className="fa fa-check" /> : ''}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderItem({ label, selected, onClick }: { label: string, selected?: boolean, onClick: () => void }): React.ReactNode {
|
export class BoardsToolBarItem extends React.Component<
|
||||||
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
|
BoardsToolBarItem.Props,
|
||||||
<div>
|
BoardsToolBarItem.State
|
||||||
{label}
|
> {
|
||||||
</div>
|
|
||||||
{selected ? <span className='fa fa-check' /> : ''}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
|
|
||||||
|
|
||||||
static TOOLBAR_ID: 'boards-toolbar';
|
static TOOLBAR_ID: 'boards-toolbar';
|
||||||
|
|
||||||
protected readonly toDispose: DisposableCollection = new DisposableCollection();
|
protected readonly toDispose: DisposableCollection =
|
||||||
|
new DisposableCollection();
|
||||||
|
|
||||||
constructor(props: BoardsToolBarItem.Props) {
|
constructor(props: BoardsToolBarItem.Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { availableBoards } = props.boardsServiceClient;
|
const { availableBoards } = props.boardsServiceProvider;
|
||||||
this.state = {
|
this.state = {
|
||||||
availableBoards,
|
availableBoards,
|
||||||
coords: 'hidden'
|
coords: 'hidden',
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('click', () => {
|
document.addEventListener('click', () => {
|
||||||
@@ -91,15 +170,17 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
override componentDidMount(): void {
|
||||||
this.props.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.setState({ availableBoards }));
|
this.props.boardsServiceProvider.onAvailableBoardsChanged(
|
||||||
|
(availableBoards) => this.setState({ availableBoards })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
override componentWillUnmount(): void {
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
|
protected readonly show = (event: React.MouseEvent<HTMLElement>): void => {
|
||||||
const { currentTarget: element } = event;
|
const { currentTarget: element } = event;
|
||||||
if (element instanceof HTMLElement) {
|
if (element instanceof HTMLElement) {
|
||||||
if (this.state.coords === 'hidden') {
|
if (this.state.coords === 'hidden') {
|
||||||
@@ -109,8 +190,8 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
|||||||
top: rect.top,
|
top: rect.top,
|
||||||
left: rect.left,
|
left: rect.left,
|
||||||
width: rect.width,
|
width: rect.width,
|
||||||
paddingTop: rect.height
|
paddingTop: rect.height,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ coords: 'hidden' });
|
this.setState({ coords: 'hidden' });
|
||||||
@@ -120,68 +201,94 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
|
|||||||
event.nativeEvent.stopImmediatePropagation();
|
event.nativeEvent.stopImmediatePropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
render(): React.ReactNode {
|
override render(): React.ReactNode {
|
||||||
const { coords, availableBoards } = this.state;
|
const { coords, availableBoards } = this.state;
|
||||||
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
|
const { selectedBoard, selectedPort } =
|
||||||
const title = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
|
this.props.boardsServiceProvider.boardsConfig;
|
||||||
const decorator = (() => {
|
|
||||||
const selectedBoard = availableBoards.find(({ selected }) => selected);
|
|
||||||
if (!selectedBoard || !selectedBoard.port) {
|
|
||||||
return 'fa fa-times notAttached'
|
|
||||||
}
|
|
||||||
if (selectedBoard.state === AvailableBoard.State.guessed) {
|
|
||||||
return 'fa fa-exclamation-triangle guessed'
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})();
|
|
||||||
|
|
||||||
return <React.Fragment>
|
const boardLabel =
|
||||||
<div className='arduino-boards-toolbar-item-container'>
|
selectedBoard?.name ||
|
||||||
<div className='arduino-boards-toolbar-item' title={title}>
|
nls.localize('arduino/board/selectBoard', 'Select Board');
|
||||||
<div className='inner-container' onClick={this.show}>
|
const selectedPortLabel = portLabel(selectedPort?.address);
|
||||||
<span className={decorator} />
|
|
||||||
<div className='label noWrapInfo'>
|
const isConnected = Boolean(selectedBoard && selectedPort);
|
||||||
<div className='noWrapInfo noselect'>
|
const protocolIcon = isConnected
|
||||||
{title}
|
? iconNameFromProtocol(selectedPort?.protocol || '')
|
||||||
</div>
|
: null;
|
||||||
</div>
|
const protocolIconClassNames = classNames(
|
||||||
<span className='fa fa-caret-down caret' />
|
'arduino-boards-toolbar-item--protocol',
|
||||||
</div>
|
'fa',
|
||||||
|
protocolIcon
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div
|
||||||
|
className="arduino-boards-toolbar-item-container"
|
||||||
|
title={selectedPortLabel}
|
||||||
|
onClick={this.show}
|
||||||
|
>
|
||||||
|
{protocolIcon && <div className={protocolIconClassNames} />}
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'arduino-boards-toolbar-item--label',
|
||||||
|
'noWrapInfo',
|
||||||
|
'noselect',
|
||||||
|
{ 'arduino-boards-toolbar-item--label-connected': isConnected }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{boardLabel}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="fa fa-caret-down caret" />
|
||||||
</div>
|
</div>
|
||||||
<BoardsDropDown
|
<BoardsDropDown
|
||||||
coords={coords}
|
coords={coords}
|
||||||
items={availableBoards.filter(AvailableBoard.hasPort).map(board => ({
|
items={availableBoards
|
||||||
|
.filter(AvailableBoard.hasPort)
|
||||||
|
.map((board) => ({
|
||||||
...board,
|
...board,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (board.state === AvailableBoard.State.incomplete) {
|
if (!board.fqbn) {
|
||||||
this.props.boardsServiceClient.boardsConfig = {
|
const previousBoardConfig =
|
||||||
selectedPort: board.port
|
this.props.boardsServiceProvider.boardsConfig;
|
||||||
|
this.props.boardsServiceProvider.boardsConfig = {
|
||||||
|
selectedPort: board.port,
|
||||||
};
|
};
|
||||||
this.openDialog();
|
this.openDialog(previousBoardConfig);
|
||||||
} else {
|
} else {
|
||||||
this.props.boardsServiceClient.boardsConfig = {
|
this.props.boardsServiceProvider.boardsConfig = {
|
||||||
selectedBoard: board,
|
selectedBoard: board,
|
||||||
selectedPort: board.port
|
selectedPort: board.port,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))}
|
|
||||||
openBoardsConfig={this.openDialog}>
|
|
||||||
</BoardsDropDown>
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected openDialog = () => {
|
|
||||||
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
|
|
||||||
this.setState({ coords: 'hidden' });
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
this.setState({ coords: 'hidden' });
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
openBoardsConfig={this.openDialog}
|
||||||
|
></BoardsDropDown>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected openDialog = async (
|
||||||
|
previousBoardConfig?: BoardsConfig.Config
|
||||||
|
): Promise<void> => {
|
||||||
|
const selectedBoardConfig =
|
||||||
|
await this.props.commands.executeCommand<BoardsConfig.Config>(
|
||||||
|
OpenBoardsConfig.Commands.OPEN_DIALOG.id
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
previousBoardConfig &&
|
||||||
|
(!selectedBoardConfig?.selectedPort ||
|
||||||
|
!selectedBoardConfig?.selectedBoard)
|
||||||
|
) {
|
||||||
|
this.props.boardsServiceProvider.boardsConfig = previousBoardConfig;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export namespace BoardsToolBarItem {
|
export namespace BoardsToolBarItem {
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly boardsServiceClient: BoardsServiceProvider;
|
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
readonly commands: CommandRegistry;
|
readonly commands: CommandRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,5 +296,27 @@ export namespace BoardsToolBarItem {
|
|||||||
availableBoards: AvailableBoard[];
|
availableBoards: AvailableBoard[];
|
||||||
coords: BoardsDropDownListCoords | 'hidden';
|
coords: BoardsDropDownListCoords | 'hidden';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconNameFromProtocol(protocol: string): string {
|
||||||
|
switch (protocol) {
|
||||||
|
case 'serial':
|
||||||
|
return 'fa-arduino-technology-usb';
|
||||||
|
case 'network':
|
||||||
|
return 'fa-arduino-technology-connection';
|
||||||
|
/*
|
||||||
|
Bluetooth ports are not listed yet from the CLI;
|
||||||
|
Not sure about the naming ('bluetooth'); make sure it's correct before uncommenting the following lines
|
||||||
|
*/
|
||||||
|
// case 'bluetooth':
|
||||||
|
// return 'fa-arduino-technology-bluetooth';
|
||||||
|
default:
|
||||||
|
return 'fa-arduino-technology-3dimensionscube';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function portLabel(portName?: string): string {
|
||||||
|
return portName
|
||||||
|
? nls.localize('arduino/board/portLabel', 'Port: {0}', portName)
|
||||||
|
: nls.localize('arduino/board/disconnected', 'Disconnected');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
import { injectable } from 'inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { BoardsListWidget } from './boards-list-widget';
|
import { BoardsListWidget } from './boards-list-widget';
|
||||||
import { BoardsPackage } from '../../common/protocol/boards-service';
|
import { BoardsPackage } from '../../common/protocol/boards-service';
|
||||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
|
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
widgetId: BoardsListWidget.WIDGET_ID,
|
widgetId: BoardsListWidget.WIDGET_ID,
|
||||||
widgetName: BoardsListWidget.WIDGET_LABEL,
|
widgetName: BoardsListWidget.WIDGET_LABEL,
|
||||||
defaultWidgetOptions: {
|
defaultWidgetOptions: {
|
||||||
area: 'left',
|
area: 'left',
|
||||||
rank: 2
|
rank: 2,
|
||||||
},
|
},
|
||||||
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
||||||
toggleKeybinding: 'CtrlCmd+Shift+B'
|
toggleKeybinding: 'CtrlCmd+Shift+B',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeLayout(): Promise<void> {
|
override async initializeLayout(): Promise<void> {
|
||||||
this.openView();
|
this.openView();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
arduino-ide-extension/src/browser/components/ProgressBar.tsx
Normal file
28
arduino-ide-extension/src/browser/components/ProgressBar.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from '@theia/core/shared/react';
|
||||||
|
|
||||||
|
export type ProgressBarProps = {
|
||||||
|
percent?: number;
|
||||||
|
showPercentage?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProgressBar({
|
||||||
|
percent = 0,
|
||||||
|
showPercentage = false,
|
||||||
|
}: ProgressBarProps): React.ReactElement {
|
||||||
|
const roundedPercent = Math.round(percent);
|
||||||
|
return (
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div className="progress-bar--outer">
|
||||||
|
<div
|
||||||
|
className="progress-bar--inner"
|
||||||
|
style={{ width: `${roundedPercent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showPercentage && (
|
||||||
|
<div className="progress-bar--percentage">
|
||||||
|
<div className="progress-bar--percentage-text">{roundedPercent}%</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,49 +1,74 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { isOSX, isWindows } from '@theia/core/lib/common/os';
|
import { isOSX, isWindows } from '@theia/core/lib/common/os';
|
||||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||||
import { Contribution, Command, MenuModelRegistry, CommandRegistry } from './contribution';
|
import {
|
||||||
|
Contribution,
|
||||||
|
Command,
|
||||||
|
MenuModelRegistry,
|
||||||
|
CommandRegistry,
|
||||||
|
} from './contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ConfigService } from '../../common/protocol';
|
import { ConfigService } from '../../common/protocol';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class About extends Contribution {
|
export class About extends Contribution {
|
||||||
|
|
||||||
@inject(ClipboardService)
|
@inject(ClipboardService)
|
||||||
protected readonly clipboardService: ClipboardService;
|
protected readonly clipboardService: ClipboardService;
|
||||||
|
|
||||||
@inject(ConfigService)
|
@inject(ConfigService)
|
||||||
protected readonly configService: ConfigService;
|
protected readonly configService: ConfigService;
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(About.Commands.ABOUT_APP, {
|
registry.registerCommand(About.Commands.ABOUT_APP, {
|
||||||
execute: () => this.showAbout()
|
execute: () => this.showAbout(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
|
||||||
commandId: About.Commands.ABOUT_APP.id,
|
commandId: About.Commands.ABOUT_APP.id,
|
||||||
label: `About ${this.applicationName}`,
|
label: nls.localize(
|
||||||
order: '0'
|
'arduino/about/label',
|
||||||
|
'About {0}',
|
||||||
|
this.applicationName
|
||||||
|
),
|
||||||
|
order: '0',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async showAbout(): Promise<void> {
|
async showAbout(): Promise<void> {
|
||||||
const { version, commit, status: cliStatus } = await this.configService.getVersion();
|
const {
|
||||||
|
version,
|
||||||
|
commit,
|
||||||
|
status: cliStatus,
|
||||||
|
} = await this.configService.getVersion();
|
||||||
const buildDate = this.buildDate;
|
const buildDate = this.buildDate;
|
||||||
const detail = (showAll: boolean) => `Version: ${remote.app.getVersion()}
|
const detail = (showAll: boolean) =>
|
||||||
Date: ${buildDate ? buildDate : 'dev build'}${buildDate && showAll ? ` (${this.ago(buildDate)})` : ''}
|
nls.localize(
|
||||||
CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}]
|
'arduino/about/detail',
|
||||||
|
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}{4} [{5}]\n\n{6}',
|
||||||
${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
|
remote.app.getVersion(),
|
||||||
`;
|
buildDate ? buildDate : nls.localize('', 'dev build'),
|
||||||
const ok = 'OK';
|
buildDate && showAll ? ` (${this.ago(buildDate)})` : '',
|
||||||
const copy = 'Copy';
|
version,
|
||||||
|
cliStatus ? ` ${cliStatus}` : '',
|
||||||
|
commit,
|
||||||
|
nls.localize(
|
||||||
|
'arduino/about/copyright',
|
||||||
|
'Copyright © {0} Arduino SA',
|
||||||
|
new Date().getFullYear().toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const ok = nls.localize('vscode/issueMainService/ok', 'OK');
|
||||||
|
const copy = nls.localize('vscode/textInputActions/copy', 'Copy');
|
||||||
const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy];
|
const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy];
|
||||||
const { response } = await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
const { response } = await remote.dialog.showMessageBox(
|
||||||
|
remote.getCurrentWindow(),
|
||||||
|
{
|
||||||
message: `${this.applicationName}`,
|
message: `${this.applicationName}`,
|
||||||
title: `${this.applicationName}`,
|
title: `${this.applicationName}`,
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@@ -51,8 +76,9 @@ ${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
|
|||||||
buttons,
|
buttons,
|
||||||
noLink: true,
|
noLink: true,
|
||||||
defaultId: buttons.indexOf(ok),
|
defaultId: buttons.indexOf(ok),
|
||||||
cancelId: buttons.indexOf(ok)
|
cancelId: buttons.indexOf(ok),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (buttons[response] === copy) {
|
if (buttons[response] === copy) {
|
||||||
await this.clipboardService.writeText(detail(false).trim());
|
await this.clipboardService.writeText(detail(false).trim());
|
||||||
@@ -72,34 +98,93 @@ ${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
|
|||||||
const other = moment(isoTime);
|
const other = moment(isoTime);
|
||||||
let result = now.diff(other, 'minute');
|
let result = now.diff(other, 'minute');
|
||||||
if (result < 60) {
|
if (result < 60) {
|
||||||
return result === 1 ? `${result} minute ago` : `${result} minute ago`;
|
return result === 1
|
||||||
|
? nls.localize(
|
||||||
|
'vscode/date/date.fromNow.minutes.singular.ago',
|
||||||
|
'{0} minute ago',
|
||||||
|
result.toString()
|
||||||
|
)
|
||||||
|
: nls.localize(
|
||||||
|
'vscode/date/date.fromNow.minutes.plural.ago',
|
||||||
|
'{0} minutes ago',
|
||||||
|
result.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
result = now.diff(other, 'hour');
|
result = now.diff(other, 'hour');
|
||||||
if (result < 25) {
|
if (result < 25) {
|
||||||
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
|
return result === 1
|
||||||
|
? nls.localize(
|
||||||
|
'vscode/date/date.fromNow.hours.singular.ago',
|
||||||
|
'{0} hour ago',
|
||||||
|
result.toString()
|
||||||
|
)
|
||||||
|
: nls.localize(
|
||||||
|
'vscode/date/date.fromNow.hours.plural.ago',
|
||||||
|
'{0} hours ago',
|
||||||
|
result.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
result = now.diff(other, 'day');
|
result = now.diff(other, 'day');
|
||||||
if (result < 8) {
|
if (result < 8) {
|
||||||
return result === 1 ? `${result} day ago` : `${result} days ago`;
|
return result === 1
|
||||||
|
? nls.localize(
|
||||||
|
'vscode/date/date.fromNow.days.singular.ago',
|
||||||
|
'{0} day ago',
|
||||||
|
result.toString()
|
||||||
|
)
|
||||||
|
: nls.localize(
|
||||||
|
'vscode/date/date.fromNow.days.plural.ago',
|
||||||
|
'{0} days ago',
|
||||||
|
result.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
result = now.diff(other, 'week');
|
result = now.diff(other, 'week');
|
||||||
if (result < 5) {
|
if (result < 5) {
|
||||||
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
|
return result === 1
|
||||||
|
? nls.localize(
|
||||||
|
'vscode/date/date.fromNow.weeks.singular.ago',
|
||||||
|
'{0} week ago',
|
||||||
|
result.toString()
|
||||||
|
)
|
||||||
|
: nls.localize(
|
||||||
|
'vscode/date/date.fromNow.weeks.plural.ago',
|
||||||
|
'{0} weeks ago',
|
||||||
|
result.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
result = now.diff(other, 'month');
|
result = now.diff(other, 'month');
|
||||||
if (result < 13) {
|
if (result < 13) {
|
||||||
return result === 1 ? `${result} month ago` : `${result} months ago`;
|
return result === 1
|
||||||
|
? nls.localize(
|
||||||
|
'vscode/date/date.fromNow.months.singular.ago',
|
||||||
|
'{0} month ago',
|
||||||
|
result.toString()
|
||||||
|
)
|
||||||
|
: nls.localize(
|
||||||
|
'vscode/date/date.fromNow.months.plural.ago',
|
||||||
|
'{0} months ago',
|
||||||
|
result.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
result = now.diff(other, 'year');
|
result = now.diff(other, 'year');
|
||||||
return result === 1 ? `${result} year ago` : `${result} years ago`;
|
return result === 1
|
||||||
|
? nls.localize(
|
||||||
|
'vscode/date/date.fromNow.years.singular.ago',
|
||||||
|
'{0} year ago',
|
||||||
|
result.toString()
|
||||||
|
)
|
||||||
|
: nls.localize(
|
||||||
|
'vscode/date/date.fromNow.years.plural.ago',
|
||||||
|
'{0} years ago',
|
||||||
|
result.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace About {
|
export namespace About {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const ABOUT_APP: Command = {
|
export const ABOUT_APP: Command = {
|
||||||
id: 'arduino-about'
|
id: 'arduino-about',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,46 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, URI } from './contribution';
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
URI,
|
||||||
|
} from './contribution';
|
||||||
import { FileDialogService } from '@theia/filesystem/lib/browser';
|
import { FileDialogService } from '@theia/filesystem/lib/browser';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class AddFile extends SketchContribution {
|
export class AddFile extends SketchContribution {
|
||||||
|
|
||||||
@inject(FileDialogService)
|
@inject(FileDialogService)
|
||||||
protected readonly fileDialogService: FileDialogService;
|
protected readonly fileDialogService: FileDialogService;
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(AddFile.Commands.ADD_FILE, {
|
registry.registerCommand(AddFile.Commands.ADD_FILE, {
|
||||||
execute: () => this.addFile()
|
execute: () => this.addFile(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
||||||
commandId: AddFile.Commands.ADD_FILE.id,
|
commandId: AddFile.Commands.ADD_FILE.id,
|
||||||
label: 'Add File...',
|
label: nls.localize('arduino/contributions/addFile', 'Add File') + '...',
|
||||||
order: '2'
|
order: '2',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async addFile(): Promise<void> {
|
protected async addFile(): Promise<void> {
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
if (!sketch) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const toAddUri = await this.fileDialogService.showOpenDialog({
|
const toAddUri = await this.fileDialogService.showOpenDialog({
|
||||||
title: 'Add File',
|
title: nls.localize('arduino/contributions/addFile', 'Add File'),
|
||||||
canSelectFiles: true,
|
canSelectFiles: true,
|
||||||
canSelectFolders: false,
|
canSelectFolders: false,
|
||||||
canSelectMany: false
|
canSelectMany: false,
|
||||||
});
|
});
|
||||||
if (!toAddUri) {
|
if (!toAddUri) {
|
||||||
return;
|
return;
|
||||||
@@ -45,24 +52,39 @@ export class AddFile extends SketchContribution {
|
|||||||
if (exists) {
|
if (exists) {
|
||||||
const { response } = await remote.dialog.showMessageBox({
|
const { response } = await remote.dialog.showMessageBox({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
title: 'Replace',
|
title: nls.localize('arduino/contributions/replaceTitle', 'Replace'),
|
||||||
buttons: ['Cancel', 'OK'],
|
buttons: [
|
||||||
message: `Replace the existing version of ${filename}?`
|
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||||
|
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||||
|
],
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/replaceMsg',
|
||||||
|
'Replace the existing version of {0}?',
|
||||||
|
filename
|
||||||
|
),
|
||||||
});
|
});
|
||||||
if (response === 0) { // Cancel
|
if (response === 0) {
|
||||||
|
// Cancel
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
|
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
|
||||||
this.messageService.info('One file added to the sketch.', { timeout: 2000 });
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/contributions/fileAdded',
|
||||||
|
'One file added to the sketch.'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
timeout: 2000,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace AddFile {
|
export namespace AddFile {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const ADD_FILE: Command = {
|
export const ADD_FILE: Command = {
|
||||||
id: 'arduino-add-file'
|
id: 'arduino-add-file',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,45 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ResponseServiceImpl } from '../response-service-impl';
|
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
|
||||||
import { Installable, LibraryService } from '../../common/protocol';
|
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class AddZipLibrary extends SketchContribution {
|
export class AddZipLibrary extends SketchContribution {
|
||||||
|
|
||||||
@inject(EnvVariablesServer)
|
@inject(EnvVariablesServer)
|
||||||
protected readonly envVariableServer: EnvVariablesServer;
|
protected readonly envVariableServer: EnvVariablesServer;
|
||||||
|
|
||||||
@inject(ResponseServiceImpl)
|
@inject(ResponseServiceClient)
|
||||||
protected readonly responseService: ResponseServiceImpl;
|
protected readonly responseService: ResponseServiceClient;
|
||||||
|
|
||||||
@inject(LibraryService)
|
@inject(LibraryService)
|
||||||
protected readonly libraryService: LibraryService;
|
protected readonly libraryService: LibraryService;
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
|
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
|
||||||
execute: () => this.addZipLibrary()
|
execute: () => this.addZipLibrary(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include'];
|
const includeLibMenuPath = [
|
||||||
// TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt.
|
...ArduinoMenus.SKETCH__UTILS_GROUP,
|
||||||
registry.registerSubmenu(includeLibMenuPath, 'Include Library', { order: '1' });
|
'0_include',
|
||||||
|
];
|
||||||
registry.registerMenuAction([...includeLibMenuPath, '1_install'], {
|
registry.registerMenuAction([...includeLibMenuPath, '1_install'], {
|
||||||
commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id,
|
commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id,
|
||||||
label: 'Add .ZIP Library...',
|
label: nls.localize('arduino/library/addZip', 'Add .ZIP Library...'),
|
||||||
order: '1'
|
order: '1',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,15 +47,18 @@ export class AddZipLibrary extends SketchContribution {
|
|||||||
const homeUri = await this.envVariableServer.getHomeDirUri();
|
const homeUri = await this.envVariableServer.getHomeDirUri();
|
||||||
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
|
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
|
||||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
|
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
|
||||||
title: "Select a zip file containing the library you'd like to add",
|
title: nls.localize(
|
||||||
|
'arduino/selectZip',
|
||||||
|
"Select a zip file containing the library you'd like to add"
|
||||||
|
),
|
||||||
defaultPath,
|
defaultPath,
|
||||||
properties: ['openFile'],
|
properties: ['openFile'],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Library',
|
name: nls.localize('arduino/library/zipLibrary', 'Library'),
|
||||||
extensions: ['zip']
|
extensions: ['zip'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
if (!canceled && filePaths.length) {
|
if (!canceled && filePaths.length) {
|
||||||
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
|
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
|
||||||
@@ -59,9 +68,12 @@ export class AddZipLibrary extends SketchContribution {
|
|||||||
if (error instanceof AlreadyInstalledError) {
|
if (error instanceof AlreadyInstalledError) {
|
||||||
const result = await new ConfirmDialog({
|
const result = await new ConfirmDialog({
|
||||||
msg: error.message,
|
msg: error.message,
|
||||||
title: 'Do you want to overwrite the existing library?',
|
title: nls.localize(
|
||||||
ok: 'Yes',
|
'arduino/library/overwriteExistingLibrary',
|
||||||
cancel: 'No'
|
'Do you want to overwrite the existing library?'
|
||||||
|
),
|
||||||
|
ok: nls.localize('vscode/extensionsUtils/yes', 'Yes'),
|
||||||
|
cancel: nls.localize('vscode/extensionsUtils/no', 'No'),
|
||||||
}).open();
|
}).open();
|
||||||
if (result) {
|
if (result) {
|
||||||
await this.doInstall(zipUri, true);
|
await this.doInstall(zipUri, true);
|
||||||
@@ -73,22 +85,43 @@ export class AddZipLibrary extends SketchContribution {
|
|||||||
|
|
||||||
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
|
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await Installable.doWithProgress({
|
await ExecuteWithProgress.doWithProgress({
|
||||||
messageService: this.messageService,
|
messageService: this.messageService,
|
||||||
progressText: `Processing ${new URI(zipUri).path.base}`,
|
progressText:
|
||||||
|
nls.localize('arduino/common/processing', 'Processing') +
|
||||||
|
` ${new URI(zipUri).path.base}`,
|
||||||
responseService: this.responseService,
|
responseService: this.responseService,
|
||||||
run: () => this.libraryService.installZip({ zipUri, overwrite })
|
run: () => this.libraryService.installZip({ zipUri, overwrite }),
|
||||||
});
|
});
|
||||||
this.messageService.info(`Successfully installed library from ${new URI(zipUri).path.base} archive`, { timeout: 3000 });
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/library/successfullyInstalledZipLibrary',
|
||||||
|
'Successfully installed library from {0} archive',
|
||||||
|
new URI(zipUri).path.base
|
||||||
|
),
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
const match = error.message.match(/library (.*?) already installed/);
|
const match = error.message.match(/library (.*?) already installed/);
|
||||||
if (match && match.length >= 2) {
|
if (match && match.length >= 2) {
|
||||||
const name = match[1].trim();
|
const name = match[1].trim();
|
||||||
if (name) {
|
if (name) {
|
||||||
throw new AlreadyInstalledError(`A library folder named ${name} already exists. Do you want to overwrite it?`, name);
|
throw new AlreadyInstalledError(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/library/namedLibraryAlreadyExists',
|
||||||
|
'A library folder named {0} already exists. Do you want to overwrite it?',
|
||||||
|
name
|
||||||
|
),
|
||||||
|
name
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new AlreadyInstalledError('A library already exists. Do you want to overwrite it?');
|
throw new AlreadyInstalledError(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/library/libraryAlreadyExists',
|
||||||
|
'A library already exists. Do you want to overwrite it?'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,22 +129,19 @@ export class AddZipLibrary extends SketchContribution {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlreadyInstalledError extends Error {
|
class AlreadyInstalledError extends Error {
|
||||||
|
|
||||||
constructor(message: string, readonly libraryName?: string) {
|
constructor(message: string, readonly libraryName?: string) {
|
||||||
super(message);
|
super(message);
|
||||||
Object.setPrototypeOf(this, AlreadyInstalledError.prototype);
|
Object.setPrototypeOf(this, AlreadyInstalledError.prototype);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace AddZipLibrary {
|
export namespace AddZipLibrary {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const ADD_ZIP_LIBRARY: Command = {
|
export const ADD_ZIP_LIBRARY: Command = {
|
||||||
id: 'arduino-add-zip-library'
|
id: 'arduino-add-zip-library',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,55 @@
|
|||||||
import { injectable } from 'inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import * as dateFormat from 'dateformat';
|
import * as dateFormat from 'dateformat';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ArchiveSketch extends SketchContribution {
|
export class ArchiveSketch extends SketchContribution {
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registerCommands(registry: CommandRegistry): void {
|
|
||||||
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
|
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
|
||||||
execute: () => this.archiveSketch()
|
execute: () => this.archiveSketch(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||||
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
|
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
|
||||||
label: 'Archive Sketch',
|
label: nls.localize('arduino/sketch/archiveSketch', 'Archive Sketch'),
|
||||||
order: '1'
|
order: '1',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async archiveSketch(): Promise<void> {
|
protected async archiveSketch(): Promise<void> {
|
||||||
const [sketch, config] = await Promise.all([
|
const [sketch, config] = await Promise.all([
|
||||||
this.sketchServiceClient.currentSketch(),
|
this.sketchServiceClient.currentSketch(),
|
||||||
this.configService.getConfiguration()
|
this.configService.getConfiguration(),
|
||||||
]);
|
]);
|
||||||
if (!sketch) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const archiveBasename = `${sketch.name}-${dateFormat(new Date(), 'yymmdd')}a.zip`;
|
const archiveBasename = `${sketch.name}-${dateFormat(
|
||||||
const defaultPath = await this.fileService.fsPath(new URI(config.sketchDirUri).resolve(archiveBasename));
|
new Date(),
|
||||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath });
|
'yymmdd'
|
||||||
|
)}a.zip`;
|
||||||
|
const defaultPath = await this.fileService.fsPath(
|
||||||
|
new URI(config.sketchDirUri).resolve(archiveBasename)
|
||||||
|
);
|
||||||
|
const { filePath, canceled } = await remote.dialog.showSaveDialog({
|
||||||
|
title: nls.localize(
|
||||||
|
'arduino/sketch/saveSketchAs',
|
||||||
|
'Save sketch folder as...'
|
||||||
|
),
|
||||||
|
defaultPath,
|
||||||
|
});
|
||||||
if (!filePath || canceled) {
|
if (!filePath || canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -41,15 +58,23 @@ export class ArchiveSketch extends SketchContribution {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.sketchService.archive(sketch, destinationUri.toString());
|
await this.sketchService.archive(sketch, destinationUri.toString());
|
||||||
this.messageService.info(`Created archive '${archiveBasename}'.`, { timeout: 2000 });
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/sketch/createdArchive',
|
||||||
|
"Created archive '{0}'.",
|
||||||
|
archiveBasename
|
||||||
|
),
|
||||||
|
{
|
||||||
|
timeout: 2000,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace ArchiveSketch {
|
export namespace ArchiveSketch {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const ARCHIVE_SKETCH: Command = {
|
export const ARCHIVE_SKETCH: Command = {
|
||||||
id: 'arduino-archive-sketch'
|
id: 'arduino-archive-sketch',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||||
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
|
import {
|
||||||
|
DisposableCollection,
|
||||||
|
Disposable,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
import { firstToUpperCase } from '../../common/utils';
|
import { firstToUpperCase } from '../../common/utils';
|
||||||
import { BoardsConfig } from '../boards/boards-config';
|
import { BoardsConfig } from '../boards/boards-config';
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
import { BoardsListWidget } from '../boards/boards-list-widget';
|
import { BoardsListWidget } from '../boards/boards-list-widget';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import { ArduinoMenus, PlaceholderMenuNode, unregisterSubmenu } from '../menu/arduino-menus';
|
import {
|
||||||
import { BoardsService, InstalledBoardWithPackage, AvailablePorts, Port } from '../../common/protocol';
|
ArduinoMenus,
|
||||||
|
PlaceholderMenuNode,
|
||||||
|
unregisterSubmenu,
|
||||||
|
} from '../menu/arduino-menus';
|
||||||
|
import {
|
||||||
|
BoardsService,
|
||||||
|
InstalledBoardWithPackage,
|
||||||
|
AvailablePorts,
|
||||||
|
Port,
|
||||||
|
} from '../../common/protocol';
|
||||||
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardSelection extends SketchContribution {
|
export class BoardSelection extends SketchContribution {
|
||||||
|
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
protected readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@@ -35,176 +47,305 @@ export class BoardSelection extends SketchContribution {
|
|||||||
|
|
||||||
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
|
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
|
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const { selectedBoard, selectedPort } = this.boardsServiceProvider.boardsConfig;
|
const { selectedBoard, selectedPort } =
|
||||||
|
this.boardsServiceProvider.boardsConfig;
|
||||||
if (!selectedBoard) {
|
if (!selectedBoard) {
|
||||||
this.messageService.info('Please select a board to obtain board info.');
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/selectBoardForInfo',
|
||||||
|
'Please select a board to obtain board info.'
|
||||||
|
)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!selectedBoard.fqbn) {
|
if (!selectedBoard.fqbn) {
|
||||||
this.messageService.info(`The platform for the selected '${selectedBoard.name}' board is not installed.`);
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/platformMissing',
|
||||||
|
"The platform for the selected '{0}' board is not installed.",
|
||||||
|
selectedBoard.name
|
||||||
|
)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!selectedPort) {
|
if (!selectedPort) {
|
||||||
this.messageService.info('Please select a port to obtain board info.');
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/selectPortForInfo',
|
||||||
|
'Please select a port to obtain board info.'
|
||||||
|
)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const boardDetails = await this.boardsService.getBoardDetails({ fqbn: selectedBoard.fqbn });
|
const boardDetails = await this.boardsService.getBoardDetails({
|
||||||
|
fqbn: selectedBoard.fqbn,
|
||||||
|
});
|
||||||
if (boardDetails) {
|
if (boardDetails) {
|
||||||
const { VID, PID } = boardDetails;
|
const { VID, PID } = boardDetails;
|
||||||
const detail = `BN: ${selectedBoard.name}
|
const detail = `BN: ${selectedBoard.name}
|
||||||
VID: ${VID}
|
VID: ${VID}
|
||||||
PID: ${PID}`;
|
PID: ${PID}`;
|
||||||
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||||
message: 'Board Info',
|
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||||
title: 'Board Info',
|
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
detail,
|
detail,
|
||||||
buttons: ['OK']
|
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onStart(): void {
|
override onStart(): void {
|
||||||
|
this.notificationCenter.onPlatformDidInstall(() => this.updateMenus());
|
||||||
|
this.notificationCenter.onPlatformDidUninstall(() => this.updateMenus());
|
||||||
|
this.boardsServiceProvider.onBoardsConfigChanged(() => this.updateMenus());
|
||||||
|
this.boardsServiceProvider.onAvailableBoardsChanged(() =>
|
||||||
|
this.updateMenus()
|
||||||
|
);
|
||||||
|
this.boardsServiceProvider.onAvailablePortsChanged(() =>
|
||||||
|
this.updateMenus()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onReady(): Promise<void> {
|
||||||
this.updateMenus();
|
this.updateMenus();
|
||||||
this.notificationCenter.onPlatformInstalled(this.updateMenus.bind(this));
|
|
||||||
this.notificationCenter.onPlatformUninstalled(this.updateMenus.bind(this));
|
|
||||||
this.boardsServiceProvider.onBoardsConfigChanged(this.updateMenus.bind(this));
|
|
||||||
this.boardsServiceProvider.onAvailableBoardsChanged(this.updateMenus.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async updateMenus(): Promise<void> {
|
protected async updateMenus(): Promise<void> {
|
||||||
const [installedBoards, availablePorts, config] = await Promise.all([
|
const [installedBoards, availablePorts, config] = await Promise.all([
|
||||||
this.installedBoards(),
|
this.installedBoards(),
|
||||||
this.boardsService.getState(),
|
this.boardsService.getState(),
|
||||||
this.boardsServiceProvider.boardsConfig
|
this.boardsServiceProvider.boardsConfig,
|
||||||
]);
|
]);
|
||||||
this.rebuildMenus(installedBoards, availablePorts, config);
|
this.rebuildMenus(installedBoards, availablePorts, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected rebuildMenus(installedBoards: InstalledBoardWithPackage[], availablePorts: AvailablePorts, config: BoardsConfig.Config): void {
|
protected rebuildMenus(
|
||||||
|
installedBoards: InstalledBoardWithPackage[],
|
||||||
|
availablePorts: AvailablePorts,
|
||||||
|
config: BoardsConfig.Config
|
||||||
|
): void {
|
||||||
this.toDisposeBeforeMenuRebuild.dispose();
|
this.toDisposeBeforeMenuRebuild.dispose();
|
||||||
|
|
||||||
// Boards submenu
|
// Boards submenu
|
||||||
const boardsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '1_boards'];
|
const boardsSubmenuPath = [
|
||||||
|
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||||
|
'1_boards',
|
||||||
|
];
|
||||||
const boardsSubmenuLabel = config.selectedBoard?.name;
|
const boardsSubmenuLabel = config.selectedBoard?.name;
|
||||||
// Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index.
|
// Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index.
|
||||||
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
|
// The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order.
|
||||||
this.menuModelRegistry.registerSubmenu(boardsSubmenuPath, `Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`, { order: '100' });
|
this.menuModelRegistry.registerSubmenu(
|
||||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)));
|
boardsSubmenuPath,
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/board',
|
||||||
|
'Board{0}',
|
||||||
|
!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''
|
||||||
|
),
|
||||||
|
{ order: '100' }
|
||||||
|
);
|
||||||
|
this.toDisposeBeforeMenuRebuild.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Ports submenu
|
// Ports submenu
|
||||||
const portsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '2_ports'];
|
const portsSubmenuPath = [
|
||||||
|
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||||
|
'2_ports',
|
||||||
|
];
|
||||||
const portsSubmenuLabel = config.selectedPort?.address;
|
const portsSubmenuLabel = config.selectedPort?.address;
|
||||||
this.menuModelRegistry.registerSubmenu(portsSubmenuPath, `Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`, { order: '101' });
|
this.menuModelRegistry.registerSubmenu(
|
||||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)));
|
portsSubmenuPath,
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/port',
|
||||||
|
'Port{0}',
|
||||||
|
portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''
|
||||||
|
),
|
||||||
|
{ order: '101' }
|
||||||
|
);
|
||||||
|
this.toDisposeBeforeMenuRebuild.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const getBoardInfo = { commandId: BoardSelection.Commands.GET_BOARD_INFO.id, label: 'Get Board Info', order: '103' };
|
const getBoardInfo = {
|
||||||
this.menuModelRegistry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, getBoardInfo);
|
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
|
||||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuAction(getBoardInfo)));
|
label: nls.localize('arduino/board/getBoardInfo', 'Get Board Info'),
|
||||||
|
order: '103',
|
||||||
|
};
|
||||||
|
this.menuModelRegistry.registerMenuAction(
|
||||||
|
ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||||
|
getBoardInfo
|
||||||
|
);
|
||||||
|
this.toDisposeBeforeMenuRebuild.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.menuModelRegistry.unregisterMenuAction(getBoardInfo)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
|
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
|
||||||
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
|
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
|
||||||
|
|
||||||
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
|
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
|
||||||
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
||||||
label: 'Boards Manager...'
|
label: `${BoardsListWidget.WIDGET_LABEL}...`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Installed boards
|
// Installed boards
|
||||||
for (const board of installedBoards) {
|
for (const board of installedBoards) {
|
||||||
const { packageId, packageName, fqbn, name } = board;
|
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
|
||||||
|
|
||||||
|
const packageLabel =
|
||||||
|
packageName +
|
||||||
|
`${manuallyInstalled
|
||||||
|
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||||
|
: ''
|
||||||
|
}`;
|
||||||
// Platform submenu
|
// Platform submenu
|
||||||
const platformMenuPath = [...boardsPackagesGroup, packageId];
|
const platformMenuPath = [...boardsPackagesGroup, packageId];
|
||||||
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
|
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
|
||||||
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageName);
|
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageLabel, {
|
||||||
|
order: packageName.toLowerCase(),
|
||||||
|
});
|
||||||
|
|
||||||
const id = `arduino-select-board--${fqbn}`;
|
const id = `arduino-select-board--${fqbn}`;
|
||||||
const command = { id };
|
const command = { id };
|
||||||
const handler = {
|
const handler = {
|
||||||
execute: () => {
|
execute: () => {
|
||||||
if (fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn) {
|
if (
|
||||||
|
fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
|
||||||
|
) {
|
||||||
this.boardsServiceProvider.boardsConfig = {
|
this.boardsServiceProvider.boardsConfig = {
|
||||||
selectedBoard: {
|
selectedBoard: {
|
||||||
name,
|
name,
|
||||||
fqbn,
|
fqbn,
|
||||||
port: this.boardsServiceProvider.boardsConfig.selectedBoard?.port // TODO: verify!
|
port: this.boardsServiceProvider.boardsConfig.selectedBoard
|
||||||
|
?.port, // TODO: verify!
|
||||||
},
|
},
|
||||||
selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort
|
selectedPort:
|
||||||
}
|
this.boardsServiceProvider.boardsConfig.selectedPort,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isToggled: () => fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
|
isToggled: () =>
|
||||||
|
fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Board menu
|
// Board menu
|
||||||
const menuAction = { commandId: id, label: name };
|
const menuAction = { commandId: id, label: name };
|
||||||
this.commandRegistry.registerCommand(command, handler);
|
this.commandRegistry.registerCommand(command, handler);
|
||||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
|
this.toDisposeBeforeMenuRebuild.push(
|
||||||
|
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
|
||||||
|
);
|
||||||
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
|
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
|
||||||
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
|
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Installed ports
|
// Installed ports
|
||||||
const registerPorts = (ports: AvailablePorts) => {
|
const registerPorts = (
|
||||||
const addresses = Object.keys(ports);
|
protocol: string,
|
||||||
if (!addresses.length) {
|
protocolOrder: number,
|
||||||
|
ports: AvailablePorts
|
||||||
|
) => {
|
||||||
|
const portIDs = Object.keys(ports);
|
||||||
|
if (!portIDs.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register placeholder for protocol
|
// Register placeholder for protocol
|
||||||
const [port] = ports[addresses[0]];
|
const menuPath = [
|
||||||
const protocol = port.protocol;
|
...portsSubmenuPath,
|
||||||
const menuPath = [...portsSubmenuPath, protocol];
|
`${protocolOrder.toString()}_${protocol}`,
|
||||||
const placeholder = new PlaceholderMenuNode(menuPath, `${firstToUpperCase(port.protocol)} ports`);
|
];
|
||||||
|
const placeholder = new PlaceholderMenuNode(
|
||||||
|
menuPath,
|
||||||
|
`${firstToUpperCase(protocol)} ports`,
|
||||||
|
{ order: protocolOrder.toString() }
|
||||||
|
);
|
||||||
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
|
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
|
||||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuNode(placeholder.id)));
|
this.toDisposeBeforeMenuRebuild.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.menuModelRegistry.unregisterMenuNode(placeholder.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
for (const address of addresses) {
|
// First we show addresses with recognized boards connected,
|
||||||
if (!!ports[address]) {
|
// then all the rest.
|
||||||
const [port, boards] = ports[address];
|
const sortedIDs = Object.keys(ports).sort((left: string, right: string): number => {
|
||||||
if (!boards.length) {
|
const [, leftBoards] = ports[left];
|
||||||
boards.push({
|
const [, rightBoards] = ports[right];
|
||||||
name: ''
|
return rightBoards.length - leftBoards.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < sortedIDs.length; i++) {
|
||||||
|
const portID = sortedIDs[i];
|
||||||
|
const [port, boards] = ports[portID];
|
||||||
|
let label = `${port.address}`;
|
||||||
|
if (boards.length) {
|
||||||
|
const boardsList = boards.map((board) => board.name).join(', ');
|
||||||
|
label = `${label} (${boardsList})`;
|
||||||
}
|
}
|
||||||
for (const { name, fqbn } of boards) {
|
const id = `arduino-select-port--${portID}`;
|
||||||
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''}`;
|
|
||||||
const command = { id };
|
const command = { id };
|
||||||
const handler = {
|
const handler = {
|
||||||
execute: () => {
|
execute: () => {
|
||||||
if (!Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)) {
|
if (
|
||||||
|
!Port.sameAs(
|
||||||
|
port,
|
||||||
|
this.boardsServiceProvider.boardsConfig.selectedPort
|
||||||
|
)
|
||||||
|
) {
|
||||||
this.boardsServiceProvider.boardsConfig = {
|
this.boardsServiceProvider.boardsConfig = {
|
||||||
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
|
selectedBoard:
|
||||||
selectedPort: port
|
this.boardsServiceProvider.boardsConfig.selectedBoard,
|
||||||
}
|
selectedPort: port,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isToggled: () => Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)
|
isToggled: () =>
|
||||||
|
Port.sameAs(
|
||||||
|
port,
|
||||||
|
this.boardsServiceProvider.boardsConfig.selectedPort
|
||||||
|
),
|
||||||
};
|
};
|
||||||
const label = `${address}${name ? ` (${name})` : ''}`;
|
|
||||||
const menuAction = {
|
const menuAction = {
|
||||||
commandId: id,
|
commandId: id,
|
||||||
label,
|
label,
|
||||||
order: `1${label}` // `1` comes after the placeholder which has order `0`
|
order: `${protocolOrder + i + 1}`,
|
||||||
};
|
};
|
||||||
this.commandRegistry.registerCommand(command, handler);
|
this.commandRegistry.registerCommand(command, handler);
|
||||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
|
this.toDisposeBeforeMenuRebuild.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.commandRegistry.unregisterCommand(command)
|
||||||
|
)
|
||||||
|
);
|
||||||
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
|
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { serial, network, unknown } = AvailablePorts.groupByProtocol(availablePorts);
|
const grouped = AvailablePorts.byProtocol(availablePorts);
|
||||||
registerPorts(serial);
|
let protocolOrder = 100;
|
||||||
registerPorts(network);
|
// We first show serial and network ports, then all the rest
|
||||||
registerPorts(unknown);
|
['serial', 'network'].forEach((protocol) => {
|
||||||
|
const ports = grouped.get(protocol);
|
||||||
|
if (ports) {
|
||||||
|
registerPorts(protocol, protocolOrder, ports);
|
||||||
|
grouped.delete(protocol);
|
||||||
|
protocolOrder = protocolOrder + 100;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
grouped.forEach((ports, protocol) => {
|
||||||
|
registerPorts(protocol, protocolOrder, ports);
|
||||||
|
protocolOrder = protocolOrder + 100;
|
||||||
|
});
|
||||||
|
|
||||||
this.mainMenuManager.update();
|
this.mainMenuManager.update();
|
||||||
}
|
}
|
||||||
@@ -213,7 +354,6 @@ PID: ${PID}`;
|
|||||||
const allBoards = await this.boardsService.searchBoards({});
|
const allBoards = await this.boardsService.searchBoards({});
|
||||||
return allBoards.filter(InstalledBoardWithPackage.is);
|
return allBoards.filter(InstalledBoardWithPackage.is);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
export namespace BoardSelection {
|
export namespace BoardSelection {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
|
|||||||
@@ -1,82 +1,87 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { nls } from '@theia/core/lib/common';
|
||||||
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { CoreService } from '../../common/protocol';
|
import { CoreService } from '../../common/protocol';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
import {
|
||||||
import { MonitorConnection } from '../monitor/monitor-connection';
|
Command,
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
CommandRegistry,
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
CoreServiceContribution,
|
||||||
|
MenuModelRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BurnBootloader extends SketchContribution {
|
export class BurnBootloader extends CoreServiceContribution {
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
@inject(CoreService)
|
|
||||||
protected readonly coreService: CoreService;
|
|
||||||
|
|
||||||
@inject(MonitorConnection)
|
|
||||||
protected readonly monitorConnection: MonitorConnection;
|
|
||||||
|
|
||||||
@inject(BoardsDataStore)
|
|
||||||
protected readonly boardsDataStore: BoardsDataStore;
|
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
|
||||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
|
||||||
|
|
||||||
@inject(OutputChannelManager)
|
|
||||||
protected readonly outputChannelManager: OutputChannelManager;
|
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
|
||||||
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
|
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
|
||||||
execute: () => this.burnBootloader()
|
execute: () => this.burnBootloader(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
|
||||||
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
|
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
|
||||||
label: 'Burn Bootloader',
|
label: nls.localize(
|
||||||
order: 'z99'
|
'arduino/bootloader/burnBootloader',
|
||||||
|
'Burn Bootloader'
|
||||||
|
),
|
||||||
|
order: 'z99',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async burnBootloader(): Promise<void> {
|
private async burnBootloader(): Promise<void> {
|
||||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
const options = await this.options();
|
||||||
if (monitorConfig) {
|
|
||||||
await this.monitorConnection.disconnect();
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
await this.doWithProgress({
|
||||||
const port = boardsConfig.selectedPort?.address;
|
progressText: nls.localize(
|
||||||
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = await Promise.all([
|
'arduino/bootloader/burningBootloader',
|
||||||
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
|
'Burning bootloader...'
|
||||||
|
),
|
||||||
|
task: (progressId, coreService) =>
|
||||||
|
coreService.burnBootloader({
|
||||||
|
...options,
|
||||||
|
progressId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/bootloader/doneBurningBootloader',
|
||||||
|
'Done burning bootloader.'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
timeout: 3000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async options(): Promise<CoreService.Options.Bootloader> {
|
||||||
|
const { boardsConfig } = this.boardsServiceProvider;
|
||||||
|
const port = boardsConfig.selectedPort;
|
||||||
|
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
|
||||||
|
await Promise.all([
|
||||||
|
this.boardsDataStore.appendConfigToFqbn(
|
||||||
|
boardsConfig.selectedBoard?.fqbn
|
||||||
|
),
|
||||||
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
|
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
|
||||||
this.preferences.get('arduino.upload.verify'),
|
this.preferences.get('arduino.upload.verify'),
|
||||||
this.preferences.get('arduino.upload.verbose')
|
this.preferences.get('arduino.upload.verbose'),
|
||||||
]);
|
]);
|
||||||
this.outputChannelManager.getChannel('Arduino').clear();
|
return {
|
||||||
await this.coreService.burnBootloader({
|
|
||||||
fqbn,
|
fqbn,
|
||||||
programmer,
|
programmer,
|
||||||
port,
|
port,
|
||||||
verify,
|
verify,
|
||||||
verbose
|
verbose,
|
||||||
});
|
};
|
||||||
this.messageService.info('Done burning bootloader.', { timeout: 3000 });
|
|
||||||
} catch (e) {
|
|
||||||
this.messageService.error(e.toString());
|
|
||||||
} finally {
|
|
||||||
if (monitorConfig) {
|
|
||||||
await this.monitorConnection.connect(monitorConfig);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace BurnBootloader {
|
export namespace BurnBootloader {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const BURN_BOOTLOADER: Command = {
|
export const BURN_BOOTLOADER: Command = {
|
||||||
id: 'arduino-burn-bootloader'
|
id: 'arduino-burn-bootloader',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
IDEUpdater,
|
||||||
|
SKIP_IDE_VERSION,
|
||||||
|
} from '../../common/protocol/ide-updater';
|
||||||
|
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
|
||||||
|
import { Contribution } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class CheckForUpdates extends Contribution {
|
||||||
|
@inject(IDEUpdater)
|
||||||
|
private readonly updater: IDEUpdater;
|
||||||
|
|
||||||
|
@inject(IDEUpdaterDialog)
|
||||||
|
private readonly updaterDialog: IDEUpdaterDialog;
|
||||||
|
|
||||||
|
@inject(LocalStorageService)
|
||||||
|
private readonly localStorage: LocalStorageService;
|
||||||
|
|
||||||
|
override onStart(): void {
|
||||||
|
this.preferences.onPreferenceChanged(
|
||||||
|
({ preferenceName, newValue, oldValue }) => {
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
switch (preferenceName) {
|
||||||
|
case 'arduino.ide.updateChannel':
|
||||||
|
case 'arduino.ide.updateBaseUrl':
|
||||||
|
this.updater.init(
|
||||||
|
this.preferences.get('arduino.ide.updateChannel'),
|
||||||
|
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onReady(): void {
|
||||||
|
this.updater
|
||||||
|
.init(
|
||||||
|
this.preferences.get('arduino.ide.updateChannel'),
|
||||||
|
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||||
|
)
|
||||||
|
.then(() => this.updater.checkForUpdates(true))
|
||||||
|
.then(async (updateInfo) => {
|
||||||
|
if (!updateInfo) return;
|
||||||
|
const versionToSkip = await this.localStorage.getData<string>(
|
||||||
|
SKIP_IDE_VERSION
|
||||||
|
);
|
||||||
|
if (versionToSkip === updateInfo.version) return;
|
||||||
|
this.updaterDialog.open(updateInfo);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.messageService.error(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/ide-updater/errorCheckingForUpdates',
|
||||||
|
'Error while checking for Arduino IDE updates.\n{0}',
|
||||||
|
e.message
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,93 +1,52 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { toArray } from '@phosphor/algorithm';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { remote } from 'electron';
|
|
||||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { SaveAsSketch } from './save-as-sketch';
|
import {
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, URI } from './contribution';
|
SketchContribution,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
URI,
|
||||||
|
} from './contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window.
|
* Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window.
|
||||||
*/
|
*/
|
||||||
@injectable()
|
@injectable()
|
||||||
export class Close extends SketchContribution {
|
export class Close extends SketchContribution {
|
||||||
|
|
||||||
@inject(EditorManager)
|
@inject(EditorManager)
|
||||||
protected readonly editorManager: EditorManager;
|
protected override readonly editorManager: EditorManager;
|
||||||
|
|
||||||
protected shell: ApplicationShell;
|
protected shell: ApplicationShell;
|
||||||
|
|
||||||
onStart(app: FrontendApplication): void {
|
override onStart(app: FrontendApplication): void {
|
||||||
this.shell = app.shell;
|
this.shell = app.shell;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(Close.Commands.CLOSE, {
|
registry.registerCommand(Close.Commands.CLOSE, {
|
||||||
execute: async () => {
|
execute: () => remote.getCurrentWindow().close()
|
||||||
|
|
||||||
// Close current editor if closeable.
|
|
||||||
const { currentEditor } = this.editorManager;
|
|
||||||
if (currentEditor && currentEditor.title.closable) {
|
|
||||||
currentEditor.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close current widget from the main area if possible.
|
|
||||||
const { currentWidget } = this.shell;
|
|
||||||
if (currentWidget) {
|
|
||||||
const currentWidgetInMain = toArray(this.shell.mainPanel.widgets()).find(widget => widget === currentWidget);
|
|
||||||
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
|
|
||||||
return currentWidgetInMain.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the sketch (window).
|
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
|
||||||
if (!sketch) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const isTemp = await this.sketchService.isTemp(sketch);
|
|
||||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
|
||||||
if (!uri) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isTemp && await this.wasTouched(uri)) {
|
|
||||||
const { response } = await remote.dialog.showMessageBox({
|
|
||||||
type: 'question',
|
|
||||||
buttons: ["Don't Save", 'Cancel', 'Save'],
|
|
||||||
message: 'Do you want to save changes to this sketch before closing?',
|
|
||||||
detail: "If you don't save, your changes will be lost."
|
|
||||||
});
|
|
||||||
if (response === 1) { // Cancel
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (response === 2) { // Save
|
|
||||||
const saved = await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { openAfterMove: false, execOnlyIfTemp: true });
|
|
||||||
if (!saved) { // If it was not saved, do bail the close.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
commandId: Close.Commands.CLOSE.id,
|
commandId: Close.Commands.CLOSE.id,
|
||||||
label: 'Close',
|
label: nls.localize('vscode/editor.contribution/close', 'Close'),
|
||||||
order: '5'
|
order: '5',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: Close.Commands.CLOSE.id,
|
command: Close.Commands.CLOSE.id,
|
||||||
keybinding: 'CtrlCmd+W'
|
keybinding: 'CtrlCmd+W',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,13 +66,12 @@ export class Close extends SketchContribution {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Close {
|
export namespace Close {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const CLOSE: Command = {
|
export const CLOSE: Command = {
|
||||||
id: 'arduino-close'
|
id: 'arduino-close',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,650 @@
|
|||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
Emitter,
|
||||||
|
MaybePromise,
|
||||||
|
nls,
|
||||||
|
notEmpty,
|
||||||
|
} from '@theia/core';
|
||||||
|
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
|
||||||
|
import URI from '@theia/core/lib/common/uri';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
Location,
|
||||||
|
Range,
|
||||||
|
} from '@theia/core/shared/vscode-languageserver-protocol';
|
||||||
|
import {
|
||||||
|
EditorWidget,
|
||||||
|
TextDocumentChangeEvent,
|
||||||
|
} from '@theia/editor/lib/browser';
|
||||||
|
import {
|
||||||
|
EditorDecoration,
|
||||||
|
TrackedRangeStickiness,
|
||||||
|
} from '@theia/editor/lib/browser/decorations/editor-decoration';
|
||||||
|
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||||
|
import * as monaco from '@theia/monaco-editor-core';
|
||||||
|
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||||
|
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
|
||||||
|
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
|
||||||
|
import { CoreError } from '../../common/protocol/core-service';
|
||||||
|
import { ErrorRevealStrategy } from '../arduino-preferences';
|
||||||
|
import { InoSelector } from '../ino-selectors';
|
||||||
|
import { fullRange } from '../utils/monaco';
|
||||||
|
import { Contribution } from './contribution';
|
||||||
|
import { CoreErrorHandler } from './core-error-handler';
|
||||||
|
|
||||||
|
interface ErrorDecoration {
|
||||||
|
/**
|
||||||
|
* This is the unique ID of the decoration given by `monaco`.
|
||||||
|
*/
|
||||||
|
readonly id: string;
|
||||||
|
/**
|
||||||
|
* The resource this decoration belongs to.
|
||||||
|
*/
|
||||||
|
readonly uri: string;
|
||||||
|
}
|
||||||
|
namespace ErrorDecoration {
|
||||||
|
export function rangeOf(
|
||||||
|
{ id, uri }: ErrorDecoration,
|
||||||
|
editorProvider: (uri: string) => Promise<MonacoEditor | undefined>
|
||||||
|
): Promise<monaco.Range | undefined>;
|
||||||
|
export function rangeOf(
|
||||||
|
{ id, uri }: ErrorDecoration,
|
||||||
|
editorProvider: MonacoEditor
|
||||||
|
): monaco.Range | undefined;
|
||||||
|
export function rangeOf(
|
||||||
|
{ id, uri }: ErrorDecoration,
|
||||||
|
editorProvider:
|
||||||
|
| ((uri: string) => Promise<MonacoEditor | undefined>)
|
||||||
|
| MonacoEditor
|
||||||
|
): MaybePromise<monaco.Range | undefined> {
|
||||||
|
if (editorProvider instanceof MonacoEditor) {
|
||||||
|
const control = editorProvider.getControl();
|
||||||
|
const model = control.getModel();
|
||||||
|
if (model) {
|
||||||
|
return control
|
||||||
|
.getDecorationsInRange(fullRange(model))
|
||||||
|
?.find(({ id: candidateId }) => id === candidateId)?.range;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return editorProvider(uri).then((editor) => {
|
||||||
|
if (editor) {
|
||||||
|
return rangeOf({ id, uri }, editor);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// export async function rangeOf(
|
||||||
|
// { id, uri }: ErrorDecoration,
|
||||||
|
// editorProvider:
|
||||||
|
// | ((uri: string) => Promise<MonacoEditor | undefined>)
|
||||||
|
// | MonacoEditor
|
||||||
|
// ): Promise<monaco.Range | undefined> {
|
||||||
|
// const editor =
|
||||||
|
// editorProvider instanceof MonacoEditor
|
||||||
|
// ? editorProvider
|
||||||
|
// : await editorProvider(uri);
|
||||||
|
// if (editor) {
|
||||||
|
// const control = editor.getControl();
|
||||||
|
// const model = control.getModel();
|
||||||
|
// if (model) {
|
||||||
|
// return control
|
||||||
|
// .getDecorationsInRange(fullRange(model))
|
||||||
|
// ?.find(({ id: candidateId }) => id === candidateId)?.range;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return undefined;
|
||||||
|
// }
|
||||||
|
export function sameAs(
|
||||||
|
left: ErrorDecoration,
|
||||||
|
right: ErrorDecoration
|
||||||
|
): boolean {
|
||||||
|
return left.id === right.id && left.uri === right.uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class CompilerErrors
|
||||||
|
extends Contribution
|
||||||
|
implements monaco.languages.CodeLensProvider
|
||||||
|
{
|
||||||
|
@inject(EditorManager)
|
||||||
|
private readonly editorManager: EditorManager;
|
||||||
|
|
||||||
|
@inject(ProtocolToMonacoConverter)
|
||||||
|
private readonly p2m: ProtocolToMonacoConverter;
|
||||||
|
|
||||||
|
@inject(MonacoToProtocolConverter)
|
||||||
|
private readonly mp2: MonacoToProtocolConverter;
|
||||||
|
|
||||||
|
@inject(CoreErrorHandler)
|
||||||
|
private readonly coreErrorHandler: CoreErrorHandler;
|
||||||
|
|
||||||
|
private readonly errors: ErrorDecoration[] = [];
|
||||||
|
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
|
||||||
|
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
|
||||||
|
private readonly onCurrentErrorDidChange =
|
||||||
|
this.currentErrorDidChangEmitter.event;
|
||||||
|
private readonly toDisposeOnCompilerErrorDidChange =
|
||||||
|
new DisposableCollection();
|
||||||
|
private shell: ApplicationShell | undefined;
|
||||||
|
private revealStrategy = ErrorRevealStrategy.Default;
|
||||||
|
private currentError: ErrorDecoration | undefined;
|
||||||
|
private get currentErrorIndex(): number {
|
||||||
|
const current = this.currentError;
|
||||||
|
if (!current) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return this.errors.findIndex((error) =>
|
||||||
|
ErrorDecoration.sameAs(error, current)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onStart(app: FrontendApplication): void {
|
||||||
|
this.shell = app.shell;
|
||||||
|
monaco.languages.registerCodeLensProvider(InoSelector, this);
|
||||||
|
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
|
||||||
|
this.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
|
||||||
|
);
|
||||||
|
this.onCurrentErrorDidChange(async (error) => {
|
||||||
|
const range = await ErrorDecoration.rangeOf(error, (uri) =>
|
||||||
|
this.monacoEditor(uri)
|
||||||
|
);
|
||||||
|
if (!range) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Could not find range of decoration: ${error.id}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const editor = await this.revealLocationInEditor({
|
||||||
|
uri: error.uri,
|
||||||
|
range: this.mp2.asRange(range),
|
||||||
|
});
|
||||||
|
if (!editor) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Failed to mark error ${error.id} as the current one.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.preferences.ready.then(() => {
|
||||||
|
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||||
|
if (preferenceName === 'arduino.compile.revealRange') {
|
||||||
|
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
||||||
|
? newValue
|
||||||
|
: ErrorRevealStrategy.Default;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(CompilerErrors.Commands.NEXT_ERROR, {
|
||||||
|
execute: () => {
|
||||||
|
const index = this.currentErrorIndex;
|
||||||
|
if (index < 0) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Could not advance to next error. Unknown current error.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextError =
|
||||||
|
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
|
||||||
|
this.markAsCurrentError(nextError);
|
||||||
|
},
|
||||||
|
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
||||||
|
});
|
||||||
|
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
|
||||||
|
execute: () => {
|
||||||
|
const index = this.currentErrorIndex;
|
||||||
|
if (index < 0) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Could not advance to previous error. Unknown current error.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previousError =
|
||||||
|
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
|
||||||
|
this.markAsCurrentError(previousError);
|
||||||
|
},
|
||||||
|
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get onDidChange(): monaco.IEvent<this> {
|
||||||
|
return this.onDidChangeEmitter.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
async provideCodeLenses(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_token: monaco.CancellationToken
|
||||||
|
): Promise<monaco.languages.CodeLensList> {
|
||||||
|
const lenses: monaco.languages.CodeLens[] = [];
|
||||||
|
if (
|
||||||
|
this.currentError &&
|
||||||
|
this.currentError.uri === model.uri.toString() &&
|
||||||
|
this.errors.length > 1
|
||||||
|
) {
|
||||||
|
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
|
||||||
|
this.monacoEditor(uri)
|
||||||
|
);
|
||||||
|
if (range) {
|
||||||
|
lenses.push(
|
||||||
|
{
|
||||||
|
range,
|
||||||
|
command: {
|
||||||
|
id: CompilerErrors.Commands.PREVIOUS_ERROR.id,
|
||||||
|
title: nls.localize(
|
||||||
|
'arduino/editor/previousError',
|
||||||
|
'Previous Error'
|
||||||
|
),
|
||||||
|
arguments: [this.currentError],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
range,
|
||||||
|
command: {
|
||||||
|
id: CompilerErrors.Commands.NEXT_ERROR.id,
|
||||||
|
title: nls.localize('arduino/editor/nextError', 'Next Error'),
|
||||||
|
arguments: [this.currentError],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
lenses,
|
||||||
|
dispose: () => {
|
||||||
|
/* NOOP */
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCompilerErrorsDidChange(
|
||||||
|
errors: CoreError.ErrorLocation[]
|
||||||
|
): Promise<void> {
|
||||||
|
this.toDisposeOnCompilerErrorDidChange.dispose();
|
||||||
|
const compilerErrorsPerResource = this.groupByResource(
|
||||||
|
await this.filter(errors)
|
||||||
|
);
|
||||||
|
const decorations = await this.decorateEditors(compilerErrorsPerResource);
|
||||||
|
this.errors.push(...decorations.errors);
|
||||||
|
this.toDisposeOnCompilerErrorDidChange.pushAll([
|
||||||
|
Disposable.create(() => (this.errors.length = 0)),
|
||||||
|
Disposable.create(() => this.onDidChangeEmitter.fire(this)),
|
||||||
|
...(await Promise.all([
|
||||||
|
decorations.dispose,
|
||||||
|
this.trackEditors(
|
||||||
|
compilerErrorsPerResource,
|
||||||
|
(editor) =>
|
||||||
|
editor.editor.onSelectionChanged((selection) =>
|
||||||
|
this.handleSelectionChange(editor, selection)
|
||||||
|
),
|
||||||
|
(editor) =>
|
||||||
|
editor.onDidDispose(() =>
|
||||||
|
this.handleEditorDidDispose(editor.editor.uri.toString())
|
||||||
|
),
|
||||||
|
(editor) =>
|
||||||
|
editor.editor.onDocumentContentChanged((event) =>
|
||||||
|
this.handleDocumentContentChange(editor, event)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
]);
|
||||||
|
const currentError = this.errors[0];
|
||||||
|
if (currentError) {
|
||||||
|
await this.markAsCurrentError(currentError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async filter(
|
||||||
|
errors: CoreError.ErrorLocation[]
|
||||||
|
): Promise<CoreError.ErrorLocation[]> {
|
||||||
|
if (!errors.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
await this.preferences.ready;
|
||||||
|
if (this.preferences['arduino.compile.experimental']) {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
// Always shows maximum one error; hence the code lens navigation is unavailable.
|
||||||
|
return [errors[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async decorateEditors(
|
||||||
|
errors: Map<string, CoreError.ErrorLocation[]>
|
||||||
|
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||||
|
const composite = await Promise.all(
|
||||||
|
[...errors.entries()].map(([uri, errors]) =>
|
||||||
|
this.decorateEditor(uri, errors)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
dispose: new DisposableCollection(
|
||||||
|
...composite.map(({ dispose }) => dispose)
|
||||||
|
),
|
||||||
|
errors: composite.reduce(
|
||||||
|
(acc, { errors }) => acc.concat(errors),
|
||||||
|
[] as ErrorDecoration[]
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async decorateEditor(
|
||||||
|
uri: string,
|
||||||
|
errors: CoreError.ErrorLocation[]
|
||||||
|
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||||
|
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||||
|
if (!editor) {
|
||||||
|
return { dispose: Disposable.NULL, errors: [] };
|
||||||
|
}
|
||||||
|
const oldDecorations = editor.editor.deltaDecorations({
|
||||||
|
oldDecorations: [],
|
||||||
|
newDecorations: errors.map((error) =>
|
||||||
|
this.compilerErrorDecoration(error.location.range)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
dispose: Disposable.create(() => {
|
||||||
|
if (editor) {
|
||||||
|
editor.editor.deltaDecorations({
|
||||||
|
oldDecorations,
|
||||||
|
newDecorations: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
errors: oldDecorations.map((id) => ({ id, uri })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private compilerErrorDecoration(range: Range): EditorDecoration {
|
||||||
|
return {
|
||||||
|
range,
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
className: 'compiler-error',
|
||||||
|
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
|
||||||
|
*/
|
||||||
|
private handleSelectionChange(editor: EditorWidget, selection: Range): void {
|
||||||
|
const monacoEditor = this.monacoEditor(editor);
|
||||||
|
if (!monacoEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uri = monacoEditor.uri.toString();
|
||||||
|
const monacoSelection = this.p2m.asRange(selection);
|
||||||
|
console.log(
|
||||||
|
'compiler-errors',
|
||||||
|
`Handling selection change in editor ${uri}. New (monaco) selection: ${monacoSelection.toJSON()}`
|
||||||
|
);
|
||||||
|
const calculatePriority = (
|
||||||
|
candidateErrorRange: monaco.Range,
|
||||||
|
currentSelection: monaco.Range
|
||||||
|
) => {
|
||||||
|
console.trace(
|
||||||
|
'compiler-errors',
|
||||||
|
`Candidate error range: ${candidateErrorRange.toJSON()}`
|
||||||
|
);
|
||||||
|
console.trace(
|
||||||
|
'compiler-errors',
|
||||||
|
`Current selection range: ${currentSelection.toJSON()}`
|
||||||
|
);
|
||||||
|
if (candidateErrorRange.intersectRanges(currentSelection)) {
|
||||||
|
console.trace('Intersects.');
|
||||||
|
return { score: 2 };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
candidateErrorRange.startLineNumber <=
|
||||||
|
currentSelection.startLineNumber &&
|
||||||
|
candidateErrorRange.endLineNumber >= currentSelection.endLineNumber
|
||||||
|
) {
|
||||||
|
console.trace('Same line.');
|
||||||
|
return { score: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.trace('No match');
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
const error = this.errors
|
||||||
|
.filter((error) => error.uri === uri)
|
||||||
|
.map((error) => ({
|
||||||
|
error,
|
||||||
|
range: ErrorDecoration.rangeOf(error, monacoEditor),
|
||||||
|
}))
|
||||||
|
.map(({ error, range }) => {
|
||||||
|
if (range) {
|
||||||
|
const priority = calculatePriority(range, monacoSelection);
|
||||||
|
if (priority) {
|
||||||
|
return { ...priority, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter(notEmpty)
|
||||||
|
.sort((left, right) => right.score - left.score) // highest first
|
||||||
|
.map(({ error }) => error)
|
||||||
|
.shift();
|
||||||
|
if (error) {
|
||||||
|
this.markAsCurrentError(error);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'compiler-errors',
|
||||||
|
`New (monaco) selection ${monacoSelection.toJSON()} does not intersect any error locations. Skipping.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This code does not deal with resource deletion, but tracks editor dispose events. It does not matter what was the cause of the editor disposal.
|
||||||
|
* If editor closes, delete the decorators.
|
||||||
|
*/
|
||||||
|
private handleEditorDidDispose(uri: string): void {
|
||||||
|
let i = this.errors.length;
|
||||||
|
// `splice` re-indexes the array. It's better to "iterate and modify" from the last element.
|
||||||
|
while (i--) {
|
||||||
|
const error = this.errors[i];
|
||||||
|
if (error.uri === uri) {
|
||||||
|
this.errors.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a document change "destroys" the range of the decoration, the decoration must be removed.
|
||||||
|
*/
|
||||||
|
private handleDocumentContentChange(
|
||||||
|
editor: EditorWidget,
|
||||||
|
event: TextDocumentChangeEvent
|
||||||
|
): void {
|
||||||
|
const monacoEditor = this.monacoEditor(editor);
|
||||||
|
if (!monacoEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A decoration location can be "destroyed", hence should be deleted when:
|
||||||
|
// - deleting range (start != end AND text is empty)
|
||||||
|
// - inserting text into range (start != end AND text is not empty)
|
||||||
|
// Filter unrelated delta changes to spare the CPU.
|
||||||
|
const relevantChanges = event.contentChanges.filter(
|
||||||
|
({ range: { start, end } }) =>
|
||||||
|
start.line !== end.line || start.character !== end.character
|
||||||
|
);
|
||||||
|
if (!relevantChanges.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedMarkers = this.errors
|
||||||
|
.filter((error) => error.uri === event.document.uri)
|
||||||
|
.map((error, index) => {
|
||||||
|
const range = ErrorDecoration.rangeOf(error, monacoEditor);
|
||||||
|
if (range) {
|
||||||
|
return { error, range, index };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter(notEmpty);
|
||||||
|
|
||||||
|
const decorationIdsToRemove = relevantChanges
|
||||||
|
.map(({ range }) => this.p2m.asRange(range))
|
||||||
|
.map((changeRange) =>
|
||||||
|
resolvedMarkers.filter(({ range: decorationRange }) =>
|
||||||
|
changeRange.containsRange(decorationRange)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.reduce((acc, curr) => acc.concat(curr), [])
|
||||||
|
.map(({ error, index }) => {
|
||||||
|
this.errors.splice(index, 1);
|
||||||
|
return error.id;
|
||||||
|
});
|
||||||
|
if (!decorationIdsToRemove.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async trackEditors(
|
||||||
|
errors: Map<string, CoreError.ErrorLocation[]>,
|
||||||
|
...track: ((editor: EditorWidget) => Disposable)[]
|
||||||
|
): Promise<Disposable> {
|
||||||
|
return new DisposableCollection(
|
||||||
|
...(await Promise.all(
|
||||||
|
Array.from(errors.keys()).map(async (uri) => {
|
||||||
|
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||||
|
if (!editor) {
|
||||||
|
return Disposable.NULL;
|
||||||
|
}
|
||||||
|
return new DisposableCollection(...track.map((t) => t(editor)));
|
||||||
|
})
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async markAsCurrentError(error: ErrorDecoration): Promise<void> {
|
||||||
|
const index = this.errors.findIndex((candidate) =>
|
||||||
|
ErrorDecoration.sameAs(candidate, error)
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Failed to mark error ${
|
||||||
|
error.id
|
||||||
|
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
|
||||||
|
({ id }) => id
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newError = this.errors[index];
|
||||||
|
if (
|
||||||
|
!this.currentError ||
|
||||||
|
!ErrorDecoration.sameAs(this.currentError, newError)
|
||||||
|
) {
|
||||||
|
this.currentError = this.errors[index];
|
||||||
|
console.log(
|
||||||
|
'compiler-errors',
|
||||||
|
`Current error changed to ${this.currentError.id}`
|
||||||
|
);
|
||||||
|
this.currentErrorDidChangEmitter.fire(this.currentError);
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The double editor activation logic is required: https://github.com/eclipse-theia/theia/issues/11284
|
||||||
|
private async revealLocationInEditor(
|
||||||
|
location: Location
|
||||||
|
): Promise<EditorWidget | undefined> {
|
||||||
|
const { uri, range } = location;
|
||||||
|
const editor = await this.editorManager.getByUri(new URI(uri), {
|
||||||
|
mode: 'activate',
|
||||||
|
});
|
||||||
|
if (editor && this.shell) {
|
||||||
|
// to avoid flickering, reveal the range here and not with `getByUri`, because it uses `at: 'center'` for the reveal option.
|
||||||
|
// TODO: check the community reaction whether it is better to set the focus at the error marker. it might cause flickering even if errors are close to each other
|
||||||
|
editor.editor.revealRange(range, { at: this.revealStrategy });
|
||||||
|
const activeWidget = await this.shell.activateWidget(editor.id);
|
||||||
|
if (!activeWidget) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`editor widget activation has failed. editor widget ${editor.id} expected to be the active one.`
|
||||||
|
);
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
if (editor !== activeWidget) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`active widget was not the same as previously activated editor. editor widget ID ${editor.id}, active widget ID: ${activeWidget.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`could not found editor widget for URI: ${uri}`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupByResource(
|
||||||
|
errors: CoreError.ErrorLocation[]
|
||||||
|
): Map<string, CoreError.ErrorLocation[]> {
|
||||||
|
return errors.reduce((acc, curr) => {
|
||||||
|
const {
|
||||||
|
location: { uri },
|
||||||
|
} = curr;
|
||||||
|
let errors = acc.get(uri);
|
||||||
|
if (!errors) {
|
||||||
|
errors = [];
|
||||||
|
acc.set(uri, errors);
|
||||||
|
}
|
||||||
|
errors.push(curr);
|
||||||
|
return acc;
|
||||||
|
}, new Map<string, CoreError.ErrorLocation[]>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
|
||||||
|
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
|
||||||
|
private monacoEditor(
|
||||||
|
uriOrWidget: string | EditorWidget
|
||||||
|
): MaybePromise<MonacoEditor | undefined> {
|
||||||
|
if (uriOrWidget instanceof EditorWidget) {
|
||||||
|
const editor = uriOrWidget.editor;
|
||||||
|
if (editor instanceof MonacoEditor) {
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return this.editorManager
|
||||||
|
.getByUri(new URI(uriOrWidget))
|
||||||
|
.then((editor) => {
|
||||||
|
if (editor) {
|
||||||
|
return this.monacoEditor(editor);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace CompilerErrors {
|
||||||
|
export namespace Commands {
|
||||||
|
export const NEXT_ERROR: Command = {
|
||||||
|
id: 'arduino-editor-next-error',
|
||||||
|
};
|
||||||
|
export const PREVIOUS_ERROR: Command = {
|
||||||
|
id: 'arduino-editor-previous-error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { inject, injectable, interfaces } from 'inversify';
|
import {
|
||||||
|
inject,
|
||||||
|
injectable,
|
||||||
|
interfaces,
|
||||||
|
postConstruct,
|
||||||
|
} from '@theia/core/shared/inversify';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { ILogger } from '@theia/core/lib/common/logger';
|
import { ILogger } from '@theia/core/lib/common/logger';
|
||||||
import { Saveable } from '@theia/core/lib/browser/saveable';
|
import { Saveable } from '@theia/core/lib/browser/saveable';
|
||||||
@@ -9,23 +14,72 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
|||||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||||
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
|
||||||
import { MenuModelRegistry, MenuContribution } from '@theia/core/lib/common/menu';
|
|
||||||
import { KeybindingRegistry, KeybindingContribution } from '@theia/core/lib/browser/keybinding';
|
|
||||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
|
||||||
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
|
||||||
import { Command, CommandRegistry, CommandContribution, CommandService } from '@theia/core/lib/common/command';
|
|
||||||
import { EditorMode } from '../editor-mode';
|
|
||||||
import { SettingsService } from '../settings';
|
|
||||||
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
|
|
||||||
import { SketchesService, ConfigService, FileSystemExt, Sketch } from '../../common/protocol';
|
|
||||||
import { ArduinoPreferences } from '../arduino-preferences';
|
|
||||||
|
|
||||||
export { Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, URI, Sketch, open };
|
import {
|
||||||
|
MenuModelRegistry,
|
||||||
|
MenuContribution,
|
||||||
|
} from '@theia/core/lib/common/menu';
|
||||||
|
import {
|
||||||
|
KeybindingRegistry,
|
||||||
|
KeybindingContribution,
|
||||||
|
} from '@theia/core/lib/browser/keybinding';
|
||||||
|
import {
|
||||||
|
TabBarToolbarContribution,
|
||||||
|
TabBarToolbarRegistry,
|
||||||
|
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||||
|
import {
|
||||||
|
FrontendApplicationContribution,
|
||||||
|
FrontendApplication,
|
||||||
|
} from '@theia/core/lib/browser/frontend-application';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
CommandContribution,
|
||||||
|
CommandService,
|
||||||
|
} from '@theia/core/lib/common/command';
|
||||||
|
import { SettingsService } from '../dialogs/settings/settings';
|
||||||
|
import {
|
||||||
|
CurrentSketch,
|
||||||
|
SketchesServiceClientImpl,
|
||||||
|
} from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
import {
|
||||||
|
SketchesService,
|
||||||
|
ConfigService,
|
||||||
|
FileSystemExt,
|
||||||
|
Sketch,
|
||||||
|
CoreService,
|
||||||
|
CoreError,
|
||||||
|
ResponseServiceClient,
|
||||||
|
} from '../../common/protocol';
|
||||||
|
import { ArduinoPreferences } from '../arduino-preferences';
|
||||||
|
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||||
|
import { nls } from '@theia/core';
|
||||||
|
import { OutputChannelManager } from '../theia/output/output-channel';
|
||||||
|
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||||
|
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||||
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
|
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
TabBarToolbarRegistry,
|
||||||
|
URI,
|
||||||
|
Sketch,
|
||||||
|
open,
|
||||||
|
};
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution, FrontendApplicationContribution {
|
export abstract class Contribution
|
||||||
|
implements
|
||||||
|
CommandContribution,
|
||||||
|
MenuContribution,
|
||||||
|
KeybindingContribution,
|
||||||
|
TabBarToolbarContribution,
|
||||||
|
FrontendApplicationContribution
|
||||||
|
{
|
||||||
@inject(ILogger)
|
@inject(ILogger)
|
||||||
protected readonly logger: ILogger;
|
protected readonly logger: ILogger;
|
||||||
|
|
||||||
@@ -38,35 +92,44 @@ export abstract class Contribution implements CommandContribution, MenuContribut
|
|||||||
@inject(WorkspaceService)
|
@inject(WorkspaceService)
|
||||||
protected readonly workspaceService: WorkspaceService;
|
protected readonly workspaceService: WorkspaceService;
|
||||||
|
|
||||||
@inject(EditorMode)
|
|
||||||
protected readonly editorMode: EditorMode;
|
|
||||||
|
|
||||||
@inject(LabelProvider)
|
@inject(LabelProvider)
|
||||||
protected readonly labelProvider: LabelProvider;
|
protected readonly labelProvider: LabelProvider;
|
||||||
|
|
||||||
@inject(SettingsService)
|
@inject(SettingsService)
|
||||||
protected readonly settingsService: SettingsService;
|
protected readonly settingsService: SettingsService;
|
||||||
|
|
||||||
onStart(app: FrontendApplication): MaybePromise<void> {
|
@inject(ArduinoPreferences)
|
||||||
|
protected readonly preferences: ArduinoPreferences;
|
||||||
|
|
||||||
|
@inject(FrontendApplicationStateService)
|
||||||
|
protected readonly appStateService: FrontendApplicationStateService;
|
||||||
|
|
||||||
|
@postConstruct()
|
||||||
|
protected init(): void {
|
||||||
|
this.appStateService.reachedState('ready').then(() => this.onReady());
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
|
||||||
}
|
onStart(app: FrontendApplication): MaybePromise<void> {}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
|
||||||
}
|
registerCommands(registry: CommandRegistry): void {}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
|
||||||
}
|
registerMenus(registry: MenuModelRegistry): void {}
|
||||||
|
|
||||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
|
||||||
}
|
registerKeybindings(registry: KeybindingRegistry): void {}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
|
||||||
|
registerToolbarItems(registry: TabBarToolbarRegistry): void {}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onReady(): MaybePromise<void> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export abstract class SketchContribution extends Contribution {
|
export abstract class SketchContribution extends Contribution {
|
||||||
|
|
||||||
@inject(FileService)
|
@inject(FileService)
|
||||||
protected readonly fileService: FileService;
|
protected readonly fileService: FileService;
|
||||||
|
|
||||||
@@ -85,9 +148,6 @@ export abstract class SketchContribution extends Contribution {
|
|||||||
@inject(SketchesServiceClientImpl)
|
@inject(SketchesServiceClientImpl)
|
||||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||||
|
|
||||||
@inject(ArduinoPreferences)
|
|
||||||
protected readonly preferences: ArduinoPreferences;
|
|
||||||
|
|
||||||
@inject(EditorManager)
|
@inject(EditorManager)
|
||||||
protected readonly editorManager: EditorManager;
|
protected readonly editorManager: EditorManager;
|
||||||
|
|
||||||
@@ -97,7 +157,7 @@ export abstract class SketchContribution extends Contribution {
|
|||||||
protected async sourceOverride(): Promise<Record<string, string>> {
|
protected async sourceOverride(): Promise<Record<string, string>> {
|
||||||
const override: Record<string, string> = {};
|
const override: Record<string, string> = {};
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
if (sketch) {
|
if (CurrentSketch.isValid(sketch)) {
|
||||||
for (const editor of this.editorManager.all) {
|
for (const editor of this.editorManager.all) {
|
||||||
const uri = editor.editor.uri;
|
const uri = editor.editor.uri;
|
||||||
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
|
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
|
||||||
@@ -107,11 +167,87 @@ export abstract class SketchContribution extends Contribution {
|
|||||||
}
|
}
|
||||||
return override;
|
return override;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export abstract class CoreServiceContribution extends SketchContribution {
|
||||||
|
@inject(BoardsDataStore)
|
||||||
|
protected readonly boardsDataStore: BoardsDataStore;
|
||||||
|
|
||||||
|
@inject(BoardsServiceProvider)
|
||||||
|
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
|
|
||||||
|
@inject(CoreService)
|
||||||
|
private readonly coreService: CoreService;
|
||||||
|
|
||||||
|
@inject(ClipboardService)
|
||||||
|
private readonly clipboardService: ClipboardService;
|
||||||
|
|
||||||
|
@inject(ResponseServiceClient)
|
||||||
|
private readonly responseService: ResponseServiceClient;
|
||||||
|
|
||||||
|
protected handleError(error: unknown): void {
|
||||||
|
this.tryToastErrorMessage(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tryToastErrorMessage(error: unknown): void {
|
||||||
|
let message: undefined | string = undefined;
|
||||||
|
if (CoreError.is(error)) {
|
||||||
|
message = error.message;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
message = error.message;
|
||||||
|
} else if (typeof error === 'string') {
|
||||||
|
message = error;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
message = JSON.stringify(error);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
if (message) {
|
||||||
|
const copyAction = nls.localize(
|
||||||
|
'arduino/coreContribution/copyError',
|
||||||
|
'Copy error messages'
|
||||||
|
);
|
||||||
|
this.messageService.error(message, copyAction).then(async (action) => {
|
||||||
|
if (action === copyAction) {
|
||||||
|
const content = await this.outputChannelManager.contentOfChannel(
|
||||||
|
'Arduino'
|
||||||
|
);
|
||||||
|
if (content) {
|
||||||
|
this.clipboardService.writeText(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async doWithProgress<T>(options: {
|
||||||
|
progressText: string;
|
||||||
|
keepOutput?: boolean;
|
||||||
|
task: (progressId: string, coreService: CoreService) => Promise<T>;
|
||||||
|
}): Promise<T> {
|
||||||
|
const { progressText, keepOutput, task } = options;
|
||||||
|
this.outputChannelManager
|
||||||
|
.getChannel('Arduino')
|
||||||
|
.show({ preserveFocus: true });
|
||||||
|
const result = await ExecuteWithProgress.doWithProgress({
|
||||||
|
messageService: this.messageService,
|
||||||
|
responseService: this.responseService,
|
||||||
|
progressText,
|
||||||
|
run: ({ progressId }) => task(progressId, this.coreService),
|
||||||
|
keepOutput,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Contribution {
|
export namespace Contribution {
|
||||||
export function configure<T>(bind: interfaces.Bind, serviceIdentifier: typeof Contribution): void {
|
export function configure(
|
||||||
|
bind: interfaces.Bind,
|
||||||
|
serviceIdentifier: typeof Contribution
|
||||||
|
): void {
|
||||||
bind(serviceIdentifier).toSelf().inSingletonScope();
|
bind(serviceIdentifier).toSelf().inSingletonScope();
|
||||||
bind(CommandContribution).toService(serviceIdentifier);
|
bind(CommandContribution).toService(serviceIdentifier);
|
||||||
bind(MenuContribution).toService(serviceIdentifier);
|
bind(MenuContribution).toService(serviceIdentifier);
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Emitter, Event } from '@theia/core';
|
||||||
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { CoreError } from '../../common/protocol/core-service';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class CoreErrorHandler {
|
||||||
|
private readonly errors: CoreError.ErrorLocation[] = [];
|
||||||
|
private readonly compilerErrorsDidChangeEmitter = new Emitter<
|
||||||
|
CoreError.ErrorLocation[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
tryHandle(error: unknown): void {
|
||||||
|
if (CoreError.is(error)) {
|
||||||
|
this.errors.length = 0;
|
||||||
|
this.errors.push(...error.data);
|
||||||
|
this.fireCompilerErrorsDidChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.errors.length = 0;
|
||||||
|
this.fireCompilerErrorsDidChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
get onCompilerErrorsDidChange(): Event<CoreError.ErrorLocation[]> {
|
||||||
|
return this.compilerErrorsDidChangeEmitter.event;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fireCompilerErrorsDidChange(): void {
|
||||||
|
this.compilerErrorsDidChangeEmitter.fire(this.errors.slice());
|
||||||
|
}
|
||||||
|
}
|
||||||
41
arduino-ide-extension/src/browser/contributions/daemon.ts
Normal file
41
arduino-ide-extension/src/browser/contributions/daemon.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { nls } from '@theia/core';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { ArduinoDaemon } from '../../common/protocol';
|
||||||
|
import { Contribution, Command, CommandRegistry } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class Daemon extends Contribution {
|
||||||
|
@inject(ArduinoDaemon)
|
||||||
|
private readonly daemon: ArduinoDaemon;
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(Daemon.Commands.START_DAEMON, {
|
||||||
|
execute: () => this.daemon.start(),
|
||||||
|
});
|
||||||
|
registry.registerCommand(Daemon.Commands.STOP_DAEMON, {
|
||||||
|
execute: () => this.daemon.stop(),
|
||||||
|
});
|
||||||
|
registry.registerCommand(Daemon.Commands.RESTART_DAEMON, {
|
||||||
|
execute: () => this.daemon.restart(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace Daemon {
|
||||||
|
export namespace Commands {
|
||||||
|
export const START_DAEMON: Command = {
|
||||||
|
id: 'arduino-start-daemon',
|
||||||
|
label: nls.localize('arduino/daemon/start', 'Start Daemon'),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
export const STOP_DAEMON: Command = {
|
||||||
|
id: 'arduino-stop-daemon',
|
||||||
|
label: nls.localize('arduino/daemon/stop', 'Stop Daemon'),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
export const RESTART_DAEMON: Command = {
|
||||||
|
id: 'arduino-restart-daemon',
|
||||||
|
label: nls.localize('arduino/daemon/restart', 'Restart Daemon'),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +1,176 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
import { Board, BoardsService, ExecutableService } from '../../common/protocol';
|
import { Board, BoardsService, ExecutableService } from '../../common/protocol';
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import { URI, Command, CommandRegistry, SketchContribution, TabBarToolbarRegistry } from './contribution';
|
import {
|
||||||
|
URI,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
SketchContribution,
|
||||||
|
TabBarToolbarRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/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()
|
@injectable()
|
||||||
export class Debug extends SketchContribution {
|
export class Debug extends SketchContribution {
|
||||||
|
|
||||||
@inject(HostedPluginSupport)
|
@inject(HostedPluginSupport)
|
||||||
protected hostedPluginSupport: HostedPluginSupport;
|
private readonly hostedPluginSupport: HostedPluginSupport;
|
||||||
|
|
||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
private readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
@inject(ExecutableService)
|
@inject(ExecutableService)
|
||||||
protected readonly executableService: ExecutableService;
|
private readonly executableService: ExecutableService;
|
||||||
|
|
||||||
@inject(BoardsService)
|
@inject(BoardsService)
|
||||||
protected readonly boardService: BoardsService;
|
private readonly boardService: BoardsService;
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
|
|
||||||
|
@inject(MainMenuManager)
|
||||||
|
private readonly mainMenuManager: MainMenuManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
|
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
|
||||||
*/
|
*/
|
||||||
protected _disabledMessages?: string = 'No board selected'; // Initial pessimism.
|
private _disabledMessages?: string = nls.localize(
|
||||||
protected disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
|
'arduino/common/noBoardSelected',
|
||||||
protected onDisabledMessageDidChange = this.disabledMessageDidChangeEmitter.event;
|
'No board selected'
|
||||||
|
); // Initial pessimism.
|
||||||
|
private disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
|
||||||
|
private onDisabledMessageDidChange =
|
||||||
|
this.disabledMessageDidChangeEmitter.event;
|
||||||
|
|
||||||
protected get disabledMessage(): string | undefined {
|
private get disabledMessage(): string | undefined {
|
||||||
return this._disabledMessages;
|
return this._disabledMessages;
|
||||||
}
|
}
|
||||||
protected set disabledMessage(message: string | undefined) {
|
private set disabledMessage(message: string | undefined) {
|
||||||
this._disabledMessages = message;
|
this._disabledMessages = message;
|
||||||
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
|
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly debugToolbarItem = {
|
private readonly debugToolbarItem = {
|
||||||
id: Debug.Commands.START_DEBUGGING.id,
|
id: Debug.Commands.START_DEBUGGING.id,
|
||||||
command: Debug.Commands.START_DEBUGGING.id,
|
command: Debug.Commands.START_DEBUGGING.id,
|
||||||
tooltip: `${this.disabledMessage ? `Debug - ${this.disabledMessage}` : 'Start Debugging'}`,
|
tooltip: `${
|
||||||
|
this.disabledMessage
|
||||||
|
? nls.localize(
|
||||||
|
'arduino/debug/debugWithMessage',
|
||||||
|
'Debug - {0}',
|
||||||
|
this.disabledMessage
|
||||||
|
)
|
||||||
|
: Debug.Commands.START_DEBUGGING.label
|
||||||
|
}`,
|
||||||
priority: 3,
|
priority: 3,
|
||||||
onDidChange: this.onDisabledMessageDidChange as Event<void>
|
onDidChange: this.onDisabledMessageDidChange as Event<void>,
|
||||||
};
|
};
|
||||||
|
|
||||||
onStart(): void {
|
override onStart(): void {
|
||||||
this.onDisabledMessageDidChange(() => this.debugToolbarItem.tooltip = `${this.disabledMessage ? `Debug - ${this.disabledMessage}` : 'Start Debugging'}`);
|
this.onDisabledMessageDidChange(
|
||||||
const refreshState = async (board: Board | undefined = this.boardsServiceProvider.boardsConfig.selectedBoard) => {
|
() =>
|
||||||
|
(this.debugToolbarItem.tooltip = `${
|
||||||
|
this.disabledMessage
|
||||||
|
? nls.localize(
|
||||||
|
'arduino/debug/debugWithMessage',
|
||||||
|
'Debug - {0}',
|
||||||
|
this.disabledMessage
|
||||||
|
)
|
||||||
|
: Debug.Commands.START_DEBUGGING.label
|
||||||
|
}`)
|
||||||
|
);
|
||||||
|
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||||
|
this.refreshState(selectedBoard)
|
||||||
|
);
|
||||||
|
this.notificationCenter.onPlatformDidInstall(() => this.refreshState());
|
||||||
|
this.notificationCenter.onPlatformDidUninstall(() => this.refreshState());
|
||||||
|
}
|
||||||
|
|
||||||
|
override onReady(): MaybePromise<void> {
|
||||||
|
this.refreshState();
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(Debug.Commands.START_DEBUGGING, {
|
||||||
|
execute: () => this.startDebug(),
|
||||||
|
isVisible: (widget) =>
|
||||||
|
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||||
|
isEnabled: () => !this.disabledMessage,
|
||||||
|
});
|
||||||
|
registry.registerCommand(Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG, {
|
||||||
|
execute: () => this.toggleCompileForDebug(),
|
||||||
|
isToggled: () => this.compileForDebug,
|
||||||
|
});
|
||||||
|
registry.registerCommand(Debug.Commands.IS_OPTIMIZE_FOR_DEBUG, {
|
||||||
|
execute: () => this.compileForDebug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||||
|
registry.registerItem(this.debugToolbarItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||||
|
commandId: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.id,
|
||||||
|
label: Debug.Commands.TOGGLE_OPTIMIZE_FOR_DEBUG.label,
|
||||||
|
order: '5',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshState(
|
||||||
|
board: Board | undefined = this.boardsServiceProvider.boardsConfig
|
||||||
|
.selectedBoard
|
||||||
|
): Promise<void> {
|
||||||
if (!board) {
|
if (!board) {
|
||||||
this.disabledMessage = 'No board selected';
|
this.disabledMessage = nls.localize(
|
||||||
|
'arduino/common/noBoardSelected',
|
||||||
|
'No board selected'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fqbn = board.fqbn;
|
const fqbn = board.fqbn;
|
||||||
if (!fqbn) {
|
if (!fqbn) {
|
||||||
this.disabledMessage = `Platform is not installed for '${board.name}'`;
|
this.disabledMessage = nls.localize(
|
||||||
|
'arduino/debug/noPlatformInstalledFor',
|
||||||
|
"Platform is not installed for '{0}'",
|
||||||
|
board.name
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const details = await this.boardService.getBoardDetails({ fqbn });
|
const details = await this.boardService.getBoardDetails({ fqbn });
|
||||||
if (!details) {
|
if (!details) {
|
||||||
this.disabledMessage = `Platform is not installed for '${board.name}'`;
|
this.disabledMessage = nls.localize(
|
||||||
|
'arduino/debug/noPlatformInstalledFor',
|
||||||
|
"Platform is not installed for '{0}'",
|
||||||
|
board.name
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { debuggingSupported } = details;
|
const { debuggingSupported } = details;
|
||||||
if (!debuggingSupported) {
|
if (!debuggingSupported) {
|
||||||
this.disabledMessage = `Debugging is not supported by '${board.name}'`;
|
this.disabledMessage = nls.localize(
|
||||||
|
'arduino/debug/debuggingNotSupported',
|
||||||
|
"Debugging is not supported by '{0}'",
|
||||||
|
board.name
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.disabledMessage = undefined;
|
this.disabledMessage = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) => refreshState(selectedBoard));
|
|
||||||
this.notificationCenter.onPlatformInstalled(() => refreshState());
|
|
||||||
this.notificationCenter.onPlatformUninstalled(() => refreshState());
|
|
||||||
refreshState();
|
|
||||||
}
|
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
private async startDebug(
|
||||||
registry.registerCommand(Debug.Commands.START_DEBUGGING, {
|
board: Board | undefined = this.boardsServiceProvider.boardsConfig
|
||||||
execute: () => this.startDebug(),
|
.selectedBoard
|
||||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
): Promise<void> {
|
||||||
isEnabled: () => !this.disabledMessage
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
|
||||||
registry.registerItem(this.debugToolbarItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async startDebug(board: Board | undefined = this.boardsServiceProvider.boardsConfig.selectedBoard): Promise<void> {
|
|
||||||
if (!board) {
|
if (!board) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -101,37 +181,63 @@ export class Debug extends SketchContribution {
|
|||||||
await this.hostedPluginSupport.didStart;
|
await this.hostedPluginSupport.didStart;
|
||||||
const [sketch, executables] = await Promise.all([
|
const [sketch, executables] = await Promise.all([
|
||||||
this.sketchServiceClient.currentSketch(),
|
this.sketchServiceClient.currentSketch(),
|
||||||
this.executableService.list()
|
this.executableService.list(),
|
||||||
]);
|
]);
|
||||||
if (!sketch) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(sketch);
|
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
|
||||||
|
sketch
|
||||||
|
);
|
||||||
const [cliPath, sketchPath, configPath] = await Promise.all([
|
const [cliPath, sketchPath, configPath] = await Promise.all([
|
||||||
this.fileService.fsPath(new URI(executables.cliUri)),
|
this.fileService.fsPath(new URI(executables.cliUri)),
|
||||||
this.fileService.fsPath(new URI(sketch.uri)),
|
this.fileService.fsPath(new URI(sketch.uri)),
|
||||||
this.fileService.fsPath(new URI(ideTempFolderUri)),
|
this.fileService.fsPath(new URI(ideTempFolderUri)),
|
||||||
])
|
]);
|
||||||
const config = {
|
const config = {
|
||||||
cliPath,
|
cliPath,
|
||||||
board: {
|
board: {
|
||||||
fqbn,
|
fqbn,
|
||||||
name
|
name,
|
||||||
},
|
},
|
||||||
sketchPath,
|
sketchPath,
|
||||||
configPath
|
configPath,
|
||||||
};
|
};
|
||||||
return this.commandService.executeCommand('arduino.debug.start', config);
|
return this.commandService.executeCommand('arduino.debug.start', config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get compileForDebug(): boolean {
|
||||||
|
const value = window.localStorage.getItem(COMPILE_FOR_DEBUG_KEY);
|
||||||
|
return value === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async toggleCompileForDebug(): Promise<void> {
|
||||||
|
const oldState = this.compileForDebug;
|
||||||
|
const newState = !oldState;
|
||||||
|
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
|
||||||
|
this.mainMenuManager.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
export namespace Debug {
|
export namespace Debug {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const START_DEBUGGING: Command = {
|
export const START_DEBUGGING = Command.toLocalizedCommand(
|
||||||
|
{
|
||||||
id: 'arduino-start-debug',
|
id: 'arduino-start-debug',
|
||||||
label: 'Start Debugging',
|
label: 'Start Debugging',
|
||||||
category: 'Arduino'
|
category: 'Arduino',
|
||||||
}
|
},
|
||||||
|
'vscode/debug.contribution/startDebuggingHelp'
|
||||||
|
);
|
||||||
|
export const TOGGLE_OPTIMIZE_FOR_DEBUG = Command.toLocalizedCommand(
|
||||||
|
{
|
||||||
|
id: 'arduino-toggle-optimize-for-debug',
|
||||||
|
label: 'Optimize for Debugging',
|
||||||
|
category: 'Arduino',
|
||||||
|
},
|
||||||
|
'arduino/debug/optimizeForDebugging'
|
||||||
|
);
|
||||||
|
export const IS_OPTIMIZE_FOR_DEBUG: Command = {
|
||||||
|
id: 'arduino-is-optimize-for-debug',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,54 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||||
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
|
|
||||||
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
|
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
|
||||||
import { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
|
import {
|
||||||
|
Contribution,
|
||||||
|
Command,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
CommandRegistry,
|
||||||
|
} from './contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import type { ICodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
|
||||||
|
import type { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
|
||||||
|
|
||||||
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
|
// TODO: [macOS]: to remove `Start Dictation...` and `Emoji & Symbol` see this thread: https://github.com/electron/electron/issues/8283#issuecomment-269522072
|
||||||
// Depends on https://github.com/eclipse-theia/theia/pull/7964
|
// Depends on https://github.com/eclipse-theia/theia/pull/7964
|
||||||
@injectable()
|
@injectable()
|
||||||
export class EditContributions extends Contribution {
|
export class EditContributions extends Contribution {
|
||||||
|
|
||||||
@inject(MonacoEditorService)
|
@inject(MonacoEditorService)
|
||||||
protected readonly codeEditorService: MonacoEditorService;
|
private readonly codeEditorService: MonacoEditorService;
|
||||||
|
|
||||||
@inject(ClipboardService)
|
@inject(ClipboardService)
|
||||||
protected readonly clipboardService: ClipboardService;
|
private readonly clipboardService: ClipboardService;
|
||||||
|
|
||||||
@inject(PreferenceService)
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
protected readonly preferences: PreferenceService;
|
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, {
|
||||||
|
execute: () => this.run('editor.action.gotoLine'),
|
||||||
registerCommands(registry: CommandRegistry): void {
|
});
|
||||||
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, { execute: () => this.run('editor.action.gotoLine') });
|
registry.registerCommand(EditContributions.Commands.TOGGLE_COMMENT, {
|
||||||
registry.registerCommand(EditContributions.Commands.TOGGLE_COMMENT, { execute: () => this.run('editor.action.commentLine') });
|
execute: () => this.run('editor.action.commentLine'),
|
||||||
registry.registerCommand(EditContributions.Commands.INDENT_LINES, { execute: () => this.run('editor.action.indentLines') });
|
});
|
||||||
registry.registerCommand(EditContributions.Commands.OUTDENT_LINES, { execute: () => this.run('editor.action.outdentLines') });
|
registry.registerCommand(EditContributions.Commands.INDENT_LINES, {
|
||||||
registry.registerCommand(EditContributions.Commands.FIND, { execute: () => this.run('actions.find') });
|
execute: () => this.run('editor.action.indentLines'),
|
||||||
registry.registerCommand(EditContributions.Commands.FIND_NEXT, { execute: () => this.run('actions.findWithSelection') });
|
});
|
||||||
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, { execute: () => this.run('editor.action.nextMatchFindAction') });
|
registry.registerCommand(EditContributions.Commands.OUTDENT_LINES, {
|
||||||
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, { execute: () => this.run('editor.action.previousSelectionMatchFindAction') });
|
execute: () => this.run('editor.action.outdentLines'),
|
||||||
|
});
|
||||||
|
registry.registerCommand(EditContributions.Commands.FIND, {
|
||||||
|
execute: () => this.run('actions.find'),
|
||||||
|
});
|
||||||
|
registry.registerCommand(EditContributions.Commands.FIND_NEXT, {
|
||||||
|
execute: () => this.run('editor.action.nextMatchFindAction'),
|
||||||
|
});
|
||||||
|
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, {
|
||||||
|
execute: () => this.run('editor.action.previousMatchFindAction'),
|
||||||
|
});
|
||||||
|
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
|
||||||
|
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
|
||||||
|
});
|
||||||
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
|
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const settings = await this.settingsService.settings();
|
const settings = await this.settingsService.settings();
|
||||||
@@ -39,7 +59,7 @@ export class EditContributions extends Contribution {
|
|||||||
}
|
}
|
||||||
await this.settingsService.update(settings);
|
await this.settingsService.update(settings);
|
||||||
await this.settingsService.save();
|
await this.settingsService.save();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
|
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
@@ -51,178 +71,188 @@ export class EditContributions extends Contribution {
|
|||||||
}
|
}
|
||||||
await this.settingsService.update(settings);
|
await this.settingsService.update(settings);
|
||||||
await this.settingsService.save();
|
await this.settingsService.save();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
/* Tools */registry.registerCommand(EditContributions.Commands.AUTO_FORMAT, { execute: () => this.run('editor.action.formatDocument') });
|
/* Tools */ registry.registerCommand(
|
||||||
|
EditContributions.Commands.AUTO_FORMAT,
|
||||||
|
{ execute: () => this.run('editor.action.formatDocument') }
|
||||||
|
);
|
||||||
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
|
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
|
||||||
execute: async () => {
|
|
||||||
const value = await this.currentValue();
|
|
||||||
if (value !== undefined) {
|
|
||||||
this.clipboardService.writeText(`[code]
|
|
||||||
${value}
|
|
||||||
[/code]`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
registry.registerCommand(EditContributions.Commands.COPY_FOR_GITHUB, {
|
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const value = await this.currentValue();
|
const value = await this.currentValue();
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
this.clipboardService.writeText(`\`\`\`cpp
|
this.clipboardService.writeText(`\`\`\`cpp
|
||||||
${value}
|
${value}
|
||||||
\`\`\``)
|
\`\`\``);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||||
commandId: CommonCommands.CUT.id,
|
commandId: CommonCommands.CUT.id,
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||||
commandId: CommonCommands.COPY.id,
|
commandId: CommonCommands.COPY.id,
|
||||||
order: '1'
|
order: '1',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||||
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
|
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||||
label: 'Copy for Forum',
|
label: nls.localize(
|
||||||
order: '2'
|
'arduino/editor/copyForForum',
|
||||||
});
|
'Copy for Forum (Markdown)'
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
),
|
||||||
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
|
order: '2',
|
||||||
label: 'Copy for GitHub',
|
|
||||||
order: '3'
|
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||||
commandId: CommonCommands.PASTE.id,
|
commandId: CommonCommands.PASTE.id,
|
||||||
order: '4'
|
order: '3',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||||
commandId: CommonCommands.SELECT_ALL.id,
|
commandId: CommonCommands.SELECT_ALL.id,
|
||||||
order: '5'
|
order: '4',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||||
commandId: EditContributions.Commands.GO_TO_LINE.id,
|
commandId: EditContributions.Commands.GO_TO_LINE.id,
|
||||||
label: 'Go to Line...',
|
label: nls.localize(
|
||||||
order: '6'
|
'vscode/standaloneStrings/gotoLineActionLabel',
|
||||||
|
'Go to Line...'
|
||||||
|
),
|
||||||
|
order: '5',
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||||
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
|
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
|
||||||
label: 'Comment/Uncomment',
|
label: nls.localize(
|
||||||
order: '0'
|
'arduino/editor/commentUncomment',
|
||||||
|
'Comment/Uncomment'
|
||||||
|
),
|
||||||
|
order: '0',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||||
commandId: EditContributions.Commands.INDENT_LINES.id,
|
commandId: EditContributions.Commands.INDENT_LINES.id,
|
||||||
label: 'Increase Indent',
|
label: nls.localize('arduino/editor/increaseIndent', 'Increase Indent'),
|
||||||
order: '1'
|
order: '1',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||||
commandId: EditContributions.Commands.OUTDENT_LINES.id,
|
commandId: EditContributions.Commands.OUTDENT_LINES.id,
|
||||||
label: 'Decrease Indent',
|
label: nls.localize('arduino/editor/decreaseIndent', 'Decrease Indent'),
|
||||||
order: '2'
|
order: '2',
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||||
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||||
label: 'Increase Font Size',
|
label: nls.localize(
|
||||||
order: '0'
|
'arduino/editor/increaseFontSize',
|
||||||
|
'Increase Font Size'
|
||||||
|
),
|
||||||
|
order: '0',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||||
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||||
label: 'Decrease Font Size',
|
label: nls.localize(
|
||||||
order: '1'
|
'arduino/editor/decreaseFontSize',
|
||||||
|
'Decrease Font Size'
|
||||||
|
),
|
||||||
|
order: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||||
commandId: EditContributions.Commands.FIND.id,
|
commandId: EditContributions.Commands.FIND.id,
|
||||||
label: 'Find',
|
label: nls.localize('vscode/findController/startFindAction', 'Find'),
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||||
commandId: EditContributions.Commands.FIND_NEXT.id,
|
commandId: EditContributions.Commands.FIND_NEXT.id,
|
||||||
label: 'Find Next',
|
label: nls.localize(
|
||||||
order: '1'
|
'vscode/findController/findNextMatchAction',
|
||||||
|
'Find Next'
|
||||||
|
),
|
||||||
|
order: '1',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||||
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
|
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
|
||||||
label: 'Find Previous',
|
label: nls.localize(
|
||||||
order: '2'
|
'vscode/findController/findPreviousMatchAction',
|
||||||
|
'Find Previous'
|
||||||
|
),
|
||||||
|
order: '2',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||||
commandId: EditContributions.Commands.USE_FOR_FIND.id,
|
commandId: EditContributions.Commands.USE_FOR_FIND.id,
|
||||||
label: 'Use Selection for Find', // XXX: The Java IDE uses `Use Selection For Find`.
|
label: nls.localize(
|
||||||
order: '3'
|
'vscode/findController/startFindWithSelectionAction',
|
||||||
|
'Use Selection for Find'
|
||||||
|
), // XXX: The Java IDE uses `Use Selection For Find`.
|
||||||
|
order: '3',
|
||||||
});
|
});
|
||||||
|
|
||||||
// `Tools`
|
// `Tools`
|
||||||
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||||
commandId: EditContributions.Commands.AUTO_FORMAT.id,
|
commandId: EditContributions.Commands.AUTO_FORMAT.id,
|
||||||
label: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
|
label: nls.localize('arduino/editor/autoFormat', 'Auto Format'), // XXX: The Java IDE uses `Use Selection For Find`.
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.COPY_FOR_FORUM.id,
|
command: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||||
keybinding: 'CtrlCmd+Shift+C',
|
keybinding: 'CtrlCmd+Shift+C',
|
||||||
when: 'editorFocus'
|
when: 'editorFocus',
|
||||||
});
|
|
||||||
registry.registerKeybinding({
|
|
||||||
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
|
|
||||||
keybinding: 'CtrlCmd+Alt+C',
|
|
||||||
when: 'editorFocus'
|
|
||||||
});
|
});
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.GO_TO_LINE.id,
|
command: EditContributions.Commands.GO_TO_LINE.id,
|
||||||
keybinding: 'CtrlCmd+L',
|
keybinding: 'CtrlCmd+L',
|
||||||
when: 'editorFocus'
|
when: 'editorFocus',
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.TOGGLE_COMMENT.id,
|
command: EditContributions.Commands.TOGGLE_COMMENT.id,
|
||||||
keybinding: 'CtrlCmd+/',
|
keybinding: 'CtrlCmd+/',
|
||||||
when: 'editorFocus'
|
when: 'editorFocus',
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||||
keybinding: 'CtrlCmd+='
|
keybinding: 'CtrlCmd+=',
|
||||||
});
|
});
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||||
keybinding: 'CtrlCmd+-'
|
keybinding: 'CtrlCmd+-',
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.FIND.id,
|
command: EditContributions.Commands.FIND.id,
|
||||||
keybinding: 'CtrlCmd+F'
|
keybinding: 'CtrlCmd+F',
|
||||||
});
|
});
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.FIND_NEXT.id,
|
command: EditContributions.Commands.FIND_NEXT.id,
|
||||||
keybinding: 'CtrlCmd+G'
|
keybinding: 'CtrlCmd+G',
|
||||||
});
|
});
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.FIND_PREVIOUS.id,
|
command: EditContributions.Commands.FIND_PREVIOUS.id,
|
||||||
keybinding: 'CtrlCmd+Shift+G'
|
keybinding: 'CtrlCmd+Shift+G',
|
||||||
});
|
});
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.USE_FOR_FIND.id,
|
command: EditContributions.Commands.USE_FOR_FIND.id,
|
||||||
keybinding: 'CtrlCmd+E'
|
keybinding: 'CtrlCmd+E',
|
||||||
});
|
});
|
||||||
|
|
||||||
// `Tools`
|
// `Tools`
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: EditContributions.Commands.AUTO_FORMAT.id,
|
command: EditContributions.Commands.AUTO_FORMAT.id,
|
||||||
keybinding: 'CtrlCmd+T'
|
keybinding: 'CtrlCmd+T',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
|
protected async current(): Promise<ICodeEditor | StandaloneCodeEditor | undefined> {
|
||||||
return this.codeEditorService.getFocusedCodeEditor() || this.codeEditorService.getActiveCodeEditor();
|
return (
|
||||||
|
this.codeEditorService.getFocusedCodeEditor() ||
|
||||||
|
this.codeEditorService.getActiveCodeEditor() || undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async currentValue(): Promise<string | undefined> {
|
protected async currentValue(): Promise<string | undefined> {
|
||||||
@@ -246,49 +276,45 @@ ${value}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace EditContributions {
|
export namespace EditContributions {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const COPY_FOR_FORUM: Command = {
|
export const COPY_FOR_FORUM: Command = {
|
||||||
id: 'arduino-copy-for-forum'
|
id: 'arduino-copy-for-forum',
|
||||||
};
|
|
||||||
export const COPY_FOR_GITHUB: Command = {
|
|
||||||
id: 'arduino-copy-for-github'
|
|
||||||
};
|
};
|
||||||
export const GO_TO_LINE: Command = {
|
export const GO_TO_LINE: Command = {
|
||||||
id: 'arduino-go-to-line'
|
id: 'arduino-go-to-line',
|
||||||
};
|
};
|
||||||
export const TOGGLE_COMMENT: Command = {
|
export const TOGGLE_COMMENT: Command = {
|
||||||
id: 'arduino-toggle-comment'
|
id: 'arduino-toggle-comment',
|
||||||
};
|
};
|
||||||
export const INDENT_LINES: Command = {
|
export const INDENT_LINES: Command = {
|
||||||
id: 'arduino-indent-lines'
|
id: 'arduino-indent-lines',
|
||||||
};
|
};
|
||||||
export const OUTDENT_LINES: Command = {
|
export const OUTDENT_LINES: Command = {
|
||||||
id: 'arduino-outdent-lines'
|
id: 'arduino-outdent-lines',
|
||||||
};
|
};
|
||||||
export const FIND: Command = {
|
export const FIND: Command = {
|
||||||
id: 'arduino-find'
|
id: 'arduino-find',
|
||||||
};
|
};
|
||||||
export const FIND_NEXT: Command = {
|
export const FIND_NEXT: Command = {
|
||||||
id: 'arduino-find-next'
|
id: 'arduino-find-next',
|
||||||
};
|
};
|
||||||
export const FIND_PREVIOUS: Command = {
|
export const FIND_PREVIOUS: Command = {
|
||||||
id: 'arduino-find-previous'
|
id: 'arduino-find-previous',
|
||||||
};
|
};
|
||||||
export const USE_FOR_FIND: Command = {
|
export const USE_FOR_FIND: Command = {
|
||||||
id: 'arduino-for-find'
|
id: 'arduino-for-find',
|
||||||
};
|
};
|
||||||
export const INCREASE_FONT_SIZE: Command = {
|
export const INCREASE_FONT_SIZE: Command = {
|
||||||
id: 'arduino-increase-font-size'
|
id: 'arduino-increase-font-size',
|
||||||
};
|
};
|
||||||
export const DECREASE_FONT_SIZE: Command = {
|
export const DECREASE_FONT_SIZE: Command = {
|
||||||
id: 'arduino-decrease-font-size'
|
id: 'arduino-decrease-font-size',
|
||||||
};
|
};
|
||||||
export const AUTO_FORMAT: Command = {
|
export const AUTO_FORMAT: Command = {
|
||||||
id: 'arduino-auto-format' // `Auto Format` should belong to `Tool`.
|
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
import * as PQueue from 'p-queue';
|
import * as PQueue from 'p-queue';
|
||||||
import { inject, injectable, postConstruct } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||||
import { MenuPath, CompositeMenuNode, SubMenuOptions } from '@theia/core/lib/common/menu';
|
import {
|
||||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
MenuPath,
|
||||||
|
CompositeMenuNode,
|
||||||
|
SubMenuOptions,
|
||||||
|
} from '@theia/core/lib/common/menu';
|
||||||
|
import {
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
import { OpenSketch } from './open-sketch';
|
import { OpenSketch } from './open-sketch';
|
||||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||||
import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
} from './contribution';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
import { Board, Sketch, SketchContainer } from '../../common/protocol';
|
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export abstract class Examples extends SketchContribution {
|
export abstract class Examples extends SketchContribution {
|
||||||
|
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
protected readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@@ -32,21 +43,24 @@ export abstract class Examples extends SketchContribution {
|
|||||||
|
|
||||||
protected readonly toDispose = new DisposableCollection();
|
protected readonly toDispose = new DisposableCollection();
|
||||||
|
|
||||||
@postConstruct()
|
protected override init(): void {
|
||||||
init(): void {
|
super.init();
|
||||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard));
|
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||||
|
this.handleBoardChanged(selectedBoard)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleBoardChanged(board: Board | undefined): void {
|
protected handleBoardChanged(board: Board | undefined): void {
|
||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
try {
|
try {
|
||||||
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
||||||
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
|
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
|
||||||
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
|
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
|
||||||
const groupPath = index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
|
const groupPath =
|
||||||
|
index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
|
||||||
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
|
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
|
||||||
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
|
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
|
||||||
parent.addNode(examples);
|
parent.addNode(examples);
|
||||||
@@ -56,21 +70,37 @@ export abstract class Examples extends SketchContribution {
|
|||||||
}
|
}
|
||||||
// Registering the same submenu multiple times has no side-effect.
|
// Registering the same submenu multiple times has no side-effect.
|
||||||
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
|
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
|
||||||
registry.registerSubmenu(ArduinoMenus.FILE__EXAMPLES_SUBMENU, 'Examples', { order: '4' });
|
registry.registerSubmenu(
|
||||||
|
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
|
||||||
|
nls.localize('arduino/examples/menu', 'Examples'),
|
||||||
|
{
|
||||||
|
order: '4',
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRecursively(
|
registerRecursively(
|
||||||
sketchContainerOrPlaceholder: SketchContainer | (Sketch | SketchContainer)[] | string,
|
sketchContainerOrPlaceholder:
|
||||||
|
| SketchContainer
|
||||||
|
| (SketchRef | SketchContainer)[]
|
||||||
|
| string,
|
||||||
menuPath: MenuPath,
|
menuPath: MenuPath,
|
||||||
pushToDispose: DisposableCollection = new DisposableCollection(),
|
pushToDispose: DisposableCollection = new DisposableCollection(),
|
||||||
subMenuOptions?: SubMenuOptions | undefined): void {
|
subMenuOptions?: SubMenuOptions | undefined
|
||||||
|
): void {
|
||||||
if (typeof sketchContainerOrPlaceholder === 'string') {
|
if (typeof sketchContainerOrPlaceholder === 'string') {
|
||||||
const placeholder = new PlaceholderMenuNode(menuPath, sketchContainerOrPlaceholder);
|
const placeholder = new PlaceholderMenuNode(
|
||||||
|
menuPath,
|
||||||
|
sketchContainerOrPlaceholder
|
||||||
|
);
|
||||||
this.menuRegistry.registerMenuNode(menuPath, placeholder);
|
this.menuRegistry.registerMenuNode(menuPath, placeholder);
|
||||||
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
|
pushToDispose.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuNode(placeholder.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const sketches: Sketch[] = [];
|
const sketches: SketchRef[] = [];
|
||||||
const children: SketchContainer[] = [];
|
const children: SketchContainer[] = [];
|
||||||
let submenuPath = menuPath;
|
let submenuPath = menuPath;
|
||||||
|
|
||||||
@@ -89,15 +119,29 @@ export abstract class Examples extends SketchContribution {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
|
children.forEach((child) =>
|
||||||
|
this.registerRecursively(child, submenuPath, pushToDispose)
|
||||||
|
);
|
||||||
for (const sketch of sketches) {
|
for (const sketch of sketches) {
|
||||||
const { uri } = sketch;
|
const { uri } = sketch;
|
||||||
const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`;
|
const commandId = `arduino-open-example-${submenuPath.join(
|
||||||
|
':'
|
||||||
|
)}--${uri}`;
|
||||||
const command = { id: commandId };
|
const command = { id: commandId };
|
||||||
const handler = this.createHandler(uri);
|
const handler = this.createHandler(uri);
|
||||||
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
|
pushToDispose.push(
|
||||||
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name, order: sketch.name.toLocaleLowerCase() });
|
this.commandRegistry.registerCommand(command, handler)
|
||||||
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
|
);
|
||||||
|
this.menuRegistry.registerMenuAction(submenuPath, {
|
||||||
|
commandId,
|
||||||
|
label: sketch.name,
|
||||||
|
order: sketch.name.toLocaleLowerCase(),
|
||||||
|
});
|
||||||
|
pushToDispose.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuAction(command)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,17 +150,18 @@ export abstract class Examples extends SketchContribution {
|
|||||||
return {
|
return {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const sketch = await this.sketchService.cloneExample(uri);
|
const sketch = await this.sketchService.cloneExample(uri);
|
||||||
return this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
|
return this.commandService.executeCommand(
|
||||||
|
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
|
sketch
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BuiltInExamples extends Examples {
|
export class BuiltInExamples extends Examples {
|
||||||
|
override async onReady(): Promise<void> {
|
||||||
onStart(): void {
|
|
||||||
this.register(); // no `await`
|
this.register(); // no `await`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,63 +171,101 @@ export class BuiltInExamples extends Examples {
|
|||||||
sketchContainers = await this.examplesService.builtIns();
|
sketchContainers = await this.examplesService.builtIns();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Could not initialize built-in examples.', e);
|
console.error('Could not initialize built-in examples.', e);
|
||||||
this.messageService.error('Could not initialize built-in examples.');
|
this.messageService.error(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/examples/couldNotInitializeExamples',
|
||||||
|
'Could not initialize built-in examples.'
|
||||||
|
)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
for (const container of ['Built-in examples', ...sketchContainers]) {
|
for (const container of [
|
||||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
|
nls.localize('arduino/examples/builtInExamples', 'Built-in examples'),
|
||||||
|
...sketchContainers,
|
||||||
|
]) {
|
||||||
|
this.registerRecursively(
|
||||||
|
container,
|
||||||
|
ArduinoMenus.EXAMPLES__BUILT_IN_GROUP,
|
||||||
|
this.toDispose
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.menuManager.update();
|
this.menuManager.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class LibraryExamples extends Examples {
|
export class LibraryExamples extends Examples {
|
||||||
|
|
||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
protected readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||||
|
|
||||||
onStart(): void {
|
override onStart(): void {
|
||||||
this.register(); // no `await`
|
this.notificationCenter.onLibraryDidInstall(() => this.register());
|
||||||
this.notificationCenter.onLibraryInstalled(() => this.register());
|
this.notificationCenter.onLibraryDidUninstall(() => this.register());
|
||||||
this.notificationCenter.onLibraryUninstalled(() => this.register());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleBoardChanged(board: Board | undefined): void {
|
override async onReady(): Promise<void> {
|
||||||
|
this.register(); // no `await`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override handleBoardChanged(board: Board | undefined): void {
|
||||||
this.register(board);
|
this.register(board);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard): Promise<void> {
|
protected async register(
|
||||||
|
board: Board | undefined = this.boardsServiceClient.boardsConfig
|
||||||
|
.selectedBoard
|
||||||
|
): Promise<void> {
|
||||||
return this.queue.add(async () => {
|
return this.queue.add(async () => {
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
const fqbn = board?.fqbn;
|
const fqbn = board?.fqbn;
|
||||||
const name = board?.name;
|
const name = board?.name;
|
||||||
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
|
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
|
||||||
const { user, current, any } = await this.examplesService.installed({ fqbn });
|
const { user, current, any } = await this.examplesService.installed({
|
||||||
|
fqbn,
|
||||||
|
});
|
||||||
if (user.length) {
|
if (user.length) {
|
||||||
(user as any).unshift('Examples from Custom Libraries');
|
(user as any).unshift(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/examples/customLibrary',
|
||||||
|
'Examples from Custom Libraries'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (name && fqbn && current.length) {
|
if (name && fqbn && current.length) {
|
||||||
(current as any).unshift(`Examples for ${name}`);
|
(current as any).unshift(
|
||||||
|
nls.localize('arduino/examples/for', 'Examples for {0}', name)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (any.length) {
|
if (any.length) {
|
||||||
(any as any).unshift('Examples for any board');
|
(any as any).unshift(
|
||||||
|
nls.localize('arduino/examples/forAny', 'Examples for any board')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (const container of user) {
|
for (const container of user) {
|
||||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, this.toDispose);
|
this.registerRecursively(
|
||||||
|
container,
|
||||||
|
ArduinoMenus.EXAMPLES__USER_LIBS_GROUP,
|
||||||
|
this.toDispose
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (const container of current) {
|
for (const container of current) {
|
||||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP, this.toDispose);
|
this.registerRecursively(
|
||||||
|
container,
|
||||||
|
ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP,
|
||||||
|
this.toDispose
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (const container of any) {
|
for (const container of any) {
|
||||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP, this.toDispose);
|
this.registerRecursively(
|
||||||
|
container,
|
||||||
|
ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP,
|
||||||
|
this.toDispose
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.menuManager.update();
|
this.menuManager.update();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { LocalStorageService } from '@theia/core/lib/browser';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { BoardsService, LibraryService } from '../../common/protocol';
|
||||||
|
import { Contribution } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class FirstStartupInstaller extends Contribution {
|
||||||
|
@inject(LocalStorageService)
|
||||||
|
private readonly localStorageService: LocalStorageService;
|
||||||
|
@inject(BoardsService)
|
||||||
|
private readonly boardsService: BoardsService;
|
||||||
|
@inject(LibraryService)
|
||||||
|
private readonly libraryService: LibraryService;
|
||||||
|
|
||||||
|
override async onReady(): Promise<void> {
|
||||||
|
const isFirstStartup = !(await this.localStorageService.getData(
|
||||||
|
FirstStartupInstaller.INIT_LIBS_AND_PACKAGES
|
||||||
|
));
|
||||||
|
if (isFirstStartup) {
|
||||||
|
const avrPackage = await this.boardsService.getBoardPackage({
|
||||||
|
id: 'arduino:avr',
|
||||||
|
});
|
||||||
|
const builtInLibrary = (
|
||||||
|
await this.libraryService.search({ query: 'Arduino_BuiltIn' })
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
let avrPackageError: Error | undefined;
|
||||||
|
let builtInLibraryError: Error | undefined;
|
||||||
|
|
||||||
|
if (avrPackage) {
|
||||||
|
try {
|
||||||
|
await this.boardsService.install({
|
||||||
|
item: avrPackage,
|
||||||
|
noOverwrite: true, // We don't want to automatically replace custom platforms the user might already have in place
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// There's no error code, I need to parse the error message: https://github.com/arduino/arduino-cli/commit/ffe4232b359fcfa87238d68acf1c3b64a1621f14#diff-10ffbdde46838dd9caa881fd1f2a5326a49f8061f6cfd7c9d430b4875a6b6895R62
|
||||||
|
if (
|
||||||
|
e.message.includes(
|
||||||
|
`Platform ${avrPackage.id}@${avrPackage.installedVersion} already installed`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// If arduino:avr installation fails because it's already installed we don't want to retry on next start-up
|
||||||
|
console.error(e);
|
||||||
|
} else {
|
||||||
|
// But if there is any other error (e.g.: no interntet cconnection), we want to retry next time
|
||||||
|
avrPackageError = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
avrPackageError = new Error('Could not find platform.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builtInLibrary) {
|
||||||
|
try {
|
||||||
|
await this.libraryService.install({
|
||||||
|
item: builtInLibrary,
|
||||||
|
installDependencies: true,
|
||||||
|
noOverwrite: true, // We don't want to automatically replace custom libraries the user might already have in place
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// There's no error code, I need to parse the error message: https://github.com/arduino/arduino-cli/commit/2ea3608453b17b1157f8a1dc892af2e13e40f4f0#diff-1de7569144d4e260f8dde0e0d00a4e2a218c57966d583da1687a70d518986649R95
|
||||||
|
if (/Library (.*) is already installed/.test(e.message)) {
|
||||||
|
// If Arduino_BuiltIn installation fails because it's already installed we don't want to retry on next start-up
|
||||||
|
console.log('error installing core', e);
|
||||||
|
} else {
|
||||||
|
// But if there is any other error (e.g.: no interntet cconnection), we want to retry next time
|
||||||
|
builtInLibraryError = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builtInLibraryError = new Error('Could not find library');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avrPackageError) {
|
||||||
|
this.messageService.error(
|
||||||
|
`Could not install Arduino AVR platform: ${avrPackageError}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (builtInLibraryError) {
|
||||||
|
this.messageService.error(
|
||||||
|
`Could not install ${builtInLibrary.name} library: ${builtInLibraryError}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!avrPackageError && !builtInLibraryError) {
|
||||||
|
await this.localStorageService.setData(
|
||||||
|
FirstStartupInstaller.INIT_LIBS_AND_PACKAGES,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace FirstStartupInstaller {
|
||||||
|
export const INIT_LIBS_AND_PACKAGES = 'initializedLibsAndPackages';
|
||||||
|
}
|
||||||
79
arduino-ide-extension/src/browser/contributions/format.ts
Normal file
79
arduino-ide-extension/src/browser/contributions/format.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { MaybePromise } from '@theia/core';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import * as monaco from '@theia/monaco-editor-core';
|
||||||
|
import { Formatter } from '../../common/protocol/formatter';
|
||||||
|
import { InoSelector } from '../ino-selectors';
|
||||||
|
import { fullRange } from '../utils/monaco';
|
||||||
|
import { Contribution, URI } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class Format
|
||||||
|
extends Contribution
|
||||||
|
implements
|
||||||
|
monaco.languages.DocumentRangeFormattingEditProvider,
|
||||||
|
monaco.languages.DocumentFormattingEditProvider
|
||||||
|
{
|
||||||
|
@inject(Formatter)
|
||||||
|
private readonly formatter: Formatter;
|
||||||
|
|
||||||
|
override onStart(): MaybePromise<void> {
|
||||||
|
monaco.languages.registerDocumentRangeFormattingEditProvider(
|
||||||
|
InoSelector,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
monaco.languages.registerDocumentFormattingEditProvider(InoSelector, this);
|
||||||
|
}
|
||||||
|
async provideDocumentRangeFormattingEdits(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
range: monaco.Range,
|
||||||
|
options: monaco.languages.FormattingOptions,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_token: monaco.CancellationToken
|
||||||
|
): Promise<monaco.languages.TextEdit[]> {
|
||||||
|
const text = await this.format(model, range, options);
|
||||||
|
return [{ range, text }];
|
||||||
|
}
|
||||||
|
|
||||||
|
async provideDocumentFormattingEdits(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
options: monaco.languages.FormattingOptions,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_token: monaco.CancellationToken
|
||||||
|
): Promise<monaco.languages.TextEdit[]> {
|
||||||
|
const range = fullRange(model);
|
||||||
|
const text = await this.format(model, range, options);
|
||||||
|
return [{ range, text }];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From the currently opened workspaces (IDE2 has always one), it calculates all possible
|
||||||
|
* folder locations where the `.clang-format` file could be.
|
||||||
|
*/
|
||||||
|
private formatterConfigFolderUris(model: monaco.editor.ITextModel): string[] {
|
||||||
|
const editorUri = new URI(model.uri.toString());
|
||||||
|
return this.workspaceService
|
||||||
|
.tryGetRoots()
|
||||||
|
.map(({ resource }) => resource)
|
||||||
|
.filter((workspaceUri) => workspaceUri.isEqualOrParent(editorUri))
|
||||||
|
.map((uri) => uri.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private format(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
range: monaco.Range,
|
||||||
|
options: monaco.languages.FormattingOptions
|
||||||
|
): Promise<string> {
|
||||||
|
console.info(
|
||||||
|
`Formatting ${model.uri.toString()} [Range: ${JSON.stringify(
|
||||||
|
range.toJSON()
|
||||||
|
)}]`
|
||||||
|
);
|
||||||
|
const content = model.getValueInRange(range);
|
||||||
|
const formatterConfigFolderUris = this.formatterConfigFolderUris(model);
|
||||||
|
return this.formatter.format({
|
||||||
|
content,
|
||||||
|
formatterConfigFolderUris,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||||
import { QuickInputService } from '@theia/core/lib/browser/quick-open/quick-input-service';
|
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { Contribution, Command, MenuModelRegistry, CommandRegistry, KeybindingRegistry } from './contribution';
|
import { QuickInputService } from '@theia/core/lib/browser/quick-input/quick-input-service';
|
||||||
|
import {
|
||||||
|
Contribution,
|
||||||
|
Command,
|
||||||
|
MenuModelRegistry,
|
||||||
|
CommandRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { IDEUpdaterCommands } from '../ide-updater/ide-updater-commands';
|
||||||
|
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
|
||||||
|
import * as monaco from '@theia/monaco-editor-core';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class Help extends Contribution {
|
export class Help extends Contribution {
|
||||||
|
|
||||||
@inject(EditorManager)
|
@inject(EditorManager)
|
||||||
protected readonly editorManager: EditorManager;
|
protected readonly editorManager: EditorManager;
|
||||||
|
|
||||||
@@ -19,15 +28,29 @@ export class Help extends Contribution {
|
|||||||
@inject(QuickInputService)
|
@inject(QuickInputService)
|
||||||
protected readonly quickInputService: QuickInputService;
|
protected readonly quickInputService: QuickInputService;
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
const open = (url: string) => this.windowService.openNewWindow(url, { external: true });
|
const open = (url: string) =>
|
||||||
const createOpenHandler = (url: string) => <CommandHandler>{
|
this.windowService.openNewWindow(url, { external: true });
|
||||||
execute: () => open(url)
|
const createOpenHandler = (url: string) =>
|
||||||
|
<CommandHandler>{
|
||||||
|
execute: () => open(url),
|
||||||
};
|
};
|
||||||
registry.registerCommand(Help.Commands.GETTING_STARTED, createOpenHandler('https://www.arduino.cc/en/Guide'));
|
registry.registerCommand(
|
||||||
registry.registerCommand(Help.Commands.ENVIRONMENT, createOpenHandler('https://www.arduino.cc/en/Guide/Environment'));
|
Help.Commands.GETTING_STARTED,
|
||||||
registry.registerCommand(Help.Commands.TROUBLESHOOTING, createOpenHandler('https://support.arduino.cc/hc/en-us'));
|
createOpenHandler('https://www.arduino.cc/en/Guide')
|
||||||
registry.registerCommand(Help.Commands.REFERENCE, createOpenHandler('https://www.arduino.cc/reference/en/'));
|
);
|
||||||
|
registry.registerCommand(
|
||||||
|
Help.Commands.ENVIRONMENT,
|
||||||
|
createOpenHandler('https://www.arduino.cc/en/Guide/Environment')
|
||||||
|
);
|
||||||
|
registry.registerCommand(
|
||||||
|
Help.Commands.TROUBLESHOOTING,
|
||||||
|
createOpenHandler('https://support.arduino.cc/hc/en-us')
|
||||||
|
);
|
||||||
|
registry.registerCommand(
|
||||||
|
Help.Commands.REFERENCE,
|
||||||
|
createOpenHandler('https://www.arduino.cc/reference/en/')
|
||||||
|
);
|
||||||
registry.registerCommand(Help.Commands.FIND_IN_REFERENCE, {
|
registry.registerCommand(Help.Commands.FIND_IN_REFERENCE, {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
let searchFor: string | undefined = undefined;
|
let searchFor: string | undefined = undefined;
|
||||||
@@ -41,97 +64,127 @@ export class Help extends Contribution {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!searchFor) {
|
if (!searchFor) {
|
||||||
searchFor = await this.quickInputService.open({
|
searchFor = await this.quickInputService.input({
|
||||||
prompt: 'Search on Arduino.cc',
|
prompt: nls.localize('arduino/help/search', 'Search on Arduino.cc'),
|
||||||
placeHolder: 'Type a keyword'
|
placeHolder: nls.localize('arduino/help/keyword', 'Type a keyword'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (searchFor) {
|
if (searchFor) {
|
||||||
return open(`https://www.arduino.cc/search?q=${encodeURIComponent(searchFor)}&tab=reference`);
|
return open(
|
||||||
}
|
`https://www.arduino.cc/search?q=${encodeURIComponent(
|
||||||
|
searchFor
|
||||||
|
)}&tab=reference`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
registry.registerCommand(Help.Commands.FAQ, createOpenHandler('https://support.arduino.cc/hc/en-us'));
|
registry.registerCommand(
|
||||||
registry.registerCommand(Help.Commands.VISIT_ARDUINO, createOpenHandler('https://www.arduino.cc/'));
|
Help.Commands.FAQ,
|
||||||
|
createOpenHandler('https://support.arduino.cc/hc/en-us')
|
||||||
|
);
|
||||||
|
registry.registerCommand(
|
||||||
|
Help.Commands.VISIT_ARDUINO,
|
||||||
|
createOpenHandler('https://www.arduino.cc/')
|
||||||
|
);
|
||||||
|
registry.registerCommand(
|
||||||
|
Help.Commands.PRIVACY_POLICY,
|
||||||
|
createOpenHandler('https://www.arduino.cc/en/privacy-policy')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
registry.unregisterMenuAction({
|
||||||
|
commandId: ElectronCommands.TOGGLE_DEVELOPER_TOOLS.id,
|
||||||
|
});
|
||||||
|
|
||||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||||
commandId: Help.Commands.GETTING_STARTED.id,
|
commandId: Help.Commands.GETTING_STARTED.id,
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||||
commandId: Help.Commands.ENVIRONMENT.id,
|
commandId: Help.Commands.ENVIRONMENT.id,
|
||||||
order: '1'
|
order: '1',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||||
commandId: Help.Commands.TROUBLESHOOTING.id,
|
commandId: Help.Commands.TROUBLESHOOTING.id,
|
||||||
order: '2'
|
order: '2',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||||
commandId: Help.Commands.REFERENCE.id,
|
commandId: Help.Commands.REFERENCE.id,
|
||||||
order: '3'
|
order: '3',
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||||
commandId: Help.Commands.FIND_IN_REFERENCE.id,
|
commandId: Help.Commands.FIND_IN_REFERENCE.id,
|
||||||
order: '4'
|
order: '4',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||||
commandId: Help.Commands.FAQ.id,
|
commandId: Help.Commands.FAQ.id,
|
||||||
order: '5'
|
order: '5',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||||
commandId: Help.Commands.VISIT_ARDUINO.id,
|
commandId: Help.Commands.VISIT_ARDUINO.id,
|
||||||
order: '6'
|
order: '6',
|
||||||
|
});
|
||||||
|
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||||
|
commandId: Help.Commands.PRIVACY_POLICY.id,
|
||||||
|
order: '7',
|
||||||
|
});
|
||||||
|
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||||
|
commandId: IDEUpdaterCommands.CHECK_FOR_UPDATES.id,
|
||||||
|
order: '8',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: Help.Commands.FIND_IN_REFERENCE.id,
|
command: Help.Commands.FIND_IN_REFERENCE.id,
|
||||||
keybinding: 'CtrlCmd+Shift+F'
|
keybinding: 'CtrlCmd+Shift+F',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Help {
|
export namespace Help {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const GETTING_STARTED: Command = {
|
export const GETTING_STARTED: Command = {
|
||||||
id: 'arduino-getting-started',
|
id: 'arduino-getting-started',
|
||||||
label: 'Getting Started',
|
label: nls.localize('arduino/help/gettingStarted', 'Getting Started'),
|
||||||
category: 'Arduino'
|
category: 'Arduino',
|
||||||
};
|
};
|
||||||
export const ENVIRONMENT: Command = {
|
export const ENVIRONMENT: Command = {
|
||||||
id: 'arduino-environment',
|
id: 'arduino-environment',
|
||||||
label: 'Environment',
|
label: nls.localize('arduino/help/environment', 'Environment'),
|
||||||
category: 'Arduino'
|
category: 'Arduino',
|
||||||
};
|
};
|
||||||
export const TROUBLESHOOTING: Command = {
|
export const TROUBLESHOOTING: Command = {
|
||||||
id: 'arduino-troubleshooting',
|
id: 'arduino-troubleshooting',
|
||||||
label: 'Troubleshooting',
|
label: nls.localize('arduino/help/troubleshooting', 'Troubleshooting'),
|
||||||
category: 'Arduino'
|
category: 'Arduino',
|
||||||
};
|
};
|
||||||
export const REFERENCE: Command = {
|
export const REFERENCE: Command = {
|
||||||
id: 'arduino-reference',
|
id: 'arduino-reference',
|
||||||
label: 'Reference',
|
label: nls.localize('arduino/help/reference', 'Reference'),
|
||||||
category: 'Arduino'
|
category: 'Arduino',
|
||||||
};
|
};
|
||||||
export const FIND_IN_REFERENCE: Command = {
|
export const FIND_IN_REFERENCE: Command = {
|
||||||
id: 'arduino-find-in-reference',
|
id: 'arduino-find-in-reference',
|
||||||
label: 'Find in Reference',
|
label: nls.localize('arduino/help/findInReference', 'Find in Reference'),
|
||||||
category: 'Arduino'
|
category: 'Arduino',
|
||||||
};
|
};
|
||||||
export const FAQ: Command = {
|
export const FAQ: Command = {
|
||||||
id: 'arduino-faq',
|
id: 'arduino-faq',
|
||||||
label: 'Frequently Asked Questions',
|
label: nls.localize('arduino/help/faq', 'Frequently Asked Questions'),
|
||||||
category: 'Arduino'
|
category: 'Arduino',
|
||||||
};
|
};
|
||||||
export const VISIT_ARDUINO: Command = {
|
export const VISIT_ARDUINO: Command = {
|
||||||
id: 'arduino-visit-arduino',
|
id: 'arduino-visit-arduino',
|
||||||
label: 'Visit Arduino.cc',
|
label: nls.localize('arduino/help/visit', 'Visit Arduino.cc'),
|
||||||
category: 'Arduino'
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
export const PRIVACY_POLICY: Command = {
|
||||||
|
id: 'arduino-privacy-policy',
|
||||||
|
label: nls.localize('arduino/help/privacyPolicy', 'Privacy Policy'),
|
||||||
|
category: 'Arduino',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import * as PQueue from 'p-queue';
|
import * as PQueue from 'p-queue';
|
||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||||
import { EditorManager } from '@theia/editor/lib/browser';
|
import { EditorManager } from '@theia/editor/lib/browser';
|
||||||
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
|
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
|
||||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
import {
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||||
import { LibraryPackage, LibraryService } from '../../common/protocol';
|
import { LibraryPackage, LibraryService } from '../../common/protocol';
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
@@ -12,10 +15,12 @@ import { LibraryListWidget } from '../library/library-list-widget';
|
|||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||||
import { NotificationCenter } from '../notification-center';
|
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';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class IncludeLibrary extends SketchContribution {
|
export class IncludeLibrary extends SketchContribution {
|
||||||
|
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
protected readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@@ -26,7 +31,7 @@ export class IncludeLibrary extends SketchContribution {
|
|||||||
protected readonly mainMenuManager: MainMenuManager;
|
protected readonly mainMenuManager: MainMenuManager;
|
||||||
|
|
||||||
@inject(EditorManager)
|
@inject(EditorManager)
|
||||||
protected readonly editorManager: EditorManager;
|
protected override readonly editorManager: EditorManager;
|
||||||
|
|
||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
protected readonly notificationCenter: NotificationCenter;
|
||||||
@@ -40,31 +45,50 @@ export class IncludeLibrary extends SketchContribution {
|
|||||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||||
protected readonly toDispose = new DisposableCollection();
|
protected readonly toDispose = new DisposableCollection();
|
||||||
|
|
||||||
onStart(): void {
|
override onStart(): void {
|
||||||
this.updateMenuActions();
|
this.boardsServiceClient.onBoardsConfigChanged(() =>
|
||||||
this.boardsServiceClient.onBoardsConfigChanged(() => this.updateMenuActions())
|
this.updateMenuActions()
|
||||||
this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions());
|
);
|
||||||
this.notificationCenter.onLibraryUninstalled(() => this.updateMenuActions());
|
this.notificationCenter.onLibraryDidInstall(() => this.updateMenuActions());
|
||||||
|
this.notificationCenter.onLibraryDidUninstall(() =>
|
||||||
|
this.updateMenuActions()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override async onReady(): Promise<void> {
|
||||||
|
this.updateMenuActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
// `Include Library` submenu
|
// `Include Library` submenu
|
||||||
const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include'];
|
const includeLibMenuPath = [
|
||||||
registry.registerSubmenu(includeLibMenuPath, 'Include Library', { order: '1' });
|
...ArduinoMenus.SKETCH__UTILS_GROUP,
|
||||||
|
'0_include',
|
||||||
|
];
|
||||||
|
registry.registerSubmenu(
|
||||||
|
includeLibMenuPath,
|
||||||
|
nls.localize('arduino/library/include', 'Include Library'),
|
||||||
|
{
|
||||||
|
order: '1',
|
||||||
|
}
|
||||||
|
);
|
||||||
// `Manage Libraries...` group.
|
// `Manage Libraries...` group.
|
||||||
registry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
|
registry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
|
||||||
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
|
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
|
||||||
label: 'Manage Libraries...'
|
label: nls.localize(
|
||||||
|
'arduino/library/manageLibraries',
|
||||||
|
'Manage Libraries...'
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
|
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
|
||||||
execute: async arg => {
|
execute: async (arg) => {
|
||||||
if (LibraryPackage.is(arg)) {
|
if (LibraryPackage.is(arg)) {
|
||||||
this.includeLibrary(arg);
|
this.includeLibrary(arg);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,13 +96,16 @@ export class IncludeLibrary extends SketchContribution {
|
|||||||
return this.queue.add(async () => {
|
return this.queue.add(async () => {
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
this.mainMenuManager.update();
|
this.mainMenuManager.update();
|
||||||
const libraries: LibraryPackage[] = []
|
const libraries: LibraryPackage[] = [];
|
||||||
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
|
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
|
||||||
// Show all libraries, when no board is selected.
|
// Show all libraries, when no board is selected.
|
||||||
// Otherwise, show libraries only for the selected board.
|
// Otherwise, show libraries only for the selected board.
|
||||||
libraries.push(...await this.libraryService.list({ fqbn }));
|
libraries.push(...(await this.libraryService.list({ fqbn })));
|
||||||
|
|
||||||
const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include'];
|
const includeLibMenuPath = [
|
||||||
|
...ArduinoMenus.SKETCH__UTILS_GROUP,
|
||||||
|
'0_include',
|
||||||
|
];
|
||||||
// `Add .ZIP Library...`
|
// `Add .ZIP Library...`
|
||||||
// TODO: implement it
|
// TODO: implement it
|
||||||
|
|
||||||
@@ -87,10 +114,17 @@ export class IncludeLibrary extends SketchContribution {
|
|||||||
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
|
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
|
||||||
const { user, rest } = LibraryPackage.groupByLocation(libraries);
|
const { user, rest } = LibraryPackage.groupByLocation(libraries);
|
||||||
if (rest.length) {
|
if (rest.length) {
|
||||||
(rest as any).unshift('Arduino libraries');
|
(rest as any).unshift(
|
||||||
|
nls.localize('arduino/library/arduinoLibraries', 'Arduino libraries')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (user.length) {
|
if (user.length) {
|
||||||
(user as any).unshift('Contributed libraries');
|
(user as any).unshift(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/library/contributedLibraries',
|
||||||
|
'Contributed libraries'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const library of user) {
|
for (const library of user) {
|
||||||
@@ -104,26 +138,42 @@ export class IncludeLibrary extends SketchContribution {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerLibrary(libraryOrPlaceholder: LibraryPackage | string, menuPath: MenuPath): Disposable {
|
protected registerLibrary(
|
||||||
|
libraryOrPlaceholder: LibraryPackage | string,
|
||||||
|
menuPath: MenuPath
|
||||||
|
): Disposable {
|
||||||
if (typeof libraryOrPlaceholder === 'string') {
|
if (typeof libraryOrPlaceholder === 'string') {
|
||||||
const placeholder = new PlaceholderMenuNode(menuPath, libraryOrPlaceholder);
|
const placeholder = new PlaceholderMenuNode(
|
||||||
|
menuPath,
|
||||||
|
libraryOrPlaceholder
|
||||||
|
);
|
||||||
this.menuRegistry.registerMenuNode(menuPath, placeholder);
|
this.menuRegistry.registerMenuNode(menuPath, placeholder);
|
||||||
return Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id));
|
return Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuNode(placeholder.id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
|
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
|
||||||
const command = { id: commandId };
|
const command = { id: commandId };
|
||||||
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, libraryOrPlaceholder) };
|
const handler = {
|
||||||
|
execute: () =>
|
||||||
|
this.commandRegistry.executeCommand(
|
||||||
|
IncludeLibrary.Commands.INCLUDE_LIBRARY.id,
|
||||||
|
libraryOrPlaceholder
|
||||||
|
),
|
||||||
|
};
|
||||||
const menuAction = { commandId, label: libraryOrPlaceholder.name };
|
const menuAction = { commandId, label: libraryOrPlaceholder.name };
|
||||||
this.menuRegistry.registerMenuAction(menuPath, menuAction);
|
this.menuRegistry.registerMenuAction(menuPath, menuAction);
|
||||||
return new DisposableCollection(
|
return new DisposableCollection(
|
||||||
this.commandRegistry.registerCommand(command, handler),
|
this.commandRegistry.registerCommand(command, handler),
|
||||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(menuAction)),
|
Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuAction(menuAction)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async includeLibrary(library: LibraryPackage): Promise<void> {
|
protected async includeLibrary(library: LibraryPackage): Promise<void> {
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
if (!sketch) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If the current editor is one of the additional files from the sketch, we use that.
|
// If the current editor is one of the additional files from the sketch, we use that.
|
||||||
@@ -131,7 +181,9 @@ export class IncludeLibrary extends SketchContribution {
|
|||||||
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
|
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||||
const editor = this.editorManager.currentEditor?.editor;
|
const editor = this.editorManager.currentEditor?.editor;
|
||||||
if (editor instanceof MonacoEditor) {
|
if (editor instanceof MonacoEditor) {
|
||||||
if (sketch.additionalFileUris.some(uri => uri === editor.uri.toString())) {
|
if (
|
||||||
|
sketch.additionalFileUris.some((uri) => uri === editor.uri.toString())
|
||||||
|
) {
|
||||||
codeEditor = editor.getControl();
|
codeEditor = editor.getControl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,22 +207,29 @@ export class IncludeLibrary extends SketchContribution {
|
|||||||
const eol = textModel.getEOL();
|
const eol = textModel.getEOL();
|
||||||
const includes = library.includes.slice();
|
const includes = library.includes.slice();
|
||||||
includes.push(''); // For the trailing new line.
|
includes.push(''); // For the trailing new line.
|
||||||
const text = includes.map(include => include ? `#include <${include}>` : eol).join(eol);
|
const text = includes
|
||||||
|
.map((include) => (include ? `#include <${include}>` : eol))
|
||||||
|
.join(eol);
|
||||||
textModel.pushStackElement(); // Start a fresh operation.
|
textModel.pushStackElement(); // Start a fresh operation.
|
||||||
textModel.pushEditOperations(cursorState, [{
|
textModel.pushEditOperations(
|
||||||
|
cursorState,
|
||||||
|
[
|
||||||
|
{
|
||||||
range: new monaco.Range(1, 1, 1, 1),
|
range: new monaco.Range(1, 1, 1, 1),
|
||||||
text,
|
text,
|
||||||
forceMoveMarkers: true
|
forceMoveMarkers: true,
|
||||||
}], () => cursorState);
|
},
|
||||||
|
],
|
||||||
|
() => cursorState
|
||||||
|
);
|
||||||
textModel.pushStackElement(); // Make it undoable.
|
textModel.pushStackElement(); // Make it undoable.
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace IncludeLibrary {
|
export namespace IncludeLibrary {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const INCLUDE_LIBRARY: Command = {
|
export const INCLUDE_LIBRARY: Command = {
|
||||||
id: 'arduino-include-library'
|
id: 'arduino-include-library',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||||
|
import { ProgressService } from '@theia/core/lib/common/progress-service';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { ProgressMessage } from '../../common/protocol';
|
||||||
|
import { NotificationCenter } from '../notification-center';
|
||||||
|
import { Contribution } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class IndexesUpdateProgress extends Contribution {
|
||||||
|
@inject(NotificationCenter)
|
||||||
|
private readonly notificationCenter: NotificationCenter;
|
||||||
|
@inject(ProgressService)
|
||||||
|
private readonly progressService: ProgressService;
|
||||||
|
private currentProgress:
|
||||||
|
| (Progress & Readonly<{ progressId: string }>)
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
override onStart(): void {
|
||||||
|
this.notificationCenter.onIndexWillUpdate((progressId) =>
|
||||||
|
this.getOrCreateProgress(progressId)
|
||||||
|
);
|
||||||
|
this.notificationCenter.onIndexUpdateDidProgress((progress) => {
|
||||||
|
this.getOrCreateProgress(progress).then((delegate) =>
|
||||||
|
delegate.report(progress)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.notificationCenter.onIndexDidUpdate((progressId) => {
|
||||||
|
this.cancelProgress(progressId);
|
||||||
|
});
|
||||||
|
this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => {
|
||||||
|
this.cancelProgress(progressId);
|
||||||
|
this.messageService.error(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrCreateProgress(
|
||||||
|
progressOrId: ProgressMessage | string
|
||||||
|
): Promise<Progress & { progressId: string }> {
|
||||||
|
const progressId = ProgressMessage.is(progressOrId)
|
||||||
|
? progressOrId.progressId
|
||||||
|
: progressOrId;
|
||||||
|
if (this.currentProgress?.progressId === progressId) {
|
||||||
|
return this.currentProgress;
|
||||||
|
}
|
||||||
|
if (this.currentProgress) {
|
||||||
|
this.currentProgress.cancel();
|
||||||
|
}
|
||||||
|
this.currentProgress = undefined;
|
||||||
|
const progress = await this.progressService.showProgress({
|
||||||
|
text: '',
|
||||||
|
options: { location: 'notification' },
|
||||||
|
});
|
||||||
|
if (ProgressMessage.is(progressOrId)) {
|
||||||
|
progress.report(progressOrId);
|
||||||
|
}
|
||||||
|
this.currentProgress = { ...progress, progressId };
|
||||||
|
return this.currentProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private cancelProgress(progressId: string) {
|
||||||
|
if (this.currentProgress) {
|
||||||
|
if (this.currentProgress.progressId !== progressId) {
|
||||||
|
console.warn(
|
||||||
|
`Mismatching progress IDs. Expected ${progressId}, got ${this.currentProgress.progressId}. Canceling anyway.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.currentProgress.cancel();
|
||||||
|
this.currentProgress = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
159
arduino-ide-extension/src/browser/contributions/ino-language.ts
Normal file
159
arduino-ide-extension/src/browser/contributions/ino-language.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { Mutex } from 'async-mutex';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
ArduinoDaemon,
|
||||||
|
BoardsService,
|
||||||
|
ExecutableService,
|
||||||
|
} from '../../common/protocol';
|
||||||
|
import { HostedPluginEvents } from '../hosted-plugin-events';
|
||||||
|
import { SketchContribution, URI } from './contribution';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
import { BoardsConfig } from '../boards/boards-config';
|
||||||
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class InoLanguage extends SketchContribution {
|
||||||
|
@inject(HostedPluginEvents)
|
||||||
|
private readonly hostedPluginEvents: HostedPluginEvents;
|
||||||
|
|
||||||
|
@inject(ExecutableService)
|
||||||
|
private readonly executableService: ExecutableService;
|
||||||
|
|
||||||
|
@inject(ArduinoDaemon)
|
||||||
|
private readonly daemon: ArduinoDaemon;
|
||||||
|
|
||||||
|
@inject(BoardsService)
|
||||||
|
private readonly boardsService: BoardsService;
|
||||||
|
|
||||||
|
@inject(BoardsServiceProvider)
|
||||||
|
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
|
|
||||||
|
private languageServerFqbn?: string;
|
||||||
|
private languageServerStartMutex = new Mutex();
|
||||||
|
|
||||||
|
override onReady(): void {
|
||||||
|
const start = (
|
||||||
|
{ selectedBoard }: BoardsConfig.Config,
|
||||||
|
forceStart = false
|
||||||
|
) => {
|
||||||
|
if (selectedBoard) {
|
||||||
|
const { name, fqbn } = selectedBoard;
|
||||||
|
if (fqbn) {
|
||||||
|
this.startLanguageServer(fqbn, name, forceStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
start(this.boardsServiceProvider.boardsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startLanguageServer(
|
||||||
|
fqbn: string,
|
||||||
|
name: string | undefined,
|
||||||
|
forceStart = false
|
||||||
|
): Promise<void> {
|
||||||
|
const port = await this.daemon.tryGetPort();
|
||||||
|
if (!port) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const release = await this.languageServerStartMutex.acquire();
|
||||||
|
try {
|
||||||
|
await this.hostedPluginEvents.didStart;
|
||||||
|
const details = await this.boardsService.getBoardDetails({ fqbn });
|
||||||
|
if (!details) {
|
||||||
|
// Core is not installed for the selected board.
|
||||||
|
console.info(
|
||||||
|
`Could not start language server for ${fqbn}. The core is not installed for the board.`
|
||||||
|
);
|
||||||
|
if (this.languageServerFqbn) {
|
||||||
|
try {
|
||||||
|
await this.commandService.executeCommand(
|
||||||
|
'arduino.languageserver.stop'
|
||||||
|
);
|
||||||
|
console.info(
|
||||||
|
`Stopped language server process for ${this.languageServerFqbn}.`
|
||||||
|
);
|
||||||
|
this.languageServerFqbn = undefined;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Failed to start language server process for ${this.languageServerFqbn}`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!forceStart && fqbn === this.languageServerFqbn) {
|
||||||
|
// NOOP
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger.info(`Starting language server: ${fqbn}`);
|
||||||
|
const log = this.preferences.get('arduino.language.log');
|
||||||
|
const realTimeDiagnostics = this.preferences.get(
|
||||||
|
'arduino.language.realTimeDiagnostics'
|
||||||
|
);
|
||||||
|
let currentSketchPath: string | undefined = undefined;
|
||||||
|
if (log) {
|
||||||
|
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||||
|
if (CurrentSketch.isValid(currentSketch)) {
|
||||||
|
currentSketchPath = await this.fileService.fsPath(
|
||||||
|
new URI(currentSketch.uri)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { clangdUri, lsUri } = await this.executableService.list();
|
||||||
|
const [clangdPath, lsPath] = await Promise.all([
|
||||||
|
this.fileService.fsPath(new URI(clangdUri)),
|
||||||
|
this.fileService.fsPath(new URI(lsUri)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.languageServerFqbn = await Promise.race([
|
||||||
|
new Promise<undefined>((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error(`Timeout after ${20_000} ms.`)),
|
||||||
|
20_000
|
||||||
|
)
|
||||||
|
),
|
||||||
|
this.commandService.executeCommand<string>(
|
||||||
|
'arduino.languageserver.start',
|
||||||
|
{
|
||||||
|
lsPath,
|
||||||
|
cliDaemonAddr: `localhost:${port}`,
|
||||||
|
clangdPath,
|
||||||
|
log: currentSketchPath ? currentSketchPath : log,
|
||||||
|
cliDaemonInstance: '1',
|
||||||
|
board: {
|
||||||
|
fqbn,
|
||||||
|
name: name ? `"${name}"` : undefined,
|
||||||
|
},
|
||||||
|
realTimeDiagnostics,
|
||||||
|
silentOutput: true,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Failed to start language server for ${fqbn}`, e);
|
||||||
|
this.languageServerFqbn = undefined;
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,41 @@
|
|||||||
import { injectable } from 'inversify';
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||||
import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
URI,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class NewSketch extends SketchContribution {
|
export class NewSketch extends SketchContribution {
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registerCommands(registry: CommandRegistry): void {
|
|
||||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
||||||
execute: () => this.newSketch()
|
execute: () => this.newSketch(),
|
||||||
});
|
});
|
||||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
|
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
|
||||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
isVisible: (widget) =>
|
||||||
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id)
|
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||||
|
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
||||||
label: 'New',
|
label: nls.localize('arduino/sketch/new', 'New'),
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: NewSketch.Commands.NEW_SKETCH.id,
|
command: NewSketch.Commands.NEW_SKETCH.id,
|
||||||
keybinding: 'CtrlCmd+N'
|
keybinding: 'CtrlCmd+N',
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
|
||||||
registry.registerItem({
|
|
||||||
id: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
|
|
||||||
command: NewSketch.Commands.NEW_SKETCH__TOOLBAR.id,
|
|
||||||
tooltip: 'New',
|
|
||||||
priority: 3
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,16 +47,15 @@ export class NewSketch extends SketchContribution {
|
|||||||
await this.messageService.error(e.toString());
|
await this.messageService.error(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace NewSketch {
|
export namespace NewSketch {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const NEW_SKETCH: Command = {
|
export const NEW_SKETCH: Command = {
|
||||||
id: 'arduino-new-sketch'
|
id: 'arduino-new-sketch',
|
||||||
};
|
};
|
||||||
export const NEW_SKETCH__TOOLBAR: Command = {
|
export const NEW_SKETCH__TOOLBAR: Command = {
|
||||||
id: 'arduino-new-sketch--toolbar'
|
id: 'arduino-new-sketch--toolbar',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { CommandRegistry } from '@theia/core';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { BoardsConfigDialog } from '../boards/boards-config-dialog';
|
||||||
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
|
import { Contribution, Command } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class OpenBoardsConfig extends Contribution {
|
||||||
|
@inject(BoardsServiceProvider)
|
||||||
|
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
|
|
||||||
|
@inject(BoardsConfigDialog)
|
||||||
|
private readonly boardsConfigDialog: BoardsConfigDialog;
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, {
|
||||||
|
execute: async (query?: string | undefined) => {
|
||||||
|
const boardsConfig = await this.boardsConfigDialog.open(query);
|
||||||
|
if (boardsConfig) {
|
||||||
|
return (this.boardsServiceProvider.boardsConfig = boardsConfig);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace OpenBoardsConfig {
|
||||||
|
export namespace Commands {
|
||||||
|
export const OPEN_DIALOG: Command = {
|
||||||
|
id: 'arduino-open-boards-dialog',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
|
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
|
||||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
import {
|
||||||
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
Sketch,
|
||||||
|
} from './contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
import { OpenSketch } from './open-sketch';
|
import { OpenSketch } from './open-sketch';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenRecentSketch extends SketchContribution {
|
export class OpenRecentSketch extends SketchContribution {
|
||||||
|
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
protected readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@@ -27,21 +35,33 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
|
|
||||||
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
||||||
|
|
||||||
onStart(): void {
|
override onStart(): void {
|
||||||
const refreshMenu = (sketches: Sketch[]) => {
|
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||||
this.register(sketches);
|
this.refreshMenu(sketches)
|
||||||
this.mainMenuManager.update();
|
);
|
||||||
};
|
|
||||||
this.notificationCenter.onRecentSketchesChanged(({ sketches }) => refreshMenu(sketches));
|
|
||||||
this.sketchService.recentlyOpenedSketches().then(refreshMenu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override async onReady(): Promise<void> {
|
||||||
registry.registerSubmenu(ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, 'Open Recent', { order: '2' });
|
this.sketchService
|
||||||
|
.recentlyOpenedSketches()
|
||||||
|
.then((sketches) => this.refreshMenu(sketches));
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
registry.registerSubmenu(
|
||||||
|
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
||||||
|
nls.localize('arduino/sketch/openRecent', 'Open Recent'),
|
||||||
|
{ order: '2' }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshMenu(sketches: Sketch[]): void {
|
||||||
|
this.register(sketches);
|
||||||
|
this.mainMenuManager.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected register(sketches: Sketch[]): void {
|
protected register(sketches: Sketch[]): void {
|
||||||
let order = 0;
|
const order = 0;
|
||||||
for (const sketch of sketches) {
|
for (const sketch of sketches) {
|
||||||
const { uri } = sketch;
|
const { uri } = sketch;
|
||||||
const toDispose = this.toDisposeBeforeRegister.get(uri);
|
const toDispose = this.toDisposeBeforeRegister.get(uri);
|
||||||
@@ -49,14 +69,33 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
toDispose.dispose();
|
toDispose.dispose();
|
||||||
}
|
}
|
||||||
const command = { id: `arduino-open-recent--${uri}` };
|
const command = { id: `arduino-open-recent--${uri}` };
|
||||||
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
|
const handler = {
|
||||||
|
execute: () =>
|
||||||
|
this.commandRegistry.executeCommand(
|
||||||
|
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
|
sketch
|
||||||
|
),
|
||||||
|
};
|
||||||
this.commandRegistry.registerCommand(command, handler);
|
this.commandRegistry.registerCommand(command, handler);
|
||||||
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, { commandId: command.id, label: sketch.name, order: String(order) });
|
this.menuRegistry.registerMenuAction(
|
||||||
this.toDisposeBeforeRegister.set(sketch.uri, new DisposableCollection(
|
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
||||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
|
{
|
||||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
|
commandId: command.id,
|
||||||
));
|
label: sketch.name,
|
||||||
|
order: String(order),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.toDisposeBeforeRegister.set(
|
||||||
|
sketch.uri,
|
||||||
|
new DisposableCollection(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.commandRegistry.unregisterCommand(command)
|
||||||
|
),
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuAction(command)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,43 @@
|
|||||||
import { injectable } from 'inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution';
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenSketchExternal extends SketchContribution {
|
export class OpenSketchExternal extends SketchContribution {
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registerCommands(registry: CommandRegistry): void {
|
|
||||||
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
|
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
|
||||||
execute: () => this.openExternal()
|
execute: () => this.openExternal(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
||||||
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||||
label: 'Show Sketch Folder',
|
label: nls.localize('arduino/sketch/showFolder', 'Show Sketch Folder'),
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||||
keybinding: 'CtrlCmd+Alt+K'
|
keybinding: 'CtrlCmd+Alt+K',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async openExternal(): Promise<void> {
|
protected async openExternal(): Promise<void> {
|
||||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||||
if (uri) {
|
if (uri) {
|
||||||
const exists = this.fileService.exists(new URI(uri));
|
const exists = await this.fileService.exists(new URI(uri));
|
||||||
if (exists) {
|
if (exists) {
|
||||||
const fsPath = await this.fileService.fsPath(new URI(uri));
|
const fsPath = await this.fileService.fsPath(new URI(uri));
|
||||||
if (fsPath) {
|
if (fsPath) {
|
||||||
@@ -40,13 +46,12 @@ export class OpenSketchExternal extends SketchContribution {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace OpenSketchExternal {
|
export namespace OpenSketchExternal {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const OPEN_EXTERNAL: Command = {
|
export const OPEN_EXTERNAL: Command = {
|
||||||
id: 'arduino-open-sketch-external'
|
id: 'arduino-open-sketch-external',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
|
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
|
||||||
|
import { SketchesError } from '../../common/protocol';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
SketchContribution,
|
||||||
|
URI,
|
||||||
|
} from './contribution';
|
||||||
|
import { SaveAsSketch } from './save-as-sketch';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class OpenSketchFiles extends SketchContribution {
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
|
||||||
|
execute: (uri: URI) => this.openSketchFiles(uri),
|
||||||
|
});
|
||||||
|
registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, {
|
||||||
|
execute: (
|
||||||
|
uri: string,
|
||||||
|
forceOpen?: boolean,
|
||||||
|
options?: EditorOpenerOptions
|
||||||
|
) => {
|
||||||
|
this.ensureOpened(uri, forceOpen, options);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openSketchFiles(uri: URI): Promise<void> {
|
||||||
|
try {
|
||||||
|
const sketch = await this.sketchService.loadSketch(uri.toString());
|
||||||
|
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||||
|
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
|
||||||
|
await this.ensureOpened(uri);
|
||||||
|
}
|
||||||
|
if (mainFileUri.endsWith('.pde')) {
|
||||||
|
const message = nls.localize(
|
||||||
|
'arduino/common/oldFormat',
|
||||||
|
"The '{0}' still uses the old `.pde` format. Do you want to switch to the new `.ino` extension?",
|
||||||
|
sketch.name
|
||||||
|
);
|
||||||
|
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||||
|
this.messageService
|
||||||
|
.info(message, nls.localize('arduino/common/later', 'Later'), yes)
|
||||||
|
.then(async (answer) => {
|
||||||
|
if (answer === yes) {
|
||||||
|
this.commandService.executeCommand(
|
||||||
|
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||||
|
{
|
||||||
|
execOnlyIfTemp: false,
|
||||||
|
openAfterMove: true,
|
||||||
|
wipeOriginal: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
this.openFallbackSketch();
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
const message =
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: typeof err === 'string'
|
||||||
|
? err
|
||||||
|
: String(err);
|
||||||
|
this.messageService.error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openFallbackSketch(): Promise<void> {
|
||||||
|
const sketch = await this.sketchService.createNewSketch();
|
||||||
|
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureOpened(
|
||||||
|
uri: string,
|
||||||
|
forceOpen = false,
|
||||||
|
options?: EditorOpenerOptions
|
||||||
|
): Promise<unknown> {
|
||||||
|
const widget = this.editorManager.all.find(
|
||||||
|
(widget) => widget.editor.uri.toString() === uri
|
||||||
|
);
|
||||||
|
if (!widget || forceOpen) {
|
||||||
|
return this.editorManager.open(
|
||||||
|
new URI(uri),
|
||||||
|
options ?? {
|
||||||
|
mode: 'reveal',
|
||||||
|
preview: false,
|
||||||
|
counter: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace OpenSketchFiles {
|
||||||
|
export namespace Commands {
|
||||||
|
export const OPEN_SKETCH_FILES: Command = {
|
||||||
|
id: 'arduino-open-sketch-files',
|
||||||
|
};
|
||||||
|
export const ENSURE_OPENED: Command = {
|
||||||
|
id: 'arduino-ensure-opened',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,30 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||||
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
|
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
import {
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||||
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
Sketch,
|
||||||
|
URI,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
} from './contribution';
|
||||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||||
import { BuiltInExamples } from './examples';
|
import { BuiltInExamples } from './examples';
|
||||||
import { Sketchbook } from './sketchbook';
|
import { Sketchbook } from './sketchbook';
|
||||||
import { SketchContainer } from '../../common/protocol';
|
import { SketchContainer } from '../../common/protocol';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenSketch extends SketchContribution {
|
export class OpenSketch extends SketchContribution {
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
@inject(MenuModelRegistry)
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
protected readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
@@ -31,14 +42,18 @@ export class OpenSketch extends SketchContribution {
|
|||||||
|
|
||||||
protected readonly toDispose = new DisposableCollection();
|
protected readonly toDispose = new DisposableCollection();
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
||||||
execute: arg => Sketch.is(arg) ? this.openSketch(arg) : this.openSketch()
|
execute: (arg) =>
|
||||||
|
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
|
||||||
});
|
});
|
||||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
||||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
isVisible: (widget) =>
|
||||||
|
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||||
execute: async (_: Widget, target: EventTarget) => {
|
execute: async (_: Widget, target: EventTarget) => {
|
||||||
const container = await this.sketchService.getSketches({ exclude: ['**/hardware/**'] });
|
const container = await this.sketchService.getSketches({
|
||||||
|
exclude: ['**/hardware/**'],
|
||||||
|
});
|
||||||
if (SketchContainer.isEmpty(container)) {
|
if (SketchContainer.isEmpty(container)) {
|
||||||
this.openSketch();
|
this.openSketch();
|
||||||
} else {
|
} else {
|
||||||
@@ -51,16 +66,36 @@ export class OpenSketch extends SketchContribution {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP, {
|
this.menuRegistry.registerMenuAction(
|
||||||
|
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
|
||||||
|
{
|
||||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
label: 'Open...'
|
label: nls.localize(
|
||||||
});
|
'vscode/workspaceActions/openFileFolder',
|
||||||
this.toDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
|
'Open...'
|
||||||
this.sketchbook.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, this.toDispose);
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.toDispose.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuAction(
|
||||||
|
OpenSketch.Commands.OPEN_SKETCH
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.sketchbook.registerRecursively(
|
||||||
|
[...container.children, ...container.sketches],
|
||||||
|
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
|
||||||
|
this.toDispose
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const containers = await this.examplesService.builtIns();
|
const containers = await this.examplesService.builtIns();
|
||||||
for (const container of containers) {
|
for (const container of containers) {
|
||||||
this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDispose);
|
this.builtInExamples.registerRecursively(
|
||||||
|
container,
|
||||||
|
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
|
||||||
|
this.toDispose
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error when collecting built-in examples.', e);
|
console.error('Error when collecting built-in examples.', e);
|
||||||
@@ -69,40 +104,35 @@ export class OpenSketch extends SketchContribution {
|
|||||||
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
||||||
anchor: {
|
anchor: {
|
||||||
x: parentElement.getBoundingClientRect().left,
|
x: parentElement.getBoundingClientRect().left,
|
||||||
y: parentElement.getBoundingClientRect().top + parentElement.offsetHeight
|
y:
|
||||||
}
|
parentElement.getBoundingClientRect().top +
|
||||||
}
|
parentElement.offsetHeight,
|
||||||
|
},
|
||||||
|
};
|
||||||
this.contextMenuRenderer.render(options);
|
this.contextMenuRenderer.render(options);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
label: 'Open...',
|
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
|
||||||
order: '1'
|
order: '1',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: OpenSketch.Commands.OPEN_SKETCH.id,
|
command: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
keybinding: 'CtrlCmd+O'
|
keybinding: 'CtrlCmd+O',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
async openSketch(
|
||||||
registry.registerItem({
|
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
|
||||||
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
): Promise<void> {
|
||||||
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
|
||||||
tooltip: 'Open',
|
|
||||||
priority: 4
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async openSketch(toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()): Promise<void> {
|
|
||||||
const sketch = await toOpen;
|
const sketch = await toOpen;
|
||||||
if (sketch) {
|
if (sketch) {
|
||||||
this.workspaceService.open(new URI(sketch.uri));
|
this.workspaceService.open(new URI(sketch.uri));
|
||||||
@@ -111,22 +141,26 @@ export class OpenSketch extends SketchContribution {
|
|||||||
|
|
||||||
protected async selectSketch(): Promise<Sketch | undefined> {
|
protected async selectSketch(): Promise<Sketch | undefined> {
|
||||||
const config = await this.configService.getConfiguration();
|
const config = await this.configService.getConfiguration();
|
||||||
const defaultPath = await this.fileService.fsPath(new URI(config.sketchDirUri));
|
const defaultPath = await this.fileService.fsPath(
|
||||||
|
new URI(config.sketchDirUri)
|
||||||
|
);
|
||||||
const { filePaths } = await remote.dialog.showOpenDialog({
|
const { filePaths } = await remote.dialog.showOpenDialog({
|
||||||
defaultPath,
|
defaultPath,
|
||||||
properties: ['createDirectory', 'openFile'],
|
properties: ['createDirectory', 'openFile'],
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'Sketch',
|
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
|
||||||
extensions: ['ino', 'pde']
|
extensions: ['ino', 'pde'],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
if (!filePaths.length) {
|
if (!filePaths.length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (filePaths.length > 1) {
|
if (filePaths.length > 1) {
|
||||||
this.logger.warn(`Multiple sketches were selected: ${filePaths}. Using the first one.`);
|
this.logger.warn(
|
||||||
|
`Multiple sketches were selected: ${filePaths}. Using the first one.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const sketchFilePath = filePaths[0];
|
const sketchFilePath = filePaths[0];
|
||||||
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
|
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
|
||||||
@@ -138,38 +172,53 @@ export class OpenSketch extends SketchContribution {
|
|||||||
const name = new URI(sketchFileUri).path.name;
|
const name = new URI(sketchFileUri).path.name;
|
||||||
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
|
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
|
||||||
const { response } = await remote.dialog.showMessageBox({
|
const { response } = await remote.dialog.showMessageBox({
|
||||||
title: 'Moving',
|
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
||||||
type: 'question',
|
type: 'question',
|
||||||
buttons: ['Cancel', 'OK'],
|
buttons: [
|
||||||
message: `The file "${nameWithExt}" needs to be inside a sketch folder named as "${name}".\nCreate this folder, move the file, and continue?`
|
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||||
|
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||||
|
],
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/sketch/movingMsg',
|
||||||
|
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
||||||
|
nameWithExt,
|
||||||
|
name
|
||||||
|
),
|
||||||
});
|
});
|
||||||
if (response === 1) { // OK
|
if (response === 1) {
|
||||||
|
// OK
|
||||||
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
|
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
|
||||||
const exists = await this.fileService.exists(newSketchUri);
|
const exists = await this.fileService.exists(newSketchUri);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
await remote.dialog.showMessageBox({
|
await remote.dialog.showMessageBox({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Error',
|
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
||||||
message: `A folder named "${name}" already exists. Can't open sketch.`
|
message: nls.localize(
|
||||||
|
'arduino/sketch/cantOpen',
|
||||||
|
'A folder named "{0}" already exists. Can\'t open sketch.',
|
||||||
|
name
|
||||||
|
),
|
||||||
});
|
});
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
await this.fileService.createFolder(newSketchUri);
|
await this.fileService.createFolder(newSketchUri);
|
||||||
await this.fileService.move(new URI(sketchFileUri), new URI(newSketchUri.resolve(nameWithExt).toString()));
|
await this.fileService.move(
|
||||||
|
new URI(sketchFileUri),
|
||||||
|
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||||
|
);
|
||||||
return this.sketchService.getSketchFolder(newSketchUri.toString());
|
return this.sketchService.getSketchFolder(newSketchUri.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace OpenSketch {
|
export namespace OpenSketch {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const OPEN_SKETCH: Command = {
|
export const OPEN_SKETCH: Command = {
|
||||||
id: 'arduino-open-sketch'
|
id: 'arduino-open-sketch',
|
||||||
};
|
};
|
||||||
export const OPEN_SKETCH__TOOLBAR: Command = {
|
export const OPEN_SKETCH__TOOLBAR: Command = {
|
||||||
id: 'arduino-open-sketch--toolbar'
|
id: 'arduino-open-sketch--toolbar',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,51 @@
|
|||||||
import { injectable } from 'inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { isOSX } from '@theia/core/lib/common/os';
|
import { isOSX } from '@theia/core/lib/common/os';
|
||||||
import { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
|
import {
|
||||||
|
Contribution,
|
||||||
|
Command,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
CommandRegistry,
|
||||||
|
} from './contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class QuitApp extends Contribution {
|
export class QuitApp extends Contribution {
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registerCommands(registry: CommandRegistry): void {
|
|
||||||
if (!isOSX) {
|
if (!isOSX) {
|
||||||
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
|
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
|
||||||
execute: () => remote.app.quit()
|
execute: () => remote.app.quit(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
|
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
|
||||||
if (!isOSX) {
|
if (!isOSX) {
|
||||||
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
|
||||||
commandId: QuitApp.Commands.QUIT_APP.id,
|
commandId: QuitApp.Commands.QUIT_APP.id,
|
||||||
label: 'Quit',
|
label: nls.localize('vscode/bulkEditService/quit', 'Quit'),
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
if (!isOSX) {
|
if (!isOSX) {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: QuitApp.Commands.QUIT_APP.id,
|
command: QuitApp.Commands.QUIT_APP.id,
|
||||||
keybinding: 'CtrlCmd+Q'
|
keybinding: 'CtrlCmd+Q',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace QuitApp {
|
export namespace QuitApp {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const QUIT_APP: Command = {
|
export const QUIT_APP: Command = {
|
||||||
id: 'arduino-quit-app'
|
id: 'arduino-quit-app',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,66 @@
|
|||||||
import { injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import * as dateFormat from 'dateformat';
|
import * as dateFormat from 'dateformat';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution';
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
URI,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
|
||||||
|
import { EditorManager } from '@theia/editor/lib/browser';
|
||||||
|
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SaveAsSketch extends SketchContribution {
|
export class SaveAsSketch extends SketchContribution {
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
@inject(ApplicationShell)
|
||||||
|
protected readonly applicationShell: ApplicationShell;
|
||||||
|
|
||||||
|
@inject(EditorManager)
|
||||||
|
protected override readonly editorManager: EditorManager;
|
||||||
|
|
||||||
|
@inject(WindowService)
|
||||||
|
protected readonly windowService: WindowService;
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
|
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
|
||||||
execute: args => this.saveAs(args)
|
execute: (args) => this.saveAs(args),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||||
label: 'Save As...',
|
label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
|
||||||
order: '7'
|
order: '7',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||||
keybinding: 'CtrlCmd+Shift+S'
|
keybinding: 'CtrlCmd+Shift+S',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves `true` if the sketch was successfully saved as something.
|
* Resolves `true` if the sketch was successfully saved as something.
|
||||||
*/
|
*/
|
||||||
async saveAs({ execOnlyIfTemp, openAfterMove, wipeOriginal }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT): Promise<boolean> {
|
async saveAs(
|
||||||
|
{
|
||||||
|
execOnlyIfTemp,
|
||||||
|
openAfterMove,
|
||||||
|
wipeOriginal,
|
||||||
|
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||||
|
): Promise<boolean> {
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
if (!sketch) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,13 +71,25 @@ export class SaveAsSketch extends SketchContribution {
|
|||||||
|
|
||||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||||
const sketchDirUri = new URI((await this.configService.getConfiguration()).sketchDirUri);
|
const sketchDirUri = new URI(
|
||||||
const exists = await this.fileService.exists(sketchDirUri.resolve(sketch.name));
|
(await this.configService.getConfiguration()).sketchDirUri
|
||||||
const defaultUri = exists
|
);
|
||||||
? sketchDirUri.resolve(sketchDirUri.resolve(`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`).toString())
|
const exists = await this.fileService.exists(
|
||||||
: sketchDirUri.resolve(sketch.name);
|
sketchDirUri.resolve(sketch.name)
|
||||||
|
);
|
||||||
|
const defaultUri = sketchDirUri.resolve(
|
||||||
|
exists
|
||||||
|
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
|
||||||
|
: sketch.name
|
||||||
|
);
|
||||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath });
|
const { filePath, canceled } = await remote.dialog.showSaveDialog({
|
||||||
|
title: nls.localize(
|
||||||
|
'arduino/sketch/saveFolderAs',
|
||||||
|
'Save sketch folder as...'
|
||||||
|
),
|
||||||
|
defaultPath,
|
||||||
|
});
|
||||||
if (!filePath || canceled) {
|
if (!filePath || canceled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -58,24 +97,70 @@ export class SaveAsSketch extends SketchContribution {
|
|||||||
if (!destinationUri) {
|
if (!destinationUri) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const workspaceUri = await this.sketchService.copy(sketch, { destinationUri });
|
const workspaceUri = await this.sketchService.copy(sketch, {
|
||||||
|
destinationUri,
|
||||||
|
});
|
||||||
|
if (workspaceUri) {
|
||||||
|
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
|
||||||
|
}
|
||||||
if (workspaceUri && openAfterMove) {
|
if (workspaceUri && openAfterMove) {
|
||||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||||
try {
|
try {
|
||||||
await this.fileService.delete(new URI(sketch.uri), { recursive: true });
|
await this.fileService.delete(new URI(sketch.uri), {
|
||||||
} catch { /* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */ }
|
recursive: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
|
||||||
}
|
}
|
||||||
this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true });
|
}
|
||||||
|
this.windowService.setSafeToShutDown();
|
||||||
|
this.workspaceService.open(new URI(workspaceUri), {
|
||||||
|
preserveWindow: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return !!workspaceUri;
|
return !!workspaceUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
|
||||||
|
const widgets = this.applicationShell.widgets;
|
||||||
|
const snapshots = new Map<string, object>();
|
||||||
|
for (const widget of widgets) {
|
||||||
|
const saveable = Saveable.getDirty(widget);
|
||||||
|
const uri = NavigatableWidget.getUri(widget);
|
||||||
|
const uriString = uri?.toString();
|
||||||
|
let relativePath: string;
|
||||||
|
if (uri && uriString!.includes(sketchUri) && 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;
|
||||||
|
relativePath = '/' + lastPart;
|
||||||
|
} else {
|
||||||
|
relativePath = uri.toString().substring(sketchUri.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();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace SaveAsSketch {
|
export namespace SaveAsSketch {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const SAVE_AS_SKETCH: Command = {
|
export const SAVE_AS_SKETCH: Command = {
|
||||||
id: 'arduino-save-as-sketch'
|
id: 'arduino-save-as-sketch',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export interface Options {
|
export interface Options {
|
||||||
@@ -90,7 +175,7 @@ export namespace SaveAsSketch {
|
|||||||
export const DEFAULT: Options = {
|
export const DEFAULT: Options = {
|
||||||
execOnlyIfTemp: false,
|
execOnlyIfTemp: false,
|
||||||
openAfterMove: true,
|
openAfterMove: true,
|
||||||
wipeOriginal: false
|
wipeOriginal: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,75 @@
|
|||||||
import { injectable } from 'inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
import { SaveAsSketch } from './save-as-sketch';
|
||||||
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SaveSketch extends SketchContribution {
|
export class SaveSketch extends SketchContribution {
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registerCommands(registry: CommandRegistry): void {
|
|
||||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
||||||
execute: () => this.saveSketch()
|
execute: () => this.saveSketch(),
|
||||||
});
|
});
|
||||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
|
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
|
||||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
isVisible: (widget) =>
|
||||||
execute: () => registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id)
|
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||||
|
execute: () =>
|
||||||
|
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||||
label: 'Save',
|
label: nls.localize('vscode/fileCommands/save', 'Save'),
|
||||||
order: '6'
|
order: '6',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: SaveSketch.Commands.SAVE_SKETCH.id,
|
command: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||||
keybinding: 'CtrlCmd+S'
|
keybinding: 'CtrlCmd+S',
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
|
||||||
registry.registerItem({
|
|
||||||
id: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
|
|
||||||
command: SaveSketch.Commands.SAVE_SKETCH__TOOLBAR.id,
|
|
||||||
tooltip: 'Save',
|
|
||||||
priority: 5
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSketch(): Promise<void> {
|
async saveSketch(): Promise<void> {
|
||||||
return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id);
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isTemp = await this.sketchService.isTemp(sketch);
|
||||||
|
if (isTemp) {
|
||||||
|
return this.commandService.executeCommand(
|
||||||
|
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||||
|
{
|
||||||
|
execOnlyIfTemp: false,
|
||||||
|
openAfterMove: true,
|
||||||
|
wipeOriginal: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace SaveSketch {
|
export namespace SaveSketch {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const SAVE_SKETCH: Command = {
|
export const SAVE_SKETCH: Command = {
|
||||||
id: 'arduino-save-sketch'
|
id: 'arduino-save-sketch',
|
||||||
};
|
};
|
||||||
export const SAVE_SKETCH__TOOLBAR: Command = {
|
export const SAVE_SKETCH__TOOLBAR: Command = {
|
||||||
id: 'arduino-save-sketch--toolbar'
|
id: 'arduino-save-sketch--toolbar',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
StatusBar,
|
||||||
|
StatusBarAlignment,
|
||||||
|
} from '@theia/core/lib/browser/status-bar/status-bar';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { BoardsConfig } from '../boards/boards-config';
|
||||||
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
|
import { Contribution } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class SelectedBoard extends Contribution {
|
||||||
|
@inject(StatusBar)
|
||||||
|
private readonly statusBar: StatusBar;
|
||||||
|
|
||||||
|
@inject(BoardsServiceProvider)
|
||||||
|
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
|
|
||||||
|
override onStart(): void {
|
||||||
|
this.boardsServiceProvider.onBoardsConfigChanged((config) =>
|
||||||
|
this.update(config)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onReady(): void {
|
||||||
|
this.update(this.boardsServiceProvider.boardsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private update({ selectedBoard, selectedPort }: BoardsConfig.Config): void {
|
||||||
|
this.statusBar.setElement('arduino-selected-board', {
|
||||||
|
alignment: StatusBarAlignment.RIGHT,
|
||||||
|
text: selectedBoard
|
||||||
|
? `$(microchip) ${selectedBoard.name}`
|
||||||
|
: `$(close) ${nls.localize(
|
||||||
|
'arduino/common/noBoardSelected',
|
||||||
|
'No board selected'
|
||||||
|
)}`,
|
||||||
|
className: 'arduino-selected-board',
|
||||||
|
});
|
||||||
|
if (selectedBoard) {
|
||||||
|
this.statusBar.setElement('arduino-selected-port', {
|
||||||
|
alignment: StatusBarAlignment.RIGHT,
|
||||||
|
text: selectedPort
|
||||||
|
? nls.localize(
|
||||||
|
'arduino/common/selectedOn',
|
||||||
|
'on {0}',
|
||||||
|
selectedPort.address
|
||||||
|
)
|
||||||
|
: nls.localize('arduino/common/notConnected', '[not connected]'),
|
||||||
|
className: 'arduino-selected-port',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { Command, MenuModelRegistry, CommandRegistry, SketchContribution, KeybindingRegistry } from './contribution';
|
import {
|
||||||
|
Command,
|
||||||
|
MenuModelRegistry,
|
||||||
|
CommandRegistry,
|
||||||
|
SketchContribution,
|
||||||
|
KeybindingRegistry,
|
||||||
|
} from './contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { Settings as Preferences, SettingsDialog } from '../settings';
|
import { Settings as Preferences } from '../dialogs/settings/settings';
|
||||||
|
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class Settings extends SketchContribution {
|
export class Settings extends SketchContribution {
|
||||||
|
|
||||||
@inject(SettingsDialog)
|
@inject(SettingsDialog)
|
||||||
protected readonly settingsDialog: SettingsDialog;
|
protected readonly settingsDialog: SettingsDialog;
|
||||||
|
|
||||||
protected settingsOpened = false;
|
protected settingsOpened = false;
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
registry.registerCommand(Settings.Commands.OPEN, {
|
registry.registerCommand(Settings.Commands.OPEN, {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
let settings: Preferences | undefined = undefined;
|
let settings: Preferences | undefined = undefined;
|
||||||
@@ -28,34 +35,44 @@ export class Settings extends SketchContribution {
|
|||||||
await this.settingsService.reset();
|
await this.settingsService.reset();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEnabled: () => !this.settingsOpened
|
isEnabled: () => !this.settingsOpened,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
|
||||||
commandId: Settings.Commands.OPEN.id,
|
commandId: Settings.Commands.OPEN.id,
|
||||||
label: 'Preferences...',
|
label:
|
||||||
order: '0'
|
nls.localize(
|
||||||
|
'vscode/preferences.contribution/preferences',
|
||||||
|
'Preferences'
|
||||||
|
) + '...',
|
||||||
|
order: '0',
|
||||||
});
|
});
|
||||||
registry.registerSubmenu(ArduinoMenus.FILE__ADVANCED_SUBMENU, 'Advanced');
|
registry.registerSubmenu(
|
||||||
|
ArduinoMenus.FILE__ADVANCED_SUBMENU,
|
||||||
|
nls.localize('arduino/menu/advanced', 'Advanced')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: Settings.Commands.OPEN.id,
|
command: Settings.Commands.OPEN.id,
|
||||||
keybinding: 'CtrlCmd+,',
|
keybinding: 'CtrlCmd+,',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Settings {
|
export namespace Settings {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const OPEN: Command = {
|
export const OPEN: Command = {
|
||||||
id: 'arduino-settings-open',
|
id: 'arduino-settings-open',
|
||||||
label: 'Open Preferences...',
|
label:
|
||||||
category: 'Arduino'
|
nls.localize(
|
||||||
}
|
'vscode/preferences.contribution/openSettings2',
|
||||||
|
'Open Preferences'
|
||||||
|
) + '...',
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
|
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
|
||||||
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
||||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
import {
|
||||||
import { URI, SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, open } from './contribution';
|
Disposable,
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
DisposableCollection,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
|
import {
|
||||||
|
URI,
|
||||||
|
SketchContribution,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
TabBarToolbarRegistry,
|
||||||
|
open,
|
||||||
|
} 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()
|
@injectable()
|
||||||
export class SketchControl extends SketchContribution {
|
export class SketchControl extends SketchContribution {
|
||||||
|
|
||||||
@inject(ApplicationShell)
|
@inject(ApplicationShell)
|
||||||
protected readonly shell: ApplicationShell;
|
protected readonly shell: ApplicationShell;
|
||||||
|
|
||||||
@@ -19,19 +37,34 @@ export class SketchControl extends SketchContribution {
|
|||||||
@inject(ContextMenuRenderer)
|
@inject(ContextMenuRenderer)
|
||||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||||
|
|
||||||
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
|
@inject(EditorManager)
|
||||||
|
protected override readonly editorManager: EditorManager;
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
@inject(SketchesServiceClientImpl)
|
||||||
registry.registerCommand(SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR, {
|
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||||
isVisible: widget => this.shell.getWidgets('main').indexOf(widget) !== -1,
|
|
||||||
|
@inject(LocalCacheFsProvider)
|
||||||
|
protected readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||||
|
|
||||||
|
protected readonly toDisposeBeforeCreateNewContextMenu =
|
||||||
|
new DisposableCollection();
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(
|
||||||
|
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR,
|
||||||
|
{
|
||||||
|
isVisible: (widget) =>
|
||||||
|
this.shell.getWidgets('main').indexOf(widget) !== -1,
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
if (!sketch) {
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = document.getElementById(SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id);
|
const target = document.getElementById(
|
||||||
|
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
||||||
|
);
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -40,90 +73,210 @@ export class SketchControl extends SketchContribution {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { mainFileUri, rootFolderFileUris } = await this.sketchService.loadSketch(sketch.uri);
|
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||||
const uris = [mainFileUri, ...rootFolderFileUris];
|
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++) {
|
for (let i = 0; i < uris.length; i++) {
|
||||||
const uri = new URI(uris[i]);
|
const uri = new URI(uris[i]);
|
||||||
const command = { id: `arduino-focus-file--${uri.toString()}` };
|
|
||||||
const handler = { execute: () => open(this.openerService, uri) };
|
// focus on the opened sketch
|
||||||
this.toDisposeBeforeCreateNewContextMenu.push(registry.registerCommand(command, handler));
|
const command = {
|
||||||
this.menuRegistry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP, {
|
id: `arduino-focus-file--${uri.toString()}`,
|
||||||
|
};
|
||||||
|
const handler = {
|
||||||
|
execute: () => open(this.openerService, uri),
|
||||||
|
};
|
||||||
|
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||||
|
registry.registerCommand(command, handler)
|
||||||
|
);
|
||||||
|
this.menuRegistry.registerMenuAction(
|
||||||
|
ArduinoMenus.SKETCH_CONTROL__CONTEXT__RESOURCES_GROUP,
|
||||||
|
{
|
||||||
commandId: command.id,
|
commandId: command.id,
|
||||||
label: this.labelProvider.getName(uri),
|
label: this.labelProvider.getName(uri),
|
||||||
order: `${i}`
|
order: `${i}`,
|
||||||
});
|
}
|
||||||
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
|
);
|
||||||
|
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuAction(command)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const options = {
|
const options = {
|
||||||
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
|
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
|
||||||
anchor: {
|
anchor: {
|
||||||
x: parentElement.getBoundingClientRect().left,
|
x: parentElement.getBoundingClientRect().left,
|
||||||
y: parentElement.getBoundingClientRect().top + parentElement.offsetHeight
|
y:
|
||||||
}
|
parentElement.getBoundingClientRect().top +
|
||||||
}
|
parentElement.offsetHeight,
|
||||||
|
},
|
||||||
|
};
|
||||||
this.contextMenuRenderer.render(options);
|
this.contextMenuRenderer.render(options);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
|
registry.registerMenuAction(
|
||||||
|
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||||
|
{
|
||||||
commandId: WorkspaceCommands.NEW_FILE.id,
|
commandId: WorkspaceCommands.NEW_FILE.id,
|
||||||
label: 'New Tab',
|
label: nls.localize('vscode/menubar/mNewTab', 'New Tab'),
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
}
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
|
);
|
||||||
commandId: WorkspaceCommands.FILE_RENAME.id,
|
|
||||||
label: 'Rename',
|
|
||||||
order: '1'
|
|
||||||
});
|
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
|
|
||||||
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
|
|
||||||
label: 'Delete',
|
|
||||||
order: '2'
|
|
||||||
});
|
|
||||||
|
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, {
|
registry.registerMenuAction(
|
||||||
|
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
|
||||||
|
{
|
||||||
commandId: CommonCommands.PREVIOUS_TAB.id,
|
commandId: CommonCommands.PREVIOUS_TAB.id,
|
||||||
label: 'Previous Tab',
|
label: nls.localize('vscode/menubar/mShowPreviousTab', 'Previous Tab'),
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
}
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, {
|
);
|
||||||
|
registry.registerMenuAction(
|
||||||
|
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
|
||||||
|
{
|
||||||
commandId: CommonCommands.NEXT_TAB.id,
|
commandId: CommonCommands.NEXT_TAB.id,
|
||||||
label: 'Next Tab',
|
label: nls.localize('vscode/menubar/mShowNextTab', 'Next Tab'),
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: WorkspaceCommands.NEW_FILE.id,
|
command: WorkspaceCommands.NEW_FILE.id,
|
||||||
keybinding: 'CtrlCmd+Shift+N'
|
keybinding: 'CtrlCmd+Shift+N',
|
||||||
});
|
});
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: CommonCommands.PREVIOUS_TAB.id,
|
command: CommonCommands.PREVIOUS_TAB.id,
|
||||||
keybinding: 'CtrlCmd+Alt+Left' // TODO: check why electron does not show the keybindings in the UI.
|
keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
|
||||||
});
|
});
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: CommonCommands.NEXT_TAB.id,
|
command: CommonCommands.NEXT_TAB.id,
|
||||||
keybinding: 'CtrlCmd+Alt+Right'
|
keybinding: 'CtrlCmd+Alt+Right',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||||
registry.registerItem({
|
registry.registerItem({
|
||||||
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
||||||
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
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 {
|
export namespace SketchControl {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
|
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
|
||||||
id: 'arduino-open-sketch-control--toolbar',
|
id: 'arduino-open-sketch-control--toolbar',
|
||||||
iconClass: 'fa fa-caret-down'
|
iconClass: 'fa fa-arduino-sketch-tabs-menu',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { SaveableWidget } from '@theia/core/lib/browser/saveable';
|
||||||
|
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 { Sketch, SketchContribution, URI } from './contribution';
|
||||||
|
import { OpenSketchFiles } from './open-sketch-files';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class SketchFilesTracker extends SketchContribution {
|
||||||
|
@inject(FileSystemFrontendContribution)
|
||||||
|
private readonly fileSystemFrontendContribution: FileSystemFrontendContribution;
|
||||||
|
private readonly toDisposeOnStop = new DisposableCollection();
|
||||||
|
|
||||||
|
override onStart(): void {
|
||||||
|
this.fileSystemFrontendContribution.onDidChangeEditorFile(
|
||||||
|
({ type, editor }) => {
|
||||||
|
if (type === FileChangeType.DELETED) {
|
||||||
|
const editorWidget = editor;
|
||||||
|
if (SaveableWidget.is(editorWidget)) {
|
||||||
|
editorWidget.closeWithoutSaving();
|
||||||
|
} else {
|
||||||
|
editorWidget.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onReady(): void {
|
||||||
|
this.sketchServiceClient.currentSketch().then(async (sketch) => {
|
||||||
|
if (
|
||||||
|
CurrentSketch.isValid(sketch) &&
|
||||||
|
!(await this.sketchService.isTemp(sketch))
|
||||||
|
) {
|
||||||
|
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
|
||||||
|
this.toDisposeOnStop.push(
|
||||||
|
this.fileService.onDidFilesChange(async (event) => {
|
||||||
|
for (const { type, resource } of event.changes) {
|
||||||
|
if (
|
||||||
|
type === FileChangeType.ADDED &&
|
||||||
|
resource.parent.toString() === sketch.uri
|
||||||
|
) {
|
||||||
|
const reloadedSketch = await this.sketchService.loadSketch(
|
||||||
|
sketch.uri
|
||||||
|
);
|
||||||
|
if (Sketch.isInSketch(resource, reloadedSketch)) {
|
||||||
|
this.commandService.executeCommand(
|
||||||
|
OpenSketchFiles.Commands.ENSURE_OPENED.id,
|
||||||
|
resource.toString(),
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
mode: 'open',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onStop(): void {
|
||||||
|
this.toDisposeOnStop.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,25 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||||
import { CommandRegistry, MenuModelRegistry } from './contribution';
|
import { CommandRegistry, MenuModelRegistry } from './contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
import { NotificationCenter } from '../notification-center';
|
import { NotificationCenter } from '../notification-center';
|
||||||
import { Examples } from './examples';
|
import { Examples } from './examples';
|
||||||
import { SketchContainer } from '../../common/protocol';
|
import {
|
||||||
|
SketchContainer,
|
||||||
|
SketchesError,
|
||||||
|
SketchRef,
|
||||||
|
} from '../../common/protocol';
|
||||||
import { OpenSketch } from './open-sketch';
|
import { OpenSketch } from './open-sketch';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class Sketchbook extends Examples {
|
export class Sketchbook extends Examples {
|
||||||
|
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
protected override readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
@inject(MenuModelRegistry)
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
protected override readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
@inject(MainMenuManager)
|
@inject(MainMenuManager)
|
||||||
protected readonly mainMenuManager: MainMenuManager;
|
protected readonly mainMenuManager: MainMenuManager;
|
||||||
@@ -23,35 +27,60 @@ export class Sketchbook extends Examples {
|
|||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
protected readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
onStart(): void {
|
override onStart(): void {
|
||||||
this.sketchService.getSketches({}).then(container => {
|
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onReady(): Promise<void> {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
this.sketchService.getSketches({}).then((container) => {
|
||||||
this.register(container);
|
this.register(container);
|
||||||
this.mainMenuManager.update();
|
this.mainMenuManager.update();
|
||||||
});
|
});
|
||||||
this.sketchServiceClient.onSketchbookDidChange(() => {
|
|
||||||
this.sketchService.getSketches({}).then(container => {
|
|
||||||
this.register(container);
|
|
||||||
this.mainMenuManager.update();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' });
|
registry.registerSubmenu(
|
||||||
|
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
|
||||||
|
nls.localize('arduino/sketch/sketchbook', 'Sketchbook'),
|
||||||
|
{ order: '3' }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected register(container: SketchContainer): void {
|
protected register(container: SketchContainer): void {
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
this.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, this.toDispose);
|
this.registerRecursively(
|
||||||
|
[...container.children, ...container.sketches],
|
||||||
|
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
|
||||||
|
this.toDispose
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createHandler(uri: string): CommandHandler {
|
protected override createHandler(uri: string): CommandHandler {
|
||||||
return {
|
return {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
const sketch = await this.sketchService.loadSketch(uri);
|
let sketch: SketchRef | undefined = undefined;
|
||||||
return this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
|
try {
|
||||||
|
sketch = await this.sketchService.loadSketch(uri);
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
// To handle the following:
|
||||||
|
// Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch.
|
||||||
|
// Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items.
|
||||||
|
this.messageService.error(err.message);
|
||||||
|
this.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (sketch) {
|
||||||
|
await this.commandService.executeCommand(
|
||||||
|
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
|
sketch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { MessageService } from '@theia/core';
|
||||||
|
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { LocalStorageService } from '@theia/core/lib/browser';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||||
|
import { ArduinoPreferences } from '../arduino-preferences';
|
||||||
|
import { SurveyNotificationService } from '../../common/protocol/survey-service';
|
||||||
|
|
||||||
|
const SURVEY_MESSAGE = nls.localize(
|
||||||
|
'arduino/survey/surveyMessage',
|
||||||
|
'Please help us improve by answering this super short survey. We value our community and would like to get to know our supporters a little better.'
|
||||||
|
);
|
||||||
|
const DO_NOT_SHOW_AGAIN = nls.localize(
|
||||||
|
'arduino/survey/dismissSurvey',
|
||||||
|
"Don't show again"
|
||||||
|
);
|
||||||
|
const GO_TO_SURVEY = nls.localize(
|
||||||
|
'arduino/survey/answerSurvey',
|
||||||
|
'Answer survey'
|
||||||
|
);
|
||||||
|
|
||||||
|
const SURVEY_BASE_URL = 'https://surveys.hotjar.com/';
|
||||||
|
const surveyId = '17887b40-e1f0-4bd6-b9f0-a37f229ccd8b';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class SurveyNotification implements FrontendApplicationContribution {
|
||||||
|
@inject(MessageService)
|
||||||
|
private readonly messageService: MessageService;
|
||||||
|
|
||||||
|
@inject(LocalStorageService)
|
||||||
|
private readonly localStorageService: LocalStorageService;
|
||||||
|
|
||||||
|
@inject(WindowService)
|
||||||
|
private readonly windowService: WindowService;
|
||||||
|
|
||||||
|
@inject(ArduinoPreferences)
|
||||||
|
private readonly arduinoPreferences: ArduinoPreferences;
|
||||||
|
|
||||||
|
@inject(SurveyNotificationService)
|
||||||
|
private readonly surveyNotificationService: SurveyNotificationService;
|
||||||
|
|
||||||
|
onStart(): void {
|
||||||
|
this.arduinoPreferences.ready.then(async () => {
|
||||||
|
if (
|
||||||
|
(await this.surveyNotificationService.isFirstInstance()) &&
|
||||||
|
this.arduinoPreferences.get('arduino.survey.notification')
|
||||||
|
) {
|
||||||
|
const surveyAnswered = await this.localStorageService.getData(
|
||||||
|
this.surveyKey(surveyId)
|
||||||
|
);
|
||||||
|
if (surveyAnswered !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const answer = await this.messageService.info(
|
||||||
|
SURVEY_MESSAGE,
|
||||||
|
DO_NOT_SHOW_AGAIN,
|
||||||
|
GO_TO_SURVEY
|
||||||
|
);
|
||||||
|
switch (answer) {
|
||||||
|
case GO_TO_SURVEY:
|
||||||
|
this.windowService.openNewWindow(SURVEY_BASE_URL + surveyId, {
|
||||||
|
external: true,
|
||||||
|
});
|
||||||
|
this.localStorageService.setData(this.surveyKey(surveyId), true);
|
||||||
|
break;
|
||||||
|
case DO_NOT_SHOW_AGAIN:
|
||||||
|
this.localStorageService.setData(this.surveyKey(surveyId), false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private surveyKey(id: string): string {
|
||||||
|
return `answered_survey:${id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
MenuModelRegistry,
|
||||||
|
CommandRegistry,
|
||||||
|
Contribution,
|
||||||
|
} from './contribution';
|
||||||
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
import { UploadCertificateDialog } from '../dialogs/certificate-uploader/certificate-uploader-dialog';
|
||||||
|
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
||||||
|
import {
|
||||||
|
PreferenceScope,
|
||||||
|
PreferenceService,
|
||||||
|
} from '@theia/core/lib/browser/preferences/preference-service';
|
||||||
|
import { ArduinoPreferences } from '../arduino-preferences';
|
||||||
|
import {
|
||||||
|
arduinoCert,
|
||||||
|
certificateList,
|
||||||
|
} from '../dialogs/certificate-uploader/utils';
|
||||||
|
import { ArduinoFirmwareUploader } from '../../common/protocol/arduino-firmware-uploader';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class UploadCertificate extends Contribution {
|
||||||
|
@inject(UploadCertificateDialog)
|
||||||
|
protected readonly dialog: UploadCertificateDialog;
|
||||||
|
|
||||||
|
@inject(ContextMenuRenderer)
|
||||||
|
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||||
|
|
||||||
|
@inject(PreferenceService)
|
||||||
|
protected readonly preferenceService: PreferenceService;
|
||||||
|
|
||||||
|
@inject(ArduinoPreferences)
|
||||||
|
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||||
|
|
||||||
|
@inject(ArduinoFirmwareUploader)
|
||||||
|
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||||
|
|
||||||
|
protected dialogOpened = false;
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(UploadCertificate.Commands.OPEN, {
|
||||||
|
execute: async () => {
|
||||||
|
try {
|
||||||
|
this.dialogOpened = true;
|
||||||
|
await this.dialog.open();
|
||||||
|
} finally {
|
||||||
|
this.dialogOpened = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEnabled: () => !this.dialogOpened,
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
|
||||||
|
execute: async (certToRemove) => {
|
||||||
|
const certs = this.arduinoPreferences.get('arduino.board.certificates');
|
||||||
|
|
||||||
|
this.preferenceService.set(
|
||||||
|
'arduino.board.certificates',
|
||||||
|
certificateList(certs)
|
||||||
|
.filter((c) => c !== certToRemove)
|
||||||
|
.join(','),
|
||||||
|
PreferenceScope.User
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isEnabled: (certToRemove) => certToRemove !== arduinoCert,
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerCommand(UploadCertificate.Commands.UPLOAD_CERT, {
|
||||||
|
execute: async ({ fqbn, address, urls }) => {
|
||||||
|
return this.arduinoFirmwareUploader.uploadCertificates(
|
||||||
|
`-b ${fqbn} -a ${address} ${urls
|
||||||
|
.map((url: string) => `-u ${url}`)
|
||||||
|
.join(' ')}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isEnabled: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
|
||||||
|
execute: async (args: any) => {
|
||||||
|
this.contextMenuRenderer.render({
|
||||||
|
menuPath: ArduinoMenus.ROOT_CERTIFICATES__CONTEXT,
|
||||||
|
anchor: {
|
||||||
|
x: args.x,
|
||||||
|
y: args.y,
|
||||||
|
},
|
||||||
|
args: [args.cert],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isEnabled: () => true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
|
||||||
|
commandId: UploadCertificate.Commands.OPEN.id,
|
||||||
|
label: UploadCertificate.Commands.OPEN.label,
|
||||||
|
order: '1',
|
||||||
|
});
|
||||||
|
|
||||||
|
registry.registerMenuAction(ArduinoMenus.ROOT_CERTIFICATES__CONTEXT, {
|
||||||
|
commandId: UploadCertificate.Commands.REMOVE_CERT.id,
|
||||||
|
label: UploadCertificate.Commands.REMOVE_CERT.label,
|
||||||
|
order: '1',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace UploadCertificate {
|
||||||
|
export namespace Commands {
|
||||||
|
export const OPEN: Command = {
|
||||||
|
id: 'arduino-upload-certificate-open',
|
||||||
|
label: nls.localize(
|
||||||
|
'arduino/certificate/uploadRootCertificates',
|
||||||
|
'Upload SSL Root Certificates'
|
||||||
|
),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OPEN_CERT_CONTEXT: Command = {
|
||||||
|
id: 'arduino-certificate-open-context',
|
||||||
|
label: nls.localize('arduino/certificate/openContext', 'Open context'),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const REMOVE_CERT: Command = {
|
||||||
|
id: 'arduino-certificate-remove',
|
||||||
|
label: nls.localize('arduino/certificate/remove', 'Remove'),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UPLOAD_CERT: Command = {
|
||||||
|
id: 'arduino-certificate-upload',
|
||||||
|
label: nls.localize('arduino/certificate/upload', 'Upload'),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
MenuModelRegistry,
|
||||||
|
CommandRegistry,
|
||||||
|
Contribution,
|
||||||
|
} from './contribution';
|
||||||
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
import { UploadFirmwareDialog } from '../dialogs/firmware-uploader/firmware-uploader-dialog';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class UploadFirmware extends Contribution {
|
||||||
|
@inject(UploadFirmwareDialog)
|
||||||
|
protected readonly dialog: UploadFirmwareDialog;
|
||||||
|
|
||||||
|
protected dialogOpened = false;
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(UploadFirmware.Commands.OPEN, {
|
||||||
|
execute: async () => {
|
||||||
|
try {
|
||||||
|
this.dialogOpened = true;
|
||||||
|
await this.dialog.open();
|
||||||
|
} finally {
|
||||||
|
this.dialogOpened = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEnabled: () => !this.dialogOpened,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
registry.registerMenuAction(ArduinoMenus.TOOLS__FIRMWARE_UPLOADER_GROUP, {
|
||||||
|
commandId: UploadFirmware.Commands.OPEN.id,
|
||||||
|
label: UploadFirmware.Commands.OPEN.label,
|
||||||
|
order: '0',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace UploadFirmware {
|
||||||
|
export namespace Commands {
|
||||||
|
export const OPEN: Command = {
|
||||||
|
id: 'arduino-upload-firmware-open',
|
||||||
|
label: nls.localize(
|
||||||
|
'arduino/firmware/updater',
|
||||||
|
'WiFi101 / WiFiNINA Firmware Updater'
|
||||||
|
),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,189 +1,316 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { Emitter } from '@theia/core/lib/common/event';
|
import { Emitter } from '@theia/core/lib/common/event';
|
||||||
import { CoreService } from '../../common/protocol';
|
import { BoardUserField, CoreService } from '../../common/protocol';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
import {
|
||||||
import { MonitorConnection } from '../monitor/monitor-connection';
|
Command,
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
CommandRegistry,
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
TabBarToolbarRegistry,
|
||||||
|
CoreServiceContribution,
|
||||||
|
} from './contribution';
|
||||||
|
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
|
||||||
|
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
import type { VerifySketchParams } from './verify-sketch';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class UploadSketch extends SketchContribution {
|
export class UploadSketch extends CoreServiceContribution {
|
||||||
|
@inject(MenuModelRegistry)
|
||||||
|
private readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
@inject(CoreService)
|
@inject(UserFieldsDialog)
|
||||||
protected readonly coreService: CoreService;
|
private readonly userFieldsDialog: UserFieldsDialog;
|
||||||
|
|
||||||
@inject(MonitorConnection)
|
private boardRequiresUserFields = false;
|
||||||
protected readonly monitorConnection: MonitorConnection;
|
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||||
|
private readonly menuActionsDisposables = new DisposableCollection();
|
||||||
|
|
||||||
@inject(BoardsDataStore)
|
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||||
protected readonly boardsDataStore: BoardsDataStore;
|
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||||
|
private uploadInProgress = false;
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
protected override init(): void {
|
||||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
super.init();
|
||||||
|
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
|
||||||
|
const userFields =
|
||||||
|
await this.boardsServiceProvider.selectedBoardUserFields();
|
||||||
|
this.boardRequiresUserFields = userFields.length > 0;
|
||||||
|
this.registerMenus(this.menuRegistry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
|
private selectedFqbnAddress(): string {
|
||||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
const { boardsConfig } = this.boardsServiceProvider;
|
||||||
|
const fqbn = boardsConfig.selectedBoard?.fqbn;
|
||||||
|
if (!fqbn) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const address =
|
||||||
|
boardsConfig.selectedBoard?.port?.address ||
|
||||||
|
boardsConfig.selectedPort?.address;
|
||||||
|
if (!address) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return fqbn + '|' + address;
|
||||||
|
}
|
||||||
|
|
||||||
protected uploadInProgress = false;
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
|
||||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
|
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
|
||||||
execute: () => this.uploadSketch(),
|
execute: async () => {
|
||||||
|
const key = this.selectedFqbnAddress();
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) {
|
||||||
|
// Deep clone the array of board fields to avoid editing the cached ones
|
||||||
|
this.userFieldsDialog.value = (
|
||||||
|
await this.boardsServiceProvider.selectedBoardUserFields()
|
||||||
|
).map((f) => ({ ...f }));
|
||||||
|
const result = await this.userFieldsDialog.open();
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cachedUserFields.set(key, result);
|
||||||
|
}
|
||||||
|
this.uploadSketch();
|
||||||
|
},
|
||||||
isEnabled: () => !this.uploadInProgress,
|
isEnabled: () => !this.uploadInProgress,
|
||||||
});
|
});
|
||||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER, {
|
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
|
||||||
|
execute: async () => {
|
||||||
|
const key = this.selectedFqbnAddress();
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = this.cachedUserFields.get(key);
|
||||||
|
// Deep clone the array of board fields to avoid editing the cached ones
|
||||||
|
this.userFieldsDialog.value = (
|
||||||
|
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
|
||||||
|
).map((f) => ({ ...f }));
|
||||||
|
|
||||||
|
const result = await this.userFieldsDialog.open();
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cachedUserFields.set(key, result);
|
||||||
|
this.uploadSketch();
|
||||||
|
},
|
||||||
|
isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
|
||||||
|
});
|
||||||
|
registry.registerCommand(
|
||||||
|
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
|
||||||
|
{
|
||||||
execute: () => this.uploadSketch(true),
|
execute: () => this.uploadSketch(true),
|
||||||
isEnabled: () => !this.uploadInProgress,
|
isEnabled: () => !this.uploadInProgress,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR, {
|
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR, {
|
||||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
isVisible: (widget) =>
|
||||||
|
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||||
isEnabled: () => !this.uploadInProgress,
|
isEnabled: () => !this.uploadInProgress,
|
||||||
isToggled: () => this.uploadInProgress,
|
isToggled: () => this.uploadInProgress,
|
||||||
execute: () => registry.executeCommand(UploadSketch.Commands.UPLOAD_SKETCH.id)
|
execute: () =>
|
||||||
|
registry.executeCommand(UploadSketch.Commands.UPLOAD_SKETCH.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
this.menuActionsDisposables.dispose();
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||||
label: 'Upload',
|
label: nls.localize('arduino/sketch/upload', 'Upload'),
|
||||||
order: '1'
|
order: '1',
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
if (this.boardRequiresUserFields) {
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
|
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||||
|
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||||
|
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||||
|
order: '2',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
|
registry.registerMenuNode(
|
||||||
|
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||||
|
new PlaceholderMenuNode(
|
||||||
|
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||||
|
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||||
|
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||||
|
{ order: '2' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||||
label: 'Upload Using Programmer',
|
label: nls.localize(
|
||||||
order: '2'
|
'arduino/sketch/uploadUsingProgrammer',
|
||||||
});
|
'Upload Using Programmer'
|
||||||
|
),
|
||||||
|
order: '3',
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
command: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||||
keybinding: 'CtrlCmd+U'
|
keybinding: 'CtrlCmd+U',
|
||||||
});
|
});
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
command: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||||
keybinding: 'CtrlCmd+Shift+U'
|
keybinding: 'CtrlCmd+Shift+U',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||||
registry.registerItem({
|
registry.registerItem({
|
||||||
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
|
id: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
|
||||||
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
|
command: UploadSketch.Commands.UPLOAD_SKETCH_TOOLBAR.id,
|
||||||
tooltip: 'Upload',
|
tooltip: nls.localize('arduino/sketch/upload', 'Upload'),
|
||||||
priority: 1,
|
priority: 1,
|
||||||
onDidChange: this.onDidChange
|
onDidChange: this.onDidChange,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadSketch(usingProgrammer: boolean = false): Promise<void> {
|
async uploadSketch(usingProgrammer = false): Promise<void> {
|
||||||
|
|
||||||
// even with buttons disabled, better to double check if an upload is already in progress
|
|
||||||
if (this.uploadInProgress) {
|
if (this.uploadInProgress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
// toggle the toolbar button and menu item state.
|
// toggle the toolbar button and menu item state.
|
||||||
// uploadInProgress will be set to false whether the upload fails or not
|
// uploadInProgress will be set to false whether the upload fails or not
|
||||||
this.uploadInProgress = true;
|
this.uploadInProgress = true;
|
||||||
this.onDidChangeEmitter.fire();
|
this.onDidChangeEmitter.fire();
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
|
||||||
if (!sketch) {
|
const verifyOptions =
|
||||||
|
await this.commandService.executeCommand<CoreService.Options.Compile>(
|
||||||
|
'arduino-verify-sketch',
|
||||||
|
<VerifySketchParams>{
|
||||||
|
exportBinaries: false,
|
||||||
|
silent: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!verifyOptions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let shouldAutoConnect = false;
|
|
||||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
|
||||||
if (monitorConfig) {
|
|
||||||
await this.monitorConnection.disconnect();
|
|
||||||
if (this.monitorConnection.autoConnect) {
|
|
||||||
shouldAutoConnect = true;
|
|
||||||
}
|
|
||||||
this.monitorConnection.autoConnect = false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
|
||||||
const [fqbn, { selectedProgrammer }, verify, verbose, sourceOverride] = await Promise.all([
|
|
||||||
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
|
|
||||||
this.boardsDataStore.getData(boardsConfig.selectedBoard?.fqbn),
|
|
||||||
this.preferences.get('arduino.upload.verify'),
|
|
||||||
this.preferences.get('arduino.upload.verbose'),
|
|
||||||
this.sourceOverride()
|
|
||||||
]);
|
|
||||||
|
|
||||||
let options: CoreService.Upload.Options | undefined = undefined;
|
const uploadOptions = await this.uploadOptions(
|
||||||
const sketchUri = sketch.uri;
|
usingProgrammer,
|
||||||
const optimizeForDebug = this.editorMode.compileForDebug;
|
verifyOptions
|
||||||
const { selectedPort } = boardsConfig;
|
);
|
||||||
const port = selectedPort?.address;
|
if (!uploadOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (usingProgrammer) {
|
// TODO: This does not belong here.
|
||||||
const programmer = selectedProgrammer;
|
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
|
||||||
options = {
|
if (
|
||||||
sketchUri,
|
uploadOptions.userFields.length === 0 &&
|
||||||
fqbn,
|
this.boardRequiresUserFields
|
||||||
optimizeForDebug,
|
) {
|
||||||
programmer,
|
this.messageService.error(
|
||||||
port,
|
nls.localize(
|
||||||
verbose,
|
'arduino/sketch/userFieldsNotFoundError',
|
||||||
verify,
|
"Can't find user fields for connected board"
|
||||||
sourceOverride
|
)
|
||||||
};
|
);
|
||||||
} else {
|
return;
|
||||||
options = {
|
|
||||||
sketchUri,
|
|
||||||
fqbn,
|
|
||||||
optimizeForDebug,
|
|
||||||
port,
|
|
||||||
verbose,
|
|
||||||
verify,
|
|
||||||
sourceOverride
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
this.outputChannelManager.getChannel('Arduino').clear();
|
|
||||||
if (usingProgrammer) {
|
await this.doWithProgress({
|
||||||
await this.coreService.uploadUsingProgrammer(options);
|
progressText: nls.localize('arduino/sketch/uploading', 'Uploading...'),
|
||||||
} else {
|
task: (progressId, coreService) =>
|
||||||
await this.coreService.upload(options);
|
coreService.upload({ ...uploadOptions, progressId }),
|
||||||
}
|
keepOutput: true,
|
||||||
this.messageService.info('Done uploading.', { timeout: 3000 });
|
});
|
||||||
|
|
||||||
|
this.messageService.info(
|
||||||
|
nls.localize('arduino/sketch/doneUploading', 'Done uploading.'),
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.messageService.error(e.toString());
|
this.handleError(e);
|
||||||
} finally {
|
} finally {
|
||||||
this.uploadInProgress = false;
|
this.uploadInProgress = false;
|
||||||
this.onDidChangeEmitter.fire();
|
this.onDidChangeEmitter.fire();
|
||||||
|
|
||||||
if (monitorConfig) {
|
|
||||||
const { board, port } = monitorConfig;
|
|
||||||
try {
|
|
||||||
await this.boardsServiceClientImpl.waitUntilAvailable(Object.assign(board, { port }), 10_000);
|
|
||||||
if (shouldAutoConnect) {
|
|
||||||
// Enabling auto-connect will trigger a connect.
|
|
||||||
this.monitorConnection.autoConnect = true;
|
|
||||||
} else {
|
|
||||||
await this.monitorConnection.connect(monitorConfig);
|
|
||||||
}
|
|
||||||
} catch (waitError) {
|
|
||||||
this.messageService.error(`Could not reconnect to serial monitor. ${waitError.toString()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async uploadOptions(
|
||||||
|
usingProgrammer: boolean,
|
||||||
|
verifyOptions: CoreService.Options.Compile
|
||||||
|
): Promise<CoreService.Options.Upload | undefined> {
|
||||||
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const userFields = this.userFields();
|
||||||
|
const { boardsConfig } = this.boardsServiceProvider;
|
||||||
|
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
|
||||||
|
await Promise.all([
|
||||||
|
verifyOptions.fqbn, // already decorated FQBN
|
||||||
|
this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)),
|
||||||
|
this.preferences.get('arduino.upload.verify'),
|
||||||
|
this.preferences.get('arduino.upload.verbose'),
|
||||||
|
]);
|
||||||
|
const port = boardsConfig.selectedPort;
|
||||||
|
return {
|
||||||
|
sketch,
|
||||||
|
fqbn,
|
||||||
|
...(usingProgrammer && { programmer }),
|
||||||
|
port,
|
||||||
|
verbose,
|
||||||
|
verify,
|
||||||
|
userFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private userFields() {
|
||||||
|
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
|
||||||
|
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
|
||||||
|
* 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 {
|
export namespace UploadSketch {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const UPLOAD_SKETCH: Command = {
|
export const UPLOAD_SKETCH: Command = {
|
||||||
id: 'arduino-upload-sketch'
|
id: 'arduino-upload-sketch',
|
||||||
|
};
|
||||||
|
export const UPLOAD_WITH_CONFIGURATION: Command & { label: string } = {
|
||||||
|
id: 'arduino-upload-with-configuration-sketch',
|
||||||
|
label: nls.localize(
|
||||||
|
'arduino/sketch/configureAndUpload',
|
||||||
|
'Configure And Upload'
|
||||||
|
),
|
||||||
|
category: 'Arduino',
|
||||||
};
|
};
|
||||||
export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = {
|
export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = {
|
||||||
id: 'arduino-upload-sketch-using-programmer'
|
id: 'arduino-upload-sketch-using-programmer',
|
||||||
};
|
};
|
||||||
export const UPLOAD_SKETCH_TOOLBAR: Command = {
|
export const UPLOAD_SKETCH_TOOLBAR: Command = {
|
||||||
id: 'arduino-upload-sketch--toolbar'
|
id: 'arduino-upload-sketch--toolbar',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +1,189 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { Emitter } from '@theia/core/lib/common/event';
|
import { Emitter } from '@theia/core/lib/common/event';
|
||||||
import { CoreService } from '../../common/protocol';
|
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
import {
|
||||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
CoreServiceContribution,
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
TabBarToolbarRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
import { CoreService } from '../../common/protocol';
|
||||||
|
import { CoreErrorHandler } from './core-error-handler';
|
||||||
|
|
||||||
|
export interface VerifySketchParams {
|
||||||
|
/**
|
||||||
|
* Same as `CoreService.Options.Compile#exportBinaries`
|
||||||
|
*/
|
||||||
|
readonly exportBinaries?: boolean;
|
||||||
|
/**
|
||||||
|
* If `true`, there won't be any UI indication of the verify command. It's `false` by default.
|
||||||
|
*/
|
||||||
|
readonly silent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class VerifySketch extends SketchContribution {
|
export class VerifySketch extends CoreServiceContribution {
|
||||||
|
@inject(CoreErrorHandler)
|
||||||
|
private readonly coreErrorHandler: CoreErrorHandler;
|
||||||
|
|
||||||
@inject(CoreService)
|
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||||
protected readonly coreService: CoreService;
|
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||||
|
private verifyInProgress = false;
|
||||||
|
|
||||||
@inject(BoardsDataStore)
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
protected readonly boardsDataStore: BoardsDataStore;
|
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
|
||||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
|
||||||
|
|
||||||
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
|
|
||||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
|
||||||
|
|
||||||
protected verifyInProgress = false;
|
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
|
||||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
|
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
|
||||||
execute: () => this.verifySketch(),
|
execute: (params?: VerifySketchParams) => this.verifySketch(params),
|
||||||
isEnabled: () => !this.verifyInProgress,
|
isEnabled: () => !this.verifyInProgress,
|
||||||
});
|
});
|
||||||
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
|
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
|
||||||
execute: () => this.verifySketch(true),
|
execute: () => this.verifySketch({ exportBinaries: true }),
|
||||||
isEnabled: () => !this.verifyInProgress,
|
isEnabled: () => !this.verifyInProgress,
|
||||||
});
|
});
|
||||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
|
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
|
||||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
isVisible: (widget) =>
|
||||||
|
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||||
isEnabled: () => !this.verifyInProgress,
|
isEnabled: () => !this.verifyInProgress,
|
||||||
isToggled: () => this.verifyInProgress,
|
isToggled: () => this.verifyInProgress,
|
||||||
execute: () => registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id)
|
execute: () =>
|
||||||
|
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||||
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
|
commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
|
||||||
label: 'Verify/Compile',
|
label: nls.localize('arduino/sketch/verifyOrCompile', 'Verify/Compile'),
|
||||||
order: '0'
|
order: '0',
|
||||||
});
|
});
|
||||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||||
commandId: VerifySketch.Commands.EXPORT_BINARIES.id,
|
commandId: VerifySketch.Commands.EXPORT_BINARIES.id,
|
||||||
label: 'Export compiled Binary',
|
label: nls.localize(
|
||||||
order: '3'
|
'arduino/sketch/exportBinary',
|
||||||
|
'Export Compiled Binary'
|
||||||
|
),
|
||||||
|
order: '4',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerKeybindings(registry: KeybindingRegistry): void {
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: VerifySketch.Commands.VERIFY_SKETCH.id,
|
command: VerifySketch.Commands.VERIFY_SKETCH.id,
|
||||||
keybinding: 'CtrlCmd+R'
|
keybinding: 'CtrlCmd+R',
|
||||||
});
|
});
|
||||||
registry.registerKeybinding({
|
registry.registerKeybinding({
|
||||||
command: VerifySketch.Commands.EXPORT_BINARIES.id,
|
command: VerifySketch.Commands.EXPORT_BINARIES.id,
|
||||||
keybinding: 'CtrlCmd+Alt+S'
|
keybinding: 'CtrlCmd+Alt+S',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||||
registry.registerItem({
|
registry.registerItem({
|
||||||
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
|
id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
|
||||||
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
|
command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
|
||||||
tooltip: 'Verify',
|
tooltip: nls.localize('arduino/sketch/verify', 'Verify'),
|
||||||
priority: 0,
|
priority: 0,
|
||||||
onDidChange: this.onDidChange
|
onDidChange: this.onDidChange,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifySketch(exportBinaries?: boolean): Promise<void> {
|
protected override handleError(error: unknown): void {
|
||||||
|
this.coreErrorHandler.tryHandle(error);
|
||||||
// even with buttons disabled, better to double check if a verify is already in progress
|
super.handleError(error);
|
||||||
if (this.verifyInProgress) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggle the toolbar button and menu item state.
|
private async verifySketch(
|
||||||
// verifyInProgress will be set to false whether the compilation fails or not
|
params?: VerifySketchParams
|
||||||
|
): Promise<CoreService.Options.Compile | undefined> {
|
||||||
|
if (this.verifyInProgress) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!params?.silent) {
|
||||||
this.verifyInProgress = true;
|
this.verifyInProgress = true;
|
||||||
this.onDidChangeEmitter.fire();
|
this.onDidChangeEmitter.fire();
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
|
||||||
|
|
||||||
if (!sketch) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
this.coreErrorHandler.reset();
|
||||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
|
||||||
const [fqbn, sourceOverride] = await Promise.all([
|
const options = await this.options(params?.exportBinaries);
|
||||||
|
if (!options) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.doWithProgress({
|
||||||
|
progressText: nls.localize(
|
||||||
|
'arduino/sketch/compile',
|
||||||
|
'Compiling sketch...'
|
||||||
|
),
|
||||||
|
task: (progressId, coreService) =>
|
||||||
|
coreService.compile({
|
||||||
|
...options,
|
||||||
|
progressId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this.messageService.info(
|
||||||
|
nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
// Returns with the used options for the compilation
|
||||||
|
// so that follow-up tasks (such as upload) can reuse the compiled code.
|
||||||
|
// Note that the `fqbn` is already decorated with the board settings, if any.
|
||||||
|
return options;
|
||||||
|
} catch (e) {
|
||||||
|
this.handleError(e);
|
||||||
|
return undefined;
|
||||||
|
} finally {
|
||||||
|
this.verifyInProgress = false;
|
||||||
|
if (!params?.silent) {
|
||||||
|
this.onDidChangeEmitter.fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async options(
|
||||||
|
exportBinaries?: boolean
|
||||||
|
): Promise<CoreService.Options.Compile | undefined> {
|
||||||
|
const sketch = await this.sketchServiceClient.currentSketch();
|
||||||
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const { boardsConfig } = this.boardsServiceProvider;
|
||||||
|
const [fqbn, sourceOverride, optimizeForDebug] = await Promise.all([
|
||||||
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
|
this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
|
||||||
this.sourceOverride()
|
this.sourceOverride(),
|
||||||
|
this.commandService.executeCommand<boolean>(
|
||||||
|
'arduino-is-optimize-for-debug'
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
const verbose = this.preferences.get('arduino.compile.verbose');
|
const verbose = this.preferences.get('arduino.compile.verbose');
|
||||||
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
|
const compilerWarnings = this.preferences.get('arduino.compile.warnings');
|
||||||
this.outputChannelManager.getChannel('Arduino').clear();
|
return {
|
||||||
await this.coreService.compile({
|
sketch,
|
||||||
sketchUri: sketch.uri,
|
|
||||||
fqbn,
|
fqbn,
|
||||||
optimizeForDebug: this.editorMode.compileForDebug,
|
optimizeForDebug: Boolean(optimizeForDebug),
|
||||||
verbose,
|
verbose,
|
||||||
exportBinaries,
|
exportBinaries,
|
||||||
sourceOverride,
|
sourceOverride,
|
||||||
compilerWarnings
|
compilerWarnings,
|
||||||
});
|
};
|
||||||
this.messageService.info('Done compiling.', { timeout: 3000 });
|
|
||||||
} catch (e) {
|
|
||||||
this.messageService.error(e.toString());
|
|
||||||
} finally {
|
|
||||||
this.verifyInProgress = false;
|
|
||||||
this.onDidChangeEmitter.fire();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace VerifySketch {
|
export namespace VerifySketch {
|
||||||
export namespace Commands {
|
export namespace Commands {
|
||||||
export const VERIFY_SKETCH: Command = {
|
export const VERIFY_SKETCH: Command = {
|
||||||
id: 'arduino-verify-sketch'
|
id: 'arduino-verify-sketch',
|
||||||
};
|
};
|
||||||
export const EXPORT_BINARIES: Command = {
|
export const EXPORT_BINARIES: Command = {
|
||||||
id: 'arduino-export-binaries'
|
id: 'arduino-export-binaries',
|
||||||
};
|
};
|
||||||
export const VERIFY_SKETCH_TOOLBAR: Command = {
|
export const VERIFY_SKETCH_TOOLBAR: Command = {
|
||||||
id: 'arduino-verify-sketch--toolbar'
|
id: 'arduino-verify-sketch--toolbar',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
534
arduino-ide-extension/src/browser/create/create-api.ts
Normal file
534
arduino-ide-extension/src/browser/create/create-api.ts
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||||
|
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 {
|
||||||
|
(response: Response): Promise<any>;
|
||||||
|
}
|
||||||
|
export namespace ResponseResultProvider {
|
||||||
|
export const NOOP: ResponseResultProvider = async () => undefined;
|
||||||
|
export const TEXT: ResponseResultProvider = (response) => response.text();
|
||||||
|
export const JSON: ResponseResultProvider = (response) => response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Utf8ArrayToStr(array: Uint8Array): string {
|
||||||
|
let out, i, c;
|
||||||
|
let char2, char3;
|
||||||
|
|
||||||
|
out = '';
|
||||||
|
const len = array.length;
|
||||||
|
i = 0;
|
||||||
|
while (i < len) {
|
||||||
|
c = array[i++];
|
||||||
|
switch (c >> 4) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
// 0xxxxxxx
|
||||||
|
out += String.fromCharCode(c);
|
||||||
|
break;
|
||||||
|
case 12:
|
||||||
|
case 13:
|
||||||
|
// 110x xxxx 10xx xxxx
|
||||||
|
char2 = array[i++];
|
||||||
|
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
|
||||||
|
break;
|
||||||
|
case 14:
|
||||||
|
// 1110 xxxx 10xx xxxx 10xx xxxx
|
||||||
|
char2 = array[i++];
|
||||||
|
char3 = array[i++];
|
||||||
|
out += String.fromCharCode(
|
||||||
|
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
|
||||||
|
return {
|
||||||
|
href: `${sketch.href}${posix.sep}${Create.arduino_secrets_file}`,
|
||||||
|
modified_at: sketch.modified_at,
|
||||||
|
created_at: sketch.created_at,
|
||||||
|
name: `${Create.arduino_secrets_file}`,
|
||||||
|
path: `${sketch.path}${posix.sep}${Create.arduino_secrets_file}`,
|
||||||
|
mimetype: 'text/x-c++src; charset=utf-8',
|
||||||
|
type: 'file',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async sketch(id: string): Promise<Create.Sketch> {
|
||||||
|
const url = new URL(`${this.domain()}/sketches/byID/${id}`);
|
||||||
|
|
||||||
|
url.searchParams.set('user_id', 'me');
|
||||||
|
const headers = await this.headers();
|
||||||
|
const result = await this.run<Create.Sketch>(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sketches(limit = 50): Promise<Create.Sketch[]> {
|
||||||
|
const url = new URL(`${this.domain()}/sketches`);
|
||||||
|
url.searchParams.set('user_id', 'me');
|
||||||
|
url.searchParams.set('limit', limit.toString());
|
||||||
|
const headers = await this.headers();
|
||||||
|
const result: { sketches: Create.Sketch[] } = { sketches: [] };
|
||||||
|
|
||||||
|
let partialSketches: Create.Sketch[] = [];
|
||||||
|
let currentOffset = 0;
|
||||||
|
do {
|
||||||
|
url.searchParams.set('offset', currentOffset.toString());
|
||||||
|
partialSketches = (
|
||||||
|
await this.run<{ sketches: Create.Sketch[] }>(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
).sketches;
|
||||||
|
if (partialSketches.length !== 0) {
|
||||||
|
result.sketches = result.sketches.concat(partialSketches);
|
||||||
|
}
|
||||||
|
currentOffset = currentOffset + limit;
|
||||||
|
} while (partialSketches.length !== 0);
|
||||||
|
|
||||||
|
result.sketches.forEach((sketch) => this.sketchCache.addSketch(sketch));
|
||||||
|
return result.sketches;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSketch(
|
||||||
|
posixPath: string,
|
||||||
|
content: string = CreateApi.defaultInoContent
|
||||||
|
): Promise<Create.Sketch> {
|
||||||
|
const url = new URL(`${this.domain()}/sketches`);
|
||||||
|
const headers = await this.headers();
|
||||||
|
const payload = {
|
||||||
|
ino: btoa(content),
|
||||||
|
path: posixPath,
|
||||||
|
user_id: 'me',
|
||||||
|
};
|
||||||
|
const init = {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
const result = await this.run<Create.Sketch>(url, init);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async readDirectory(
|
||||||
|
posixPath: string,
|
||||||
|
options: {
|
||||||
|
recursive?: boolean;
|
||||||
|
match?: string;
|
||||||
|
skipSketchCache?: boolean;
|
||||||
|
} = {}
|
||||||
|
): Promise<Create.Resource[]> {
|
||||||
|
const url = new URL(
|
||||||
|
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
|
||||||
|
);
|
||||||
|
if (options.recursive) {
|
||||||
|
url.searchParams.set('deep', 'true');
|
||||||
|
}
|
||||||
|
if (options.match) {
|
||||||
|
url.searchParams.set('name_like', options.match);
|
||||||
|
}
|
||||||
|
const headers = await this.headers();
|
||||||
|
|
||||||
|
const cachedSketch = this.sketchCache.getSketch(posixPath);
|
||||||
|
|
||||||
|
const sketchPromise = options.skipSketchCache
|
||||||
|
? (cachedSketch && this.sketch(cachedSketch.id)) || Promise.resolve(null)
|
||||||
|
: Promise.resolve(this.sketchCache.getSketch(posixPath));
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
sketchPromise,
|
||||||
|
this.run<Create.RawResource[]>(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.then(async ([sketch, result]) => {
|
||||||
|
if (posixPath.length && posixPath !== posix.sep) {
|
||||||
|
if (sketch && sketch.secrets && sketch.secrets.length > 0) {
|
||||||
|
result.push(this.getSketchSecretStat(sketch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.filter(
|
||||||
|
(res) => !Create.do_not_sync_files.includes(res.name)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((reason) => {
|
||||||
|
if (reason?.status === 404) return [] as Create.Resource[];
|
||||||
|
else throw reason;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDirectory(posixPath: string): Promise<void> {
|
||||||
|
const url = new URL(
|
||||||
|
`${this.domain()}/files/d/$HOME/sketches_v2${posixPath}`
|
||||||
|
);
|
||||||
|
const headers = await this.headers();
|
||||||
|
await this.run(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stat(posixPath: string): Promise<Create.Resource> {
|
||||||
|
// The root is a directory read.
|
||||||
|
if (posixPath === '/') {
|
||||||
|
throw new Error('Stating the root is not supported');
|
||||||
|
}
|
||||||
|
// The RESTful API has different endpoints for files and directories.
|
||||||
|
// The RESTful API does not provide specific error codes, only HTP 500.
|
||||||
|
// We query the parent directory and look for the file with the last segment.
|
||||||
|
const parentPosixPath = createPaths.parentPosix(posixPath);
|
||||||
|
const basename = createPaths.basename(posixPath);
|
||||||
|
|
||||||
|
let resources;
|
||||||
|
if (basename === Create.arduino_secrets_file) {
|
||||||
|
const sketch = this.sketchCache.getSketch(parentPosixPath);
|
||||||
|
resources = sketch ? [this.getSketchSecretStat(sketch)] : [];
|
||||||
|
} else {
|
||||||
|
resources = await this.readDirectory(parentPosixPath, {
|
||||||
|
match: basename,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const resource = resources.find(
|
||||||
|
({ path }) => createPaths.splitSketchPath(path)[1] === posixPath
|
||||||
|
);
|
||||||
|
if (!resource) {
|
||||||
|
throw new CreateError(`Not found: ${posixPath}.`, 404);
|
||||||
|
}
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async toggleSecretsInclude(
|
||||||
|
path: string,
|
||||||
|
data: string,
|
||||||
|
mode: 'add' | 'remove'
|
||||||
|
) {
|
||||||
|
const includeString = `#include "${Create.arduino_secrets_file}"`;
|
||||||
|
const includeRegexp = new RegExp(includeString + '\\s*', 'g');
|
||||||
|
|
||||||
|
const basename = createPaths.basename(path);
|
||||||
|
if (mode === 'add') {
|
||||||
|
const doesIncludeSecrets = includeRegexp.test(data);
|
||||||
|
|
||||||
|
if (doesIncludeSecrets) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sketch = this.sketchCache.getSketch(createPaths.parentPosix(path));
|
||||||
|
|
||||||
|
if (
|
||||||
|
sketch &&
|
||||||
|
(sketch.name + '.ino' === basename ||
|
||||||
|
sketch.name + '.pde' === basename) &&
|
||||||
|
sketch.secrets &&
|
||||||
|
sketch.secrets.length > 0
|
||||||
|
) {
|
||||||
|
return includeString + '\n' + data;
|
||||||
|
}
|
||||||
|
} else if (mode === 'remove') {
|
||||||
|
return data.replace(includeRegexp, '');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(posixPath: string): Promise<string> {
|
||||||
|
const basename = createPaths.basename(posixPath);
|
||||||
|
|
||||||
|
if (basename === Create.arduino_secrets_file) {
|
||||||
|
const parentPosixPath = createPaths.parentPosix(posixPath);
|
||||||
|
|
||||||
|
//retrieve the sketch id from the cache
|
||||||
|
const cacheSketch = this.sketchCache.getSketch(parentPosixPath);
|
||||||
|
if (!cacheSketch) {
|
||||||
|
throw new Error(`Unable to find sketch ${parentPosixPath} in cache`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a fresh copy of the sketch in order to guarantee fresh secrets
|
||||||
|
const sketch = await this.sketch(cacheSketch.id);
|
||||||
|
if (!sketch) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to get a fresh copy of the sketch ${cacheSketch.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.sketchCache.addSketch(sketch);
|
||||||
|
|
||||||
|
let file = '';
|
||||||
|
if (sketch && sketch.secrets) {
|
||||||
|
for (const item of sketch.secrets) {
|
||||||
|
file += `#define ${item.name} "${item.value}"\r\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(
|
||||||
|
`${this.domain()}/files/f/$HOME/sketches_v2${posixPath}`
|
||||||
|
);
|
||||||
|
const headers = await this.headers();
|
||||||
|
const result = await this.run<{ data: string; path: string }>(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
let { data } = result;
|
||||||
|
|
||||||
|
// add includes to main arduino file
|
||||||
|
data = await this.toggleSecretsInclude(posixPath, atob(data), 'add');
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(
|
||||||
|
posixPath: string,
|
||||||
|
content: string | Uint8Array
|
||||||
|
): Promise<void> {
|
||||||
|
const basename = createPaths.basename(posixPath);
|
||||||
|
|
||||||
|
if (basename === Create.arduino_secrets_file) {
|
||||||
|
const parentPosixPath = createPaths.parentPosix(posixPath);
|
||||||
|
|
||||||
|
const sketch = this.sketchCache.getSketch(parentPosixPath);
|
||||||
|
|
||||||
|
if (sketch) {
|
||||||
|
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
|
||||||
|
const headers = await this.headers();
|
||||||
|
|
||||||
|
// parse the secret file
|
||||||
|
const secrets = (
|
||||||
|
typeof content === 'string' ? content : Utf8ArrayToStr(content)
|
||||||
|
)
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.reduce((prev, curr) => {
|
||||||
|
// check if the line contains a secret
|
||||||
|
const secret = curr.split('SECRET_')[1] || null;
|
||||||
|
if (!secret) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const regexp = /(\S*)\s+([\S\s]*)/g;
|
||||||
|
const tokens = regexp.exec(secret) || [];
|
||||||
|
const name = tokens[1].length > 0 ? `SECRET_${tokens[1]}` : '';
|
||||||
|
|
||||||
|
let value = '';
|
||||||
|
if (tokens[2].length > 0) {
|
||||||
|
value = JSON.parse(
|
||||||
|
JSON.stringify(
|
||||||
|
tokens[2].replace(/^['"]?/g, '').replace(/['"]?$/g, '')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.length === 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...prev, { name, value }];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
id: sketch.id,
|
||||||
|
libraries: sketch.libraries,
|
||||||
|
secrets: { data: secrets },
|
||||||
|
};
|
||||||
|
|
||||||
|
// replace the sketch in the cache with the one we are pushing
|
||||||
|
// TODO: we should do a get after the POST, in order to be sure the cache
|
||||||
|
// is updated the most recent metadata
|
||||||
|
this.sketchCache.addSketch(sketch);
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
await this.run(url, init);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not upload "do_not_sync" files/directoris and their descendants
|
||||||
|
const segments = posixPath.split(posix.sep) || [];
|
||||||
|
if (
|
||||||
|
segments.some((segment) => Create.do_not_sync_files.includes(segment))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(
|
||||||
|
`${this.domain()}/files/f/$HOME/sketches_v2${posixPath}`
|
||||||
|
);
|
||||||
|
const headers = await this.headers();
|
||||||
|
|
||||||
|
let data: string =
|
||||||
|
typeof content === 'string' ? content : Utf8ArrayToStr(content);
|
||||||
|
data = await this.toggleSecretsInclude(posixPath, data, 'remove');
|
||||||
|
|
||||||
|
const payload = { data: btoa(data) };
|
||||||
|
const init = {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
await this.run(url, init);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(posixPath: string): Promise<void> {
|
||||||
|
await this.delete(posixPath, 'f');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDirectory(posixPath: string): Promise<void> {
|
||||||
|
await this.delete(posixPath, 'd');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async delete(posixPath: string, type: ResourceType): Promise<void> {
|
||||||
|
const url = new URL(
|
||||||
|
`${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}`
|
||||||
|
);
|
||||||
|
const headers = await this.headers();
|
||||||
|
await this.run(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async rename(fromPosixPath: string, toPosixPath: string): Promise<void> {
|
||||||
|
const url = new URL(`${this.domain('v3')}/files/mv`);
|
||||||
|
const headers = await this.headers();
|
||||||
|
const payload = {
|
||||||
|
from: `$HOME/sketches_v2${fromPosixPath}`,
|
||||||
|
to: `$HOME/sketches_v2${toPosixPath}`,
|
||||||
|
};
|
||||||
|
const init = {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
await this.run(url, init, ResponseResultProvider.NOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
async editSketch({
|
||||||
|
id,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
}): Promise<Create.Sketch> {
|
||||||
|
const url = new URL(`${this.domain()}/sketches/${id}`);
|
||||||
|
|
||||||
|
const headers = await this.headers();
|
||||||
|
const result = await this.run<Create.Sketch>(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ id, ...params }),
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async copy(fromPosixPath: string, toPosixPath: string): Promise<void> {
|
||||||
|
const payload = {
|
||||||
|
from: `$HOME/sketches_v2${fromPosixPath}`,
|
||||||
|
to: `$HOME/sketches_v2${toPosixPath}`,
|
||||||
|
};
|
||||||
|
const url = new URL(`${this.domain('v3')}/files/cp`);
|
||||||
|
const headers = await this.headers();
|
||||||
|
const init = {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
await this.run(url, init, ResponseResultProvider.NOOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async run<T>(
|
||||||
|
requestInfo: RequestInfo | URL,
|
||||||
|
init: RequestInit | undefined,
|
||||||
|
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await fetch(
|
||||||
|
requestInfo instanceof URL ? requestInfo.toString() : requestInfo,
|
||||||
|
init
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
let details: string | undefined = undefined;
|
||||||
|
try {
|
||||||
|
details = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Cloud not get the error details.', e);
|
||||||
|
}
|
||||||
|
const { statusText, status } = response;
|
||||||
|
throw new CreateError(statusText, status, details);
|
||||||
|
}
|
||||||
|
const result = await resultProvider(response);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async headers(): Promise<Record<string, string>> {
|
||||||
|
const token = await this.token();
|
||||||
|
return {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
accept: 'application/json',
|
||||||
|
authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private domain(apiVersion = 'v2'): string {
|
||||||
|
const endpoint =
|
||||||
|
this.arduinoPreferences['arduino.cloud.sketchSyncEndpoint'];
|
||||||
|
return `${endpoint}/${apiVersion}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async token(): Promise<string> {
|
||||||
|
return this.authenticationService.session?.accessToken || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace CreateApi {
|
||||||
|
export const defaultInoContent = `/*
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
|
}
|
||||||
198
arduino-ide-extension/src/browser/create/create-fs-provider.ts
Normal file
198
arduino-ide-extension/src/browser/create/create-fs-provider.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import URI from '@theia/core/lib/common/uri';
|
||||||
|
import { Event } from '@theia/core/lib/common/event';
|
||||||
|
import {
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
|
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||||
|
import {
|
||||||
|
Stat,
|
||||||
|
FileType,
|
||||||
|
FileChange,
|
||||||
|
FileWriteOptions,
|
||||||
|
FileDeleteOptions,
|
||||||
|
FileOverwriteOptions,
|
||||||
|
FileSystemProvider,
|
||||||
|
FileSystemProviderError,
|
||||||
|
FileSystemProviderErrorCode,
|
||||||
|
FileSystemProviderCapabilities,
|
||||||
|
WatchOptions,
|
||||||
|
} from '@theia/filesystem/lib/common/files';
|
||||||
|
import {
|
||||||
|
FileService,
|
||||||
|
FileServiceContribution,
|
||||||
|
} from '@theia/filesystem/lib/browser/file-service';
|
||||||
|
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||||
|
import { CreateApi } from './create-api';
|
||||||
|
import { CreateUri } from './create-uri';
|
||||||
|
import { SketchesService } from '../../common/protocol';
|
||||||
|
import { ArduinoPreferences } from '../arduino-preferences';
|
||||||
|
import { Create } from './typings';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class CreateFsProvider
|
||||||
|
implements
|
||||||
|
FileSystemProvider,
|
||||||
|
FrontendApplicationContribution,
|
||||||
|
FileServiceContribution
|
||||||
|
{
|
||||||
|
@inject(AuthenticationClientService)
|
||||||
|
protected readonly authenticationService: AuthenticationClientService;
|
||||||
|
|
||||||
|
@inject(CreateApi)
|
||||||
|
protected readonly createApi: CreateApi;
|
||||||
|
|
||||||
|
@inject(SketchesService)
|
||||||
|
protected readonly sketchesService: SketchesService;
|
||||||
|
|
||||||
|
@inject(ArduinoPreferences)
|
||||||
|
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||||
|
|
||||||
|
protected readonly toDispose = new DisposableCollection();
|
||||||
|
|
||||||
|
readonly onFileWatchError: Event<void> = Event.None;
|
||||||
|
readonly onDidChangeFile: Event<readonly FileChange[]> = Event.None;
|
||||||
|
readonly onDidChangeCapabilities: Event<void> = Event.None;
|
||||||
|
readonly capabilities: FileSystemProviderCapabilities =
|
||||||
|
FileSystemProviderCapabilities.FileReadWrite |
|
||||||
|
FileSystemProviderCapabilities.PathCaseSensitive |
|
||||||
|
FileSystemProviderCapabilities.Access;
|
||||||
|
|
||||||
|
onStop(): void {
|
||||||
|
this.toDispose.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
registerFileSystemProviders(service: FileService): void {
|
||||||
|
service.onWillActivateFileSystemProvider((event) => {
|
||||||
|
if (event.scheme === CreateUri.scheme) {
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
service.registerProvider(CreateUri.scheme, this);
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(uri: URI, opts: WatchOptions): Disposable {
|
||||||
|
return Disposable.NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async stat(uri: URI): Promise<Stat> {
|
||||||
|
if (CreateUri.equals(CreateUri.root, uri)) {
|
||||||
|
this.getCreateApi; // This will throw when not logged in.
|
||||||
|
return {
|
||||||
|
type: FileType.Directory,
|
||||||
|
ctime: 0,
|
||||||
|
mtime: 0,
|
||||||
|
size: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const resource = await this.getCreateApi.stat(uri.path.toString());
|
||||||
|
const mtime = Date.parse(resource.modified_at);
|
||||||
|
return {
|
||||||
|
type: this.toFileType(resource.type),
|
||||||
|
ctime: mtime,
|
||||||
|
mtime,
|
||||||
|
size: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async mkdir(uri: URI): Promise<void> {
|
||||||
|
await this.getCreateApi.createDirectory(uri.path.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
async readdir(uri: URI): Promise<[string, FileType][]> {
|
||||||
|
const resources = await this.getCreateApi.readDirectory(
|
||||||
|
uri.path.toString()
|
||||||
|
);
|
||||||
|
return resources.map(({ name, type }) => [name, this.toFileType(type)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(uri: URI, opts: FileDeleteOptions): Promise<void> {
|
||||||
|
if (!opts.recursive) {
|
||||||
|
throw new Error(
|
||||||
|
'Arduino Create file-system provider does not support non-recursive deletion.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const stat = await this.stat(uri);
|
||||||
|
if (!stat) {
|
||||||
|
throw new FileSystemProviderError(
|
||||||
|
'File not found.',
|
||||||
|
FileSystemProviderErrorCode.FileNotFound
|
||||||
|
);
|
||||||
|
}
|
||||||
|
switch (stat.type) {
|
||||||
|
case FileType.Directory: {
|
||||||
|
await this.getCreateApi.deleteDirectory(uri.path.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FileType.File: {
|
||||||
|
await this.getCreateApi.deleteFile(uri.path.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new FileSystemProviderError(
|
||||||
|
`Unexpected file type '${stat.type}' for resource: ${uri.toString()}`,
|
||||||
|
FileSystemProviderErrorCode.Unknown
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rename(
|
||||||
|
oldUri: URI,
|
||||||
|
newUri: URI,
|
||||||
|
options: FileOverwriteOptions
|
||||||
|
): Promise<void> {
|
||||||
|
await this.getCreateApi.rename(
|
||||||
|
oldUri.path.toString(),
|
||||||
|
newUri.path.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(uri: URI): Promise<Uint8Array> {
|
||||||
|
const content = await this.getCreateApi.readFile(uri.path.toString());
|
||||||
|
return new TextEncoder().encode(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(
|
||||||
|
uri: URI,
|
||||||
|
content: Uint8Array,
|
||||||
|
options: FileWriteOptions
|
||||||
|
): Promise<void> {
|
||||||
|
await this.getCreateApi.writeFile(uri.path.toString(), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async access(uri: URI, mode?: number): Promise<void> {
|
||||||
|
this.getCreateApi; // Will throw if not logged in.
|
||||||
|
}
|
||||||
|
|
||||||
|
public toFileType(type: Create.ResourceType): FileType {
|
||||||
|
switch (type) {
|
||||||
|
case 'file':
|
||||||
|
return FileType.File;
|
||||||
|
case 'sketch':
|
||||||
|
case 'folder':
|
||||||
|
return FileType.Directory;
|
||||||
|
default:
|
||||||
|
return FileType.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get getCreateApi(): CreateApi {
|
||||||
|
const { session } = this.authenticationService;
|
||||||
|
if (!session) {
|
||||||
|
throw new FileSystemProviderError(
|
||||||
|
'Not logged in.',
|
||||||
|
FileSystemProviderErrorCode.NoPermissions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createApi.init(
|
||||||
|
this.authenticationService,
|
||||||
|
this.arduinoPreferences
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
arduino-ide-extension/src/browser/create/create-paths.ts
Normal file
59
arduino-ide-extension/src/browser/create/create-paths.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
export const posix = { sep: '/' };
|
||||||
|
|
||||||
|
// TODO: poor man's `path.join(path, '..')` in the browser.
|
||||||
|
export function parentPosix(path: string): string {
|
||||||
|
const segments = path.split(posix.sep) || [];
|
||||||
|
segments.pop();
|
||||||
|
let modified = segments.join(posix.sep);
|
||||||
|
if (path.charAt(path.length - 1) === posix.sep) {
|
||||||
|
modified += posix.sep;
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function basename(path: string): string {
|
||||||
|
const segments = path.split(posix.sep) || [];
|
||||||
|
return segments.pop()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function posixSegments(posixPath: string): string[] {
|
||||||
|
return posixPath.split(posix.sep).filter((segment) => !!segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the `raw` path into two segments, a root that contains user information and the relevant POSIX path. \
|
||||||
|
* For examples:
|
||||||
|
* ```
|
||||||
|
* `29ad0829759028dde9b877343fa3b0e1:testrest/sketches_v2/xxx_folder/xxx_sub_folder/sketch_in_folder/sketch_in_folder.ino`
|
||||||
|
* ```
|
||||||
|
* will be:
|
||||||
|
* ```
|
||||||
|
* ['29ad0829759028dde9b877343fa3b0e1:testrest/sketches_v2', '/xxx_folder/xxx_sub_folder/sketch_in_folder/sketch_in_folder.ino']
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function splitSketchPath(
|
||||||
|
raw: string,
|
||||||
|
sep = '/sketches_v2/'
|
||||||
|
): [string, string] {
|
||||||
|
if (!sep) {
|
||||||
|
throw new Error('Invalid separator. Cannot be zero length.');
|
||||||
|
}
|
||||||
|
const index = raw.indexOf(sep);
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error(`Invalid path pattern. Raw path was '${raw}'.`);
|
||||||
|
}
|
||||||
|
const createRoot = raw.substring(0, index + sep.length - 1); // TODO: validate the `createRoot` format.
|
||||||
|
const posixPath = raw.substr(index + sep.length - 1);
|
||||||
|
if (!posixPath) {
|
||||||
|
throw new Error(`Could not extract POSIX path from '${raw}'.`);
|
||||||
|
}
|
||||||
|
return [createRoot, posixPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toPosixPath(raw: string): string {
|
||||||
|
if (raw === posix.sep) {
|
||||||
|
return posix.sep; // Handles the root resource case.
|
||||||
|
}
|
||||||
|
const [, posixPath] = splitSketchPath(raw);
|
||||||
|
return posixPath;
|
||||||
|
}
|
||||||
37
arduino-ide-extension/src/browser/create/create-uri.ts
Normal file
37
arduino-ide-extension/src/browser/create/create-uri.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { URI as Uri } from 'vscode-uri';
|
||||||
|
import URI from '@theia/core/lib/common/uri';
|
||||||
|
import { toPosixPath, parentPosix, posix } from './create-paths';
|
||||||
|
import { Create } from './typings';
|
||||||
|
|
||||||
|
export namespace CreateUri {
|
||||||
|
export const scheme = 'arduino-create';
|
||||||
|
export const root = toUri(posix.sep);
|
||||||
|
|
||||||
|
export function toUri(posixPathOrResource: string | Create.Resource): URI {
|
||||||
|
const posixPath =
|
||||||
|
typeof posixPathOrResource === 'string'
|
||||||
|
? posixPathOrResource
|
||||||
|
: toPosixPath(posixPathOrResource.path);
|
||||||
|
return new URI(Uri.parse(posixPath).with({ scheme, authority: 'create' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function is(uri: URI): boolean {
|
||||||
|
return uri.scheme === scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function equals(left: URI, right: URI): boolean {
|
||||||
|
return is(left) && is(right) && left.toString() === right.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parent(uri: URI): URI {
|
||||||
|
if (!is(uri)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid URI scheme. Expected '${scheme}' got '${uri.scheme}' instead.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (equals(uri, root)) {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
return toUri(parentPosix(uri.path.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
73
arduino-ide-extension/src/browser/create/typings.ts
Normal file
73
arduino-ide-extension/src/browser/create/typings.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
export namespace Create {
|
||||||
|
export interface Sketch {
|
||||||
|
readonly name: string;
|
||||||
|
readonly path: string;
|
||||||
|
readonly modified_at: string;
|
||||||
|
readonly created_at: string;
|
||||||
|
|
||||||
|
readonly secrets?: { name: string; value: string }[];
|
||||||
|
|
||||||
|
readonly id: string;
|
||||||
|
readonly is_public: boolean;
|
||||||
|
readonly board_fqbn: '';
|
||||||
|
readonly board_name: '';
|
||||||
|
readonly board_type: 'serial' | 'network' | 'cloud' | '';
|
||||||
|
readonly href?: string;
|
||||||
|
readonly libraries: string[];
|
||||||
|
readonly tutorials: string[] | null;
|
||||||
|
readonly types: string[] | null;
|
||||||
|
readonly user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResourceType = 'sketch' | 'folder' | 'file';
|
||||||
|
export const arduino_secrets_file = 'arduino_secrets.h';
|
||||||
|
export const do_not_sync_files = ['.theia', 'sketch.json'];
|
||||||
|
export interface Resource {
|
||||||
|
readonly name: string;
|
||||||
|
/**
|
||||||
|
* Note: this path is **not** the POSIX path we use. It has the leading segments with the `user_id`.
|
||||||
|
*/
|
||||||
|
readonly path: string;
|
||||||
|
readonly type: ResourceType;
|
||||||
|
readonly sketchId?: string;
|
||||||
|
readonly modified_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
|
||||||
|
readonly created_at: string; // As an ISO-8601 formatted string: `YYYY-MM-DDTHH:mm:ss.sssZ`
|
||||||
|
readonly children?: number; // For 'sketch' and 'folder' types.
|
||||||
|
readonly size?: number; // For 'sketch' type only.
|
||||||
|
readonly isPublic?: boolean; // For 'sketch' type only.
|
||||||
|
|
||||||
|
readonly mimetype?: string; // For 'file' type.
|
||||||
|
readonly href?: string;
|
||||||
|
}
|
||||||
|
export namespace Resource {
|
||||||
|
export function is(arg: any): arg is Resource {
|
||||||
|
return (
|
||||||
|
!!arg &&
|
||||||
|
'name' in arg &&
|
||||||
|
typeof arg['name'] === 'string' &&
|
||||||
|
'path' in arg &&
|
||||||
|
typeof arg['path'] === 'string' &&
|
||||||
|
'type' in arg &&
|
||||||
|
typeof arg['type'] === 'string' &&
|
||||||
|
'modified_at' in arg &&
|
||||||
|
typeof arg['modified_at'] === 'string' &&
|
||||||
|
(arg['type'] === 'sketch' ||
|
||||||
|
arg['type'] === 'folder' ||
|
||||||
|
arg['type'] === 'file')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RawResource = Omit<Resource, 'sketchId' | 'isPublic'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
readonly status: number,
|
||||||
|
readonly details?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, CreateError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user