mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-16 13:49:28 +00:00
Compare commits
493 Commits
2.0.0-beta
...
fspissu/po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f84c15bea0 | ||
|
|
fe3fbb189c | ||
|
|
23c7f5f848 | ||
|
|
f1144efb93 | ||
|
|
9cec643cab | ||
|
|
1a7784a540 | ||
|
|
d24a3911f8 | ||
|
|
3735553003 | ||
|
|
f6d112e1f6 | ||
|
|
cc2d557706 | ||
|
|
103acc4b7e | ||
|
|
c3dc7c6307 | ||
|
|
7d6a2d5e33 | ||
|
|
6984c52b92 | ||
|
|
3a70547770 | ||
|
|
8a85b5c3d8 | ||
|
|
b998d35524 | ||
|
|
ddec64c4a5 | ||
|
|
8fed08003e | ||
|
|
8454c625f7 | ||
|
|
60df322f09 | ||
|
|
8bfb140e7c | ||
|
|
260227e79a | ||
|
|
cc310bf1a5 | ||
|
|
dbd52e2f34 | ||
|
|
9cd03bec46 | ||
|
|
c29452a858 | ||
|
|
7d91f2d8cb | ||
|
|
f6275f9f62 | ||
|
|
0d0550974a | ||
|
|
4e882d25d9 | ||
|
|
f93f78039b | ||
|
|
2b2463b834 | ||
|
|
0773c3915c | ||
|
|
2f5afe0d9c | ||
|
|
b8370686ec | ||
|
|
3b2d12eff9 | ||
|
|
cdaaa5584d | ||
|
|
3476de27f7 | ||
|
|
b55cfc2052 | ||
|
|
44751c370b | ||
|
|
32d904ca36 | ||
|
|
5424dfcf70 | ||
|
|
b8bf1eefa2 | ||
|
|
93291b6811 | ||
|
|
87ebcbe77e | ||
|
|
99b10942bb | ||
|
|
960a2d0634 | ||
|
|
e577de4e8e | ||
|
|
f3ef95cfe2 | ||
|
|
bc264d1adf | ||
|
|
5444395f34 | ||
|
|
2d2be1f6d0 | ||
|
|
1e269ac83d | ||
|
|
0c49709f26 | ||
|
|
019b2d5588 | ||
|
|
aa0807ca3f | ||
|
|
61a11a0857 | ||
|
|
0c20ae0e28 | ||
|
|
945a8f4841 | ||
|
|
ae76432944 | ||
|
|
40807db65e | ||
|
|
da22f1ed11 | ||
|
|
32b70efd5c | ||
|
|
6f07717369 | ||
|
|
d6cb23f782 | ||
|
|
9ac2638335 | ||
|
|
96cf09d594 | ||
|
|
8380c82028 | ||
|
|
5eb2926407 | ||
|
|
a4ab204400 | ||
|
|
6416c431c6 | ||
|
|
8f88aa69bf | ||
|
|
3c2b2a0734 | ||
|
|
39538f163f | ||
|
|
9ef04bb8d6 | ||
|
|
707f3bef61 | ||
|
|
878395221a | ||
|
|
6a35bbfa7e | ||
|
|
42f6f43870 | ||
|
|
6983c5bf7f | ||
|
|
b3ab5cbd2a | ||
|
|
8a5995920a | ||
|
|
8de6cf84d9 | ||
|
|
f5c36bb691 | ||
|
|
364f8b8e51 | ||
|
|
671d2eabd4 | ||
|
|
9a65ef6ea8 | ||
|
|
4e590ab618 | ||
|
|
026e80e7fc | ||
|
|
fdf6f0f9c8 | ||
|
|
0151e4c224 | ||
|
|
e8b0ea4f2d | ||
|
|
7c1ca04c75 | ||
|
|
0ba88d5ab6 | ||
|
|
96e229d803 | ||
|
|
d07d83fdfe | ||
|
|
5f82577bc1 | ||
|
|
35fcfb89c1 | ||
|
|
6e3fe08c4c | ||
|
|
7f06b148f4 | ||
|
|
bf303d1b2f | ||
|
|
59ca91d805 | ||
|
|
69bb0aa385 | ||
|
|
565970e779 | ||
|
|
fec3b1138b | ||
|
|
dcc0c0aa5d | ||
|
|
76673cb553 | ||
|
|
8f95fd6ca6 | ||
|
|
4907ef2a47 | ||
|
|
9ae3402631 | ||
|
|
d0dfc656e6 | ||
|
|
df3a34eec6 | ||
|
|
20cc34ca9d | ||
|
|
1b7f86b231 | ||
|
|
0d545bea0e | ||
|
|
204d71b2dd | ||
|
|
5cb9166c83 | ||
|
|
7828cc11ac | ||
|
|
34a7fdb733 | ||
|
|
7c361cf2d1 | ||
|
|
8beade0867 | ||
|
|
3afc2d7e4b | ||
|
|
d40401437a | ||
|
|
10ac7fd50a | ||
|
|
07962e81d4 | ||
|
|
785775327b | ||
|
|
80dfa5b7dd | ||
|
|
40425d49e0 | ||
|
|
0c87fa9877 | ||
|
|
5b79320302 | ||
|
|
1da2dfc349 | ||
|
|
d7bbfc515d | ||
|
|
0c22884729 | ||
|
|
fc9107c084 | ||
|
|
474d5e5975 | ||
|
|
f7f644cf36 | ||
|
|
b5f9aa0f15 | ||
|
|
cc5cf3b165 | ||
|
|
125bd64c91 | ||
|
|
ca47e8a09a | ||
|
|
52804a5b52 | ||
|
|
3ec62642dd | ||
|
|
1281ad1932 | ||
|
|
de32bddc20 | ||
|
|
79ea0fa9a6 | ||
|
|
683219dc1c | ||
|
|
d674ab9b73 | ||
|
|
5be1f9d7fe | ||
|
|
9e2b73a045 | ||
|
|
75e00c2bae | ||
|
|
989300f25d | ||
|
|
5226636fed | ||
|
|
8b3f3c69fc | ||
|
|
a39ab47e70 | ||
|
|
9cabd40429 | ||
|
|
6e3681896c | ||
|
|
8a1cabd2bc | ||
|
|
7a3e6789d1 | ||
|
|
92bc5ecf7b | ||
|
|
aebec0f942 | ||
|
|
54db9bbce8 | ||
|
|
676eb2f588 | ||
|
|
ce273adf77 | ||
|
|
0b33b51700 | ||
|
|
36ac47b975 | ||
|
|
bf193b1cac | ||
|
|
879aedeaa3 | ||
|
|
d556ee95c0 | ||
|
|
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 | ||
|
|
cd0f1b3163 | ||
|
|
4fa2024266 | ||
|
|
852bf9b73e | ||
|
|
db48ed616b | ||
|
|
0dd1e45233 | ||
|
|
2a55ddd757 | ||
|
|
3240bf7f3d | ||
|
|
0d0ad9efae | ||
|
|
9aff90b0af | ||
|
|
8071298598 | ||
|
|
c86d82d273 | ||
|
|
fa9334eb7a | ||
|
|
c50d45c663 | ||
|
|
c20f832ddf | ||
|
|
cb2ef78c0a | ||
|
|
68af4c38fe | ||
|
|
a8df2444a9 | ||
|
|
d45dd6beef | ||
|
|
1ab5634789 | ||
|
|
80bddc238d | ||
|
|
8a692d0ce5 | ||
|
|
98671225ac | ||
|
|
f106c97f1e | ||
|
|
369a8f4307 | ||
|
|
4e7f8291e8 | ||
|
|
26a1db3cf8 | ||
|
|
a3f7b795a0 | ||
|
|
b422fc5298 | ||
|
|
ab320eb0b0 | ||
|
|
b17b7a6de7 | ||
|
|
562b77aec3 | ||
|
|
cdd5cfdfc1 | ||
|
|
f712ec986f | ||
|
|
c75b954041 | ||
|
|
3fb087f1ad | ||
|
|
27292774d7 | ||
|
|
da424f34cc |
64
.eslintrc.js
Normal file
64
.eslintrc.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
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/*',
|
||||||
|
'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://www.arduino.cc/en/software#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://www.arduino.cc/en/software#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.
|
|
||||||
19
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
19
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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: Issue report guide
|
||||||
|
url: https://github.com/arduino/arduino-ide/blob/main/docs/contributor-guide/issues.md#issue-report-guide
|
||||||
|
about: Learn about submitting issue reports to this repository.
|
||||||
|
- name: Contributor guide
|
||||||
|
url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide
|
||||||
|
about: Learn about contributing to this project.
|
||||||
|
- 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://www.arduino.cc/en/software#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://www.arduino.cc/en/software#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)
|
||||||
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# See: https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#about-the-dependabotyml-file
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
# Configure check for outdated GitHub Actions actions in workflows.
|
||||||
|
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/dependabot/README.md
|
||||||
|
# See: https://docs.github.com/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: / # Check the repository's workflows under /.github/workflows/
|
||||||
|
assignees:
|
||||||
|
- per1234
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
labels:
|
||||||
|
- "topic: infrastructure"
|
||||||
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
|
||||||
190
.github/workflows/build.yml
vendored
190
.github/workflows/build.yml
vendored
@@ -4,42 +4,80 @@ 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)
|
||||||
|
|
||||||
jobs:
|
env:
|
||||||
|
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||||
|
GO_VERSION: "1.17"
|
||||||
|
JOB_TRANSFER_ARTIFACT: build-artifacts
|
||||||
|
CHANGELOG_ARTIFACTS: changelog
|
||||||
|
|
||||||
|
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
|
||||||
- os: ubuntu-latest
|
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: 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
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Node.js 12.x
|
- name: Install Node.js 16.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '12.14.1'
|
node-version: '16.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@v4
|
||||||
with:
|
with:
|
||||||
python-version: '2.7'
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Taskfile
|
||||||
|
uses: arduino/setup-task@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
version: 3.x
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -47,41 +85,72 @@ 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
|
||||||
|
|
||||||
- name: Upload [GitHub Actions]
|
- name: Upload [GitHub Actions]
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||||
path: electron/build/dist/build-artifacts/
|
path: electron/build/dist/build-artifacts/
|
||||||
|
|
||||||
|
artifacts:
|
||||||
|
name: ${{ matrix.artifact.name }} artifact
|
||||||
|
needs: build
|
||||||
|
if: always() && needs.build.result != 'skipped'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
artifact:
|
||||||
|
- path: '*Linux_64bit.zip'
|
||||||
|
name: Linux_X86-64_zip
|
||||||
|
- path: '*Linux_64bit.AppImage'
|
||||||
|
name: Linux_X86-64_app_image
|
||||||
|
- path: '*macOS_64bit.dmg'
|
||||||
|
name: macOS_dmg
|
||||||
|
- path: '*macOS_64bit.zip'
|
||||||
|
name: macOS_zip
|
||||||
|
- path: '*Windows_64bit.exe'
|
||||||
|
name: Windows_X86-64_interactive_installer
|
||||||
|
- path: '*Windows_64bit.msi'
|
||||||
|
name: Windows_X86-64_MSI
|
||||||
|
- path: '*Windows_64bit.zip'
|
||||||
|
name: Windows_X86-64_zip
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download job transfer artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||||
|
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||||
|
|
||||||
|
- name: Upload tester build artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact.name }}
|
||||||
|
path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }}
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -89,7 +158,7 @@ jobs:
|
|||||||
BODY: ${{ steps.changelog.outputs.BODY }}
|
BODY: ${{ steps.changelog.outputs.BODY }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # To fetch all history for all branches and tags.
|
fetch-depth: 0 # To fetch all history for all branches and tags.
|
||||||
|
|
||||||
@@ -114,14 +183,14 @@ jobs:
|
|||||||
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
|
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
|
||||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
|
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
|
||||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
|
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
|
||||||
echo "::set-output name=BODY::$OUTPUT_SAFE_BODY"
|
echo "BODY=$OUTPUT_SAFE_BODY" >> $GITHUB_OUTPUT
|
||||||
echo "$BODY" > CHANGELOG.txt
|
echo "$BODY" > CHANGELOG.txt
|
||||||
|
|
||||||
- name: Upload Changelog [GitHub Actions]
|
- name: Upload Changelog [GitHub Actions]
|
||||||
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
|
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||||
path: CHANGELOG.txt
|
path: CHANGELOG.txt
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
@@ -130,53 +199,70 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download [GitHub Actions]
|
- name: Download [GitHub Actions]
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||||
path: build-artifacts
|
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||||
|
|
||||||
- name: Publish Nightly [S3]
|
- name: Publish Nightly [S3]
|
||||||
uses: docker://plugins/s3
|
uses: docker://plugins/s3
|
||||||
env:
|
env:
|
||||||
PLUGIN_SOURCE: "build-artifacts/*"
|
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
|
||||||
PLUGIN_STRIP_PREFIX: "build-artifacts/"
|
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]
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||||
path: build-artifacts
|
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||||
|
|
||||||
- name: Get Tag
|
- name: Get Tag
|
||||||
id: tag_name
|
id: tag_name
|
||||||
run: |
|
run: |
|
||||||
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
|
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Publish Release [GitHub]
|
- name: Publish Release [GitHub]
|
||||||
uses: svenstaro/upload-release-action@2.2.0
|
uses: svenstaro/upload-release-action@2.3.0
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
|
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
|
||||||
file: build-artifacts/*
|
file: ${{ env.JOB_TRANSFER_ARTIFACT }}/*
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref }}
|
||||||
file_glob: true
|
file_glob: true
|
||||||
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: "build-artifacts/*"
|
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
|
||||||
PLUGIN_STRIP_PREFIX: "build-artifacts/"
|
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 }}
|
||||||
|
|
||||||
|
clean:
|
||||||
|
# This job must run after all jobs that use the transfer artifact.
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
- publish
|
||||||
|
- release
|
||||||
|
- artifacts
|
||||||
|
if: always() && needs.build.result != 'skipped'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Remove unneeded job transfer artifact
|
||||||
|
uses: geekyeggo/delete-artifact@v2
|
||||||
|
with:
|
||||||
|
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||||
|
|||||||
47
.github/workflows/check-certificates.yml
vendored
47
.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,11 +104,11 @@ 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"
|
||||||
|
|
||||||
echo "::set-output name=days::$DAYS_BEFORE_EXPIRATION"
|
echo "days=$DAYS_BEFORE_EXPIRATION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Check if expiration notification period has been reached
|
- name: Check if expiration notification period has been reached
|
||||||
id: check-expiration
|
id: check-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
|
||||||
|
|||||||
53
.github/workflows/check-i18n-task.yml
vendored
Normal file
53
.github/workflows/check-i18n-task.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Check Internationalization
|
||||||
|
|
||||||
|
env:
|
||||||
|
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||||
|
GO_VERSION: "1.17"
|
||||||
|
|
||||||
|
# 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@v3
|
||||||
|
|
||||||
|
- name: Install Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Taskfile
|
||||||
|
uses: arduino/setup-task@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
version: 3.x
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn
|
||||||
|
|
||||||
|
- name: Check for errors
|
||||||
|
run: yarn i18n:check
|
||||||
55
.github/workflows/compose-full-changelog.yml
vendored
Normal file
55
.github/workflows/compose-full-changelog.yml
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: 16.x
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-changelog:
|
||||||
|
if: github.repository == 'arduino/arduino-ide'
|
||||||
|
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: Get Tag
|
||||||
|
id: tag_name
|
||||||
|
run: |
|
||||||
|
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
45
.github/workflows/i18n-nightly-push.yml
vendored
Normal file
45
.github/workflows/i18n-nightly-push.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: i18n-nightly-push
|
||||||
|
|
||||||
|
env:
|
||||||
|
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||||
|
GO_VERSION: "1.17"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# run every day at 1AM
|
||||||
|
- cron: '0 1 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
push-to-transifex:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Task
|
||||||
|
uses: arduino/setup-task@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
version: 3.x
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
53
.github/workflows/i18n-weekly-pull.yml
vendored
Normal file
53
.github/workflows/i18n-weekly-pull.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: i18n-weekly-pull
|
||||||
|
|
||||||
|
env:
|
||||||
|
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||||
|
GO_VERSION: "1.17"
|
||||||
|
|
||||||
|
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@v3
|
||||||
|
|
||||||
|
- name: Install Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Task
|
||||||
|
uses: arduino/setup-task@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
version: 3.x
|
||||||
|
|
||||||
|
- 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@v4
|
||||||
|
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@v3
|
||||||
|
|
||||||
|
- name: Download JSON schema for labels configuration file
|
||||||
|
id: download-schema
|
||||||
|
uses: carlosperate/download-file-action@v2
|
||||||
|
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@v2
|
||||||
|
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@v3
|
||||||
|
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 "flag=--dry-run" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Download configuration files artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
|
||||||
|
path: ${{ env.CONFIGURATIONS_FOLDER }}
|
||||||
|
|
||||||
|
- name: Remove unneeded artifact
|
||||||
|
uses: geekyeggo/delete-artifact@v2
|
||||||
|
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 }}
|
||||||
62
.github/workflows/themes-weekly-pull.yml
vendored
Normal file
62
.github/workflows/themes-weekly-pull.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
name: themes-weekly-pull
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# run every friday at 5AM
|
||||||
|
- cron: '0 5 * * 5'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||||
|
GO_VERSION: "1.17"
|
||||||
|
NODE_VERSION: 16.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 Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Install Task
|
||||||
|
uses: arduino/setup-task@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
version: 3.x
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
}
|
||||||
195
.vscode/launch.json
vendored
195
.vscode/launch.json
vendored
@@ -1,9 +1,107 @@
|
|||||||
{
|
{
|
||||||
// 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": "launch",
|
||||||
|
"name": "App (Electron) [Dev]",
|
||||||
|
"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",
|
||||||
|
"--content-trace",
|
||||||
|
"--open-devtools"
|
||||||
|
],
|
||||||
|
"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": "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",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Run Test [current]",
|
||||||
|
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
|
||||||
|
"args": [
|
||||||
|
"--require",
|
||||||
|
"reflect-metadata/Reflect",
|
||||||
|
"--require",
|
||||||
|
"ignore-styles",
|
||||||
|
"--no-timeouts",
|
||||||
|
"--colors",
|
||||||
|
"**/${fileBasenameNoExtension}.js"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
|
||||||
|
},
|
||||||
|
"sourceMaps": true,
|
||||||
|
"smartStep": true,
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"outputCapture": "std"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
@@ -16,94 +114,15 @@
|
|||||||
"name": "Electron Packager",
|
"name": "Electron Packager",
|
||||||
"program": "${workspaceRoot}/electron/packager/index.js",
|
"program": "${workspaceRoot}/electron/packager/index.js",
|
||||||
"cwd": "${workspaceFolder}/electron/packager"
|
"cwd": "${workspaceFolder}/electron/packager"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "App (Electron)",
|
|
||||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
|
||||||
"windows": {
|
|
||||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "development",
|
|
||||||
"NODE_PRESERVE_SYMLINKS": "1"
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"program": "${workspaceRoot}/electron-app/src-gen/frontend/electron-main.js",
|
|
||||||
"protocol": "inspector",
|
|
||||||
"args": [
|
|
||||||
"--log-level=debug",
|
|
||||||
"--hostname=localhost",
|
|
||||||
"--no-cluster",
|
|
||||||
"--remote-debugging-port=9222",
|
|
||||||
"--no-app-auto-install",
|
|
||||||
"--plugins=local-dir:plugins"
|
|
||||||
],
|
],
|
||||||
"env": {
|
"compounds": [
|
||||||
"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"
|
|
||||||
],
|
|
||||||
"smartStep": true,
|
|
||||||
"internalConsoleOptions": "openOnSessionStart",
|
|
||||||
"outputCapture": "std"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "node",
|
"name": "Launch Electron Backend & Frontend",
|
||||||
"request": "launch",
|
"configurations": [
|
||||||
"name": "App (Browser)",
|
"App (Electron)",
|
||||||
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
|
"Attach to Electron Frontend"
|
||||||
"args": [
|
]
|
||||||
"--hostname=0.0.0.0",
|
|
||||||
"--port=3000",
|
|
||||||
"--no-cluster",
|
|
||||||
"--no-app-auto-install",
|
|
||||||
"--plugins=local-dir:plugins"
|
|
||||||
],
|
|
||||||
"windows": {
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "development",
|
|
||||||
"NODE_PRESERVE_SYMLINKS": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"NODE_ENV": "development"
|
|
||||||
},
|
|
||||||
"sourceMaps": true,
|
|
||||||
"outFiles": [
|
|
||||||
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
|
|
||||||
"${workspaceRoot}/browser-app/lib/**/*.js",
|
|
||||||
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
|
|
||||||
],
|
|
||||||
"smartStep": true,
|
|
||||||
"internalConsoleOptions": "openOnSessionStart",
|
|
||||||
"outputCapture": "std"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"protocol": "inspector",
|
|
||||||
"name": "Run Test [current]",
|
|
||||||
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
|
|
||||||
"args": [
|
|
||||||
"--require",
|
|
||||||
"reflect-metadata/Reflect",
|
|
||||||
"--no-timeouts",
|
|
||||||
"--colors",
|
|
||||||
"**/${fileBasenameNoExtension}.js"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
|
|
||||||
},
|
|
||||||
"sourceMaps": true,
|
|
||||||
"smartStep": true,
|
|
||||||
"internalConsoleOptions": "openOnSessionStart",
|
|
||||||
"outputCapture": "std"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
32
.vscode/tasks.json
vendored
32
.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": [
|
||||||
{
|
{
|
||||||
@@ -14,17 +12,6 @@
|
|||||||
"clear": false
|
"clear": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": "Arduino IDE - Start Browser App",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "yarn --cwd ./browser-app start",
|
|
||||||
"group": "build",
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "always",
|
|
||||||
"panel": "new",
|
|
||||||
"clear": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "Arduino IDE - Watch IDE Extension",
|
"label": "Arduino IDE - Watch IDE Extension",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
@@ -35,17 +22,6 @@
|
|||||||
"panel": "new",
|
"panel": "new",
|
||||||
"clear": false
|
"clear": false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
{
|
|
||||||
"label": "Arduino IDE - Watch Browser App",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "yarn --cwd ./browser-app watch",
|
|
||||||
"group": "build",
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "always",
|
|
||||||
"panel": "new",
|
|
||||||
"clear": false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Arduino IDE - Watch Electron App",
|
"label": "Arduino IDE - Watch Electron App",
|
||||||
@@ -58,14 +34,6 @@
|
|||||||
"clear": false
|
"clear": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": "Arduino IDE - Watch All [Browser]",
|
|
||||||
"type": "shell",
|
|
||||||
"dependsOn": [
|
|
||||||
"Arduino IDE - Watch IDE Extension",
|
|
||||||
"Arduino IDE - Watch Browser App"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "Arduino IDE - Watch All [Electron]",
|
"label": "Arduino IDE - Watch All [Electron]",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
|||||||
137
BUILDING.md
137
BUILDING.md
@@ -1,136 +1,3 @@
|
|||||||
# Development
|
# Development Guide
|
||||||
|
|
||||||
This page includes technical documentation for developers who want to build the IDE locally and contribute to the project.
|
|
||||||
|
|
||||||
## Architecture overview
|
|
||||||
|
|
||||||
The IDE consists of three major parts:
|
|
||||||
- the _Electron main_ process,
|
|
||||||
- the _backend_, and
|
|
||||||
- the _frontend_.
|
|
||||||
|
|
||||||
The _Electron main_ process is responsible for:
|
|
||||||
- creating the application,
|
|
||||||
- managing the application lifecycle via listeners, and
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
In normal browsers, web pages usually run in a sandboxed environment, and accessing native resources are disallowed. However, Electron has the power to use Node.js APIs in the web pages allowing lower-level OS interactions. Due to security reasons, accessing native resources is an undesired behavior in the IDE. So by convention, we do not use Node.js APIs. (Note: the Node.js integration is [not yet disabled](https://github.com/eclipse-theia/theia/issues/2018) although it is not used). In the IDE, only the _backend_ allows OS interaction.
|
|
||||||
|
|
||||||
The _backend_ process is responsible for:
|
|
||||||
- providing access to the filesystem,
|
|
||||||
- communicating with the [Arduino CLI](https://github.com/arduino/arduino-cli) via gRPC,
|
|
||||||
- running your terminal,
|
|
||||||
- exposing additional RESTful APIs,
|
|
||||||
- performing the Git commands in the local repositories,
|
|
||||||
- hosting and running any VS Code extensions, or
|
|
||||||
- executing VS Code tasks<sup>[[2]]</sup>.
|
|
||||||
|
|
||||||
The _Electron main_ process spawns the _backend_ process. There is always exactly one _backend_ process. However, due to performance considerations, the _backend_ spawns several sub-processes for the filesystem watching, Git repository discovery, etc. The communication between the _backend_ process and its sub-processes is established via IPC. Besides spawning sub-processes, the _backend_ will start an HTTP server on a random available port, and serves the web application as static content. When the sub-processes are up and running, and the HTTP server is also listening, the _backend_ process sends the HTTP server port to the _Electron main_ process via IPC. The _Electron main_ process will load the _backend_'s endpoint in the `BrowserWindow`.
|
|
||||||
|
|
||||||
The _frontend_ is running as an Electron renderer process and can invoke services implemented on the _backend_. The communication between the _backend_ and the _frontend_ is done via JSON-RPC over a websocket connection. This means, the services running in the _frontend_ are all proxies, and will ask the corresponding service implementation on the _backend_.
|
|
||||||
|
|
||||||
[1]: https://www.electronjs.org/docs/tutorial/application-architecture#differences-between-main-process-and-renderer-process
|
|
||||||
[2]: https://code.visualstudio.com/Docs/editor/tasks
|
|
||||||
|
|
||||||
|
|
||||||
## Build from source
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Build
|
|
||||||
```sh
|
|
||||||
yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rebuild the native dependencies
|
|
||||||
```sh
|
|
||||||
yarn rebuild:electron
|
|
||||||
```
|
|
||||||
|
|
||||||
### Start
|
|
||||||
```sh
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
### CI
|
|
||||||
|
|
||||||
This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions).
|
|
||||||
|
|
||||||
- _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts can be downloaded from the GitHub Actions page. Note: [due to a limitation](https://github.com/actions/upload-artifact/issues/80#issuecomment-630030144) with the GH Actions UI, you cannot download a particular build, but you have to get all together inside the `build-artifacts.zip`.
|
|
||||||
- _Nightly_ builds run every day at 03:00 GMT from the `main` branch.
|
|
||||||
- _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build:
|
|
||||||
- Create a local tag:
|
|
||||||
```sh
|
|
||||||
git tag -a 1.2.3 -m "Creating a new tag for the `1.2.3` release."
|
|
||||||
```
|
|
||||||
- Push it to the remote:
|
|
||||||
```sh
|
|
||||||
git push origin 1.2.3
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
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.
|
|
||||||
Due to this limitation, Mac users have two options for testing contributions from forks:
|
|
||||||
|
|
||||||
### The Safe approach (recommended)
|
|
||||||
|
|
||||||
Follow [the instructions above](#build-from-source) to create the build environment locally, then build the code you want to test.
|
|
||||||
|
|
||||||
### The Risky approach
|
|
||||||
|
|
||||||
*Please note that this approach is risky as you are lowering the security on your system, therefore we strongly discourage you from following it.*
|
|
||||||
1. Use [this guide](https://help.apple.com/xcode/mac/10.2/index.html?localePath=en.lproj#/dev9b7736b0e), in order to disable Gatekeeper (at your own risk!).
|
|
||||||
1. Download the unsigned artifact provided by the CI workflow run related to the Pull Request at each push.
|
|
||||||
1. Re-enable Gatekeeper after tests are done, following the guide linked above.
|
|
||||||
|
|
||||||
### Creating a release
|
|
||||||
|
|
||||||
You will not need to create a new release yourself as the Arduino team takes care of this on a regular basis, but we are documenting the process here. Let's assume the current version is `0.1.3` and you want to release `0.2.0`.
|
|
||||||
|
|
||||||
- Make sure the `main` state represents what you want to release and you're on `main`.
|
|
||||||
- Prepare a release-candidate build on a branch:
|
|
||||||
```bash
|
|
||||||
git branch 0.2.0-rc \
|
|
||||||
&& git checkout 0.2.0-rc
|
|
||||||
```
|
|
||||||
- Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one:
|
|
||||||
```bash
|
|
||||||
yarn update:version 0.2.0
|
|
||||||
```
|
|
||||||
- This should generate multiple outgoing changes with the version update.
|
|
||||||
- Commit your changes and push to the remote:
|
|
||||||
```bash
|
|
||||||
git add . \
|
|
||||||
&& git commit -s -m "Updated versions to 0.2.0" \
|
|
||||||
&& git push
|
|
||||||
```
|
|
||||||
- Create the GH PR the workflow starts automatically.
|
|
||||||
- Once you're happy with the RC, merge the changes to the `main`.
|
|
||||||
- Create a tag and push it:
|
|
||||||
```bash
|
|
||||||
git tag -a 0.2.0 -m "0.2.0" \
|
|
||||||
&& git push origin 0.2.0
|
|
||||||
```
|
|
||||||
- The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases).
|
|
||||||
- If you do not want to release the `EXE` and `MSI` installers, wipe them manually.
|
|
||||||
- If you do not like the generated changelog, modify it and update the GH release.
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
* *Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?*
|
|
||||||
|
|
||||||
Yes. It is possible but not recommended. The CLI exposes a set of functionality via [gRPC](https://github.com/arduino/arduino-cli/tree/master/rpc) and the IDE uses this API to communicate with the CLI. Before we build a new version of IDE, we pin a specific version of CLI and use the corresponding `proto` files to generate TypeScript modules for gRPC. This means, a particular version of IDE is compliant only with the pinned version of CLI. Mismatching IDE and CLI versions might not be able to communicate with each other. This could cause unpredictable IDE behavior.
|
|
||||||
|
|
||||||
* *I have understood that not all versions of the CLI are compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?*
|
|
||||||
|
|
||||||
[Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at:
|
|
||||||
- Windows: `C:\path\to\Arduino IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
|
|
||||||
- macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
|
|
||||||
- Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
|
|
||||||
|
|
||||||
|
This documentation has been moved [**here**](docs/development.md#development-guide).
|
||||||
|
|||||||
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
|
|
||||||
48
README.md
48
README.md
@@ -1,43 +1,18 @@
|
|||||||
<img src="https://content.arduino.cc/website/Arduino_logo_teal.svg" height="100" align="right" />
|
<img src="https://content.arduino.cc/website/Arduino_logo_teal.svg" height="100" align="right" />
|
||||||
|
|
||||||
# Arduino IDE 2.x (beta)
|
# Arduino IDE 2.x
|
||||||
|
|
||||||
[](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
|
[](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
|
||||||
|
|
||||||
This repository contains the source code of the Arduino IDE 2.x, which is currently in beta stage. If you're looking for the stable IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
|
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
|
||||||
|
|
||||||
The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is based on the [Theia IDE](https://theia-ide.org/) framework and built with [Electron](https://www.electronjs.org/). The backend operations such as compilation and uploading are offloaded to an [arduino-cli](https://github.com/arduino/arduino-cli) instance running in daemon mode. This new IDE was developed with the goal of preserving the same interface and user experience of the previous major version in order to provide a frictionless upgrade.
|
The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is based on the [Theia IDE](https://theia-ide.org/) framework and built with [Electron](https://www.electronjs.org/). The backend operations such as compilation and uploading are offloaded to an [arduino-cli](https://github.com/arduino/arduino-cli) instance running in daemon mode. This new IDE was developed with the goal of preserving the same interface and user experience of the previous major version in order to provide a frictionless upgrade.
|
||||||
|
|
||||||
> ⚠️ This is **beta** software. Help us test it!
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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 release version and nightly builds from the [software download page on the Arduino website](https://www.arduino.cc/en/software).
|
||||||
### Nightly builds
|
|
||||||
|
|
||||||
These builds are generated every day at 03:00 GMT from the `main` branch and
|
|
||||||
should be considered unstable:
|
|
||||||
|
|
||||||
Platform | 32 bit | 64 bit |
|
|
||||||
--------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
|
||||||
Linux | | [Nightly Linux 64 bit] |
|
|
||||||
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] |
|
|
||||||
macOS | | [Nightly macOS 64 bit] |
|
|
||||||
|
|
||||||
[🚧 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 Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
|
|
||||||
[Nightly Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
|
|
||||||
[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
|
|
||||||
generated builds by replacing `latest` with the latest available build
|
|
||||||
date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
|
|
||||||
replaced with `20190806`)
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
@@ -45,10 +20,9 @@ If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-u
|
|||||||
|
|
||||||
## Bugs & Issues
|
## Bugs & Issues
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
* Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
|
See [**the issue report guide**](docs/contributor-guide/issues.md#issue-report-guide) for instructions.
|
||||||
* 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
|
||||||
|
|
||||||
@@ -60,14 +34,16 @@ e-mail contact: security@arduino.cc
|
|||||||
|
|
||||||
## Contributions and development
|
## Contributions and development
|
||||||
|
|
||||||
Contributions are very welcome! You can browse the list of open issues to see what's needed and then you can submit your code using a Pull Request. Please provide detailed descriptions. We also appreciate any help in testing issues and patches contributed by other users.
|
Contributions are very welcome! There are several ways to participate in this project, including:
|
||||||
|
|
||||||
This repository contains the main code, but two more repositories are included during the build process:
|
- Fixing bugs
|
||||||
|
- Beta testing
|
||||||
|
- Translation
|
||||||
|
|
||||||
* [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
|
See [**the contributor guide**](docs/CONTRIBUTING.md#contributor-guide) for more information.
|
||||||
* [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
|
|
||||||
|
See the [**development guide**](docs/development.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.
|
|
||||||
## 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
|
||||||
|
|
||||||
@@ -54,3 +57,26 @@ The Config Service knows about your system, like for example the default sketch
|
|||||||
### `"arduino"` configuration in the `package.json`:
|
### `"arduino"` configuration in the `package.json`:
|
||||||
- `"cli"`:
|
- `"cli"`:
|
||||||
- `"version"` type `string` | `{ owner: string, repo: string, commitish?: string }`: if the type is a `string` and is a valid semver, it will get the corresponding [released](https://github.com/arduino/arduino-cli/releases) CLI. If the type is `string` and is a [date in `YYYYMMDD`](https://arduino.github.io/arduino-cli/latest/installation/#nightly-builds) format, it will get a nightly CLI. If the type is an object, a CLI, build from the sources in the `owner/repo` will be used. If `commitish` is not defined, the HEAD of the default branch will be used. In any other cases an error is thrown.
|
- `"version"` type `string` | `{ owner: string, repo: string, commitish?: string }`: if the type is a `string` and is a valid semver, it will get the corresponding [released](https://github.com/arduino/arduino-cli/releases) CLI. If the type is `string` and is a [date in `YYYYMMDD`](https://arduino.github.io/arduino-cli/latest/installation/#nightly-builds) format, it will get a nightly CLI. If the type is an object, a CLI, build from the sources in the `owner/repo` will be used. If `commitish` is not defined, the HEAD of the default branch will be used. In any other cases an error is thrown.
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
`yarn --cwd arduino-ide-extension generate-protocol`
|
||||||
|
|
||||||
|
### Update **clangd** and **ClangFormat**
|
||||||
|
|
||||||
|
The [**clangd** C++ language server](https://clangd.llvm.org/) and the [**ClangFormat** code formatter](https://clang.llvm.org/docs/ClangFormat.html) tool dependencies are managed in parallel. Updating them to a different version is done by the following procedure:
|
||||||
|
|
||||||
|
1. If the target version is not already [available from the `arduino/clang-static-binaries` repository](https://github.com/arduino/clang-static-binaries/releases), submit [an issue there](https://github.com/arduino/clang-static-binaries/issues) requesting a build and wait for that to be completed.
|
||||||
|
1. Validate the **ClangFormat** configuration for the target version by following the instructions [**here**](https://github.com/arduino/tooling-project-assets/tree/main/other/clang-format-configuration#clangformat-version-updates)
|
||||||
|
1. Submit a pull request in the `arduino/arduino-ide` repository to update the version in the `arduino.clangd.version` key of [`package.json`](package.json).
|
||||||
|
1. Submit a pull request in [the `arduino/tooling-project-assets` repository](https://github.com/arduino/tooling-project-assets) to update the version in the `vars.DEFAULT_CLANG_FORMAT_VERSION` field of [`Taskfile.yml`](https://github.com/arduino/tooling-project-assets/blob/main/Taskfile.yml).
|
||||||
|
|
||||||
|
### 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,77 +1,108 @@
|
|||||||
{
|
{
|
||||||
"name": "arduino-ide-extension",
|
"name": "arduino-ide-extension",
|
||||||
"version": "2.0.0-beta.4",
|
"version": "2.0.3",
|
||||||
"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/markers": "next",
|
"@theia/keymaps": "1.25.0",
|
||||||
"@theia/monaco": "next",
|
"@theia/markers": "1.25.0",
|
||||||
"@theia/navigator": "next",
|
"@theia/monaco": "1.25.0",
|
||||||
"@theia/outline-view": "next",
|
"@theia/navigator": "1.25.0",
|
||||||
"@theia/preferences": "next",
|
"@theia/outline-view": "1.25.0",
|
||||||
"@theia/output": "next",
|
"@theia/output": "1.25.0",
|
||||||
"@theia/search-in-workspace": "next",
|
"@theia/preferences": "1.25.0",
|
||||||
"@theia/terminal": "next",
|
"@theia/search-in-workspace": "1.25.0",
|
||||||
"@theia/workspace": "next",
|
"@theia/terminal": "1.25.0",
|
||||||
|
"@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.2.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",
|
||||||
@@ -80,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"
|
||||||
},
|
},
|
||||||
@@ -92,7 +126,8 @@
|
|||||||
},
|
},
|
||||||
"mocha": {
|
"mocha": {
|
||||||
"require": [
|
"require": [
|
||||||
"reflect-metadata/Reflect"
|
"reflect-metadata/Reflect",
|
||||||
|
"ignore-styles"
|
||||||
],
|
],
|
||||||
"reporter": "spec",
|
"reporter": "spec",
|
||||||
"colors": true,
|
"colors": true,
|
||||||
@@ -112,16 +147,27 @@
|
|||||||
"frontend": "lib/browser/arduino-ide-frontend-module"
|
"frontend": "lib/browser/arduino-ide-frontend-module"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"frontend": "lib/browser/theia/core/browser-menu-module",
|
|
||||||
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
|
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"electronMain": "lib/electron-main/arduino-electron-main-module"
|
"electronMain": "lib/electron-main/arduino-electron-main-module"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"arduino": {
|
"arduino": {
|
||||||
"cli": {
|
"cli": {
|
||||||
"version": "0.17.0"
|
"version": "0.29.0"
|
||||||
|
},
|
||||||
|
"fwuploader": {
|
||||||
|
"version": "2.2.2"
|
||||||
|
},
|
||||||
|
"clangd": {
|
||||||
|
"version": "14.0.0"
|
||||||
|
},
|
||||||
|
"languageServer": {
|
||||||
|
"version": "0.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 { taskBuildFromGit } = 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,27 @@
|
|||||||
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';
|
if (arch === 'arm64') {
|
||||||
|
return 'macOS_ARM64.tar.gz';
|
||||||
|
}
|
||||||
|
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 +70,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 {
|
||||||
|
taskBuildFromGit(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.10.0';
|
||||||
|
|
||||||
(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,130 @@
|
|||||||
// - 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 'darwin-arm64':
|
||||||
clangdExecutablePath = path.join(build, 'bin', 'clangd')
|
clangdExecutablePath = path.join(build, 'clangd');
|
||||||
lsSuffix = 'Linux_amd64.zip';
|
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||||
clangdPrefix = 'linux'
|
lsSuffix = 'macOS_ARM64.tar.gz';
|
||||||
|
clangdSuffix = 'macOS_ARM64';
|
||||||
break;
|
break;
|
||||||
case 'win32':
|
case 'linux-x64':
|
||||||
clangdExecutablePath = path.join(build, 'bin', 'clangd.exe')
|
clangdExecutablePath = path.join(build, 'clangd');
|
||||||
lsSuffix = 'Windows_amd64.zip';
|
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||||
clangdPrefix = 'windows';
|
lsSuffix = 'Linux_64bit.tar.gz';
|
||||||
|
clangdSuffix = 'Linux_64bit';
|
||||||
break;
|
break;
|
||||||
|
case 'win32-x64':
|
||||||
|
clangdExecutablePath = path.join(build, 'clangd.exe');
|
||||||
|
clangFormatExecutablePath = path.join(build, 'clang-format.exe');
|
||||||
|
lsSuffix = 'Windows_64bit.zip';
|
||||||
|
clangdSuffix = 'Windows_64bit';
|
||||||
|
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}`);
|
||||||
}
|
};
|
||||||
|
|||||||
110
arduino-ide-extension/scripts/utils.js
Normal file
110
arduino-ide-extension/scripts/utils.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Clones something from GitHub and builds it with [`Task`](https://taskfile.dev/).
|
||||||
|
*
|
||||||
|
* @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.taskBuildFromGit = (version, destinationPath, taskName) => {
|
||||||
|
return buildFromGit('task', version, destinationPath, taskName);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {
|
||||||
|
return buildFromGit('go', version, destinationPath, taskName);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `command` is either `go` or `task`.
|
||||||
|
*/
|
||||||
|
function buildFromGit(command, 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(`${command} 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,452 +1,292 @@
|
|||||||
import { Mutex } from 'async-mutex';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger } 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 {
|
||||||
|
MAIN_MENU_BAR,
|
||||||
|
MenuContribution,
|
||||||
|
MenuModelRegistry,
|
||||||
|
} from '@theia/core';
|
||||||
|
import {
|
||||||
|
FrontendApplication,
|
||||||
|
FrontendApplicationContribution,
|
||||||
} 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 } 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 { ArduinoPreferences } from './arduino-preferences';
|
||||||
import * as React from 'react';
|
|
||||||
import { remote } from 'electron';
|
|
||||||
import { MainMenuManager } from '../common/main-menu-manager';
|
|
||||||
import { BoardsService, CoreService, Port, SketchesService, ExecutableService } from '../common/protocol';
|
|
||||||
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 { ArduinoMenus } from './menu/arduino-menus';
|
||||||
import { MonitorConnection } from './monitor/monitor-connection';
|
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
|
||||||
import { WorkspaceService } from './theia/workspace/workspace-service';
|
|
||||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
import { SerialPlotterContribution } from './serial/plotter/plotter-frontend-contribution';
|
||||||
import { OutputService } from '../common/protocol/output-service';
|
|
||||||
import { ArduinoPreferences } from './arduino-preferences';
|
|
||||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
|
||||||
import { SaveAsSketch } from './contributions/save-as-sketch';
|
|
||||||
|
|
||||||
@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 fileSystem: FileService;
|
|
||||||
|
|
||||||
@inject(SketchesService)
|
|
||||||
protected 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(OutputService)
|
|
||||||
protected readonly outputService: OutputService;
|
|
||||||
|
|
||||||
@inject(ArduinoPreferences)
|
@inject(ArduinoPreferences)
|
||||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
private readonly arduinoPreferences: ArduinoPreferences;
|
||||||
|
|
||||||
@inject(SketchesServiceClientImpl)
|
@inject(FrontendApplicationStateService)
|
||||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
private readonly appStateService: FrontendApplicationStateService;
|
||||||
|
|
||||||
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined;
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onStart(app: FrontendApplication): void {
|
onStart(app: FrontendApplication): 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();
|
||||||
protected languageServerFqbn?: string;
|
const zoomLevel = this.arduinoPreferences.get(
|
||||||
protected languageServerStartMutex = new Mutex();
|
'arduino.window.zoomLevel'
|
||||||
protected async startLanguageServer(fqbn: string, name: string | undefined): Promise<void> {
|
);
|
||||||
const release = await this.languageServerStartMutex.acquire();
|
webContents.setZoomLevel(zoomLevel);
|
||||||
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.fileSystem.fsPath(new URI(currentSketch.uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
|
|
||||||
const [clangdPath, cliPath, lsPath] = await Promise.all([
|
|
||||||
this.fileSystem.fsPath(new URI(clangdUri)),
|
|
||||||
this.fileSystem.fsPath(new URI(cliUri)),
|
|
||||||
this.fileSystem.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.commandRegistry.executeCommand<string>('arduino.languageserver.start', {
|
|
||||||
lsPath,
|
|
||||||
cliPath,
|
|
||||||
clangdPath,
|
|
||||||
log: currentSketchPath ? currentSketchPath : log,
|
|
||||||
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): Promise<any> {
|
|
||||||
const widget = this.editorManager.all.find(widget => widget.editor.uri.toString() === uri);
|
|
||||||
if (!widget || forceOpen) {
|
|
||||||
return this.editorManager.open(new URI(uri));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,74 +1,304 @@
|
|||||||
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.checkForUpdates': {
|
||||||
|
type: 'boolean',
|
||||||
|
description: nls.localize(
|
||||||
|
'arduino/preferences/checkForUpdate',
|
||||||
|
"Receive notifications of available updates for the IDE, boards, and libraries. Requires an IDE restart after change. It's true by default."
|
||||||
|
),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
'arduino.sketch.inoBlueprint': {
|
||||||
|
type: 'string',
|
||||||
|
markdownDescription: nls.localize(
|
||||||
|
'arduino/preferences/sketch/inoBlueprint',
|
||||||
|
'Absolute filesystem path to the default `.ino` blueprint file. If specified, the content of the blueprint file will be used for every new sketch created by the IDE. The sketches will be generated with the default Arduino content if not specified. Unaccessible blueprint files are ignored. **A restart of the IDE is needed** for this setting to take effect.'
|
||||||
|
),
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'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;
|
||||||
|
'arduino.sketch.inoBlueprint': string;
|
||||||
|
'arduino.checkForUpdates': 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,18 +1,41 @@
|
|||||||
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 } 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 { InstallationProgressDialog } from '../widgets/progress-dialog';
|
import { nls } from '@theia/core/lib/common';
|
||||||
import { BoardsConfig } from './boards-config';
|
import { NotificationCenter } from '../notification-center';
|
||||||
|
import { InstallManually } from '../../common/nls';
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -23,40 +46,236 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
|||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||||
|
|
||||||
|
@inject(ResponseServiceClient)
|
||||||
|
protected readonly responseService: ResponseServiceClient;
|
||||||
|
|
||||||
@inject(BoardsListWidgetFrontendContribution)
|
@inject(BoardsListWidgetFrontendContribution)
|
||||||
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||||
|
|
||||||
|
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
|
||||||
|
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;
|
});
|
||||||
|
|
||||||
|
// 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) {
|
if (selectedBoard) {
|
||||||
this.boardsService.search({}).then(packages => {
|
this.ensureCoreExists(selectedBoard, selectedPort);
|
||||||
const candidates = packages
|
}
|
||||||
.filter(pkg => BoardsPackage.contains(selectedBoard, pkg))
|
|
||||||
.filter(({ installable, installedVersion }) => installable && !installedVersion);
|
setEventListeners();
|
||||||
for (const candidate of candidates) {
|
});
|
||||||
// tslint:disable-next-line:max-line-length
|
}
|
||||||
this.messageService.info(`The \`"${candidate.name}"\` 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 => {
|
|
||||||
if (answer === 'Yes') {
|
private removeNotificationByBoard(selectedBoard: Board): void {
|
||||||
const dialog = new InstallationProgressDialog(candidate.name, candidate.availableVersions[0]);
|
const index = this.notifications.findIndex((notification) =>
|
||||||
dialog.open();
|
Board.sameAs(notification, selectedBoard)
|
||||||
try {
|
);
|
||||||
await this.boardsService.install({ item: candidate });
|
if (index !== -1) {
|
||||||
} finally {
|
this.notifications.splice(index, 1);
|
||||||
dialog.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (answer) {
|
|
||||||
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
|
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)
|
||||||
|
// and matches based on the board name
|
||||||
|
// 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
|
||||||
|
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
|
||||||
|
if (packagesForBoard.some(({ installedVersion }) => !!installedVersion)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filter the installable (not installed) packages,
|
||||||
|
// 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
|
||||||
|
const candidates = packagesForBoard.filter(
|
||||||
|
({ installable, installedVersion }) => installable && !installedVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
return candidates[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 actions: AutoInstallPromptActions = [
|
||||||
|
{
|
||||||
|
key: InstallManually,
|
||||||
|
handler: () => {
|
||||||
|
this.boardsManagerFrontendContribution
|
||||||
|
.openView({ reveal: true })
|
||||||
|
.then((widget) =>
|
||||||
|
widget.refresh({
|
||||||
|
query: candidate.name.toLocaleLowerCase(),
|
||||||
|
type: 'All',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isAcceptance: true,
|
||||||
|
key: yes,
|
||||||
|
handler: () => {
|
||||||
|
return Installable.installWithProgress({
|
||||||
|
installable: this.boardsService,
|
||||||
|
item: candidate,
|
||||||
|
messageService: this.messageService,
|
||||||
|
responseService: this.responseService,
|
||||||
|
version: candidate.availableVersions[0],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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,22 @@
|
|||||||
import { injectable, inject, postConstruct } from 'inversify';
|
import {
|
||||||
import { Message } from '@phosphor/messaging';
|
injectable,
|
||||||
import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
|
inject,
|
||||||
|
postConstruct,
|
||||||
|
} from '@theia/core/shared/inversify';
|
||||||
|
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||||
|
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 +28,38 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
|||||||
|
|
||||||
protected config: BoardsConfig.Config = {};
|
protected config: BoardsConfig.Config = {};
|
||||||
|
|
||||||
constructor(@inject(BoardsConfigDialogProps) protected readonly props: BoardsConfigDialogProps) {
|
constructor(
|
||||||
super(props);
|
@inject(BoardsConfigDialogProps)
|
||||||
|
protected override readonly props: BoardsConfigDialogProps
|
||||||
|
) {
|
||||||
|
super({ ...props, maxWidth: 500 });
|
||||||
|
|
||||||
|
this.node.id = 'select-board-dialog-container';
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -56,20 +70,21 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
|||||||
const head = document.createElement('div');
|
const head = document.createElement('div');
|
||||||
head.classList.add('head');
|
head.classList.add('head');
|
||||||
|
|
||||||
const title = document.createElement('div');
|
|
||||||
title.textContent = 'Select Other Board & Port';
|
|
||||||
title.classList.add('title');
|
|
||||||
head.appendChild(title);
|
|
||||||
|
|
||||||
const text = document.createElement('div');
|
const text = document.createElement('div');
|
||||||
text.classList.add('text');
|
text.classList.add('text');
|
||||||
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('p');
|
const p = document.createElement('div');
|
||||||
p.textContent = paragraph;
|
p.textContent = paragraph;
|
||||||
text.appendChild(p);
|
text.appendChild(p);
|
||||||
}
|
}
|
||||||
@@ -77,39 +92,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 +139,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,23 @@
|
|||||||
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,
|
||||||
|
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 +29,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 +38,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 +96,198 @@ 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.boardsServiceProvider.onAvailablePortsChanged(
|
||||||
this.props.notificationCenter.onPlatformUninstalled(() => this.updateBoards(this.state.query)),
|
({ newState, oldState }) => {
|
||||||
this.props.notificationCenter.onIndexUpdated(() => this.updateBoards(this.state.query)),
|
const removedPorts = oldState.filter(
|
||||||
this.props.notificationCenter.onDaemonStarted(() => this.updateBoards(this.state.query)),
|
(oldPort) =>
|
||||||
this.props.notificationCenter.onDaemonStopped(() => this.setState({ searchResults: [] })),
|
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
|
||||||
this.props.onFilteredTextDidChangeEvent(query => this.setState({ query }, () => this.updateBoards(this.state.query)))
|
);
|
||||||
|
this.updatePorts(newState, removedPorts);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
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.onIndexUpdateDidComplete(() =>
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
override render(): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderContainer(
|
||||||
|
nls.localize('arduino/board/boards', 'boards'),
|
||||||
|
this.renderBoards.bind(this)
|
||||||
|
)}
|
||||||
|
{this.renderContainer(
|
||||||
|
nls.localize('arduino/board/ports', 'ports'),
|
||||||
|
this.renderPorts.bind(this),
|
||||||
|
this.renderPortsFooter.bind(this)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): React.ReactNode {
|
protected renderContainer(
|
||||||
return <div className='body'>
|
title: string,
|
||||||
{this.renderContainer('boards', this.renderBoards.bind(this))}
|
contentRenderer: () => React.ReactNode,
|
||||||
{this.renderContainer('ports', this.renderPorts.bind(this), this.renderPortsFooter.bind(this))}
|
footerRenderer?: () => React.ReactNode
|
||||||
</div>;
|
): React.ReactNode {
|
||||||
}
|
return (
|
||||||
|
<div className="container">
|
||||||
protected renderContainer(title: string, contentRenderer: () => React.ReactNode, footerRenderer?: () => React.ReactNode): React.ReactNode {
|
<div className="content">
|
||||||
return <div className='container'>
|
<div className="title">{title}</div>
|
||||||
<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 +295,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,98 +304,131 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>
|
const boardsList = Array.from(distinctBoards.values()).map((board) => (
|
||||||
<div className='search'>
|
<Item<BoardWithPackage>
|
||||||
<input
|
key={toKey(board)}
|
||||||
type='search'
|
|
||||||
value={query}
|
|
||||||
className='theia-input'
|
|
||||||
placeholder='SEARCH BOARD'
|
|
||||||
onChange={this.updateBoards}
|
|
||||||
ref={this.focusNodeSet}
|
|
||||||
/>
|
|
||||||
<i className='fa fa-search'></i>
|
|
||||||
</div>
|
|
||||||
<div className='boards list'>
|
|
||||||
{Array.from(distinctBoards.values()).map(board => <Item<BoardWithPackage>
|
|
||||||
key={`${board.name}-${board.packageName}`}
|
|
||||||
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}
|
||||||
/>)}
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="search">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={query}
|
||||||
|
className="theia-input"
|
||||||
|
placeholder={nls.localize(
|
||||||
|
'arduino/board/searchBoard',
|
||||||
|
'Search board'
|
||||||
|
)}
|
||||||
|
onChange={this.updateBoards}
|
||||||
|
ref={this.focusNodeSet}
|
||||||
|
/>
|
||||||
|
<i className="fa fa-search"></i>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
{boardsList.length > 0 ? (
|
||||||
|
<div className="boards list">{boardsList}</div>
|
||||||
|
) : (
|
||||||
|
<div className="no-result">
|
||||||
|
{nls.localize(
|
||||||
|
'arduino/board/noBoardsFound',
|
||||||
|
'No boards found for "{0}"',
|
||||||
|
query
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
No ports discovered
|
Port.visiblePorts(this.availableBoards)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return !ports.length ? (
|
||||||
|
<div className="no-result">
|
||||||
|
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
|
||||||
</div>
|
</div>
|
||||||
) :
|
) : (
|
||||||
(
|
<div className="ports list">
|
||||||
<div className='ports list'>
|
{ports.map((port) => (
|
||||||
{ports.map(port => <Item<Port>
|
<Item<Port>
|
||||||
key={Port.toString(port)}
|
key={`${Port.keyOf(port)}`}
|
||||||
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>
|
||||||
|
{nls.localize('arduino/board/showAllPorts', '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}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: String(i).padStart(4),
|
||||||
|
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,36 +1,92 @@
|
|||||||
import { inject, injectable, postConstruct } from 'inversify';
|
import {
|
||||||
import { BoardsPackage, BoardsService } from '../../common/protocol/boards-service';
|
inject,
|
||||||
|
injectable,
|
||||||
|
postConstruct,
|
||||||
|
} from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
BoardSearch,
|
||||||
|
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';
|
||||||
|
import { BoardsFilterRenderer } from '../widgets/component-list/filter-renderer';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||||
|
|
||||||
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) service: BoardsService,
|
||||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardsPackage>) {
|
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
|
||||||
|
@inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
|
||||||
|
) {
|
||||||
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,
|
||||||
itemRenderer
|
itemDeprecated: (item: BoardsPackage) => item.deprecated,
|
||||||
|
itemRenderer,
|
||||||
|
filterRenderer,
|
||||||
|
defaultSearchOptions: { query: '', type: 'All' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 override async install({
|
||||||
|
item,
|
||||||
|
progressId,
|
||||||
|
version,
|
||||||
|
}: {
|
||||||
|
item: BoardsPackage;
|
||||||
|
progressId: string;
|
||||||
|
version: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
await super.install({ item, progressId, version });
|
||||||
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/succesfullyInstalledPlatform',
|
||||||
|
'Successfully installed platform {0}:{1}',
|
||||||
|
item.name,
|
||||||
|
version
|
||||||
|
),
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async uninstall({
|
||||||
|
item,
|
||||||
|
progressId,
|
||||||
|
}: {
|
||||||
|
item: BoardsPackage;
|
||||||
|
progressId: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
await super.uninstall({ item, progressId });
|
||||||
|
this.messageService.info(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/succesfullyUninstalledPlatform',
|
||||||
|
'Successfully uninstalled platform {0}:{1}',
|
||||||
|
item.name,
|
||||||
|
item.installedVersion!
|
||||||
|
),
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
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 {
|
||||||
|
Command,
|
||||||
|
CommandContribution,
|
||||||
|
CommandRegistry,
|
||||||
|
CommandService,
|
||||||
|
} 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 { StorageService } from '@theia/core/lib/browser/storage-service';
|
|
||||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||||
import { RecursiveRequired } from '../../common/types';
|
import { RecursiveRequired } from '../../common/types';
|
||||||
import {
|
import {
|
||||||
@@ -11,26 +16,36 @@ import {
|
|||||||
BoardsService,
|
BoardsService,
|
||||||
BoardsPackage,
|
BoardsPackage,
|
||||||
AttachedBoardsChangeEvent,
|
AttachedBoardsChangeEvent,
|
||||||
BoardWithPackage
|
BoardWithPackage,
|
||||||
|
BoardUserField,
|
||||||
|
AvailablePorts,
|
||||||
} 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 { CommandService } from '@theia/core';
|
import { StorageWrapper } from '../storage-wrapper';
|
||||||
import { ArduinoCommands } from '../arduino-commands';
|
import { nls } from '@theia/core/lib/common';
|
||||||
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
|
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||||
|
import { Unknown } from '../../common/nls';
|
||||||
|
import {
|
||||||
|
StartupTask,
|
||||||
|
StartupTaskProvider,
|
||||||
|
} from '../../electron-common/startup-task';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsServiceProvider implements FrontendApplicationContribution {
|
export class BoardsServiceProvider
|
||||||
|
implements
|
||||||
|
FrontendApplicationContribution,
|
||||||
|
StartupTaskProvider,
|
||||||
|
CommandContribution
|
||||||
|
{
|
||||||
@inject(ILogger)
|
@inject(ILogger)
|
||||||
protected logger: ILogger;
|
protected logger: ILogger;
|
||||||
|
|
||||||
@inject(MessageService)
|
@inject(MessageService)
|
||||||
protected messageService: MessageService;
|
protected messageService: MessageService;
|
||||||
|
|
||||||
@inject(StorageService)
|
|
||||||
protected storageService: StorageService;
|
|
||||||
|
|
||||||
@inject(BoardsService)
|
@inject(BoardsService)
|
||||||
protected boardsService: BoardsService;
|
protected boardsService: BoardsService;
|
||||||
|
|
||||||
@@ -40,8 +55,19 @@ 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<{
|
||||||
|
newState: Port[];
|
||||||
|
oldState: Port[];
|
||||||
|
}>();
|
||||||
|
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -50,48 +76,180 @@ 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.
|
||||||
protected _availablePorts: Port[] = [];
|
protected _availablePorts: Port[] = [];
|
||||||
protected _availableBoards: AvailableBoard[] = [];
|
protected _availableBoards: AvailableBoard[] = [];
|
||||||
|
|
||||||
|
private lastBoardsConfigOnUpload: BoardsConfig.Config | undefined;
|
||||||
|
private lastAvailablePortsOnUpload: Port[] | undefined;
|
||||||
|
private boardConfigToAutoSelect: BoardsConfig.Config | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\
|
* Unlike `onAttachedBoardsChanged` this event fires when the user modifies the selected board in the IDE.\
|
||||||
* This even also fires, when the boards package was not available for the currently selected board,
|
* This event 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 () => {
|
||||||
this.boardsService.getAttachedBoards(),
|
const [state] = await Promise.all([
|
||||||
this.boardsService.getAvailablePorts(),
|
this.boardsService.getState(),
|
||||||
this.loadState()
|
this.loadState(),
|
||||||
]).then(([attachedBoards, availablePorts]) => {
|
]);
|
||||||
|
const { boards: attachedBoards, ports: availablePorts } =
|
||||||
|
AvailablePorts.split(state);
|
||||||
this._attachedBoards = attachedBoards;
|
this._attachedBoards = attachedBoards;
|
||||||
|
const oldState = this._availablePorts.slice();
|
||||||
this._availablePorts = availablePorts;
|
this._availablePorts = availablePorts;
|
||||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
this.onAvailablePortsChangedEmitter.fire({
|
||||||
|
newState: this._availablePorts.slice(),
|
||||||
|
oldState,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.reconcileAvailableBoards();
|
||||||
|
|
||||||
|
this.tryReconnect();
|
||||||
|
this._reconciled.resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
|
registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(USE_INHERITED_CONFIG, {
|
||||||
|
execute: (inheritedConfig: BoardsConfig.Config) =>
|
||||||
|
this.inheritedConfig.resolve(inheritedConfig),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get reconciled(): Promise<void> {
|
||||||
|
return this._reconciled.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotBoardDiscoveryOnUpload(): void {
|
||||||
|
this.lastBoardsConfigOnUpload = this._boardsConfig;
|
||||||
|
this.lastAvailablePortsOnUpload = this._availablePorts;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearBoardDiscoverySnapshot(): void {
|
||||||
|
this.lastBoardsConfigOnUpload = undefined;
|
||||||
|
this.lastAvailablePortsOnUpload = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private portToAutoSelectCanBeDerived(): boolean {
|
||||||
|
return Boolean(
|
||||||
|
this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptPostUploadAutoSelect(): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.portToAutoSelectCanBeDerived()) {
|
||||||
|
this.attemptAutoSelect({
|
||||||
|
ports: this._availablePorts,
|
||||||
|
boards: this._availableBoards,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 2000); // 2 second delay same as IDE 1.8
|
||||||
|
}
|
||||||
|
|
||||||
|
private attemptAutoSelect(
|
||||||
|
newState: AttachedBoardsChangeEvent['newState']
|
||||||
|
): void {
|
||||||
|
this.deriveBoardConfigToAutoSelect(newState);
|
||||||
|
this.tryReconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private deriveBoardConfigToAutoSelect(
|
||||||
|
newState: AttachedBoardsChangeEvent['newState']
|
||||||
|
): void {
|
||||||
|
if (!this.portToAutoSelectCanBeDerived()) {
|
||||||
|
this.boardConfigToAutoSelect = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldPorts = this.lastAvailablePortsOnUpload!;
|
||||||
|
const { ports: newPorts, boards: newBoards } = newState;
|
||||||
|
|
||||||
|
const appearedPorts =
|
||||||
|
oldPorts.length > 0
|
||||||
|
? newPorts.filter((newPort: Port) =>
|
||||||
|
oldPorts.every((oldPort: Port) => !Port.sameAs(newPort, oldPort))
|
||||||
|
)
|
||||||
|
: newPorts;
|
||||||
|
|
||||||
|
for (const port of appearedPorts) {
|
||||||
|
const boardOnAppearedPort = newBoards.find((board: Board) =>
|
||||||
|
Port.sameAs(board.port, port)
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload!;
|
||||||
|
|
||||||
|
if (
|
||||||
|
boardOnAppearedPort &&
|
||||||
|
lastBoardsConfigOnUpload.selectedBoard &&
|
||||||
|
Board.sameAs(
|
||||||
|
boardOnAppearedPort,
|
||||||
|
lastBoardsConfigOnUpload.selectedBoard
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.clearBoardDiscoverySnapshot();
|
||||||
|
|
||||||
|
this.boardConfigToAutoSelect = {
|
||||||
|
selectedBoard: boardOnAppearedPort,
|
||||||
|
selectedPort: port,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
this.logger.info('------------------------------------------');
|
this.logger.info('------------------------------------------');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._attachedBoards = event.newState.boards;
|
this._attachedBoards = event.newState.boards;
|
||||||
|
const oldState = this._availablePorts.slice();
|
||||||
this._availablePorts = event.newState.ports;
|
this._availablePorts = event.newState.ports;
|
||||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
this.onAvailablePortsChangedEmitter.fire({
|
||||||
|
newState: this._availablePorts.slice(),
|
||||||
|
oldState,
|
||||||
|
});
|
||||||
|
this.reconcileAvailableBoards().then(() => {
|
||||||
|
const { uploadInProgress } = event;
|
||||||
|
// avoid attempting "auto-selection" while an
|
||||||
|
// upload is in progress
|
||||||
|
if (!uploadInProgress) {
|
||||||
|
this.attemptAutoSelect(event.newState);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected notifyPlatformInstalled(event: { item: BoardsPackage }): void {
|
protected notifyPlatformInstalled(event: { item: BoardsPackage }): void {
|
||||||
@@ -99,12 +257,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;
|
||||||
}
|
}
|
||||||
@@ -112,13 +277,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
|
||||||
@@ -131,7 +315,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.
|
||||||
@@ -140,58 +326,70 @@ 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.
|
|
||||||
// See documentation on `latestValidBoardsConfig`.
|
|
||||||
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
|
|
||||||
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
|
|
||||||
&& this.latestValidBoardsConfig.selectedBoard.name === board.name) {
|
|
||||||
|
|
||||||
this.boardsConfig = {
|
if (!this.boardConfigToAutoSelect) return false;
|
||||||
...this.latestValidBoardsConfig,
|
|
||||||
selectedPort: board.port
|
this.boardsConfig = this.boardConfigToAutoSelect;
|
||||||
};
|
this.boardConfigToAutoSelect = undefined;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)) {
|
||||||
@@ -199,13 +397,29 @@ 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) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const fqbn = this._boardsConfig.selectedBoard.fqbn;
|
||||||
|
if (!fqbn) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// Protocol must be set to `default` when uploading without a port selected:
|
||||||
|
// https://arduino.github.io/arduino-cli/dev/platform-specification/#sketch-upload-configuration
|
||||||
|
const protocol = this._boardsConfig.selectedPort?.protocol || 'default';
|
||||||
|
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,15 +427,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;
|
||||||
}
|
}
|
||||||
@@ -234,8 +453,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;
|
||||||
}
|
}
|
||||||
@@ -243,14 +462,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;
|
||||||
}
|
}
|
||||||
@@ -262,19 +497,43 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
return this._availableBoards;
|
return this._availableBoards;
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitUntilAvailable(what: Board & { port: Port }, timeout?: number): Promise<void> {
|
/**
|
||||||
|
* @deprecated Do not use this API, it will be removed. This is a hack to be able to set the missing port `properties` before an upload.
|
||||||
|
*
|
||||||
|
* See: https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236.
|
||||||
|
*/
|
||||||
|
// TODO: remove this API and fix the selected board config store/restore correctly.
|
||||||
|
get availablePorts(): Port[] {
|
||||||
|
return this._availablePorts.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -286,70 +545,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.visiblePorts(attachedBoards)
|
||||||
|
);
|
||||||
|
|
||||||
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)
|
||||||
|
);
|
||||||
|
// "board" will always be falsey for
|
||||||
|
// port that was originally mapped
|
||||||
|
// to unknown board and then selected
|
||||||
|
// manually by user
|
||||||
|
|
||||||
|
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.sameAs(boardPort, boardsConfig.selectedPort), // to avoid double selection
|
||||||
|
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: Unknown,
|
||||||
|
port: boardPort,
|
||||||
|
state: AvailableBoard.State.incomplete,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
availableBoards.push(availableBoard);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boardsConfig.selectedBoard && !availableBoards.some(({ selected }) => selected)) {
|
if (
|
||||||
|
boardsConfig.selectedBoard &&
|
||||||
|
availableBoards.every(({ selected }) => !selected)
|
||||||
|
) {
|
||||||
|
let port = boardsConfig.selectedPort;
|
||||||
|
// If the selected board has the same port of an unknown board
|
||||||
|
// that is already in availableBoards we might get a duplicate port.
|
||||||
|
// So we remove the one already in the array and add the selected one.
|
||||||
|
const found = availableBoards.findIndex(
|
||||||
|
(board) => board.port?.address === boardsConfig.selectedPort?.address
|
||||||
|
);
|
||||||
|
if (found >= 0) {
|
||||||
|
// get the "Unknown board port" that we will substitute,
|
||||||
|
// then we can include it in the "availableBoard object"
|
||||||
|
// pushed below; to ensure addressLabel is included
|
||||||
|
port = availableBoards[found].port;
|
||||||
|
availableBoards.splice(found, 1);
|
||||||
|
}
|
||||||
availableBoards.push({
|
availableBoards.push({
|
||||||
...boardsConfig.selectedBoard,
|
...boardsConfig.selectedBoard,
|
||||||
port: boardsConfig.selectedPort,
|
port,
|
||||||
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.storageService.getData<Board>(key);
|
return this.getData<Board>(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async saveState(): Promise<void> {
|
protected async saveState(): Promise<void> {
|
||||||
@@ -360,21 +666,25 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
|||||||
const { selectedBoard, selectedPort } = this.boardsConfig;
|
const { selectedBoard, selectedPort } = this.boardsConfig;
|
||||||
if (selectedBoard && selectedPort) {
|
if (selectedBoard && selectedPort) {
|
||||||
const key = this.getLastSelectedBoardOnPortKey(selectedPort);
|
const key = this.getLastSelectedBoardOnPortKey(selectedPort);
|
||||||
await this.storageService.setData(key, selectedBoard);
|
await this.setData(key, selectedBoard);
|
||||||
}
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig),
|
this.setData('latest-valid-boards-config', this.latestValidBoardsConfig),
|
||||||
this.storageService.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.storageService.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)) {
|
||||||
@@ -382,15 +692,64 @@ 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.
|
||||||
const storedLatestBoardsConfig = await this.storageService.getData<BoardsConfig.Config | undefined>('latest-boards-config');
|
let storedLatestBoardsConfig = await this.getData<
|
||||||
|
BoardsConfig.Config | undefined
|
||||||
|
>('latest-boards-config');
|
||||||
|
// Try to get from the startup task. Wait for it, then timeout. Maybe it never arrives.
|
||||||
|
if (!storedLatestBoardsConfig) {
|
||||||
|
storedLatestBoardsConfig = await Promise.race([
|
||||||
|
this.inheritedConfig.promise,
|
||||||
|
new Promise<undefined>((resolve) =>
|
||||||
|
setTimeout(() => resolve(undefined), 2_000)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
if (storedLatestBoardsConfig) {
|
if (storedLatestBoardsConfig) {
|
||||||
this.latestBoardsConfig = storedLatestBoardsConfig;
|
this.latestBoardsConfig = storedLatestBoardsConfig;
|
||||||
this.boardsConfig = this.latestBoardsConfig;
|
this.boardsConfig = this.latestBoardsConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setData<T>(key: string, value: T): Promise<void> {
|
||||||
|
return this.commandService.executeCommand(
|
||||||
|
StorageWrapper.Commands.SET_DATA.id,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getData<T>(key: string): Promise<T | undefined> {
|
||||||
|
return this.commandService.executeCommand<T>(
|
||||||
|
StorageWrapper.Commands.GET_DATA.id,
|
||||||
|
key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks(): StartupTask[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
command: USE_INHERITED_CONFIG.id,
|
||||||
|
args: [this.boardsConfig],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It should be neither visible nor called from outside.
|
||||||
|
*
|
||||||
|
* This service creates a startup task with the current board config and
|
||||||
|
* passes the task to the electron-main process so that the new window
|
||||||
|
* can inherit the boards config state of this service.
|
||||||
|
*
|
||||||
|
* Note that the state is always set, but new windows might ignore it.
|
||||||
|
* For example, the new window already has a valid boards config persisted to the local storage.
|
||||||
|
*/
|
||||||
|
const USE_INHERITED_CONFIG: Command = {
|
||||||
|
id: 'arduino-use-inherited-boards-config',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a ready-to-use board, either the user has configured it or was automatically recognized by the CLI.
|
* Representation of a ready-to-use board, either the user has configured it or was automatically recognized by the CLI.
|
||||||
* An available board was not necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`.
|
* An available board was not necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`.
|
||||||
@@ -404,7 +763,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.
|
||||||
@@ -417,47 +775,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.addressLabel}
|
||||||
|
</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,40 @@
|
|||||||
import { injectable } from 'inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { BoardsListWidget } from './boards-list-widget';
|
import {
|
||||||
import { BoardsPackage } from '../../common/protocol/boards-service';
|
BoardSearch,
|
||||||
|
BoardsPackage,
|
||||||
|
} from '../../common/protocol/boards-service';
|
||||||
|
import { URI } from '../contributions/contribution';
|
||||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||||
|
import { BoardsListWidget } from './boards-list-widget';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
|
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||||
|
BoardsPackage,
|
||||||
|
BoardSearch
|
||||||
|
> {
|
||||||
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> {
|
protected canParse(uri: URI): boolean {
|
||||||
this.openView();
|
try {
|
||||||
|
BoardSearch.UriParser.parse(uri);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected parse(uri: URI): BoardSearch | undefined {
|
||||||
|
return BoardSearch.UriParser.parse(uri);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,47 @@
|
|||||||
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;
|
private 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> {
|
private 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,
|
||||||
|
modal: true,
|
||||||
});
|
});
|
||||||
if (!toAddUri) {
|
if (!toAddUri) {
|
||||||
return;
|
return;
|
||||||
@@ -45,24 +53,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,53 +1,68 @@
|
|||||||
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 { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
|
||||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { InstallationProgressDialog } from '../widgets/progress-dialog';
|
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||||
import { LibraryService } from '../../common/protocol';
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||||
import { ConfirmDialog } from '@theia/core/lib/browser';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
|
||||||
|
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||||
|
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;
|
private readonly envVariableServer: EnvVariablesServer;
|
||||||
|
|
||||||
|
@inject(ResponseServiceClient)
|
||||||
|
private readonly responseService: ResponseServiceClient;
|
||||||
|
|
||||||
@inject(LibraryService)
|
@inject(LibraryService)
|
||||||
protected readonly libraryService: LibraryService;
|
private 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',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addZipLibrary(): Promise<void> {
|
private async addZipLibrary(): Promise<void> {
|
||||||
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",
|
remote.getCurrentWindow(),
|
||||||
|
{
|
||||||
|
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]);
|
||||||
try {
|
try {
|
||||||
@@ -56,9 +71,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);
|
||||||
@@ -69,45 +87,64 @@ export class AddZipLibrary extends SketchContribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
|
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
|
||||||
const dialog = new InstallationProgressDialog('Installing library', 'zip');
|
|
||||||
try {
|
try {
|
||||||
this.outputChannelManager.getChannel('Arduino').clear();
|
await ExecuteWithProgress.doWithProgress({
|
||||||
dialog.open();
|
messageService: this.messageService,
|
||||||
await this.libraryService.installZip({ zipUri, overwrite });
|
progressText:
|
||||||
|
nls.localize('arduino/common/processing', 'Processing') +
|
||||||
|
` ${new URI(zipUri).path.base}`,
|
||||||
|
responseService: this.responseService,
|
||||||
|
run: () => this.libraryService.installZip({ zipUri, overwrite }),
|
||||||
|
});
|
||||||
|
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?'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.messageService.error(error.toString());
|
this.messageService.error(error.toString());
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
|
||||||
dialog.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,58 @@
|
|||||||
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> {
|
private 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(
|
||||||
|
remote.getCurrentWindow(),
|
||||||
|
{
|
||||||
|
title: nls.localize(
|
||||||
|
'arduino/sketch/saveSketchAs',
|
||||||
|
'Save sketch folder as...'
|
||||||
|
),
|
||||||
|
defaultPath,
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!filePath || canceled) {
|
if (!filePath || canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -41,15 +61,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,31 @@
|
|||||||
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 {
|
||||||
import { firstToUpperCase } from '../../common/utils';
|
DisposableCollection,
|
||||||
|
Disposable,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
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,185 +46,324 @@ 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) {
|
installedBoards.forEach((board, index) => {
|
||||||
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,
|
||||||
|
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
|
||||||
|
};
|
||||||
this.commandRegistry.registerCommand(command, handler);
|
this.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,
|
||||||
|
nls.localize(
|
||||||
|
'arduino/board/typeOfPorts',
|
||||||
|
'{0} ports',
|
||||||
|
Port.Protocols.protocolLabel(protocol)
|
||||||
|
),
|
||||||
|
{ order: protocolOrder.toString().padStart(4) }
|
||||||
|
);
|
||||||
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(
|
||||||
if (!boards.length) {
|
(left: string, right: string): number => {
|
||||||
boards.push({
|
const [, leftBoards] = ports[left];
|
||||||
name: ''
|
const [, rightBoards] = ports[right];
|
||||||
});
|
return rightBoards.length - leftBoards.length;
|
||||||
}
|
}
|
||||||
for (const { name, fqbn } of boards) {
|
);
|
||||||
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''}`;
|
|
||||||
|
for (let i = 0; i < sortedIDs.length; i++) {
|
||||||
|
const portID = sortedIDs[i];
|
||||||
|
const [port, boards] = ports[portID];
|
||||||
|
let label = `${port.addressLabel}`;
|
||||||
|
if (boards.length) {
|
||||||
|
const boardsList = boards.map((board) => board.name).join(', ');
|
||||||
|
label = `${label} (${boardsList})`;
|
||||||
|
}
|
||||||
|
const id = `arduino-select-port--${portID}`;
|
||||||
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: String(protocolOrder + i + 1).padStart(4),
|
||||||
};
|
};
|
||||||
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.groupByProtocol(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();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
|
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
|
||||||
const allBoards = await this.boardsService.searchBoards({});
|
const allBoards = await this.boardsService.getInstalledBoards();
|
||||||
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,88 @@
|
|||||||
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;
|
this.clearVisibleNotification();
|
||||||
if (monitorConfig) {
|
const options = await this.options();
|
||||||
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: 1000 });
|
|
||||||
} 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,69 @@
|
|||||||
|
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 CheckForIDEUpdates 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(() => {
|
||||||
|
if (!this.preferences['arduino.checkForUpdates']) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return 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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import type { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { InstallManually, Later } from '../../common/nls';
|
||||||
|
import {
|
||||||
|
ArduinoComponent,
|
||||||
|
BoardsPackage,
|
||||||
|
BoardsService,
|
||||||
|
LibraryPackage,
|
||||||
|
LibraryService,
|
||||||
|
ResponseServiceClient,
|
||||||
|
Searchable,
|
||||||
|
} from '../../common/protocol';
|
||||||
|
import { Installable } from '../../common/protocol/installable';
|
||||||
|
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||||
|
import { BoardsListWidgetFrontendContribution } from '../boards/boards-widget-frontend-contribution';
|
||||||
|
import { LibraryListWidgetFrontendContribution } from '../library/library-widget-frontend-contribution';
|
||||||
|
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||||
|
import type { ListWidget } from '../widgets/component-list/list-widget';
|
||||||
|
import { Command, CommandRegistry, Contribution } from './contribution';
|
||||||
|
|
||||||
|
const NoUpdates = nls.localize(
|
||||||
|
'arduino/checkForUpdates/noUpdates',
|
||||||
|
'There are no recent updates available.'
|
||||||
|
);
|
||||||
|
const PromptUpdateBoards = nls.localize(
|
||||||
|
'arduino/checkForUpdates/promptUpdateBoards',
|
||||||
|
'Updates are available for some of your boards.'
|
||||||
|
);
|
||||||
|
const PromptUpdateLibraries = nls.localize(
|
||||||
|
'arduino/checkForUpdates/promptUpdateLibraries',
|
||||||
|
'Updates are available for some of your libraries.'
|
||||||
|
);
|
||||||
|
const UpdatingBoards = nls.localize(
|
||||||
|
'arduino/checkForUpdates/updatingBoards',
|
||||||
|
'Updating boards...'
|
||||||
|
);
|
||||||
|
const UpdatingLibraries = nls.localize(
|
||||||
|
'arduino/checkForUpdates/updatingLibraries',
|
||||||
|
'Updating libraries...'
|
||||||
|
);
|
||||||
|
const InstallAll = nls.localize(
|
||||||
|
'arduino/checkForUpdates/installAll',
|
||||||
|
'Install All'
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Task<T extends ArduinoComponent> {
|
||||||
|
readonly run: () => Promise<void>;
|
||||||
|
readonly item: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Updatable = { type: 'Updatable' } as const;
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class CheckForUpdates extends Contribution {
|
||||||
|
@inject(WindowServiceExt)
|
||||||
|
private readonly windowService: WindowServiceExt;
|
||||||
|
@inject(ResponseServiceClient)
|
||||||
|
private readonly responseService: ResponseServiceClient;
|
||||||
|
@inject(BoardsService)
|
||||||
|
private readonly boardsService: BoardsService;
|
||||||
|
@inject(LibraryService)
|
||||||
|
private readonly libraryService: LibraryService;
|
||||||
|
@inject(BoardsListWidgetFrontendContribution)
|
||||||
|
private readonly boardsContribution: BoardsListWidgetFrontendContribution;
|
||||||
|
@inject(LibraryListWidgetFrontendContribution)
|
||||||
|
private readonly librariesContribution: LibraryListWidgetFrontendContribution;
|
||||||
|
|
||||||
|
override registerCommands(register: CommandRegistry): void {
|
||||||
|
register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
|
||||||
|
execute: () => this.checkForUpdates(false),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override async onReady(): Promise<void> {
|
||||||
|
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||||
|
if (checkForUpdates) {
|
||||||
|
this.windowService.isFirstWindow().then((firstWindow) => {
|
||||||
|
if (firstWindow) {
|
||||||
|
this.checkForUpdates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkForUpdates(silent = true) {
|
||||||
|
const [boardsPackages, libraryPackages] = await Promise.all([
|
||||||
|
this.boardsService.search(Updatable),
|
||||||
|
this.libraryService.search(Updatable),
|
||||||
|
]);
|
||||||
|
this.promptUpdateBoards(boardsPackages);
|
||||||
|
this.promptUpdateLibraries(libraryPackages);
|
||||||
|
if (!libraryPackages.length && !boardsPackages.length && !silent) {
|
||||||
|
this.messageService.info(NoUpdates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private promptUpdateBoards(items: BoardsPackage[]): void {
|
||||||
|
this.prompt({
|
||||||
|
items,
|
||||||
|
installable: this.boardsService,
|
||||||
|
viewContribution: this.boardsContribution,
|
||||||
|
viewSearchOptions: { query: '', ...Updatable },
|
||||||
|
promptMessage: PromptUpdateBoards,
|
||||||
|
updatingMessage: UpdatingBoards,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private promptUpdateLibraries(items: LibraryPackage[]): void {
|
||||||
|
this.prompt({
|
||||||
|
items,
|
||||||
|
installable: this.libraryService,
|
||||||
|
viewContribution: this.librariesContribution,
|
||||||
|
viewSearchOptions: { query: '', topic: 'All', ...Updatable },
|
||||||
|
promptMessage: PromptUpdateLibraries,
|
||||||
|
updatingMessage: UpdatingLibraries,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private prompt<
|
||||||
|
T extends ArduinoComponent,
|
||||||
|
S extends Searchable.Options
|
||||||
|
>(options: {
|
||||||
|
items: T[];
|
||||||
|
installable: Installable<T>;
|
||||||
|
viewContribution: AbstractViewContribution<ListWidget<T, S>>;
|
||||||
|
viewSearchOptions: S;
|
||||||
|
promptMessage: string;
|
||||||
|
updatingMessage: string;
|
||||||
|
}): void {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
installable,
|
||||||
|
viewContribution,
|
||||||
|
promptMessage: message,
|
||||||
|
viewSearchOptions,
|
||||||
|
updatingMessage,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (!items.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.messageService
|
||||||
|
.info(message, Later, InstallManually, InstallAll)
|
||||||
|
.then((answer) => {
|
||||||
|
if (answer === InstallAll) {
|
||||||
|
const tasks = items.map((item) =>
|
||||||
|
this.createInstallTask(item, installable)
|
||||||
|
);
|
||||||
|
this.executeTasks(updatingMessage, tasks);
|
||||||
|
} else if (answer === InstallManually) {
|
||||||
|
viewContribution
|
||||||
|
.openView({ reveal: true })
|
||||||
|
.then((widget) => widget.refresh(viewSearchOptions));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeTasks(
|
||||||
|
message: string,
|
||||||
|
tasks: Task<ArduinoComponent>[]
|
||||||
|
): Promise<void> {
|
||||||
|
if (tasks.length) {
|
||||||
|
return ExecuteWithProgress.withProgress(
|
||||||
|
message,
|
||||||
|
this.messageService,
|
||||||
|
async (progress) => {
|
||||||
|
try {
|
||||||
|
const total = tasks.length;
|
||||||
|
let count = 0;
|
||||||
|
for (const { run, item } of tasks) {
|
||||||
|
try {
|
||||||
|
await run(); // runs update sequentially. // TODO: is parallel update desired?
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.messageService.error(
|
||||||
|
`Failed to update ${item.name}. ${err}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
progress.report({ work: { total, done: ++count } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
progress.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createInstallTask<T extends ArduinoComponent>(
|
||||||
|
item: T,
|
||||||
|
installable: Installable<T>
|
||||||
|
): Task<T> {
|
||||||
|
const latestVersion = item.availableVersions[0];
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
run: () =>
|
||||||
|
Installable.installWithProgress({
|
||||||
|
installable,
|
||||||
|
item,
|
||||||
|
version: latestVersion,
|
||||||
|
messageService: this.messageService,
|
||||||
|
responseService: this.responseService,
|
||||||
|
keepOutput: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace CheckForUpdates {
|
||||||
|
export namespace Commands {
|
||||||
|
export const CHECK_FOR_UPDATES: Command = Command.toLocalizedCommand(
|
||||||
|
{
|
||||||
|
id: 'arduino-check-for-updates',
|
||||||
|
label: 'Check for Arduino Updates',
|
||||||
|
category: 'Arduino',
|
||||||
|
},
|
||||||
|
'arduino/checkForUpdates/checkForUpdates'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,42 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { toArray } from '@phosphor/algorithm';
|
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
|
||||||
import { remote } from 'electron';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
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 type { MaybePromise } from '@theia/core/lib/common/types';
|
||||||
|
import type {
|
||||||
|
FrontendApplication,
|
||||||
|
OnWillStopAction,
|
||||||
|
} from '@theia/core/lib/browser/frontend-application';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
import {
|
||||||
|
SketchContribution,
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
Sketch,
|
||||||
|
URI,
|
||||||
|
} from './contribution';
|
||||||
|
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
import { SaveAsSketch } from './save-as-sketch';
|
import { SaveAsSketch } from './save-as-sketch';
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, URI } from './contribution';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
|
private shell: ApplicationShell | undefined;
|
||||||
|
|
||||||
@inject(EditorManager)
|
override onStart(app: FrontendApplication): MaybePromise<void> {
|
||||||
protected readonly editorManager: EditorManager;
|
|
||||||
|
|
||||||
protected shell: ApplicationShell;
|
|
||||||
|
|
||||||
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: () => {
|
||||||
|
|
||||||
// Close current editor if closeable.
|
// Close current editor if closeable.
|
||||||
const { currentEditor } = this.editorManager;
|
const { currentEditor } = this.editorManager;
|
||||||
if (currentEditor && currentEditor.title.closable) {
|
if (currentEditor && currentEditor.title.closable) {
|
||||||
@@ -35,62 +44,155 @@ export class Close extends SketchContribution {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.shell) {
|
||||||
// Close current widget from the main area if possible.
|
// Close current widget from the main area if possible.
|
||||||
const { currentWidget } = this.shell;
|
const { currentWidget } = this.shell;
|
||||||
if (currentWidget) {
|
if (currentWidget) {
|
||||||
const currentWidgetInMain = toArray(this.shell.mainPanel.widgets()).find(widget => widget === currentWidget);
|
const currentWidgetInMain = toArray(
|
||||||
|
this.shell.mainPanel.widgets()
|
||||||
|
).find((widget) => widget === currentWidget);
|
||||||
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
|
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
|
||||||
return currentWidgetInMain.close();
|
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();
|
|
||||||
}
|
}
|
||||||
|
return remote.getCurrentWindow().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: '6',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `FrontendApplicationContribution#onWillStop`
|
||||||
|
onWillStop(): OnWillStopAction {
|
||||||
|
return {
|
||||||
|
reason: 'save-sketch',
|
||||||
|
action: () => {
|
||||||
|
return this.showSaveSketchDialog();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If returns with `true`, IDE2 will close. Otherwise, it won't.
|
||||||
|
*/
|
||||||
|
private async showSaveSketchDialog(): Promise<boolean> {
|
||||||
|
const sketch = await this.isCurrentSketchTemp();
|
||||||
|
if (!sketch) {
|
||||||
|
// Normal close workflow: if there are dirty editors prompt the user.
|
||||||
|
if (!this.shell) {
|
||||||
|
console.error(
|
||||||
|
`Could not get the application shell. Something went wrong.`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.shell.canSaveAll()) {
|
||||||
|
const prompt = await this.prompt(false);
|
||||||
|
switch (prompt) {
|
||||||
|
case Prompt.DoNotSave:
|
||||||
|
return true;
|
||||||
|
case Prompt.Cancel:
|
||||||
|
return false;
|
||||||
|
case Prompt.Save: {
|
||||||
|
await this.shell.saveAll();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected prompt: ${prompt}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If non of the sketch files were ever touched, do not prompt the save dialog. (#1274)
|
||||||
|
const wereTouched = await Promise.all(
|
||||||
|
Sketch.uris(sketch).map((uri) => this.wasTouched(uri))
|
||||||
|
);
|
||||||
|
if (wereTouched.every((wasTouched) => !Boolean(wasTouched))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = await this.prompt(true);
|
||||||
|
switch (prompt) {
|
||||||
|
case Prompt.DoNotSave:
|
||||||
|
return true;
|
||||||
|
case Prompt.Cancel:
|
||||||
|
return false;
|
||||||
|
case Prompt.Save: {
|
||||||
|
// If `save as` was canceled by user, the result will be `undefined`, otherwise the new URI.
|
||||||
|
const result = await this.commandService.executeCommand(
|
||||||
|
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||||
|
{
|
||||||
|
execOnlyIfTemp: false,
|
||||||
|
openAfterMove: false,
|
||||||
|
wipeOriginal: true,
|
||||||
|
markAsRecentlyOpened: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return !!result;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected prompt: ${prompt}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prompt(isTemp: boolean): Promise<Prompt> {
|
||||||
|
const { response } = await remote.dialog.showMessageBox(
|
||||||
|
remote.getCurrentWindow(),
|
||||||
|
{
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/sketch/saveSketch',
|
||||||
|
'Save your sketch to open it again later.'
|
||||||
|
),
|
||||||
|
title: nls.localize(
|
||||||
|
'theia/core/quitTitle',
|
||||||
|
'Are you sure you want to quit?'
|
||||||
|
),
|
||||||
|
type: 'question',
|
||||||
|
buttons: [
|
||||||
|
nls.localizeByDefault("Don't Save"),
|
||||||
|
Dialog.CANCEL,
|
||||||
|
nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'),
|
||||||
|
],
|
||||||
|
defaultId: 2, // `Save`/`Save As...` button index is the default.
|
||||||
|
}
|
||||||
|
);
|
||||||
|
switch (response) {
|
||||||
|
case 0:
|
||||||
|
return Prompt.DoNotSave;
|
||||||
|
case 1:
|
||||||
|
return Prompt.Cancel;
|
||||||
|
case 2:
|
||||||
|
return Prompt.Save;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected response: ${response}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isCurrentSketchTemp(): Promise<false | Sketch> {
|
||||||
|
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||||
|
if (CurrentSketch.isValid(currentSketch)) {
|
||||||
|
const isTemp = await this.sketchService.isTemp(currentSketch);
|
||||||
|
if (isTemp) {
|
||||||
|
return currentSketch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
|
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
|
||||||
*/
|
*/
|
||||||
@@ -100,7 +202,7 @@ export class Close extends SketchContribution {
|
|||||||
const { editor } = editorWidget;
|
const { editor } = editorWidget;
|
||||||
if (editor instanceof MonacoEditor) {
|
if (editor instanceof MonacoEditor) {
|
||||||
const versionId = editor.getControl().getModel()?.getVersionId();
|
const versionId = editor.getControl().getModel()?.getVersionId();
|
||||||
if (Number.isInteger(versionId) && versionId! > 1) {
|
if (this.isInteger(versionId) && versionId > 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,12 +210,21 @@ export class Close extends SketchContribution {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isInteger(arg: unknown): arg is number {
|
||||||
|
return Number.isInteger(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Prompt {
|
||||||
|
Save,
|
||||||
|
DoNotSave,
|
||||||
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
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,804 @@
|
|||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
Emitter,
|
||||||
|
MaybeArray,
|
||||||
|
MaybePromise,
|
||||||
|
nls,
|
||||||
|
notEmpty,
|
||||||
|
} from '@theia/core';
|
||||||
|
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
|
||||||
|
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
|
||||||
|
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 { OutputUri } from '@theia/output/lib/common/output-uri';
|
||||||
|
import { CoreError } from '../../common/protocol/core-service';
|
||||||
|
import { ErrorRevealStrategy } from '../arduino-preferences';
|
||||||
|
import { ArduinoOutputSelector, InoSelector } from '../selectors';
|
||||||
|
import { Contribution } from './contribution';
|
||||||
|
import { CoreErrorHandler } from './core-error-handler';
|
||||||
|
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||||
|
|
||||||
|
interface ErrorDecorationRef {
|
||||||
|
/**
|
||||||
|
* This is the unique ID of the decoration given by `monaco`.
|
||||||
|
*/
|
||||||
|
readonly id: string;
|
||||||
|
/**
|
||||||
|
* The resource this decoration belongs to.
|
||||||
|
*/
|
||||||
|
readonly uri: string;
|
||||||
|
}
|
||||||
|
export namespace ErrorDecorationRef {
|
||||||
|
export function is(arg: unknown): arg is ErrorDecorationRef {
|
||||||
|
if (typeof arg === 'object') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const object = arg as any;
|
||||||
|
return (
|
||||||
|
'uri' in object &&
|
||||||
|
typeof object['uri'] === 'string' &&
|
||||||
|
'id' in object &&
|
||||||
|
typeof object['id'] === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
export function sameAs(
|
||||||
|
left: ErrorDecorationRef,
|
||||||
|
right: ErrorDecorationRef
|
||||||
|
): boolean {
|
||||||
|
return left.id === right.id && left.uri === right.uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorDecoration extends ErrorDecorationRef {
|
||||||
|
/**
|
||||||
|
* The range of the error location the error in the compiler output from the CLI.
|
||||||
|
*/
|
||||||
|
readonly rangesInOutput: monaco.Range[];
|
||||||
|
}
|
||||||
|
namespace ErrorDecoration {
|
||||||
|
export function rangeOf(
|
||||||
|
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||||
|
decorations: ErrorDecoration
|
||||||
|
): monaco.Range | undefined;
|
||||||
|
export function rangeOf(
|
||||||
|
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||||
|
decorations: ErrorDecoration[]
|
||||||
|
): (monaco.Range | undefined)[];
|
||||||
|
export function rangeOf(
|
||||||
|
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||||
|
decorations: ErrorDecoration | ErrorDecoration[]
|
||||||
|
): MaybePromise<MaybeArray<monaco.Range | undefined>> {
|
||||||
|
if (editorOrModel) {
|
||||||
|
const allDecorations = getAllDecorations(editorOrModel);
|
||||||
|
if (allDecorations) {
|
||||||
|
if (Array.isArray(decorations)) {
|
||||||
|
return decorations.map(({ id: decorationId }) =>
|
||||||
|
findRangeOf(decorationId, allDecorations)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return findRangeOf(decorations.id, allDecorations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.isArray(decorations)
|
||||||
|
? decorations.map(() => undefined)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
function findRangeOf(
|
||||||
|
decorationId: string,
|
||||||
|
allDecorations: { id: string; range?: monaco.Range }[]
|
||||||
|
): monaco.Range | undefined {
|
||||||
|
return allDecorations.find(
|
||||||
|
({ id: candidateId }) => candidateId === decorationId
|
||||||
|
)?.range;
|
||||||
|
}
|
||||||
|
function getAllDecorations(
|
||||||
|
editorOrModel: MonacoEditor | ITextModel
|
||||||
|
): { id: string; range?: monaco.Range }[] {
|
||||||
|
if (editorOrModel instanceof MonacoEditor) {
|
||||||
|
const model = editorOrModel.getControl().getModel();
|
||||||
|
if (!model) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return model.getAllDecorations();
|
||||||
|
}
|
||||||
|
return editorOrModel.getAllDecorations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class CompilerErrors
|
||||||
|
extends Contribution
|
||||||
|
implements monaco.languages.CodeLensProvider, monaco.languages.LinkProvider
|
||||||
|
{
|
||||||
|
@inject(EditorManager)
|
||||||
|
private readonly editorManager: EditorManager;
|
||||||
|
|
||||||
|
@inject(ProtocolToMonacoConverter)
|
||||||
|
private readonly p2m: ProtocolToMonacoConverter;
|
||||||
|
|
||||||
|
@inject(MonacoToProtocolConverter)
|
||||||
|
private readonly m2p: MonacoToProtocolConverter;
|
||||||
|
|
||||||
|
@inject(CoreErrorHandler)
|
||||||
|
private readonly coreErrorHandler: CoreErrorHandler;
|
||||||
|
|
||||||
|
private revealStrategy = ErrorRevealStrategy.Default;
|
||||||
|
private experimental = false;
|
||||||
|
|
||||||
|
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 currentError: ErrorDecoration | undefined;
|
||||||
|
private get currentErrorIndex(): number {
|
||||||
|
const current = this.currentError;
|
||||||
|
if (!current) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return this.errors.findIndex((error) =>
|
||||||
|
ErrorDecorationRef.sameAs(error, current)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onStart(app: FrontendApplication): void {
|
||||||
|
this.shell = app.shell;
|
||||||
|
monaco.languages.registerCodeLensProvider(InoSelector, this);
|
||||||
|
monaco.languages.registerLinkProvider(ArduinoOutputSelector, this);
|
||||||
|
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
|
||||||
|
this.handleCompilerErrorsDidChange(errors)
|
||||||
|
);
|
||||||
|
this.onCurrentErrorDidChange(async (error) => {
|
||||||
|
const monacoEditor = await this.monacoEditor(error.uri);
|
||||||
|
const monacoRange = ErrorDecoration.rangeOf(monacoEditor, error);
|
||||||
|
if (!monacoRange) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Could not find range of decoration: ${error.id}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const range = this.m2p.asRange(monacoRange);
|
||||||
|
const editor = await this.revealLocationInEditor({
|
||||||
|
uri: error.uri,
|
||||||
|
range,
|
||||||
|
});
|
||||||
|
if (!editor) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Failed to mark error ${error.id} as the current one.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const monacoEditor = this.monacoEditor(editor);
|
||||||
|
if (monacoEditor) {
|
||||||
|
monacoEditor.cursor = range.start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override onReady(): MaybePromise<void> {
|
||||||
|
this.preferences.ready.then(() => {
|
||||||
|
this.experimental = Boolean(
|
||||||
|
this.preferences['arduino.compile.experimental']
|
||||||
|
);
|
||||||
|
const strategy = this.preferences['arduino.compile.revealRange'];
|
||||||
|
this.revealStrategy = ErrorRevealStrategy.is(strategy)
|
||||||
|
? strategy
|
||||||
|
: ErrorRevealStrategy.Default;
|
||||||
|
this.preferences.onPreferenceChanged(
|
||||||
|
({ preferenceName, newValue, oldValue }) => {
|
||||||
|
if (newValue === oldValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (preferenceName) {
|
||||||
|
case 'arduino.compile.revealRange': {
|
||||||
|
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
||||||
|
? newValue
|
||||||
|
: ErrorRevealStrategy.Default;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'arduino.compile.experimental': {
|
||||||
|
this.experimental = Boolean(newValue);
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
return this.markAsCurrentError(nextError, {
|
||||||
|
forceReselect: true,
|
||||||
|
reveal: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isEnabled: () =>
|
||||||
|
this.experimental && !!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];
|
||||||
|
return this.markAsCurrentError(previousError, {
|
||||||
|
forceReselect: true,
|
||||||
|
reveal: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isEnabled: () =>
|
||||||
|
this.experimental && !!this.currentError && this.errors.length > 1,
|
||||||
|
});
|
||||||
|
registry.registerCommand(CompilerErrors.Commands.MARK_AS_CURRENT, {
|
||||||
|
execute: (arg: unknown) => {
|
||||||
|
if (ErrorDecorationRef.is(arg)) {
|
||||||
|
return this.markAsCurrentError(
|
||||||
|
{ id: arg.id, uri: new URI(arg.uri).toString() }, // Make sure the URI fragments are encoded. On Windows, `C:` is encoded as `C%3A`.
|
||||||
|
{ forceReselect: true, reveal: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEnabled: () => !!this.errors.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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.experimental &&
|
||||||
|
this.currentError &&
|
||||||
|
this.currentError.uri === model.uri.toString() &&
|
||||||
|
this.errors.length > 1
|
||||||
|
) {
|
||||||
|
const monacoEditor = await this.monacoEditor(model.uri);
|
||||||
|
const range = ErrorDecoration.rangeOf(monacoEditor, this.currentError);
|
||||||
|
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 */
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async provideLinks(
|
||||||
|
model: monaco.editor.ITextModel,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_token: monaco.CancellationToken
|
||||||
|
): Promise<monaco.languages.ILinksList> {
|
||||||
|
const links: monaco.languages.ILink[] = [];
|
||||||
|
if (
|
||||||
|
model.uri.scheme === OutputUri.SCHEME &&
|
||||||
|
model.uri.path === '/Arduino'
|
||||||
|
) {
|
||||||
|
links.push(
|
||||||
|
...this.errors
|
||||||
|
.filter((decoration) => !!decoration.rangesInOutput.length)
|
||||||
|
.map(({ rangesInOutput, id, uri }) =>
|
||||||
|
rangesInOutput.map(
|
||||||
|
(range) =>
|
||||||
|
<monaco.languages.ILink>{
|
||||||
|
range,
|
||||||
|
url: monaco.Uri.parse(`command://`).with({
|
||||||
|
query: JSON.stringify({ id, uri }),
|
||||||
|
path: CompilerErrors.Commands.MARK_AS_CURRENT.id,
|
||||||
|
}),
|
||||||
|
tooltip: nls.localize(
|
||||||
|
'arduino/editor/revealError',
|
||||||
|
'Reveal Error'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.reduce((acc, curr) => acc.concat(curr), [])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn('unexpected URI: ' + model.uri.toString());
|
||||||
|
}
|
||||||
|
return { links };
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveLink(
|
||||||
|
link: monaco.languages.ILink,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_token: monaco.CancellationToken
|
||||||
|
): Promise<monaco.languages.ILink | undefined> {
|
||||||
|
if (!this.experimental) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const { url } = link;
|
||||||
|
if (url) {
|
||||||
|
const candidateUri = new URI(
|
||||||
|
typeof url === 'string' ? url : url.toString()
|
||||||
|
);
|
||||||
|
const candidateId = candidateUri.path.toString();
|
||||||
|
const error = this.errors.find((error) => error.id === candidateId);
|
||||||
|
if (error) {
|
||||||
|
const monacoEditor = await this.monacoEditor(error.uri);
|
||||||
|
const range = ErrorDecoration.rangeOf(monacoEditor, error);
|
||||||
|
if (range) {
|
||||||
|
return {
|
||||||
|
range,
|
||||||
|
url: monaco.Uri.parse(error.uri),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCompilerErrorsDidChange(
|
||||||
|
errors: CoreError.ErrorLocation[]
|
||||||
|
): Promise<void> {
|
||||||
|
this.toDisposeOnCompilerErrorDidChange.dispose();
|
||||||
|
const groupedErrors = this.groupBy(
|
||||||
|
errors,
|
||||||
|
(error: CoreError.ErrorLocation) => error.location.uri
|
||||||
|
);
|
||||||
|
const decorations = await this.decorateEditors(groupedErrors);
|
||||||
|
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(
|
||||||
|
groupedErrors,
|
||||||
|
(editor) =>
|
||||||
|
editor.onSelectionChanged((selection) =>
|
||||||
|
this.handleSelectionChange(editor, selection)
|
||||||
|
),
|
||||||
|
(editor) =>
|
||||||
|
editor.onDispose(() =>
|
||||||
|
this.handleEditorDidDispose(editor.uri.toString())
|
||||||
|
),
|
||||||
|
(editor) =>
|
||||||
|
editor.onDocumentContentChanged((event) =>
|
||||||
|
this.handleDocumentContentChange(editor, event)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
]);
|
||||||
|
const currentError = this.errors[0];
|
||||||
|
if (currentError) {
|
||||||
|
await this.markAsCurrentError(currentError, {
|
||||||
|
forceReselect: true,
|
||||||
|
reveal: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.monacoEditor(uri);
|
||||||
|
if (!editor) {
|
||||||
|
return { dispose: Disposable.NULL, errors: [] };
|
||||||
|
}
|
||||||
|
const oldDecorations = editor.deltaDecorations({
|
||||||
|
oldDecorations: [],
|
||||||
|
newDecorations: errors.map((error) =>
|
||||||
|
this.compilerErrorDecoration(error.location.range)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
dispose: Disposable.create(() => {
|
||||||
|
if (editor) {
|
||||||
|
editor.deltaDecorations({
|
||||||
|
oldDecorations,
|
||||||
|
newDecorations: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
errors: oldDecorations.map((id, index) => ({
|
||||||
|
id,
|
||||||
|
uri,
|
||||||
|
rangesInOutput: errors[index].rangesInOutput.map((range) =>
|
||||||
|
this.p2m.asRange(range)
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private compilerErrorDecoration(range: Range): EditorDecoration {
|
||||||
|
return {
|
||||||
|
range,
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
className: 'compiler-error',
|
||||||
|
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
monacoEditor: MonacoEditor,
|
||||||
|
selection: Range
|
||||||
|
): void {
|
||||||
|
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 errorsPerResource = this.errors.filter((error) => error.uri === uri);
|
||||||
|
const rangesPerResource = ErrorDecoration.rangeOf(
|
||||||
|
monacoEditor,
|
||||||
|
errorsPerResource
|
||||||
|
);
|
||||||
|
const error = rangesPerResource
|
||||||
|
.map((range, index) => ({ error: errorsPerResource[index], range }))
|
||||||
|
.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 the text document changes in the line where compiler errors are, the compiler errors will be removed.
|
||||||
|
*/
|
||||||
|
private handleDocumentContentChange(
|
||||||
|
monacoEditor: MonacoEditor,
|
||||||
|
event: TextDocumentChangeEvent
|
||||||
|
): void {
|
||||||
|
const errorsPerResource = this.errors.filter(
|
||||||
|
(error) => error.uri === event.document.uri
|
||||||
|
);
|
||||||
|
let editorOrModel: MonacoEditor | ITextModel = monacoEditor;
|
||||||
|
const doc = event.document;
|
||||||
|
if (doc instanceof MonacoEditorModel) {
|
||||||
|
editorOrModel = doc.textEditorModel;
|
||||||
|
}
|
||||||
|
const rangesPerResource = ErrorDecoration.rangeOf(
|
||||||
|
editorOrModel,
|
||||||
|
errorsPerResource
|
||||||
|
);
|
||||||
|
const resolvedDecorations = rangesPerResource.map((range, index) => ({
|
||||||
|
error: errorsPerResource[index],
|
||||||
|
range,
|
||||||
|
}));
|
||||||
|
const decoratorsToRemove = event.contentChanges
|
||||||
|
.map(({ range }) => this.p2m.asRange(range))
|
||||||
|
.map((changedRange) =>
|
||||||
|
resolvedDecorations
|
||||||
|
.filter(({ range: decorationRange }) => {
|
||||||
|
if (!decorationRange) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const affects =
|
||||||
|
changedRange.startLineNumber <= decorationRange.startLineNumber &&
|
||||||
|
changedRange.endLineNumber >= decorationRange.endLineNumber;
|
||||||
|
console.log(
|
||||||
|
'compiler-errors',
|
||||||
|
`decoration range: ${decorationRange.toString()}, change range: ${changedRange.toString()}, affects: ${affects}`
|
||||||
|
);
|
||||||
|
return affects;
|
||||||
|
})
|
||||||
|
.map(({ error }) => {
|
||||||
|
const index = this.errors.findIndex((candidate) =>
|
||||||
|
ErrorDecorationRef.sameAs(candidate, error)
|
||||||
|
);
|
||||||
|
return index !== -1 ? { error, index } : undefined;
|
||||||
|
})
|
||||||
|
.filter(notEmpty)
|
||||||
|
)
|
||||||
|
.reduce((acc, curr) => acc.concat(curr), [])
|
||||||
|
.sort((left, right) => left.index - right.index); // highest index last
|
||||||
|
|
||||||
|
if (decoratorsToRemove.length) {
|
||||||
|
let i = decoratorsToRemove.length;
|
||||||
|
while (i--) {
|
||||||
|
this.errors.splice(decoratorsToRemove[i].index, 1);
|
||||||
|
}
|
||||||
|
monacoEditor.getControl().deltaDecorations(
|
||||||
|
decoratorsToRemove.map(({ error }) => error.id),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
this.onDidChangeEmitter.fire(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async trackEditors(
|
||||||
|
errors: Map<string, CoreError.ErrorLocation[]>,
|
||||||
|
...track: ((editor: MonacoEditor) => Disposable)[]
|
||||||
|
): Promise<Disposable> {
|
||||||
|
return new DisposableCollection(
|
||||||
|
...(await Promise.all(
|
||||||
|
Array.from(errors.keys()).map(async (uri) => {
|
||||||
|
const editor = await this.monacoEditor(uri);
|
||||||
|
if (!editor) {
|
||||||
|
return Disposable.NULL;
|
||||||
|
}
|
||||||
|
return new DisposableCollection(...track.map((t) => t(editor)));
|
||||||
|
})
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async markAsCurrentError(
|
||||||
|
ref: ErrorDecorationRef,
|
||||||
|
options?: { forceReselect?: boolean; reveal?: boolean }
|
||||||
|
): Promise<void> {
|
||||||
|
const index = this.errors.findIndex((candidate) =>
|
||||||
|
ErrorDecorationRef.sameAs(candidate, ref)
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
console.warn(
|
||||||
|
'compiler-errors',
|
||||||
|
`Failed to mark error ${
|
||||||
|
ref.id
|
||||||
|
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
|
||||||
|
({ id }) => id
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newError = this.errors[index];
|
||||||
|
if (
|
||||||
|
options?.forceReselect ||
|
||||||
|
!this.currentError ||
|
||||||
|
!ErrorDecorationRef.sameAs(this.currentError, newError)
|
||||||
|
) {
|
||||||
|
this.currentError = this.errors[index];
|
||||||
|
console.log(
|
||||||
|
'compiler-errors',
|
||||||
|
`Current error changed to ${this.currentError.id}`
|
||||||
|
);
|
||||||
|
if (options?.reveal) {
|
||||||
|
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 find editor widget for URI: ${uri}`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupBy<K, V>(
|
||||||
|
elements: V[],
|
||||||
|
extractKey: (element: V) => K
|
||||||
|
): Map<K, V[]> {
|
||||||
|
return elements.reduce((acc, curr) => {
|
||||||
|
const key = extractKey(curr);
|
||||||
|
let values = acc.get(key);
|
||||||
|
if (!values) {
|
||||||
|
values = [];
|
||||||
|
acc.set(key, values);
|
||||||
|
}
|
||||||
|
values.push(curr);
|
||||||
|
return acc;
|
||||||
|
}, new Map<K, V[]>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
|
||||||
|
private monacoEditor(
|
||||||
|
uri: string | monaco.Uri
|
||||||
|
): Promise<MonacoEditor | undefined>;
|
||||||
|
private monacoEditor(
|
||||||
|
uriOrWidget: string | monaco.Uri | 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',
|
||||||
|
};
|
||||||
|
export const MARK_AS_CURRENT: Command = {
|
||||||
|
id: 'arduino-editor-mark-as-current-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';
|
||||||
@@ -7,25 +12,76 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
|||||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
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 { 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';
|
||||||
|
import { NotificationManager } from '../theia/messages/notifications-manager';
|
||||||
|
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||||
|
import { WorkspaceService } from '../theia/workspace/workspace-service';
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
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 +94,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 +150,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 +159,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 +169,118 @@ 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;
|
||||||
|
|
||||||
|
@inject(NotificationManager)
|
||||||
|
private readonly notificationManager: NotificationManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the internal (Theia) ID of the notification that is currently visible.
|
||||||
|
* It's stored here as a field to be able to close it before executing any new core command (such as verify, upload, etc.)
|
||||||
|
*/
|
||||||
|
private visibleNotificationId: string | undefined;
|
||||||
|
|
||||||
|
protected clearVisibleNotification(): void {
|
||||||
|
if (this.visibleNotificationId) {
|
||||||
|
this.notificationManager.clear(this.visibleNotificationId);
|
||||||
|
this.visibleNotificationId = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (message.includes('Missing FQBN (Fully Qualified Board Name)')) {
|
||||||
|
message = nls.localize(
|
||||||
|
'arduino/coreContribution/noBoardSelected',
|
||||||
|
'No board selected. Please select your Arduino board from the Tools > Board menu.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const copyAction = nls.localize(
|
||||||
|
'arduino/coreContribution/copyError',
|
||||||
|
'Copy error messages'
|
||||||
|
);
|
||||||
|
this.visibleNotificationId = this.notificationId(message, copyAction);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private notificationId(message: string, ...actions: string[]): string {
|
||||||
|
return this.notificationManager.getMessageId({
|
||||||
|
text: message,
|
||||||
|
actions,
|
||||||
|
type: MessageType.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { SketchesError } from '../../common/protocol';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
SketchContribution,
|
||||||
|
Sketch,
|
||||||
|
} from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class DeleteSketch extends SketchContribution {
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
|
||||||
|
execute: (uri: string) => this.deleteSketch(uri),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteSketch(uri: string): Promise<void> {
|
||||||
|
const sketch = await this.loadSketch(uri);
|
||||||
|
if (!sketch) {
|
||||||
|
console.info(`Sketch not found at ${uri}. Skipping deletion.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this.sketchService.deleteSketch(sketch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadSketch(uri: string): Promise<Sketch | undefined> {
|
||||||
|
try {
|
||||||
|
const sketch = await this.sketchService.loadSketch(uri);
|
||||||
|
return sketch;
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace DeleteSketch {
|
||||||
|
export namespace Commands {
|
||||||
|
export const DELETE_SKETCH: Command = {
|
||||||
|
id: 'arduino-delete-sketch',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,228 +1,216 @@
|
|||||||
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, { 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.FIND, { execute: () => this.run('actions.find') });
|
|
||||||
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.USE_FOR_FIND, { execute: () => this.run('editor.action.previousSelectionMatchFindAction') });
|
|
||||||
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
|
|
||||||
execute: async () => {
|
|
||||||
const settings = await this.settingsService.settings();
|
|
||||||
if (settings.autoScaleInterface) {
|
|
||||||
settings.interfaceScale = settings.interfaceScale + 1;
|
|
||||||
} else {
|
|
||||||
settings.editorFontSize = settings.editorFontSize + 1;
|
|
||||||
}
|
|
||||||
await this.settingsService.update(settings);
|
|
||||||
await this.settingsService.save();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
|
registry.registerCommand(EditContributions.Commands.TOGGLE_COMMENT, {
|
||||||
execute: async () => {
|
execute: () => this.run('editor.action.commentLine'),
|
||||||
const settings = await this.settingsService.settings();
|
|
||||||
if (settings.autoScaleInterface) {
|
|
||||||
settings.interfaceScale = settings.interfaceScale - 1;
|
|
||||||
} else {
|
|
||||||
settings.editorFontSize = settings.editorFontSize - 1;
|
|
||||||
}
|
|
||||||
await this.settingsService.update(settings);
|
|
||||||
await this.settingsService.save();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
/* Tools */registry.registerCommand(EditContributions.Commands.AUTO_FORMAT, { execute: () => this.run('editor.action.formatDocument') });
|
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.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'),
|
||||||
|
});
|
||||||
|
/* 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__CODE_CONTROL_GROUP, {
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
commandId: EditContributions.Commands.AUTO_FORMAT.id,
|
||||||
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
label: nls.localize('arduino/editor/autoFormat', 'Auto Format'),
|
||||||
label: 'Increase Font Size',
|
order: '3',
|
||||||
order: '0'
|
|
||||||
});
|
|
||||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
|
||||||
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
|
||||||
label: '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({
|
|
||||||
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
|
||||||
keybinding: 'CtrlCmd+='
|
|
||||||
});
|
|
||||||
registry.registerKeybinding({
|
|
||||||
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
|
||||||
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<
|
||||||
return this.codeEditorService.getFocusedCodeEditor() || this.codeEditorService.getActiveCodeEditor();
|
ICodeEditor | StandaloneCodeEditor | undefined
|
||||||
|
> {
|
||||||
|
return (
|
||||||
|
this.codeEditorService.getFocusedCodeEditor() ||
|
||||||
|
this.codeEditorService.getActiveCodeEditor() ||
|
||||||
|
undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async currentValue(): Promise<string | undefined> {
|
protected async currentValue(): Promise<string | undefined> {
|
||||||
@@ -246,49 +234,39 @@ ${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 = {
|
|
||||||
id: 'arduino-increase-font-size'
|
|
||||||
};
|
|
||||||
export const DECREASE_FONT_SIZE: Command = {
|
|
||||||
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,24 +1,43 @@
|
|||||||
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 { MenuPath, CompositeMenuNode, SubMenuOptions } from '@theia/core/lib/common/menu';
|
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
import {
|
||||||
|
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,
|
||||||
|
SketchesError,
|
||||||
|
Sketch,
|
||||||
|
CoreService,
|
||||||
|
} 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;
|
private readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
@inject(MenuModelRegistry)
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
private readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
@inject(MainMenuManager)
|
@inject(MainMenuManager)
|
||||||
protected readonly menuManager: MainMenuManager;
|
protected readonly menuManager: MainMenuManager;
|
||||||
@@ -26,26 +45,38 @@ export abstract class Examples extends SketchContribution {
|
|||||||
@inject(ExamplesService)
|
@inject(ExamplesService)
|
||||||
protected readonly examplesService: ExamplesService;
|
protected readonly examplesService: ExamplesService;
|
||||||
|
|
||||||
|
@inject(CoreService)
|
||||||
|
protected readonly coreService: CoreService;
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
@inject(BoardsServiceProvider)
|
||||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||||
|
|
||||||
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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
||||||
protected handleBoardChanged(board: Board | undefined): void {
|
protected handleBoardChanged(board: Board | undefined): void {
|
||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
registerMenus(registry: MenuModelRegistry): void {
|
protected abstract update(options?: {
|
||||||
|
board?: Board | undefined;
|
||||||
|
forceRefresh?: boolean;
|
||||||
|
}): 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);
|
||||||
@@ -55,21 +86,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;
|
||||||
|
|
||||||
@@ -88,97 +135,189 @@ 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 = {
|
const handler = this.createHandler(uri);
|
||||||
execute: async () => {
|
pushToDispose.push(
|
||||||
const sketch = await this.sketchService.cloneExample(uri);
|
this.commandRegistry.registerCommand(command, handler)
|
||||||
this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
|
);
|
||||||
}
|
this.menuRegistry.registerMenuAction(submenuPath, {
|
||||||
};
|
commandId,
|
||||||
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
|
label: sketch.name,
|
||||||
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name, order: sketch.name.toLocaleLowerCase() });
|
order: sketch.name.toLocaleLowerCase(),
|
||||||
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
|
});
|
||||||
|
pushToDispose.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.menuRegistry.unregisterMenuAction(command)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected createHandler(uri: string): CommandHandler {
|
||||||
|
return {
|
||||||
|
execute: async () => {
|
||||||
|
const sketch = await this.clone(uri);
|
||||||
|
if (sketch) {
|
||||||
|
try {
|
||||||
|
return this.commandService.executeCommand(
|
||||||
|
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
|
sketch
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
||||||
|
this.update({
|
||||||
|
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||||
|
forceRefresh: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clone(uri: string): Promise<Sketch | undefined> {
|
||||||
|
try {
|
||||||
|
const sketch = await this.sketchService.cloneExample(uri);
|
||||||
|
return sketch;
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
this.messageService.error(err.message);
|
||||||
|
this.update({
|
||||||
|
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||||
|
forceRefresh: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BuiltInExamples extends Examples {
|
export class BuiltInExamples extends Examples {
|
||||||
|
override async onReady(): Promise<void> {
|
||||||
onStart(): void {
|
this.update(); // no `await`
|
||||||
this.register(); // no `await`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async register(): Promise<void> {
|
protected override async update(): Promise<void> {
|
||||||
let sketchContainers: SketchContainer[] | undefined;
|
let sketchContainers: SketchContainer[] | undefined;
|
||||||
try {
|
try {
|
||||||
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;
|
private readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||||
|
|
||||||
onStart(): void {
|
override onStart(): void {
|
||||||
this.register(); // no `await`
|
this.notificationCenter.onLibraryDidInstall(() => this.update());
|
||||||
this.notificationCenter.onLibraryInstalled(() => this.register());
|
this.notificationCenter.onLibraryDidUninstall(() => this.update());
|
||||||
this.notificationCenter.onLibraryUninstalled(() => this.register());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleBoardChanged(board: Board | undefined): void {
|
override async onReady(): Promise<void> {
|
||||||
this.register(board);
|
this.update(); // no `await`
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard): Promise<void> {
|
protected override handleBoardChanged(board: Board | undefined): void {
|
||||||
|
this.update({ board });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async update(
|
||||||
|
options: { board?: Board; forceRefresh?: boolean } = {
|
||||||
|
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
const { board, forceRefresh } = options;
|
||||||
return this.queue.add(async () => {
|
return this.queue.add(async () => {
|
||||||
this.toDispose.dispose();
|
this.toDispose.dispose();
|
||||||
if (!board || !board.fqbn) {
|
if (forceRefresh) {
|
||||||
return;
|
await this.coreService.refresh();
|
||||||
}
|
}
|
||||||
const { fqbn, name } = board;
|
const fqbn = board?.fqbn;
|
||||||
const { user, current, any } = await this.examplesService.installed({ fqbn });
|
const name = board?.name;
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
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 (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,104 @@
|
|||||||
|
import { LocalStorageService } from '@theia/core/lib/browser';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
BoardsService,
|
||||||
|
LibraryLocation,
|
||||||
|
LibraryService,
|
||||||
|
} from '../../common/protocol';
|
||||||
|
import { Contribution } from './contribution';
|
||||||
|
|
||||||
|
const Arduino_BuiltIn = 'Arduino_BuiltIn';
|
||||||
|
|
||||||
|
@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 })
|
||||||
|
).find(({ name }) => name === Arduino_BuiltIn); // Filter by `name` to ensure "exact match". See: https://github.com/arduino/arduino-ide/issues/1526.
|
||||||
|
|
||||||
|
let avrPackageError: Error | undefined;
|
||||||
|
let builtInLibraryError: Error | undefined;
|
||||||
|
|
||||||
|
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 Internet connection), 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
|
||||||
|
installLocation: LibraryLocation.BUILTIN,
|
||||||
|
});
|
||||||
|
} 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 Internet connection), 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 ${Arduino_BuiltIn} 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';
|
||||||
|
}
|
||||||
78
arduino-ide-extension/src/browser/contributions/format.ts
Normal file
78
arduino-ide-extension/src/browser/contributions/format.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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 '../selectors';
|
||||||
|
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 = model.getFullModelRange();
|
||||||
|
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,31 @@ 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://docs.arduino.cc/software/ide-v2/tutorials/getting-started-ide-v2'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
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 +66,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,14 +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;
|
||||||
// Do not show board specific examples, when no board is selected.
|
// Show all libraries, when no board is selected.
|
||||||
if (fqbn) {
|
// 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
|
||||||
|
|
||||||
@@ -88,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) {
|
||||||
@@ -105,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.
|
||||||
@@ -132,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,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.onIndexUpdateWillStart(({ progressId }) =>
|
||||||
|
this.getOrCreateProgress(progressId)
|
||||||
|
);
|
||||||
|
this.notificationCenter.onIndexUpdateDidProgress((progress) => {
|
||||||
|
this.getOrCreateProgress(progress).then((delegate) =>
|
||||||
|
delegate.report(progress)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.notificationCenter.onIndexUpdateDidComplete(({ 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import {
|
||||||
|
Contribution,
|
||||||
|
Command,
|
||||||
|
MenuModelRegistry,
|
||||||
|
KeybindingRegistry,
|
||||||
|
} from './contribution';
|
||||||
|
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||||
|
import {
|
||||||
|
CommandRegistry,
|
||||||
|
DisposableCollection,
|
||||||
|
MaybePromise,
|
||||||
|
nls,
|
||||||
|
} from '@theia/core/lib/common';
|
||||||
|
|
||||||
|
import { Settings } from '../dialogs/settings/settings';
|
||||||
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
|
import debounce = require('lodash.debounce');
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class InterfaceScale extends Contribution {
|
||||||
|
@inject(MenuModelRegistry)
|
||||||
|
private readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
|
@inject(MainMenuManager)
|
||||||
|
private readonly mainMenuManager: MainMenuManager;
|
||||||
|
|
||||||
|
private readonly menuActionsDisposables = new DisposableCollection();
|
||||||
|
private fontScalingEnabled: InterfaceScale.FontScalingEnabled = {
|
||||||
|
increase: true,
|
||||||
|
decrease: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
private currentSettings: Settings;
|
||||||
|
private updateSettingsDebounced = debounce(
|
||||||
|
async () => {
|
||||||
|
await this.settingsService.update(this.currentSettings);
|
||||||
|
await this.settingsService.save();
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
{ maxWait: 200 }
|
||||||
|
);
|
||||||
|
|
||||||
|
override onStart(): MaybePromise<void> {
|
||||||
|
const updateCurrent = (settings: Settings) => {
|
||||||
|
this.currentSettings = settings;
|
||||||
|
this.updateFontScalingEnabled();
|
||||||
|
};
|
||||||
|
this.settingsService.onDidChange((settings) => updateCurrent(settings));
|
||||||
|
this.settingsService.settings().then((settings) => updateCurrent(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(InterfaceScale.Commands.INCREASE_FONT_SIZE, {
|
||||||
|
execute: () => this.updateFontSize('increase'),
|
||||||
|
isEnabled: () => this.fontScalingEnabled.increase,
|
||||||
|
});
|
||||||
|
registry.registerCommand(InterfaceScale.Commands.DECREASE_FONT_SIZE, {
|
||||||
|
execute: () => this.updateFontSize('decrease'),
|
||||||
|
isEnabled: () => this.fontScalingEnabled.decrease,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
this.menuActionsDisposables.dispose();
|
||||||
|
const increaseFontSizeMenuAction = {
|
||||||
|
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||||
|
label: nls.localize(
|
||||||
|
'arduino/editor/increaseFontSize',
|
||||||
|
'Increase Font Size'
|
||||||
|
),
|
||||||
|
order: '0',
|
||||||
|
};
|
||||||
|
const decreaseFontSizeMenuAction = {
|
||||||
|
commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
|
||||||
|
label: nls.localize(
|
||||||
|
'arduino/editor/decreaseFontSize',
|
||||||
|
'Decrease Font Size'
|
||||||
|
),
|
||||||
|
order: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.fontScalingEnabled.increase) {
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
|
registry.registerMenuAction(
|
||||||
|
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||||
|
increaseFontSizeMenuAction
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
|
registry.registerMenuNode(
|
||||||
|
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||||
|
new PlaceholderMenuNode(
|
||||||
|
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||||
|
increaseFontSizeMenuAction.label,
|
||||||
|
{ order: increaseFontSizeMenuAction.order }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.fontScalingEnabled.decrease) {
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
|
this.menuRegistry.registerMenuAction(
|
||||||
|
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||||
|
decreaseFontSizeMenuAction
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
|
this.menuRegistry.registerMenuNode(
|
||||||
|
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||||
|
new PlaceholderMenuNode(
|
||||||
|
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||||
|
decreaseFontSizeMenuAction.label,
|
||||||
|
{ order: decreaseFontSizeMenuAction.order }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.mainMenuManager.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateFontScalingEnabled(): void {
|
||||||
|
let fontScalingEnabled = {
|
||||||
|
increase: true,
|
||||||
|
decrease: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.currentSettings.autoScaleInterface) {
|
||||||
|
fontScalingEnabled = {
|
||||||
|
increase:
|
||||||
|
this.currentSettings.interfaceScale + InterfaceScale.ZoomLevel.STEP <=
|
||||||
|
InterfaceScale.ZoomLevel.MAX,
|
||||||
|
decrease:
|
||||||
|
this.currentSettings.interfaceScale - InterfaceScale.ZoomLevel.STEP >=
|
||||||
|
InterfaceScale.ZoomLevel.MIN,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
fontScalingEnabled = {
|
||||||
|
increase:
|
||||||
|
this.currentSettings.editorFontSize + InterfaceScale.FontSize.STEP <=
|
||||||
|
InterfaceScale.FontSize.MAX,
|
||||||
|
decrease:
|
||||||
|
this.currentSettings.editorFontSize - InterfaceScale.FontSize.STEP >=
|
||||||
|
InterfaceScale.FontSize.MIN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChanged = Object.keys(fontScalingEnabled).some(
|
||||||
|
(key: keyof InterfaceScale.FontScalingEnabled) =>
|
||||||
|
fontScalingEnabled[key] !== this.fontScalingEnabled[key]
|
||||||
|
);
|
||||||
|
if (isChanged) {
|
||||||
|
this.fontScalingEnabled = fontScalingEnabled;
|
||||||
|
this.registerMenus(this.menuRegistry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateFontSize(mode: 'increase' | 'decrease'): void {
|
||||||
|
if (this.currentSettings.autoScaleInterface) {
|
||||||
|
mode === 'increase'
|
||||||
|
? (this.currentSettings.interfaceScale += InterfaceScale.ZoomLevel.STEP)
|
||||||
|
: (this.currentSettings.interfaceScale -=
|
||||||
|
InterfaceScale.ZoomLevel.STEP);
|
||||||
|
} else {
|
||||||
|
mode === 'increase'
|
||||||
|
? (this.currentSettings.editorFontSize += InterfaceScale.FontSize.STEP)
|
||||||
|
: (this.currentSettings.editorFontSize -= InterfaceScale.FontSize.STEP);
|
||||||
|
}
|
||||||
|
this.updateFontScalingEnabled();
|
||||||
|
this.updateSettingsDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
|
registry.registerKeybinding({
|
||||||
|
command: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||||
|
keybinding: 'CtrlCmd+=',
|
||||||
|
});
|
||||||
|
registry.registerKeybinding({
|
||||||
|
command: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
|
||||||
|
keybinding: 'CtrlCmd+-',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace InterfaceScale {
|
||||||
|
export namespace Commands {
|
||||||
|
export const INCREASE_FONT_SIZE: Command = {
|
||||||
|
id: 'arduino-increase-font-size',
|
||||||
|
};
|
||||||
|
export const DECREASE_FONT_SIZE: Command = {
|
||||||
|
id: 'arduino-decrease-font-size',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace ZoomLevel {
|
||||||
|
export const MIN = -8;
|
||||||
|
export const MAX = 9;
|
||||||
|
export const STEP = 1;
|
||||||
|
|
||||||
|
export function toPercentage(scale: number): number {
|
||||||
|
return scale * 20 + 100;
|
||||||
|
}
|
||||||
|
export function fromPercentage(percentage: number): number {
|
||||||
|
return (percentage - 100) / 20;
|
||||||
|
}
|
||||||
|
export namespace Step {
|
||||||
|
export function toPercentage(step: number): number {
|
||||||
|
return step * 20;
|
||||||
|
}
|
||||||
|
export function fromPercentage(percentage: number): number {
|
||||||
|
return percentage / 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace FontSize {
|
||||||
|
export const MIN = 8;
|
||||||
|
export const MAX = 72;
|
||||||
|
export const STEP = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FontScalingEnabled {
|
||||||
|
increase: boolean;
|
||||||
|
decrease: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
import { DialogError } from '@theia/core/lib/browser/dialogs';
|
||||||
|
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||||
|
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||||
|
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||||
|
import { Widget } from '@theia/core/lib/browser/widgets/widget';
|
||||||
|
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
||||||
|
import {
|
||||||
|
Disposable,
|
||||||
|
DisposableCollection,
|
||||||
|
} from '@theia/core/lib/common/disposable';
|
||||||
|
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||||
|
import {
|
||||||
|
Progress,
|
||||||
|
ProgressUpdate,
|
||||||
|
} from '@theia/core/lib/common/message-service-protocol';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
|
import type { AuthenticationSession } from '../../node/auth/types';
|
||||||
|
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||||
|
import { CreateApi } from '../create/create-api';
|
||||||
|
import { CreateUri } from '../create/create-uri';
|
||||||
|
import { Create } from '../create/typings';
|
||||||
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog';
|
||||||
|
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||||
|
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||||
|
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
|
||||||
|
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
|
||||||
|
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
|
||||||
|
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
|
||||||
|
import { Command, CommandRegistry, Contribution, URI } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class NewCloudSketch extends Contribution {
|
||||||
|
@inject(CreateApi)
|
||||||
|
private readonly createApi: CreateApi;
|
||||||
|
@inject(SketchbookWidgetContribution)
|
||||||
|
private readonly widgetContribution: SketchbookWidgetContribution;
|
||||||
|
@inject(AuthenticationClientService)
|
||||||
|
private readonly authenticationService: AuthenticationClientService;
|
||||||
|
@inject(MainMenuManager)
|
||||||
|
private readonly mainMenuManager: MainMenuManager;
|
||||||
|
|
||||||
|
private readonly toDispose = new DisposableCollection();
|
||||||
|
private _session: AuthenticationSession | undefined;
|
||||||
|
private _enabled: boolean;
|
||||||
|
|
||||||
|
override onReady(): void {
|
||||||
|
this.toDispose.pushAll([
|
||||||
|
this.authenticationService.onSessionDidChange((session) => {
|
||||||
|
const oldSession = this._session;
|
||||||
|
this._session = session;
|
||||||
|
if (!!oldSession !== !!this._session) {
|
||||||
|
this.mainMenuManager.update();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||||
|
if (preferenceName === 'arduino.cloud.enabled') {
|
||||||
|
const oldEnabled = this._enabled;
|
||||||
|
this._enabled = Boolean(newValue);
|
||||||
|
if (this._enabled !== oldEnabled) {
|
||||||
|
this.mainMenuManager.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
this._enabled = this.preferences['arduino.cloud.enabled'];
|
||||||
|
this._session = this.authenticationService.session;
|
||||||
|
if (this._session) {
|
||||||
|
this.mainMenuManager.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onStop(): void {
|
||||||
|
this.toDispose.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
|
||||||
|
execute: () => this.createNewSketch(),
|
||||||
|
isEnabled: () => !!this._session,
|
||||||
|
isVisible: () => this._enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
|
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||||
|
label: nls.localize('arduino/cloudSketch/new', 'New Remote Sketch'),
|
||||||
|
order: '1',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
|
registry.registerKeybinding({
|
||||||
|
command: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||||
|
keybinding: 'CtrlCmd+Alt+N',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createNewSketch(
|
||||||
|
initialValue?: string | undefined
|
||||||
|
): Promise<unknown> {
|
||||||
|
const widget = await this.widgetContribution.widget;
|
||||||
|
const treeModel = this.treeModelFrom(widget);
|
||||||
|
if (!treeModel) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const rootNode = CompositeTreeNode.is(treeModel.root)
|
||||||
|
? treeModel.root
|
||||||
|
: undefined;
|
||||||
|
if (!rootNode) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.openWizard(rootNode, treeModel, initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private withProgress(
|
||||||
|
value: string,
|
||||||
|
treeModel: CloudSketchbookTreeModel
|
||||||
|
): (progress: Progress) => Promise<unknown> {
|
||||||
|
return async (progress: Progress) => {
|
||||||
|
let result: Create.Sketch | undefined | 'conflict';
|
||||||
|
try {
|
||||||
|
progress.report({
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/cloudSketch/creating',
|
||||||
|
"Creating remote sketch '{0}'...",
|
||||||
|
value
|
||||||
|
),
|
||||||
|
});
|
||||||
|
result = await this.createApi.createSketch(value);
|
||||||
|
} catch (err) {
|
||||||
|
if (isConflict(err)) {
|
||||||
|
result = 'conflict';
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (result) {
|
||||||
|
progress.report({
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/cloudSketch/synchronizing',
|
||||||
|
"Synchronizing sketchbook, pulling '{0}'...",
|
||||||
|
value
|
||||||
|
),
|
||||||
|
});
|
||||||
|
await treeModel.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result === 'conflict') {
|
||||||
|
return this.createNewSketch(value);
|
||||||
|
}
|
||||||
|
if (result) {
|
||||||
|
return this.open(treeModel, result);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async open(
|
||||||
|
treeModel: CloudSketchbookTreeModel,
|
||||||
|
newSketch: Create.Sketch
|
||||||
|
): Promise<URI | undefined> {
|
||||||
|
const id = CreateUri.toUri(newSketch).path.toString();
|
||||||
|
const node = treeModel.getNode(id);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find remote sketchbook tree node with Tree node ID: ${id}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||||
|
throw new Error(
|
||||||
|
`Remote sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await treeModel.sketchbookTree().pull({ node });
|
||||||
|
} catch (err) {
|
||||||
|
if (isNotFound(err)) {
|
||||||
|
await treeModel.refresh();
|
||||||
|
this.messageService.error(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/newCloudSketch/notFound',
|
||||||
|
"Could not pull the remote sketch '{0}'. It does not exist.",
|
||||||
|
newSketch.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return this.commandService.executeCommand(
|
||||||
|
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||||
|
{ node }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private treeModelFrom(
|
||||||
|
widget: SketchbookWidget
|
||||||
|
): CloudSketchbookTreeModel | undefined {
|
||||||
|
const treeWidget = widget.getTreeWidget();
|
||||||
|
if (treeWidget instanceof CloudSketchbookTreeWidget) {
|
||||||
|
const model = treeWidget.model;
|
||||||
|
if (model instanceof CloudSketchbookTreeModel) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openWizard(
|
||||||
|
rootNode: CompositeTreeNode,
|
||||||
|
treeModel: CloudSketchbookTreeModel,
|
||||||
|
initialValue?: string | undefined
|
||||||
|
): Promise<unknown> {
|
||||||
|
const existingNames = rootNode.children
|
||||||
|
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||||
|
.map(({ fileStat }) => fileStat.name);
|
||||||
|
return new NewCloudSketchDialog(
|
||||||
|
{
|
||||||
|
title: nls.localize(
|
||||||
|
'arduino/newCloudSketch/newSketchTitle',
|
||||||
|
'Name of a new Remote Sketch'
|
||||||
|
),
|
||||||
|
parentUri: CreateUri.root,
|
||||||
|
initialValue,
|
||||||
|
validate: (input) => {
|
||||||
|
if (existingNames.includes(input)) {
|
||||||
|
return nls.localize(
|
||||||
|
'arduino/newCloudSketch/sketchAlreadyExists',
|
||||||
|
"Remote sketch '{0}' already exists.",
|
||||||
|
input
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// This is how https://create.arduino.cc/editor/ works when renaming a sketch.
|
||||||
|
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return nls.localize(
|
||||||
|
'arduino/newCloudSketch/invalidSketchName',
|
||||||
|
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.labelProvider,
|
||||||
|
(value) => this.withProgress(value, treeModel)
|
||||||
|
).open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace NewCloudSketch {
|
||||||
|
export namespace Commands {
|
||||||
|
export const NEW_CLOUD_SKETCH: Command = {
|
||||||
|
id: 'arduino-new-cloud-sketch',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConflict(err: unknown): boolean {
|
||||||
|
return isErrorWithStatusOf(err, 409);
|
||||||
|
}
|
||||||
|
function isNotFound(err: unknown): boolean {
|
||||||
|
return isErrorWithStatusOf(err, 404);
|
||||||
|
}
|
||||||
|
function isErrorWithStatusOf(
|
||||||
|
err: unknown,
|
||||||
|
status: number
|
||||||
|
): err is Error & { status: number } {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const object = err as any;
|
||||||
|
return 'status' in object && object.status === status;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
class NewCloudSketchDialog extends WorkspaceInputDialog {
|
||||||
|
constructor(
|
||||||
|
@inject(WorkspaceInputDialogProps)
|
||||||
|
protected override readonly props: WorkspaceInputDialogProps,
|
||||||
|
@inject(LabelProvider)
|
||||||
|
protected override readonly labelProvider: LabelProvider,
|
||||||
|
private readonly withProgress: (
|
||||||
|
value: string
|
||||||
|
) => (progress: Progress) => Promise<unknown>
|
||||||
|
) {
|
||||||
|
super(props, labelProvider);
|
||||||
|
}
|
||||||
|
protected override async accept(): Promise<void> {
|
||||||
|
if (!this.resolve) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.acceptCancellationSource.cancel();
|
||||||
|
this.acceptCancellationSource = new CancellationTokenSource();
|
||||||
|
const token = this.acceptCancellationSource.token;
|
||||||
|
const value = this.value;
|
||||||
|
const error = await this.isValid(value, 'open');
|
||||||
|
if (token.isCancellationRequested) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!DialogError.getResult(error)) {
|
||||||
|
this.setErrorMessage(error);
|
||||||
|
} else {
|
||||||
|
const spinner = document.createElement('div');
|
||||||
|
spinner.classList.add('spinner');
|
||||||
|
const disposables = new DisposableCollection();
|
||||||
|
try {
|
||||||
|
this.toggleButtons(true);
|
||||||
|
disposables.push(Disposable.create(() => this.toggleButtons(false)));
|
||||||
|
|
||||||
|
const closeParent = this.closeCrossNode.parentNode;
|
||||||
|
closeParent?.removeChild(this.closeCrossNode);
|
||||||
|
disposables.push(
|
||||||
|
Disposable.create(() => {
|
||||||
|
closeParent?.appendChild(this.closeCrossNode);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.errorMessageNode.classList.add('progress');
|
||||||
|
disposables.push(
|
||||||
|
Disposable.create(() =>
|
||||||
|
this.errorMessageNode.classList.remove('progress')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorParent = this.errorMessageNode.parentNode;
|
||||||
|
errorParent?.insertBefore(spinner, this.errorMessageNode);
|
||||||
|
disposables.push(
|
||||||
|
Disposable.create(() => errorParent?.removeChild(spinner))
|
||||||
|
);
|
||||||
|
|
||||||
|
const cancellationSource = new CancellationTokenSource();
|
||||||
|
const progress: Progress = {
|
||||||
|
id: v4(),
|
||||||
|
cancel: () => cancellationSource.cancel(),
|
||||||
|
report: (update: ProgressUpdate) => {
|
||||||
|
this.setProgressMessage(update);
|
||||||
|
},
|
||||||
|
result: Promise.resolve(value),
|
||||||
|
};
|
||||||
|
await this.withProgress(value)(progress);
|
||||||
|
} finally {
|
||||||
|
disposables.dispose();
|
||||||
|
}
|
||||||
|
this.resolve(value);
|
||||||
|
Widget.detach(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleButtons(disabled: boolean): void {
|
||||||
|
if (this.acceptButton) {
|
||||||
|
this.acceptButton.disabled = disabled;
|
||||||
|
}
|
||||||
|
if (this.closeButton) {
|
||||||
|
this.closeButton.disabled = disabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setProgressMessage(update: ProgressUpdate): void {
|
||||||
|
if (update.work && update.work.done === update.work.total) {
|
||||||
|
this.errorMessageNode.innerText = '';
|
||||||
|
} else {
|
||||||
|
if (update.message) {
|
||||||
|
this.errorMessageNode.innerText = update.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,35 @@
|
|||||||
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 {
|
||||||
import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
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, {
|
|
||||||
isVisible: widget => 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 Sketch'),
|
||||||
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 +41,12 @@ 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 = {
|
|
||||||
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,24 @@
|
|||||||
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';
|
||||||
|
import { SketchesError } from '../../common/protocol';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenRecentSketch extends SketchContribution {
|
export class OpenRecentSketch extends SketchContribution {
|
||||||
|
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
protected readonly commandRegistry: CommandRegistry;
|
||||||
|
|
||||||
@@ -25,38 +34,78 @@ export class OpenRecentSketch extends SketchContribution {
|
|||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
protected readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
protected toDispose = new 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.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private update(forceUpdate?: boolean): void {
|
||||||
|
this.sketchService
|
||||||
|
.recentlyOpenedSketches(forceUpdate)
|
||||||
|
.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;
|
||||||
|
this.toDispose.dispose();
|
||||||
for (const sketch of sketches) {
|
for (const sketch of sketches) {
|
||||||
const { uri } = sketch;
|
const { uri } = sketch;
|
||||||
const toDispose = this.toDisposeBeforeRegister.get(uri);
|
|
||||||
if (toDispose) {
|
|
||||||
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: async () => {
|
||||||
|
try {
|
||||||
|
await this.commandRegistry.executeCommand(
|
||||||
|
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
|
sketch
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
this.update(true);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
this.commandRegistry.registerCommand(command, handler);
|
this.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.toDispose.pushAll([
|
||||||
|
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,217 @@
|
|||||||
|
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 { Later } from '../../common/nls';
|
||||||
|
import { Sketch, SketchesError } from '../../common/protocol';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandRegistry,
|
||||||
|
SketchContribution,
|
||||||
|
URI,
|
||||||
|
} from './contribution';
|
||||||
|
import { SaveAsSketch } from './save-as-sketch';
|
||||||
|
import { promptMoveSketch } from './open-sketch';
|
||||||
|
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||||
|
import { Deferred, wait } from '@theia/core/lib/common/promise-util';
|
||||||
|
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||||
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
|
|
||||||
|
@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, Later, yes).then((answer) => {
|
||||||
|
if (answer === yes) {
|
||||||
|
this.commandService.executeCommand(
|
||||||
|
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||||
|
{
|
||||||
|
execOnlyIfTemp: false,
|
||||||
|
openAfterMove: true,
|
||||||
|
wipeOriginal: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { workspaceError } = this.workspaceService;
|
||||||
|
// This happens when the IDE2 has been started (from either a terminal or clicking on an `ino` file) with a /path/to/invalid/sketch. (#964)
|
||||||
|
if (SketchesError.InvalidName.is(workspaceError)) {
|
||||||
|
await this.promptMove(workspaceError);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// This happens when the user gracefully closed IDE2, all went well
|
||||||
|
// but the main sketch file was renamed outside of IDE2 and when the user restarts the IDE2
|
||||||
|
// the workspace path still exists, but the sketch path is not valid anymore. (#964)
|
||||||
|
if (SketchesError.InvalidName.is(err)) {
|
||||||
|
const movedSketch = await this.promptMove(err);
|
||||||
|
if (!movedSketch) {
|
||||||
|
// If user did not accept the move, or move was not possible, force reload with a fallback.
|
||||||
|
return this.openFallbackSketch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
return 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 promptMove(
|
||||||
|
err: ApplicationError<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
invalidMainSketchUri: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
): Promise<Sketch | undefined> {
|
||||||
|
const { invalidMainSketchUri } = err.data;
|
||||||
|
requestAnimationFrame(() => this.messageService.error(err.message));
|
||||||
|
await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
|
||||||
|
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
|
||||||
|
fileService: this.fileService,
|
||||||
|
sketchService: this.sketchService,
|
||||||
|
labelProvider: this.labelProvider,
|
||||||
|
});
|
||||||
|
if (movedSketch) {
|
||||||
|
this.workspaceService.open(new URI(movedSketch.uri), {
|
||||||
|
preserveWindow: true,
|
||||||
|
});
|
||||||
|
return movedSketch;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openFallbackSketch(): Promise<void> {
|
||||||
|
const sketch = await this.sketchService.createNewSketch();
|
||||||
|
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
const disposables = new DisposableCollection();
|
||||||
|
const deferred = new Deferred<EditorWidget>();
|
||||||
|
// An editor can be in two primary states:
|
||||||
|
// - The editor is not yet opened. The `widget` is `undefined`. With `editorManager#open`, Theia will create an editor and fire an `editorManager#onCreated` event.
|
||||||
|
// - The editor is opened. Can be active, current, or open.
|
||||||
|
// - If the editor has the focus (the cursor blinks in the editor): it's the active editor.
|
||||||
|
// - If the editor does not have the focus (the focus is on a different widget or the context menu is opened in the editor): it's the current editor.
|
||||||
|
// - If the editor is not the top editor in the main area, it's opened.
|
||||||
|
if (!widget) {
|
||||||
|
// If the widget is `undefined`, IDE2 expects one `onCreate` event. Subscribe to the `onCreated` event
|
||||||
|
// and resolve the promise with the editor only when the new editor's visibility changes.
|
||||||
|
disposables.push(
|
||||||
|
this.editorManager.onCreated((editor) => {
|
||||||
|
if (editor.editor.uri.toString() === uri) {
|
||||||
|
if (editor.isAttached && editor.isVisible) {
|
||||||
|
deferred.resolve(editor);
|
||||||
|
} else {
|
||||||
|
disposables.push(
|
||||||
|
editor.onDidChangeVisibility((visible) => {
|
||||||
|
if (visible) {
|
||||||
|
// wait an animation frame. although the visible and attached props are true the editor is not there.
|
||||||
|
// let the browser render the widget
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
requestAnimationFrame(() => deferred.resolve(editor)),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editorManager
|
||||||
|
.open(
|
||||||
|
new URI(uri),
|
||||||
|
options ?? {
|
||||||
|
mode: 'reveal',
|
||||||
|
preview: false,
|
||||||
|
counter: 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((editorWidget) => {
|
||||||
|
// If the widget was defined, it was already opened.
|
||||||
|
// The editor is expected to be attached to the shell and visible in the UI.
|
||||||
|
// The deferred promise does not have to wait for the `editorManager#onCreated` event.
|
||||||
|
// It can resolve earlier.
|
||||||
|
if (!widget) {
|
||||||
|
deferred.resolve(editorWidget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
|
||||||
|
const result = await Promise.race([
|
||||||
|
deferred.promise,
|
||||||
|
wait(timeout).then(() => {
|
||||||
|
disposables.dispose();
|
||||||
|
return 'timeout';
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
if (result === 'timeout') {
|
||||||
|
console.warn(
|
||||||
|
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,132 +1,111 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { remote } from 'electron';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||||
|
import {
|
||||||
|
SketchesError,
|
||||||
|
SketchesService,
|
||||||
|
SketchRef,
|
||||||
|
} from '../../common/protocol';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
import {
|
||||||
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
Command,
|
||||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
CommandRegistry,
|
||||||
import { BuiltInExamples } from './examples';
|
KeybindingRegistry,
|
||||||
import { Sketchbook } from './sketchbook';
|
MenuModelRegistry,
|
||||||
import { SketchContainer } from '../../common/protocol';
|
Sketch,
|
||||||
|
SketchContribution,
|
||||||
|
URI,
|
||||||
|
} from './contribution';
|
||||||
|
|
||||||
|
export type SketchLocation = string | URI | SketchRef;
|
||||||
|
export namespace SketchLocation {
|
||||||
|
export function toUri(location: SketchLocation): URI {
|
||||||
|
if (typeof location === 'string') {
|
||||||
|
return new URI(location);
|
||||||
|
} else if (SketchRef.is(location)) {
|
||||||
|
return toUri(location.uri);
|
||||||
|
} else {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function is(arg: unknown): arg is SketchLocation {
|
||||||
|
return typeof arg === 'string' || arg instanceof URI || SketchRef.is(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class OpenSketch extends SketchContribution {
|
export class OpenSketch extends SketchContribution {
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
@inject(MenuModelRegistry)
|
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
|
||||||
|
|
||||||
@inject(ContextMenuRenderer)
|
|
||||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
||||||
|
|
||||||
@inject(BuiltInExamples)
|
|
||||||
protected readonly builtInExamples: BuiltInExamples;
|
|
||||||
|
|
||||||
@inject(ExamplesService)
|
|
||||||
protected readonly examplesService: ExamplesService;
|
|
||||||
|
|
||||||
@inject(Sketchbook)
|
|
||||||
protected readonly sketchbook: Sketchbook;
|
|
||||||
|
|
||||||
protected readonly toDispose = new DisposableCollection();
|
|
||||||
|
|
||||||
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: async (arg) => {
|
||||||
});
|
const toOpen = !SketchLocation.is(arg)
|
||||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
? await this.selectSketch()
|
||||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
: arg;
|
||||||
execute: async (_: Widget, target: EventTarget) => {
|
if (toOpen) {
|
||||||
const container = await this.sketchService.getSketches({ exclude: ['**/hardware/**'] });
|
return this.openSketch(toOpen);
|
||||||
if (SketchContainer.isEmpty(container)) {
|
|
||||||
this.openSketch();
|
|
||||||
} else {
|
|
||||||
this.toDispose.dispose();
|
|
||||||
if (!(target instanceof HTMLElement)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { parentElement } = target;
|
|
||||||
if (!parentElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP, {
|
|
||||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
|
||||||
label: 'Open...'
|
|
||||||
});
|
|
||||||
this.toDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
|
|
||||||
this.sketchbook.registerRecursively([...container.children, ...container.sketches], ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, this.toDispose);
|
|
||||||
try {
|
|
||||||
const containers = await this.examplesService.builtIns();
|
|
||||||
for (const container of containers) {
|
|
||||||
this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDispose);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error when collecting built-in examples.', e);
|
|
||||||
}
|
|
||||||
const options = {
|
|
||||||
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
|
||||||
anchor: {
|
|
||||||
x: parentElement.getBoundingClientRect().left,
|
|
||||||
y: parentElement.getBoundingClientRect().top + parentElement.offsetHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.contextMenuRenderer.render(options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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: '2',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> {
|
||||||
registry.registerItem({
|
if (!toOpen) {
|
||||||
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
return;
|
||||||
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
}
|
||||||
tooltip: 'Open',
|
const uri = SketchLocation.toUri(toOpen);
|
||||||
priority: 4
|
try {
|
||||||
});
|
await this.sketchService.loadSketch(uri.toString());
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
this.messageService.error(err.message);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
this.workspaceService.open(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
async openSketch(toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()): Promise<void> {
|
private async selectSketch(): Promise<Sketch | undefined> {
|
||||||
const sketch = await toOpen;
|
|
||||||
if (sketch) {
|
|
||||||
this.workspaceService.open(new URI(sketch.uri));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
||||||
const { filePaths } = await remote.dialog.showOpenDialog({
|
new URI(config.sketchDirUri)
|
||||||
|
);
|
||||||
|
const { filePaths } = await remote.dialog.showOpenDialog(
|
||||||
|
remote.getCurrentWindow(),
|
||||||
|
{
|
||||||
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);
|
||||||
@@ -135,41 +114,71 @@ export class OpenSketch extends SketchContribution {
|
|||||||
return sketch;
|
return sketch;
|
||||||
}
|
}
|
||||||
if (Sketch.isSketchFile(sketchFileUri)) {
|
if (Sketch.isSketchFile(sketchFileUri)) {
|
||||||
const name = new URI(sketchFileUri).path.name;
|
return promptMoveSketch(sketchFileUri, {
|
||||||
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
|
fileService: this.fileService,
|
||||||
const { response } = await remote.dialog.showMessageBox({
|
sketchService: this.sketchService,
|
||||||
title: 'Moving',
|
labelProvider: this.labelProvider,
|
||||||
type: 'question',
|
|
||||||
buttons: ['Cancel', 'OK'],
|
|
||||||
message: `The file "${nameWithExt}" needs to be inside a sketch folder named as "${name}".\nCreate this folder, move the file, and continue?`
|
|
||||||
});
|
});
|
||||||
if (response === 1) { // OK
|
|
||||||
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
|
|
||||||
const exists = await this.fileService.exists(newSketchUri);
|
|
||||||
if (exists) {
|
|
||||||
await remote.dialog.showMessageBox({
|
|
||||||
type: 'error',
|
|
||||||
title: 'Error',
|
|
||||||
message: `A folder named "${name}" already exists. Can't open sketch.`
|
|
||||||
});
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
await this.fileService.createFolder(newSketchUri);
|
|
||||||
await this.fileService.move(new URI(sketchFileUri), new URI(newSketchUri.resolve(nameWithExt).toString()));
|
|
||||||
return this.sketchService.getSketchFolder(newSketchUri.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = {
|
|
||||||
id: 'arduino-open-sketch--toolbar'
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function promptMoveSketch(
|
||||||
|
sketchFileUri: string | URI,
|
||||||
|
options: {
|
||||||
|
fileService: FileService;
|
||||||
|
sketchService: SketchesService;
|
||||||
|
labelProvider: LabelProvider;
|
||||||
|
}
|
||||||
|
): Promise<Sketch | undefined> {
|
||||||
|
const { fileService, sketchService, labelProvider } = options;
|
||||||
|
const uri =
|
||||||
|
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
|
||||||
|
const name = uri.path.name;
|
||||||
|
const nameWithExt = labelProvider.getName(uri);
|
||||||
|
const { response } = await remote.dialog.showMessageBox({
|
||||||
|
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
||||||
|
type: 'question',
|
||||||
|
buttons: [
|
||||||
|
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||||
|
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||||
|
],
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/sketch/movingMsg',
|
||||||
|
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
||||||
|
nameWithExt,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (response === 1) {
|
||||||
|
// OK
|
||||||
|
const newSketchUri = uri.parent.resolve(name);
|
||||||
|
const exists = await fileService.exists(newSketchUri);
|
||||||
|
if (exists) {
|
||||||
|
await remote.dialog.showMessageBox({
|
||||||
|
type: 'error',
|
||||||
|
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
||||||
|
message: nls.localize(
|
||||||
|
'arduino/sketch/cantOpen',
|
||||||
|
'A folder named "{0}" already exists. Can\'t open sketch.',
|
||||||
|
name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
await fileService.createFolder(newSketchUri);
|
||||||
|
await fileService.move(
|
||||||
|
uri,
|
||||||
|
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||||
|
);
|
||||||
|
return sketchService.getSketchFolder(newSketchUri.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,68 @@
|
|||||||
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 { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
import { WorkspaceInput } from '@theia/workspace/lib/browser';
|
||||||
|
import { StartupTask } from '../../electron-common/startup-task';
|
||||||
|
import { DeleteSketch } from './delete-sketch';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SaveAsSketch extends SketchContribution {
|
export class SaveAsSketch extends SketchContribution {
|
||||||
|
@inject(ApplicationShell)
|
||||||
|
private readonly applicationShell: ApplicationShell;
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
@inject(WindowService)
|
||||||
|
private 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> {
|
private async saveAs(
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
{
|
||||||
if (!sketch) {
|
execOnlyIfTemp,
|
||||||
|
openAfterMove,
|
||||||
|
wipeOriginal,
|
||||||
|
markAsRecentlyOpened,
|
||||||
|
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||||
|
): Promise<boolean> {
|
||||||
|
const [sketch, configuration] = await Promise.all([
|
||||||
|
this.sketchServiceClient.currentSketch(),
|
||||||
|
this.configService.getConfiguration(),
|
||||||
|
]);
|
||||||
|
if (!CurrentSketch.isValid(sketch)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,15 +71,38 @@ export class SaveAsSketch extends SketchContribution {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sketchUri = new URI(sketch.uri);
|
||||||
|
const sketchbookDirUri = new URI(configuration.sketchDirUri);
|
||||||
|
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
|
||||||
|
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
|
||||||
|
// Otherwise, it proposes the parent folder of the current sketch.
|
||||||
|
const containerDirUri = isTemp
|
||||||
|
? sketchbookDirUri
|
||||||
|
: !sketchbookDirUri.isEqualOrParent(sketchUri)
|
||||||
|
? sketchbookDirUri
|
||||||
|
: sketchUri.parent;
|
||||||
|
const exists = await this.fileService.exists(
|
||||||
|
containerDirUri.resolve(sketch.name)
|
||||||
|
);
|
||||||
|
|
||||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
// If target 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 defaultUri = containerDirUri.resolve(
|
||||||
const exists = await this.fileService.exists(sketchDirUri.resolve(sketch.name));
|
exists
|
||||||
const defaultUri = exists
|
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
|
||||||
? sketchDirUri.resolve(sketchDirUri.resolve(`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`).toString())
|
: sketch.name
|
||||||
: sketchDirUri.resolve(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(
|
||||||
|
remote.getCurrentWindow(),
|
||||||
|
{
|
||||||
|
title: nls.localize(
|
||||||
|
'arduino/sketch/saveFolderAs',
|
||||||
|
'Save sketch folder as...'
|
||||||
|
),
|
||||||
|
defaultPath,
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!filePath || canceled) {
|
if (!filePath || canceled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -58,24 +110,72 @@ 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, {
|
||||||
if (workspaceUri && openAfterMove) {
|
destinationUri,
|
||||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
});
|
||||||
try {
|
if (workspaceUri) {
|
||||||
await this.fileService.delete(new URI(sketch.uri), { recursive: true });
|
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
|
||||||
} catch { /* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */ }
|
if (markAsRecentlyOpened) {
|
||||||
|
this.sketchService.markAsRecentlyOpened(workspaceUri);
|
||||||
}
|
}
|
||||||
this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true });
|
}
|
||||||
|
const options: WorkspaceInput & StartupTask.Owner = {
|
||||||
|
preserveWindow: true,
|
||||||
|
tasks: [],
|
||||||
|
};
|
||||||
|
if (workspaceUri && openAfterMove) {
|
||||||
|
this.windowService.setSafeToShutDown();
|
||||||
|
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||||
|
options.tasks.push({
|
||||||
|
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||||
|
args: [sketch.uri],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.workspaceService.open(new URI(workspaceUri), options);
|
||||||
}
|
}
|
||||||
return !!workspaceUri;
|
return !!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 {
|
||||||
@@ -85,12 +185,14 @@ export namespace SaveAsSketch {
|
|||||||
* Ignored if `openAfterMove` is `false`.
|
* Ignored if `openAfterMove` is `false`.
|
||||||
*/
|
*/
|
||||||
readonly wipeOriginal?: boolean;
|
readonly wipeOriginal?: boolean;
|
||||||
|
readonly markAsRecentlyOpened?: boolean;
|
||||||
}
|
}
|
||||||
export namespace Options {
|
export namespace Options {
|
||||||
export const DEFAULT: Options = {
|
export const DEFAULT: Options = {
|
||||||
execOnlyIfTemp: false,
|
execOnlyIfTemp: false,
|
||||||
openAfterMove: true,
|
openAfterMove: true,
|
||||||
wipeOriginal: false
|
wipeOriginal: false,
|
||||||
|
markAsRecentlyOpened: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,65 @@
|
|||||||
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 { SaveAsSketch } from './save-as-sketch';
|
||||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
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, {
|
|
||||||
isVisible: widget => 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: '7',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = {
|
|
||||||
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,33 +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__SETTINGS_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,
|
||||||
|
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: String(i).padStart(4),
|
||||||
});
|
}
|
||||||
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,65 @@
|
|||||||
|
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 } 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)) {
|
||||||
|
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,46 +1,63 @@
|
|||||||
import { inject, injectable } from 'inversify';
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
import { CommandRegistry, MenuModelRegistry } from './contribution';
|
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||||
|
import { MenuModelRegistry } from './contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
|
||||||
import { NotificationCenter } from '../notification-center';
|
|
||||||
import { Examples } from './examples';
|
import { Examples } from './examples';
|
||||||
import { SketchContainer } from '../../common/protocol';
|
import { SketchContainer, SketchesError } from '../../common/protocol';
|
||||||
|
import { OpenSketch } from './open-sketch';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class Sketchbook extends Examples {
|
export class Sketchbook extends Examples {
|
||||||
|
override onStart(): void {
|
||||||
|
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||||
|
}
|
||||||
|
|
||||||
@inject(CommandRegistry)
|
override async onReady(): Promise<void> {
|
||||||
protected readonly commandRegistry: CommandRegistry;
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
@inject(MenuModelRegistry)
|
protected override update(): void {
|
||||||
protected readonly menuRegistry: MenuModelRegistry;
|
this.sketchService.getSketches({}).then((container) => {
|
||||||
|
|
||||||
@inject(MainMenuManager)
|
|
||||||
protected readonly mainMenuManager: MainMenuManager;
|
|
||||||
|
|
||||||
@inject(NotificationCenter)
|
|
||||||
protected readonly notificationCenter: NotificationCenter;
|
|
||||||
|
|
||||||
onStart(): void {
|
|
||||||
this.sketchService.getSketches({}).then(container => {
|
|
||||||
this.register(container);
|
this.register(container);
|
||||||
this.mainMenuManager.update();
|
this.menuManager.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 {
|
private 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 override createHandler(uri: string): CommandHandler {
|
||||||
|
return {
|
||||||
|
execute: async () => {
|
||||||
|
try {
|
||||||
|
await this.commandService.executeCommand(
|
||||||
|
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
|
uri
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (SketchesError.NotFound.is(err)) {
|
||||||
|
// Force update the menu items to remove the absent sketch.
|
||||||
|
this.update();
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
|
import type { IpcRendererEvent } from '@theia/core/electron-shared/electron';
|
||||||
|
import { ipcRenderer } from '@theia/core/electron-shared/electron';
|
||||||
|
import { injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { StartupTask } from '../../electron-common/startup-task';
|
||||||
|
import { Contribution } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class StartupTasks extends Contribution {
|
||||||
|
override onReady(): void {
|
||||||
|
ipcRenderer.once(
|
||||||
|
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
|
||||||
|
(_: IpcRendererEvent, args: unknown) => {
|
||||||
|
console.debug(
|
||||||
|
`Received the startup tasks from the electron main process. Args: ${JSON.stringify(
|
||||||
|
args
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
if (!StartupTask.has(args)) {
|
||||||
|
console.warn(`Could not detect 'tasks' from the signal. Skipping.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tasks = args.tasks;
|
||||||
|
if (tasks.length) {
|
||||||
|
console.log(`Executing startup tasks:`);
|
||||||
|
tasks.forEach(({ command, args = [] }) => {
|
||||||
|
console.log(
|
||||||
|
` - '${command}' ${
|
||||||
|
args.length ? `, args: ${JSON.stringify(args)}` : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
this.commandService
|
||||||
|
.executeCommand(command, ...args)
|
||||||
|
.catch((err) =>
|
||||||
|
console.error(
|
||||||
|
`Error occurred when executing the startup task '${command}'${
|
||||||
|
args?.length ? ` with args: '${JSON.stringify(args)}` : ''
|
||||||
|
}.`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const { id } = remote.getCurrentWindow();
|
||||||
|
console.debug(
|
||||||
|
`Signalling app ready event to the electron main process. Sender ID: ${id}.`
|
||||||
|
);
|
||||||
|
ipcRenderer.send(StartupTask.Messaging.APP_READY_SIGNAL(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,193 @@
|
|||||||
|
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { CoreService, IndexType } from '../../common/protocol';
|
||||||
|
import { NotificationCenter } from '../notification-center';
|
||||||
|
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||||
|
import { Command, CommandRegistry, Contribution } from './contribution';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class UpdateIndexes extends Contribution {
|
||||||
|
@inject(WindowServiceExt)
|
||||||
|
private readonly windowService: WindowServiceExt;
|
||||||
|
@inject(LocalStorageService)
|
||||||
|
private readonly localStorage: LocalStorageService;
|
||||||
|
@inject(CoreService)
|
||||||
|
private readonly coreService: CoreService;
|
||||||
|
@inject(NotificationCenter)
|
||||||
|
private readonly notificationCenter: NotificationCenter;
|
||||||
|
|
||||||
|
protected override init(): void {
|
||||||
|
super.init();
|
||||||
|
this.notificationCenter.onIndexUpdateDidComplete(({ summary }) =>
|
||||||
|
Promise.all(
|
||||||
|
Object.entries(summary).map(([type, updatedAt]) =>
|
||||||
|
this.setLastUpdateDateTime(type as IndexType, updatedAt)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override onReady(): void {
|
||||||
|
this.checkForUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(UpdateIndexes.Commands.UPDATE_INDEXES, {
|
||||||
|
execute: () => this.updateIndexes(IndexType.All, true),
|
||||||
|
});
|
||||||
|
registry.registerCommand(UpdateIndexes.Commands.UPDATE_PLATFORM_INDEX, {
|
||||||
|
execute: () => this.updateIndexes(['platform'], true),
|
||||||
|
});
|
||||||
|
registry.registerCommand(UpdateIndexes.Commands.UPDATE_LIBRARY_INDEX, {
|
||||||
|
execute: () => this.updateIndexes(['library'], true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkForUpdates(): Promise<void> {
|
||||||
|
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||||
|
if (!checkForUpdates) {
|
||||||
|
console.debug(
|
||||||
|
'[update-indexes]: `arduino.checkForUpdates` is `false`. Skipping updating the indexes.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.windowService.isFirstWindow()) {
|
||||||
|
const summary = await this.coreService.indexUpdateSummaryBeforeInit();
|
||||||
|
if (summary.message) {
|
||||||
|
this.messageService.error(summary.message);
|
||||||
|
}
|
||||||
|
const typesToCheck = IndexType.All.filter((type) => !(type in summary));
|
||||||
|
if (Object.keys(summary).length) {
|
||||||
|
console.debug(
|
||||||
|
`[update-indexes]: Detected an index update summary before the core gRPC client initialization. Updating local storage with ${JSON.stringify(
|
||||||
|
summary
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.debug(
|
||||||
|
'[update-indexes]: No index update summary was available before the core gRPC client initialization. Checking the status of the all the index types.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await Promise.allSettled([
|
||||||
|
...Object.entries(summary).map(([type, updatedAt]) =>
|
||||||
|
this.setLastUpdateDateTime(type as IndexType, updatedAt)
|
||||||
|
),
|
||||||
|
this.updateIndexes(typesToCheck),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateIndexes(
|
||||||
|
types: IndexType[],
|
||||||
|
force = false
|
||||||
|
): Promise<void> {
|
||||||
|
const updatedAt = new Date().toISOString();
|
||||||
|
return Promise.all(
|
||||||
|
types.map((type) => this.needsIndexUpdate(type, updatedAt, force))
|
||||||
|
).then((needsIndexUpdateResults) => {
|
||||||
|
const typesToUpdate = needsIndexUpdateResults.filter(IndexType.is);
|
||||||
|
if (typesToUpdate.length) {
|
||||||
|
console.debug(
|
||||||
|
`[update-indexes]: Requesting the index update of type: ${JSON.stringify(
|
||||||
|
typesToUpdate
|
||||||
|
)} with date time: ${updatedAt}.`
|
||||||
|
);
|
||||||
|
return this.coreService.updateIndex({ types: typesToUpdate });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async needsIndexUpdate(
|
||||||
|
type: IndexType,
|
||||||
|
now: string,
|
||||||
|
force = false
|
||||||
|
): Promise<IndexType | false> {
|
||||||
|
if (force) {
|
||||||
|
console.debug(
|
||||||
|
`[update-indexes]: Update for index type: '${type}' was forcefully requested.`
|
||||||
|
);
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
const lastUpdateIsoDateTime = await this.getLastUpdateDateTime(type);
|
||||||
|
if (!lastUpdateIsoDateTime) {
|
||||||
|
console.debug(
|
||||||
|
`[update-indexes]: No last update date time was persisted for index type: '${type}'. Index update is required.`
|
||||||
|
);
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
const lastUpdateDateTime = Date.parse(lastUpdateIsoDateTime);
|
||||||
|
if (Number.isNaN(lastUpdateDateTime)) {
|
||||||
|
console.debug(
|
||||||
|
`[update-indexes]: Invalid last update date time was persisted for index type: '${type}'. Last update date time was: ${lastUpdateDateTime}. Index update is required.`
|
||||||
|
);
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
const diff = new Date(now).getTime() - lastUpdateDateTime;
|
||||||
|
const needsIndexUpdate = diff >= this.threshold;
|
||||||
|
console.debug(
|
||||||
|
`[update-indexes]: Update for index type '${type}' is ${
|
||||||
|
needsIndexUpdate ? '' : 'not '
|
||||||
|
}required. Now: ${now}, Last index update date time: ${new Date(
|
||||||
|
lastUpdateDateTime
|
||||||
|
).toISOString()}, diff: ${diff} ms, threshold: ${this.threshold} ms.`
|
||||||
|
);
|
||||||
|
return needsIndexUpdate ? type : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLastUpdateDateTime(
|
||||||
|
type: IndexType
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const key = this.storageKeyOf(type);
|
||||||
|
return this.localStorage.getData<string>(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setLastUpdateDateTime(
|
||||||
|
type: IndexType,
|
||||||
|
updatedAt: string
|
||||||
|
): Promise<void> {
|
||||||
|
const key = this.storageKeyOf(type);
|
||||||
|
return this.localStorage.setData<string>(key, updatedAt).finally(() => {
|
||||||
|
console.debug(
|
||||||
|
`[update-indexes]: Updated the last index update date time of '${type}' to ${updatedAt}.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private storageKeyOf(type: IndexType): string {
|
||||||
|
return `index-last-update-time--${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get threshold(): number {
|
||||||
|
return 4 * 60 * 60 * 1_000; // four hours in millis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace UpdateIndexes {
|
||||||
|
export namespace Commands {
|
||||||
|
export const UPDATE_INDEXES: Command & { label: string } = {
|
||||||
|
id: 'arduino-update-indexes',
|
||||||
|
label: nls.localize(
|
||||||
|
'arduino/updateIndexes/updateIndexes',
|
||||||
|
'Update Indexes'
|
||||||
|
),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
export const UPDATE_PLATFORM_INDEX: Command & { label: string } = {
|
||||||
|
id: 'arduino-update-package-index',
|
||||||
|
label: nls.localize(
|
||||||
|
'arduino/updateIndexes/updatePackageIndex',
|
||||||
|
'Update Package Index'
|
||||||
|
),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
export const UPDATE_LIBRARY_INDEX: Command & { label: string } = {
|
||||||
|
id: 'arduino-update-library-index',
|
||||||
|
label: nls.localize(
|
||||||
|
'arduino/updateIndexes/updateLibraryIndex',
|
||||||
|
'Update Library Index'
|
||||||
|
),
|
||||||
|
category: 'Arduino',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,243 @@
|
|||||||
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 { CoreService, Port } 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 { 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 { deepClone, nls } from '@theia/core/lib/common';
|
||||||
|
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||||
|
import type { VerifySketchParams } from './verify-sketch';
|
||||||
|
import { UserFields } from './user-fields';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class UploadSketch extends SketchContribution {
|
export class UploadSketch extends CoreServiceContribution {
|
||||||
|
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||||
|
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||||
|
private uploadInProgress = false;
|
||||||
|
|
||||||
@inject(CoreService)
|
@inject(UserFields)
|
||||||
protected readonly coreService: CoreService;
|
private readonly userFields: UserFields;
|
||||||
|
|
||||||
@inject(MonitorConnection)
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
protected readonly monitorConnection: MonitorConnection;
|
|
||||||
|
|
||||||
@inject(BoardsDataStore)
|
|
||||||
protected readonly boardsDataStore: BoardsDataStore;
|
|
||||||
|
|
||||||
@inject(BoardsServiceProvider)
|
|
||||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
|
||||||
|
|
||||||
protected readonly onDidChangeEmitter = new Emitter<Readonly<void>>();
|
|
||||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
|
||||||
|
|
||||||
protected uploadInProgress = false;
|
|
||||||
|
|
||||||
registerCommands(registry: CommandRegistry): void {
|
|
||||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
|
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
|
||||||
execute: () => this.uploadSketch(),
|
execute: async () => {
|
||||||
|
if (await this.userFields.checkUserFieldsDialog()) {
|
||||||
|
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 () => {
|
||||||
|
if (await this.userFields.checkUserFieldsDialog(true)) {
|
||||||
|
this.uploadSketch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEnabled: () => !this.uploadInProgress && this.userFields.isRequired(),
|
||||||
|
});
|
||||||
|
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 {
|
||||||
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',
|
||||||
});
|
});
|
||||||
|
|
||||||
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.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
|
||||||
this.onDidChangeEmitter.fire();
|
this.onDidChangeEmitter.fire();
|
||||||
const sketch = await this.sketchServiceClient.currentSketch();
|
this.clearVisibleNotification();
|
||||||
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) {
|
if (!this.userFields.checkUserFieldsForUpload()) {
|
||||||
const programmer = selectedProgrammer;
|
return;
|
||||||
options = {
|
|
||||||
sketchUri,
|
|
||||||
fqbn,
|
|
||||||
optimizeForDebug,
|
|
||||||
programmer,
|
|
||||||
port,
|
|
||||||
verbose,
|
|
||||||
verify,
|
|
||||||
sourceOverride
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
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: 1000 });
|
});
|
||||||
|
|
||||||
|
this.messageService.info(
|
||||||
|
nls.localize('arduino/sketch/doneUploading', 'Done uploading.'),
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.messageService.error(e.toString());
|
this.userFields.notifyFailedWithError(e);
|
||||||
|
this.handleError(e);
|
||||||
} finally {
|
} finally {
|
||||||
this.uploadInProgress = false;
|
this.uploadInProgress = false;
|
||||||
|
this.boardsServiceProvider.attemptPostUploadAutoSelect();
|
||||||
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.getUserFields();
|
||||||
|
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 = this.maybeUpdatePortProperties(boardsConfig.selectedPort);
|
||||||
|
return {
|
||||||
|
sketch,
|
||||||
|
fqbn,
|
||||||
|
...(usingProgrammer && { programmer }),
|
||||||
|
port,
|
||||||
|
verbose,
|
||||||
|
verify,
|
||||||
|
userFields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a hack to ensure that the port object has the `properties` when uploading.(https://github.com/arduino/arduino-ide/issues/740)
|
||||||
|
* This method works around a bug when restoring a `port` persisted by an older version of IDE2. See the bug [here](https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236).
|
||||||
|
*
|
||||||
|
* Before the upload, this method checks the available ports and makes sure that the `properties` of an available port, and the port selected by the user have the same `properties`.
|
||||||
|
* This method does not update any state (for example, the `BoardsConfig.Config`) but uses the correct `properties` for the `upload`.
|
||||||
|
*/
|
||||||
|
private maybeUpdatePortProperties(port: Port | undefined): Port | undefined {
|
||||||
|
if (port) {
|
||||||
|
const key = Port.keyOf(port);
|
||||||
|
for (const candidate of this.boardsServiceProvider.availablePorts) {
|
||||||
|
if (key === Port.keyOf(candidate) && candidate.properties) {
|
||||||
|
return {
|
||||||
|
...port,
|
||||||
|
properties: deepClone(candidate.properties),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
|
||||||
|
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
|
||||||
|
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
|
||||||
|
*/
|
||||||
|
private sanitizeFqbn(fqbn: string | undefined): string | undefined {
|
||||||
|
if (!fqbn) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const [vendor, arch, id] = fqbn.split(':');
|
||||||
|
return `${vendor}:${arch}:${id}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace UploadSketch {
|
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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
arduino-ide-extension/src/browser/contributions/user-fields.ts
Normal file
147
arduino-ide-extension/src/browser/contributions/user-fields.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
||||||
|
import { BoardUserField, CoreError } from '../../common/protocol';
|
||||||
|
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||||
|
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
|
||||||
|
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||||
|
import { MenuModelRegistry, Contribution } from './contribution';
|
||||||
|
import { UploadSketch } from './upload-sketch';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class UserFields extends Contribution {
|
||||||
|
private boardRequiresUserFields = false;
|
||||||
|
private userFieldsSet = false;
|
||||||
|
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||||
|
private readonly menuActionsDisposables = new DisposableCollection();
|
||||||
|
|
||||||
|
@inject(UserFieldsDialog)
|
||||||
|
private readonly userFieldsDialog: UserFieldsDialog;
|
||||||
|
|
||||||
|
@inject(BoardsServiceProvider)
|
||||||
|
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||||
|
|
||||||
|
@inject(MenuModelRegistry)
|
||||||
|
private readonly menuRegistry: MenuModelRegistry;
|
||||||
|
|
||||||
|
protected override init(): void {
|
||||||
|
super.init();
|
||||||
|
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
|
||||||
|
const userFields =
|
||||||
|
await this.boardsServiceProvider.selectedBoardUserFields();
|
||||||
|
this.boardRequiresUserFields = userFields.length > 0;
|
||||||
|
this.registerMenus(this.menuRegistry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
this.menuActionsDisposables.dispose();
|
||||||
|
if (this.boardRequiresUserFields) {
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
|
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||||
|
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||||
|
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||||
|
order: '2',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.menuActionsDisposables.push(
|
||||||
|
registry.registerMenuNode(
|
||||||
|
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||||
|
new PlaceholderMenuNode(
|
||||||
|
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||||
|
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||||
|
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||||
|
{ order: '2' }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectedFqbnAddress(): string | undefined {
|
||||||
|
const { boardsConfig } = this.boardsServiceProvider;
|
||||||
|
const fqbn = boardsConfig.selectedBoard?.fqbn;
|
||||||
|
if (!fqbn) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const address =
|
||||||
|
boardsConfig.selectedBoard?.port?.address ||
|
||||||
|
boardsConfig.selectedPort?.address ||
|
||||||
|
'';
|
||||||
|
return fqbn + '|' + address;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showUserFieldsDialog(
|
||||||
|
key: string
|
||||||
|
): Promise<BoardUserField[] | undefined> {
|
||||||
|
const cached = this.cachedUserFields.get(key);
|
||||||
|
// Deep clone the array of board fields to avoid editing the cached ones
|
||||||
|
this.userFieldsDialog.value = cached
|
||||||
|
? cached.slice()
|
||||||
|
: await this.boardsServiceProvider.selectedBoardUserFields();
|
||||||
|
const result = await this.userFieldsDialog.open();
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.userFieldsSet = true;
|
||||||
|
this.cachedUserFields.set(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkUserFieldsDialog(forceOpen = false): Promise<boolean> {
|
||||||
|
const key = this.selectedFqbnAddress();
|
||||||
|
if (!key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
If the board requires to be configured with user fields, we want
|
||||||
|
to show the user fields dialog, but only if they weren't already
|
||||||
|
filled in or if they were filled in, but the previous upload failed.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
!forceOpen &&
|
||||||
|
(!this.boardRequiresUserFields ||
|
||||||
|
(this.cachedUserFields.has(key) && this.userFieldsSet))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const userFieldsFilledIn = Boolean(await this.showUserFieldsDialog(key));
|
||||||
|
return userFieldsFilledIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUserFieldsForUpload(): boolean {
|
||||||
|
// TODO: This does not belong here.
|
||||||
|
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
|
||||||
|
if (!this.boardRequiresUserFields || this.getUserFields().length > 0) {
|
||||||
|
this.userFieldsSet = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.messageService.error(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/sketch/userFieldsNotFoundError',
|
||||||
|
"Can't find user fields for connected board"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.userFieldsSet = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserFields(): BoardUserField[] {
|
||||||
|
const fqbnAddress = this.selectedFqbnAddress();
|
||||||
|
if (!fqbnAddress) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.cachedUserFields.get(fqbnAddress) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
isRequired(): boolean {
|
||||||
|
return this.boardRequiresUserFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyFailedWithError(e: Error): void {
|
||||||
|
if (this.boardRequiresUserFields && CoreError.UploadFailed.is(e)) {
|
||||||
|
this.userFieldsSet = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,135 +1,190 @@
|
|||||||
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.clearVisibleNotification();
|
||||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
this.coreErrorHandler.reset();
|
||||||
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: 1000 });
|
|
||||||
} 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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user