mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-25 11:08:32 +00:00
Compare commits
568 Commits
2.0.0-beta
...
2.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bf38d804e | ||
|
|
9dec9c5a18 | ||
|
|
43b5d4e22f | ||
|
|
fe19e0ef26 | ||
|
|
c0af297f48 | ||
|
|
c97e34aa04 | ||
|
|
01ee045beb | ||
|
|
cf6f83c8a2 | ||
|
|
4deaf4fb76 | ||
|
|
d68bc4abdb | ||
|
|
4f07515ee8 | ||
|
|
25b545d4c4 | ||
|
|
79b6b7ecc0 | ||
|
|
5d264ef5b6 | ||
|
|
f63ee85fa3 | ||
|
|
083a7069f0 | ||
|
|
f5621db85d | ||
|
|
658f117e93 | ||
|
|
6140ae525c | ||
|
|
afb02da806 | ||
|
|
692f29fe1a | ||
|
|
40e797966f | ||
|
|
a15a94a339 | ||
|
|
ca687cfe40 | ||
|
|
32e17745f1 | ||
|
|
432f3654df | ||
|
|
197cea2a60 | ||
|
|
b2bf368db9 | ||
|
|
287b2e3f41 | ||
|
|
da0fecfd0f | ||
|
|
76f9f635d8 | ||
|
|
3f05396222 | ||
|
|
644e6079b3 | ||
|
|
1d342cdbd0 | ||
|
|
908ec4c544 | ||
|
|
7c86f1f9d3 | ||
|
|
f8c01e379c | ||
|
|
af468a73bc | ||
|
|
d3a863911c | ||
|
|
c4172ee8e1 | ||
|
|
ed8ed15168 | ||
|
|
32f0426f01 | ||
|
|
200c00244b | ||
|
|
1104467329 | ||
|
|
5695fd8afb | ||
|
|
d0e383853f | ||
|
|
3bc412b42f | ||
|
|
f553d6919d | ||
|
|
d6a4b0f910 | ||
|
|
c0488d1f64 | ||
|
|
81195431b0 | ||
|
|
87109e6559 | ||
|
|
c0af1e62e8 | ||
|
|
ac9cce16f7 | ||
|
|
3ad660927f | ||
|
|
8778d70ad7 | ||
|
|
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 | ||
|
|
f6e623ca9c | ||
|
|
1e0f52bbdd | ||
|
|
6dadd1775a | ||
|
|
067cc8766a | ||
|
|
15b0564212 | ||
|
|
e90fa27ebf | ||
|
|
ef03d3f583 | ||
|
|
5c8669d699 | ||
|
|
9cd91464e3 | ||
|
|
5a262d42c1 | ||
|
|
eadc993854 | ||
|
|
c64ac48fe3 | ||
|
|
ac502053d7 | ||
|
|
1d8eb0d544 | ||
|
|
e94702349b | ||
|
|
d648159f43 | ||
|
|
acbd98d0f8 | ||
|
|
22e02e19b8 | ||
|
|
7ee6d5ad8f | ||
|
|
19aa3dd8a5 |
63
.eslintrc.js
Normal file
63
.eslintrc.js
Normal file
@@ -0,0 +1,63 @@
|
||||
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-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
|
||||
230
.github/workflows/build.yml
vendored
230
.github/workflows/build.yml
vendored
@@ -4,41 +4,80 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '!.github/workflows/build.yml'
|
||||
- '.vscode/**'
|
||||
- 'docs/**'
|
||||
- 'scripts/**'
|
||||
- 'static/**'
|
||||
- '*.md'
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+*'
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '!.github/workflows/build.yml'
|
||||
- '.vscode/**'
|
||||
- 'docs/**'
|
||||
- 'scripts/**'
|
||||
- 'static/**'
|
||||
- '*.md'
|
||||
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.19"
|
||||
JOB_TRANSFER_ARTIFACT: build-artifacts
|
||||
CHANGELOG_ARTIFACTS: changelog
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build (${{ matrix.config.os }})
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- os: windows-latest
|
||||
- os: ubuntu-latest
|
||||
- os: windows-2019
|
||||
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX # Name of the secret that contains the certificate.
|
||||
certificate-password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD # Name of the secret that contains the certificate password.
|
||||
certificate-extension: pfx # File extension for the certificate.
|
||||
- os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259
|
||||
- os: 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 }}
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
- name: Install Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '12.14.1'
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Python 2.7
|
||||
uses: actions/setup-python@v2
|
||||
- name: Install Python 3.x
|
||||
uses: actions/setup-python@v4
|
||||
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
|
||||
shell: bash
|
||||
@@ -46,36 +85,72 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AC_USERNAME: ${{ secrets.AC_USERNAME }}
|
||||
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
|
||||
AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
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_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
|
||||
run: |
|
||||
# See: https://www.electron.build/code-signing
|
||||
if [ "${{ runner.OS }}" = "macOS" ]; then
|
||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.p12"
|
||||
# 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
|
||||
echo "${{ secrets.APPLE_SIGNING_CERTIFICATE_P12 }}" | base64 --decode > "$CSC_LINK"
|
||||
# See: https://www.electron.build/code-signing
|
||||
if [ $CAN_SIGN = false ]; then
|
||||
echo "Skipping the app signing: certificate not provided."
|
||||
else
|
||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}"
|
||||
echo "${{ secrets[matrix.config.certificate-secret] }}" | base64 --decode > "$CSC_LINK"
|
||||
export CSC_KEY_PASSWORD="${{ secrets[matrix.config.certificate-password-secret] }}"
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
yarn --cwd ./electron/packager/
|
||||
yarn --cwd ./electron/packager/ package
|
||||
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/ package
|
||||
|
||||
- name: Upload [GitHub Actions]
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
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:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
@@ -83,7 +158,7 @@ jobs:
|
||||
BODY: ${{ steps.changelog.outputs.BODY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # To fetch all history for all branches and tags.
|
||||
|
||||
@@ -92,49 +167,53 @@ jobs:
|
||||
env:
|
||||
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
run: |
|
||||
export LATEST_TAG=$(git describe --abbrev=0)
|
||||
export GIT_LOG=$(git log --pretty=" - %s [%h]" $LATEST_TAG..HEAD | sed 's/ *$//g')
|
||||
if [ "$IS_RELEASE" = true ]; then
|
||||
export BODY=$(echo -e "$GIT_LOG")
|
||||
else
|
||||
export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-ide/releases/tag/$LATEST_TAG)")
|
||||
if [ -z "$GIT_LOG" ]; then
|
||||
export BODY="There were no changes since version $LATEST_TAG_WITH_LINK."
|
||||
else
|
||||
export BODY=$(echo -e "Changes since version $LATEST_TAG_WITH_LINK:\n$GIT_LOG")
|
||||
fi
|
||||
export LATEST_TAG=$(git describe --abbrev=0)
|
||||
export GIT_LOG=$(git log --pretty=" - %s [%h]" $LATEST_TAG..HEAD | sed 's/ *$//g')
|
||||
if [ "$IS_RELEASE" = true ]; then
|
||||
export BODY=$(echo -e "$GIT_LOG")
|
||||
else
|
||||
export LATEST_TAG_WITH_LINK=$(echo "[$LATEST_TAG](https://github.com/arduino/arduino-ide/releases/tag/$LATEST_TAG)")
|
||||
if [ -z "$GIT_LOG" ]; then
|
||||
export BODY="There were no changes since version $LATEST_TAG_WITH_LINK."
|
||||
else
|
||||
export BODY=$(echo -e "Changes since version $LATEST_TAG_WITH_LINK:\n$GIT_LOG")
|
||||
fi
|
||||
echo -e "$BODY"
|
||||
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
|
||||
echo "::set-output name=BODY::$OUTPUT_SAFE_BODY"
|
||||
echo "$BODY" > CHANGELOG.txt
|
||||
fi
|
||||
echo -e "$BODY"
|
||||
|
||||
# Set workflow step output
|
||||
# See: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
DELIMITER="$RANDOM"
|
||||
echo "BODY<<$DELIMITER" >> $GITHUB_OUTPUT
|
||||
echo "$BODY" >> $GITHUB_OUTPUT
|
||||
echo "$DELIMITER" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "$BODY" > CHANGELOG.txt
|
||||
|
||||
- name: Upload Changelog [GitHub Actions]
|
||||
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:
|
||||
name: build-artifacts
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: CHANGELOG.txt
|
||||
|
||||
publish:
|
||||
needs: changelog
|
||||
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
|
||||
if: github.repository == 'arduino/arduino-ide' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'))
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download [GitHub Actions]
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build-artifacts
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
|
||||
- name: Publish Nightly [S3]
|
||||
uses: docker://plugins/s3
|
||||
env:
|
||||
PLUGIN_SOURCE: "build-artifacts/*"
|
||||
PLUGIN_STRIP_PREFIX: "build-artifacts/"
|
||||
PLUGIN_TARGET: "/arduino-ide/nightly"
|
||||
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
|
||||
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
|
||||
PLUGIN_TARGET: '/arduino-ide/nightly'
|
||||
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -145,32 +224,49 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download [GitHub Actions]
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: build-artifacts
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
|
||||
- name: Get Tag
|
||||
id: tag_name
|
||||
run: |
|
||||
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
|
||||
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Publish Release [GitHub]
|
||||
uses: svenstaro/upload-release-action@2.2.0
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
|
||||
file: build-artifacts/*
|
||||
file: ${{ env.JOB_TRANSFER_ARTIFACT }}/*
|
||||
tag: ${{ github.ref }}
|
||||
file_glob: true
|
||||
body: ${{ needs.changelog.outputs.BODY }}
|
||||
|
||||
- name: Publish Release [S3]
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
uses: docker://plugins/s3
|
||||
env:
|
||||
PLUGIN_SOURCE: "build-artifacts/*"
|
||||
PLUGIN_STRIP_PREFIX: "build-artifacts/"
|
||||
PLUGIN_TARGET: "/arduino-ide"
|
||||
PLUGIN_SOURCE: '${{ env.JOB_TRANSFER_ARTIFACT }}/*'
|
||||
PLUGIN_STRIP_PREFIX: '${{ env.JOB_TRANSFER_ARTIFACT }}/'
|
||||
PLUGIN_TARGET: '/arduino-ide'
|
||||
PLUGIN_BUCKET: ${{ secrets.DOWNLOADS_BUCKET }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
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 }}
|
||||
|
||||
55
.github/workflows/check-certificates.yml
vendored
55
.github/workflows/check-certificates.yml
vendored
@@ -1,30 +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:
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/check-certificates.ya?ml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/check-certificates.ya?ml'
|
||||
schedule:
|
||||
# run every 10 hours
|
||||
- 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
|
||||
# Run every 10 hours.
|
||||
- cron: '0 */10 * * *'
|
||||
workflow_dispatch:
|
||||
repository_dispatch:
|
||||
|
||||
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
|
||||
|
||||
jobs:
|
||||
check-certificates:
|
||||
name: ${{ matrix.certificate.identifier }}
|
||||
# Only run when the workflow will have access to the certificate secrets.
|
||||
if: >
|
||||
(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')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
matrix:
|
||||
certificate:
|
||||
- identifier: macOS signing certificate # Text used to identify the certificate in notifications
|
||||
certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # The name of the secret that contains the certificate
|
||||
password-secret: KEYCHAIN_PASSWORD # The name of the secret that contains the certificate password
|
||||
# Additional certificate definitions can be added to this list.
|
||||
- identifier: macOS signing certificate # Text used to identify certificate in notifications.
|
||||
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
|
||||
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX
|
||||
password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD
|
||||
@@ -32,7 +43,7 @@ jobs:
|
||||
steps:
|
||||
- name: Set certificate path environment variable
|
||||
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"
|
||||
|
||||
- name: Decode certificate
|
||||
@@ -48,24 +59,25 @@ jobs:
|
||||
(
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-noout -passin env:CERTIFICATE_PASSWORD
|
||||
-legacy \
|
||||
-noout \
|
||||
-passin env:CERTIFICATE_PASSWORD
|
||||
) || (
|
||||
echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
|
||||
exit 1
|
||||
)
|
||||
|
||||
# See: https://github.com/rtCamp/action-slack-notify
|
||||
- name: Slack notification of certificate verification failure
|
||||
if: failure()
|
||||
uses: rtCamp/action-slack-notify@v2.1.0
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_MESSAGE: |
|
||||
:warning::warning::warning::warning:
|
||||
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} verification failed!!!
|
||||
:warning::warning::warning::warning:
|
||||
SLACK_COLOR: danger
|
||||
MSG_MINIMAL: true
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
|
||||
- name: Get days remaining before certificate expiration date
|
||||
env:
|
||||
@@ -77,6 +89,7 @@ jobs:
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-clcerts \
|
||||
-legacy \
|
||||
-nodes \
|
||||
-passin env:CERTIFICATE_PASSWORD
|
||||
) | (
|
||||
@@ -94,11 +107,11 @@ jobs:
|
||||
|
||||
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 "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
|
||||
id: check-expiration
|
||||
@@ -109,14 +122,14 @@ jobs:
|
||||
fi
|
||||
|
||||
- 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'
|
||||
uses: rtCamp/action-slack-notify@v2.1.0
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.TEAM_TOOLING_CHANNEL_SLACK_WEBHOOK }}
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_MESSAGE: |
|
||||
:warning::warning::warning::warning:
|
||||
WARNING: ${{ github.repository }} ${{ matrix.certificate.identifier }} will expire in ${{ steps.get-days-before-expiration.outputs.days }} days!!!
|
||||
:warning::warning::warning::warning:
|
||||
SLACK_COLOR: danger
|
||||
MSG_MINIMAL: true
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
|
||||
55
.github/workflows/check-i18n-task.yml
vendored
Normal file
55
.github/workflows/check-i18n-task.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Check Internationalization
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
# 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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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.19"
|
||||
|
||||
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.19"
|
||||
|
||||
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.19"
|
||||
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>
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -4,15 +4,22 @@ node_modules/
|
||||
lib/
|
||||
downloads/
|
||||
build/
|
||||
Examples/
|
||||
arduino-ide-extension/Examples/
|
||||
!electron/build/
|
||||
src-gen/
|
||||
*webpack.config.js
|
||||
webpack.config.js
|
||||
gen-webpack.config.js
|
||||
.DS_Store
|
||||
# switching from `electron` to `browser` in dev mode.
|
||||
.browser_modules
|
||||
yarn*.log
|
||||
# For the VS Code extensions used by Theia.
|
||||
plugins
|
||||
# the config files for the CLI
|
||||
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
|
||||
# any Arduino LS generated log files
|
||||
inols*.log
|
||||
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 80,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
199
.vscode/launch.json
vendored
199
.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",
|
||||
"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",
|
||||
"--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",
|
||||
"--no-ping-timeout",
|
||||
],
|
||||
"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",
|
||||
"--app-project-path=${workspaceRoot}/electron-app",
|
||||
"--remote-debugging-port=9222",
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:../plugins",
|
||||
"--hosted-plugin-inspect=9339",
|
||||
"--no-ping-timeout",
|
||||
],
|
||||
"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",
|
||||
"request": "attach",
|
||||
@@ -16,94 +114,15 @@
|
||||
"name": "Electron Packager",
|
||||
"program": "${workspaceRoot}/electron/packager/index.js",
|
||||
"cwd": "${workspaceFolder}/electron/packager"
|
||||
},
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"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": {
|
||||
"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",
|
||||
"request": "launch",
|
||||
"name": "App (Browser)",
|
||||
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
|
||||
"args": [
|
||||
"--hostname=0.0.0.0",
|
||||
"--port=3000",
|
||||
"--no-cluster",
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:plugins"
|
||||
],
|
||||
"windows": {
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"NODE_PRESERVE_SYMLINKS": "1"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"outFiles": [
|
||||
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
|
||||
"${workspaceRoot}/browser-app/lib/**/*.js",
|
||||
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
|
||||
],
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"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"
|
||||
"name": "Launch Electron Backend & Frontend",
|
||||
"configurations": [
|
||||
"App (Electron)",
|
||||
"Attach to Electron Frontend"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
19
.vscode/settings.json
vendored
19
.vscode/settings.json
vendored
@@ -1,21 +1,12 @@
|
||||
{
|
||||
"tslint.enable": true,
|
||||
"tslint.configFile": "./tslint.json",
|
||||
"editor.formatOnSave": true,
|
||||
"files.exclude": {
|
||||
"**/lib": false
|
||||
},
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false,
|
||||
"[typescript]": {
|
||||
"editor.tabSize": 4
|
||||
"search.exclude": {
|
||||
"arduino-ide-extension/src/test/node/__test_sketchbook__": true
|
||||
},
|
||||
"[json]": {
|
||||
"editor.tabSize": 2
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"[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",
|
||||
"tasks": [
|
||||
{
|
||||
@@ -14,17 +12,6 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Start Browser App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app start",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch IDE Extension",
|
||||
"type": "shell",
|
||||
@@ -35,17 +22,6 @@
|
||||
"panel": "new",
|
||||
"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",
|
||||
@@ -58,14 +34,6 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch All [Browser]",
|
||||
"type": "shell",
|
||||
"dependsOn": [
|
||||
"Arduino IDE - Watch IDE Extension",
|
||||
"Arduino IDE - Watch Browser App"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch All [Electron]",
|
||||
"type": "shell",
|
||||
|
||||
3
BUILDING.md
Normal file
3
BUILDING.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Development Guide
|
||||
|
||||
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
|
||||
170
README.md
170
README.md
@@ -1,165 +1,53 @@
|
||||
# Arduino IDE
|
||||
<img src="https://content.arduino.cc/website/Arduino_logo_teal.svg" height="100" align="right" />
|
||||
|
||||
# Arduino IDE 2.x
|
||||
|
||||
[](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
|
||||
|
||||
### Download
|
||||
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.
|
||||
|
||||
You can download the latest version of the Arduino IDE application for the supported platforms from the [GitHub release page](https://github.com/arduino/arduino-ide/releases) or following the links in the following table.
|
||||
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.
|
||||
|
||||
#### Latest version
|
||||

|
||||
|
||||
Platform | 32 bit | 64 bit |
|
||||
--------- | ------------------------ | ------------------------------------------------------------------------------ |
|
||||
Linux | | [Linux 64 bit] |
|
||||
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
|
||||
Windows | | [Windows 64 bit installer]<br />[Windows 64 bit MSI]<br />[Windows 64 bit ZIP] |
|
||||
macOS | | [macOS 64 bit] |
|
||||
## Download
|
||||
|
||||
[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/287
|
||||
[Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Linux_64bit.zip
|
||||
[Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.exe
|
||||
[Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.msi
|
||||
[Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_Windows_64bit.zip
|
||||
[macOS 64 bit]: https://downloads.arduino.cc/arduino-ide/arduino-ide_latest_macOS_64bit.dmg
|
||||
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).
|
||||
|
||||
#### Previous versions
|
||||
## Support
|
||||
|
||||
These are available from the [GitHub releases page](https://github.com/arduino/arduino-ide/releases).
|
||||
If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-us/categories/360002212660-Software-and-Downloads) and browse the [forum](https://forum.arduino.cc/index.php?board=150.0).
|
||||
|
||||
#### Nightly builds
|
||||
## Bugs & Issues
|
||||
|
||||
These builds are generated every day at 03:00 GMT from the `main` branch and
|
||||
should be considered unstable. In order to get the latest nightly build
|
||||
available for the supported platform, use the following links:
|
||||
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.
|
||||
|
||||
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] |
|
||||
See [**the issue report guide**](docs/contributor-guide/issues.md#issue-report-guide) for instructions.
|
||||
|
||||
[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/287
|
||||
[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
|
||||
### Security
|
||||
|
||||
> 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` )
|
||||
If you think you found a vulnerability or other security-related bug in this project, please read our
|
||||
[security policy](https://github.com/arduino/arduino-ide/security/policy) and report the bug to our Security Team 🛡️
|
||||
Thank you!
|
||||
|
||||
### Build from source
|
||||
e-mail contact: security@arduino.cc
|
||||
|
||||
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.
|
||||
## Contributions and development
|
||||
|
||||
### Build
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
Contributions are very welcome! There are several ways to participate in this project, including:
|
||||
|
||||
### Rebuild the native dependencies
|
||||
```sh
|
||||
yarn rebuild:electron
|
||||
```
|
||||
- Fixing bugs
|
||||
- Beta testing
|
||||
- Translation
|
||||
|
||||
### Start
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
See [**the contributor guide**](docs/CONTRIBUTING.md#contributor-guide) for more information.
|
||||
|
||||
### CI
|
||||
See the [**development guide**](docs/development.md) for a technical overview of the application and instructions for building the code.
|
||||
|
||||
This project is built on [GitHub Actions](https://github.com/bcmi-labs/arduino-editor/actions?query=workflow%3A%22Arduino+Pro+IDE%22).
|
||||
## Donations
|
||||
|
||||
- _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
|
||||
```
|
||||
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.
|
||||
|
||||
### Creating a GH release
|
||||
This section guides you through how to create a new release. Let's assume the current version is `0.1.3` and you want to release `0.2.0`.
|
||||
## License
|
||||
|
||||
- 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 Pro IDE [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
|
||||
|
||||
- Q: Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?
|
||||
- A: 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.
|
||||
|
||||
- Q: I have understood that not all versions of the CLI is compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?
|
||||
- A: [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`.
|
||||
|
||||
### Architecture overview
|
||||
|
||||
The Pro 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 Pro 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 Pro IDE, only the _backend_ allows OS interaction.
|
||||
|
||||
The _backend_ process is responsible for:
|
||||
- providing access to the filesystem,
|
||||
- communicating with the 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
|
||||
The code contained in this repository and the executable distributions are licensed under the terms of the GNU AGPLv3. The executable distributions contain third-party code licensed under other compatible licenses such as GPLv2, MIT and BSD-3. If you have questions about licensing please contact us at [license@arduino.cc](mailto:license@arduino.cc).
|
||||
|
||||
@@ -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
|
||||
- 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/node/monitor-service-impl.ts](./src/node/monitor-service-impl.ts) implements the service backend:
|
||||
- [src/common/protocol/serial-service.ts](./src/common/protocol/serial-service.ts) implements the common classes and interfaces
|
||||
- [src/node/serial/serial-service-impl.ts](./src/node/serial/serial-service-impl.ts) implements the service backend:
|
||||
- connecting to / disconnecting from a board
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -54,3 +57,26 @@ The Config Service knows about your system, like for example the default sketch
|
||||
### `"arduino"` configuration in the `package.json`:
|
||||
- `"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.
|
||||
|
||||
#### 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,89 +1,128 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "2.0.0-beta.3",
|
||||
"version": "2.0.4",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "MIT",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"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",
|
||||
"compose-changelog": "node ./scripts/compose-changelog.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-examples": "node ./scripts/download-examples.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",
|
||||
"watch": "tsc -w",
|
||||
"test": "mocha \"./lib/test/**/*.test.js\"",
|
||||
"test:slow": "mocha \"./lib/test/**/*.slow-test.js\" --slow 5000",
|
||||
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.1.1",
|
||||
"@theia/application-package": "next",
|
||||
"@theia/core": "next",
|
||||
"@theia/editor": "next",
|
||||
"@theia/filesystem": "next",
|
||||
"@theia/git": "next",
|
||||
"@theia/markers": "next",
|
||||
"@theia/monaco": "next",
|
||||
"@theia/navigator": "next",
|
||||
"@theia/outline-view": "next",
|
||||
"@theia/preferences": "next",
|
||||
"@theia/output": "next",
|
||||
"@theia/search-in-workspace": "next",
|
||||
"@theia/terminal": "next",
|
||||
"@theia/workspace": "next",
|
||||
"@grpc/grpc-js": "^1.6.7",
|
||||
"@theia/application-package": "1.31.1",
|
||||
"@theia/core": "1.31.1",
|
||||
"@theia/debug": "1.31.1",
|
||||
"@theia/editor": "1.31.1",
|
||||
"@theia/electron": "1.31.1",
|
||||
"@theia/filesystem": "1.31.1",
|
||||
"@theia/keymaps": "1.31.1",
|
||||
"@theia/markers": "1.31.1",
|
||||
"@theia/messages": "1.31.1",
|
||||
"@theia/monaco": "1.31.1",
|
||||
"@theia/monaco-editor-core": "1.67.2",
|
||||
"@theia/navigator": "1.31.1",
|
||||
"@theia/outline-view": "1.31.1",
|
||||
"@theia/output": "1.31.1",
|
||||
"@theia/plugin-ext": "1.31.1",
|
||||
"@theia/preferences": "1.31.1",
|
||||
"@theia/scm": "1.31.1",
|
||||
"@theia/search-in-workspace": "1.31.1",
|
||||
"@theia/terminal": "1.31.1",
|
||||
"@theia/typehierarchy": "1.31.1",
|
||||
"@theia/workspace": "1.31.1",
|
||||
"@tippyjs/react": "^4.2.5",
|
||||
"@types/auth0-js": "^9.14.0",
|
||||
"@types/btoa": "^1.2.3",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/deepmerge": "^2.2.0",
|
||||
"@types/glob": "^5.0.35",
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/js-yaml": "^3.12.2",
|
||||
"@types/keytar": "^4.4.0",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/ncp": "^2.0.4",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/p-queue": "^2.3.1",
|
||||
"@types/ps-tree": "^1.1.0",
|
||||
"@types/react-select": "^3.0.0",
|
||||
"@types/react-tabs": "^2.3.2",
|
||||
"@types/sinon": "^7.5.2",
|
||||
"@types/react-virtualized": "^9.21.21",
|
||||
"@types/temp": "^0.8.34",
|
||||
"@types/which": "^1.3.1",
|
||||
"ajv": "^6.5.3",
|
||||
"@vscode/debugprotocol": "^1.51.0",
|
||||
"arduino-serial-plotter-webapp": "0.2.0",
|
||||
"async-mutex": "^0.3.0",
|
||||
"css-element-queries": "^1.2.0",
|
||||
"auth0-js": "^9.14.0",
|
||||
"btoa": "^1.2.1",
|
||||
"classnames": "^2.3.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"dateformat": "^3.0.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fuzzy": "^0.1.3",
|
||||
"deepmerge": "2.0.1",
|
||||
"electron-updater": "^4.6.5",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"filename-reserved-regex": "^2.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"google-protobuf": "^3.11.4",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"google-protobuf": "^3.20.1",
|
||||
"hash.js": "^1.1.7",
|
||||
"js-yaml": "^3.13.1",
|
||||
"just-diff": "^5.1.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"keytar": "7.2.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"minimatch": "^3.1.2",
|
||||
"ncp": "^2.0.0",
|
||||
"p-queue": "^5.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"open": "^8.0.6",
|
||||
"p-debounce": "^2.1.0",
|
||||
"p-queue": "^2.4.2",
|
||||
"ps-tree": "^1.2.0",
|
||||
"react-disable": "^0.1.0",
|
||||
"react-select": "^3.0.4",
|
||||
"query-string": "^7.0.1",
|
||||
"react-disable": "^0.1.1",
|
||||
"react-markdown": "^8.0.0",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
"react-select": "^5.6.0",
|
||||
"react-tabs": "^3.1.2",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"react-window": "^1.8.6",
|
||||
"semver": "^7.3.2",
|
||||
"string-natural-compare": "^2.0.3",
|
||||
"temp": "^0.9.1",
|
||||
"temp-dir": "^2.0.0",
|
||||
"tree-kill": "^1.2.1",
|
||||
"upath": "^1.1.2",
|
||||
"which": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^18.12.0",
|
||||
"@types/chai": "^4.2.7",
|
||||
"@types/chai-string": "^1.4.2",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"chai": "^4.2.0",
|
||||
"chai-string": "^1.5.0",
|
||||
"decompress": "^4.2.0",
|
||||
"decompress-tarbz2": "^4.1.1",
|
||||
"decompress-targz": "^4.1.1",
|
||||
"decompress-unzip": "^4.0.1",
|
||||
"download": "^7.1.0",
|
||||
"grpc_tools_node_protoc_ts": "^4.1.0",
|
||||
"mocha": "^7.0.0",
|
||||
"mockdate": "^3.0.5",
|
||||
"moment": "^2.24.0",
|
||||
"protoc": "^1.0.4",
|
||||
"shelljs": "^0.8.3",
|
||||
"sinon": "^9.0.1",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^11.1.0"
|
||||
},
|
||||
@@ -92,7 +131,8 @@
|
||||
},
|
||||
"mocha": {
|
||||
"require": [
|
||||
"reflect-metadata/Reflect"
|
||||
"reflect-metadata/Reflect",
|
||||
"ignore-styles"
|
||||
],
|
||||
"reporter": "spec",
|
||||
"colors": true,
|
||||
@@ -112,11 +152,10 @@
|
||||
"frontend": "lib/browser/arduino-ide-frontend-module"
|
||||
},
|
||||
{
|
||||
"frontend": "lib/browser/theia/core/browser-menu-module",
|
||||
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
|
||||
},
|
||||
{
|
||||
"frontend": "lib/browser/boards/quick-open/boards-quick-open-module"
|
||||
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
|
||||
},
|
||||
{
|
||||
"electronMain": "lib/electron-main/arduino-electron-main-module"
|
||||
@@ -124,7 +163,16 @@
|
||||
],
|
||||
"arduino": {
|
||||
"cli": {
|
||||
"version": "0.16.1"
|
||||
"version": "0.31.0"
|
||||
},
|
||||
"fwuploader": {
|
||||
"version": "2.2.2"
|
||||
},
|
||||
"clangd": {
|
||||
"version": "14.0.0"
|
||||
},
|
||||
"languageServer": {
|
||||
"version": "0.7.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,141 +1,90 @@
|
||||
// @ts-check
|
||||
|
||||
(async () => {
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const semver = require('semver');
|
||||
const moment = require('moment');
|
||||
const downloader = require('./downloader');
|
||||
const { taskBuildFromGit } = require('./utils');
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const temp = require('temp');
|
||||
const shell = require('shelljs');
|
||||
const semver = require('semver');
|
||||
const moment = require('moment');
|
||||
const downloader = require('./downloader');
|
||||
const version = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
if (!pkg) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const version = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
if (!pkg) {
|
||||
return undefined;
|
||||
const { arduino } = pkg;
|
||||
if (!arduino) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { cli } = arduino;
|
||||
if (!cli) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { version } = cli;
|
||||
return version;
|
||||
})();
|
||||
|
||||
if (!version) {
|
||||
shell.echo(`Could not retrieve CLI version info from the 'package.json'.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const { platform, arch } = process;
|
||||
const buildFolder = path.join(__dirname, '..', 'build');
|
||||
const cliName = `arduino-cli${platform === 'win32' ? '.exe' : ''}`;
|
||||
const destinationPath = path.join(buildFolder, cliName);
|
||||
|
||||
if (typeof version === 'string') {
|
||||
const suffix = (() => {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
if (arch === 'arm64') {
|
||||
return 'macOS_ARM64.tar.gz';
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const { arduino } = pkg;
|
||||
if (!arduino) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { cli } = arduino;
|
||||
if (!cli) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { version } = cli;
|
||||
return version;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!version) {
|
||||
shell.echo(`Could not retrieve CLI version info from the 'package.json'.`);
|
||||
shell.exit(1);
|
||||
if (!suffix) {
|
||||
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const { platform, arch } = process;
|
||||
const buildFolder = path.join(__dirname, '..', 'build');
|
||||
const cliName = `arduino-cli${platform === 'win32' ? '.exe' : ''}`;
|
||||
const destinationPath = path.join(buildFolder, cliName);
|
||||
|
||||
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 CLI is not available for ${platform} ${arch}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
if (semver.valid(version)) {
|
||||
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}'`);
|
||||
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
||||
} else if (moment(version, 'YYYYMMDD', true).isValid()) {
|
||||
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}'`);
|
||||
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
||||
} else {
|
||||
shell.echo(`🔥 Could not interpret 'version': ${version}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
if (semver.valid(version)) {
|
||||
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}'`
|
||||
);
|
||||
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
||||
} else if (moment(version, 'YYYYMMDD', true).isValid()) {
|
||||
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}'`
|
||||
);
|
||||
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
|
||||
} 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 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.');
|
||||
|
||||
shell.echo(`🔥 Could not interpret 'version': ${version}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
} else {
|
||||
taskBuildFromGit(version, destinationPath, 'CLI');
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,33 +1,96 @@
|
||||
// @ts-check
|
||||
|
||||
// The version to use.
|
||||
const version = '1.9.0';
|
||||
const version = '1.10.0';
|
||||
|
||||
(async () => {
|
||||
const os = require('os');
|
||||
const { promises: fs } = require('fs');
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const { v4 } = require('uuid');
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const { v4 } = require('uuid');
|
||||
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
|
||||
if (shell.mkdir('-p', repository).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const repository = path.join(os.tmpdir(), `${v4()}-arduino-examples`);
|
||||
if (shell.mkdir('-p', repository).code !== 0) {
|
||||
shell.exit(1);
|
||||
process.exit(1);
|
||||
if (
|
||||
shell.exec(
|
||||
`git clone https://github.com/arduino/arduino-examples.git ${repository}`
|
||||
).code !== 0
|
||||
) {
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
if (
|
||||
shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`)
|
||||
.code !== 0
|
||||
) {
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const destination = path.join(__dirname, '..', 'Examples');
|
||||
shell.mkdir('-p', 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;
|
||||
}
|
||||
|
||||
if (shell.exec(`git clone https://github.com/arduino/arduino-examples.git ${repository}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
process.exit(1);
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const destination = path.join(__dirname, '..', 'Examples');
|
||||
shell.mkdir('-p', destination);
|
||||
shell.cp('-fR', path.join(repository, 'examples', '*'), destination);
|
||||
|
||||
};
|
||||
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}
|
||||
|
||||
(() => {
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const downloader = require('./downloader');
|
||||
const { goBuildFromGit } = require('./utils');
|
||||
|
||||
const DEFAULT_ALS_VERSION = 'nightly';
|
||||
const DEFAULT_CLANGD_VERSION = 'snapshot_20210124';
|
||||
const [DEFAULT_LS_VERSION, DEFAULT_CLANGD_VERSION] = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
if (!pkg) return [undefined, undefined];
|
||||
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const downloader = require('./downloader');
|
||||
const { arduino } = pkg;
|
||||
if (!arduino) return [undefined, undefined];
|
||||
|
||||
const yargs = require('yargs')
|
||||
.option('ls-version', {
|
||||
alias: 'lv',
|
||||
default: DEFAULT_ALS_VERSION,
|
||||
choices: ['nightly'],
|
||||
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`
|
||||
})
|
||||
.option('clangd-version', {
|
||||
alias: 'cv',
|
||||
default: DEFAULT_CLANGD_VERSION,
|
||||
choices: ['snapshot_20210124'],
|
||||
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`
|
||||
})
|
||||
.option('force-download', {
|
||||
alias: 'fd',
|
||||
default: false,
|
||||
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`
|
||||
})
|
||||
.version(false).parse();
|
||||
const { languageServer, clangd } = arduino;
|
||||
if (!languageServer) return [undefined, undefined];
|
||||
if (!clangd) return [undefined, undefined];
|
||||
|
||||
const alsVersion = yargs['ls-version'];
|
||||
const clangdVersion = yargs['clangd-version']
|
||||
const force = yargs['force-download'];
|
||||
const { platform, arch } = process;
|
||||
return [languageServer.version, clangd.version];
|
||||
})();
|
||||
|
||||
const build = path.join(__dirname, '..', 'build');
|
||||
const lsExecutablePath = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
|
||||
if (!DEFAULT_LS_VERSION) {
|
||||
shell.echo(
|
||||
`Could not retrieve Arduino Language Server version info from the 'package.json'.`
|
||||
);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
let clangdExecutablePath, lsSuffix, clangdPrefix;
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
clangdExecutablePath = path.join(build, 'bin', 'clangd')
|
||||
lsSuffix = 'macOS_amd64.zip';
|
||||
clangdPrefix = 'mac';
|
||||
break;
|
||||
case 'linux':
|
||||
clangdExecutablePath = path.join(build, 'bin', 'clangd')
|
||||
lsSuffix = 'Linux_amd64.zip';
|
||||
clangdPrefix = 'linux'
|
||||
break;
|
||||
case 'win32':
|
||||
clangdExecutablePath = path.join(build, 'bin', 'clangd.exe')
|
||||
lsSuffix = 'Windows_amd64.zip';
|
||||
clangdPrefix = 'windows';
|
||||
break;
|
||||
if (!DEFAULT_CLANGD_VERSION) {
|
||||
shell.echo(
|
||||
`Could not retrieve clangd version info from the 'package.json'.`
|
||||
);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const yargs = require('yargs')
|
||||
.option('ls-version', {
|
||||
alias: 'lv',
|
||||
default: DEFAULT_LS_VERSION,
|
||||
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_LS_VERSION}.`,
|
||||
})
|
||||
.option('clangd-version', {
|
||||
alias: 'cv',
|
||||
default: DEFAULT_CLANGD_VERSION,
|
||||
choices: [DEFAULT_CLANGD_VERSION, 'snapshot_20210124'],
|
||||
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`,
|
||||
})
|
||||
.option('force-download', {
|
||||
alias: 'fd',
|
||||
default: false,
|
||||
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`,
|
||||
})
|
||||
.version(false)
|
||||
.parse();
|
||||
|
||||
const lsVersion = yargs['ls-version'];
|
||||
const clangdVersion = yargs['clangd-version'];
|
||||
const force = yargs['force-download'];
|
||||
const { platform, arch } = process;
|
||||
const platformArch = platform + '-' + arch;
|
||||
const build = path.join(__dirname, '..', 'build');
|
||||
const lsExecutablePath = path.join(
|
||||
build,
|
||||
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
|
||||
);
|
||||
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
|
||||
|
||||
switch (platformArch) {
|
||||
case 'darwin-x64':
|
||||
clangdExecutablePath = path.join(build, 'clangd');
|
||||
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||
lsSuffix = 'macOS_64bit.tar.gz';
|
||||
clangdSuffix = 'macOS_64bit';
|
||||
break;
|
||||
case 'darwin-arm64':
|
||||
clangdExecutablePath = path.join(build, 'clangd');
|
||||
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||
lsSuffix = 'macOS_ARM64.tar.gz';
|
||||
clangdSuffix = 'macOS_ARM64';
|
||||
break;
|
||||
case 'linux-x64':
|
||||
clangdExecutablePath = path.join(build, 'clangd');
|
||||
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||
lsSuffix = 'Linux_64bit.tar.gz';
|
||||
clangdSuffix = 'Linux_64bit';
|
||||
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 || !clangdSuffix) {
|
||||
shell.echo(
|
||||
`The arduino-language-server is not available for ${platform} ${arch}.`
|
||||
);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
if (typeof lsVersion === 'string') {
|
||||
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/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.
|
||||
|
||||
const clangdFormatUrl = `https://downloads.arduino.cc/tools/clang-format_${clangdVersion}_${clangdSuffix}.tar.bz2`;
|
||||
downloader.downloadUnzipAll(
|
||||
clangdFormatUrl,
|
||||
build,
|
||||
clangFormatExecutablePath,
|
||||
force,
|
||||
{
|
||||
strip: 1,
|
||||
}
|
||||
if (!lsSuffix) {
|
||||
shell.echo(`The arduino-language-server is not available for ${platform} ${arch}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${alsVersion === 'nightly' ? 'nightly/arduino-language-server' : 'arduino-language-server_' + alsVersion}_${lsSuffix}`;
|
||||
downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force);
|
||||
|
||||
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd-${clangdPrefix}-${clangdVersion}.zip`;
|
||||
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.
|
||||
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -5,16 +5,17 @@ const download = require('download');
|
||||
const decompress = require('decompress');
|
||||
const unzip = require('decompress-unzip');
|
||||
const untargz = require('decompress-targz');
|
||||
const untarbz2 = require('decompress-tarbz2');
|
||||
|
||||
process.on('unhandledRejection', (reason, _) => {
|
||||
shell.echo(String(reason));
|
||||
shell.exit(1);
|
||||
throw reason;
|
||||
shell.echo(String(reason));
|
||||
shell.exit(1);
|
||||
throw reason;
|
||||
});
|
||||
process.on('uncaughtException', error => {
|
||||
shell.echo(String(error));
|
||||
shell.exit(1);
|
||||
throw error;
|
||||
process.on('uncaughtException', (error) => {
|
||||
shell.echo(String(error));
|
||||
shell.exit(1);
|
||||
throw error;
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -23,98 +24,109 @@ process.on('uncaughtException', error => {
|
||||
* @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.
|
||||
*/
|
||||
exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) => {
|
||||
if (fs.existsSync(targetFile) && !force) {
|
||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(path.dirname(targetFile))) {
|
||||
if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) {
|
||||
shell.echo('Could not create new directory.');
|
||||
shell.exit(1);
|
||||
}
|
||||
exports.downloadUnzipFile = async (
|
||||
url,
|
||||
targetFile,
|
||||
filePrefix,
|
||||
force = false
|
||||
) => {
|
||||
if (fs.existsSync(targetFile) && !force) {
|
||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(path.dirname(targetFile))) {
|
||||
if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) {
|
||||
shell.echo('Could not create new directory.');
|
||||
shell.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const downloads = path.join(__dirname, '..', 'downloads');
|
||||
if (shell.rm('-rf', targetFile, downloads).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
const downloads = path.join(__dirname, '..', 'downloads');
|
||||
if (shell.rm('-rf', targetFile, downloads).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
shell.echo(`>>> Downloading from '${url}'...`);
|
||||
const data = await download(url);
|
||||
shell.echo(`<<< Download succeeded.`);
|
||||
shell.echo(`>>> Downloading from '${url}'...`);
|
||||
const data = await download(url);
|
||||
shell.echo(`<<< Download succeeded.`);
|
||||
|
||||
shell.echo('>>> Decompressing...');
|
||||
const files = await decompress(data, downloads, {
|
||||
plugins: [
|
||||
unzip(),
|
||||
untargz()
|
||||
]
|
||||
});
|
||||
if (files.length === 0) {
|
||||
shell.echo('Error ocurred while decompressing the archive.');
|
||||
shell.exit(1);
|
||||
}
|
||||
const fileIndex = files.findIndex(f => f.path.startsWith(filePrefix));
|
||||
if (fileIndex === -1) {
|
||||
shell.echo(`The downloaded artifact does not contain any file with prefix ${filePrefix}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('<<< Decompressing succeeded.');
|
||||
shell.echo('>>> Decompressing...');
|
||||
const files = await decompress(data, downloads, {
|
||||
plugins: [unzip(), untargz(), untarbz2()],
|
||||
});
|
||||
if (files.length === 0) {
|
||||
shell.echo('Error ocurred while decompressing the archive.');
|
||||
shell.exit(1);
|
||||
}
|
||||
const fileIndex = files.findIndex((f) => f.path.startsWith(filePrefix));
|
||||
if (fileIndex === -1) {
|
||||
shell.echo(
|
||||
`The downloaded artifact does not contain any file with prefix ${filePrefix}.`
|
||||
);
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('<<< Decompressing succeeded.');
|
||||
|
||||
if (shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile).code !== 0) {
|
||||
shell.echo(`Could not move file to target path: ${targetFile}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
if (!fs.existsSync(targetFile)) {
|
||||
shell.echo(`Could not find file: ${targetFile}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`Done: ${targetFile}`);
|
||||
}
|
||||
if (
|
||||
shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile)
|
||||
.code !== 0
|
||||
) {
|
||||
shell.echo(`Could not move file to target path: ${targetFile}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
if (!fs.existsSync(targetFile)) {
|
||||
shell.echo(`Could not find file: ${targetFile}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`Done: ${targetFile}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param url {string} Download URL
|
||||
* @param targetDir {string} Directory into which to decompress the archive
|
||||
* @param targetFile {string} Path to the main file expected after decompressing
|
||||
* @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) => {
|
||||
if (fs.existsSync(targetFile) && !force) {
|
||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
if (shell.mkdir('-p', targetDir).code !== 0) {
|
||||
shell.echo('Could not create new directory.');
|
||||
shell.exit(1);
|
||||
}
|
||||
exports.downloadUnzipAll = async (
|
||||
url,
|
||||
targetDir,
|
||||
targetFile,
|
||||
force,
|
||||
decompressOptions = undefined
|
||||
) => {
|
||||
if (fs.existsSync(targetFile) && !force) {
|
||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||
return;
|
||||
}
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
if (shell.mkdir('-p', targetDir).code !== 0) {
|
||||
shell.echo('Could not create new directory.');
|
||||
shell.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
shell.echo(`>>> Downloading from '${url}'...`);
|
||||
const data = await download(url);
|
||||
shell.echo(`<<< Download succeeded.`);
|
||||
shell.echo(`>>> Downloading from '${url}'...`);
|
||||
const data = await download(url);
|
||||
shell.echo(`<<< Download succeeded.`);
|
||||
|
||||
shell.echo('>>> Decompressing...');
|
||||
let options = {
|
||||
plugins: [
|
||||
unzip(),
|
||||
untargz()
|
||||
]
|
||||
};
|
||||
if (decompressOptions) {
|
||||
options = Object.assign(options, decompressOptions)
|
||||
}
|
||||
const files = await decompress(data, targetDir, options);
|
||||
if (files.length === 0) {
|
||||
shell.echo('Error ocurred while decompressing the archive.');
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('<<< Decompressing succeeded.');
|
||||
shell.echo('>>> Decompressing...');
|
||||
let options = {
|
||||
plugins: [unzip(), untargz(), untarbz2()],
|
||||
};
|
||||
if (decompressOptions) {
|
||||
options = Object.assign(options, decompressOptions);
|
||||
}
|
||||
const files = await decompress(data, targetDir, options);
|
||||
if (files.length === 0) {
|
||||
shell.echo('Error ocurred while decompressing the archive.');
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('<<< Decompressing succeeded.');
|
||||
|
||||
if (!fs.existsSync(targetFile)) {
|
||||
shell.echo(`Could not find file: ${targetFile}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`Done: ${targetFile}`);
|
||||
}
|
||||
if (!fs.existsSync(targetFile)) {
|
||||
shell.echo(`Could not find file: ${targetFile}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
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,442 +1,286 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { MAIN_MENU_BAR, MenuContribution, MenuModelRegistry, SelectionService, ILogger } from '@theia/core';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import {
|
||||
ContextMenuRenderer,
|
||||
FrontendApplication, FrontendApplicationContribution,
|
||||
OpenerService, StatusBar, StatusBarAlignment
|
||||
} from '@theia/core/lib/browser';
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import {
|
||||
MAIN_MENU_BAR,
|
||||
MenuContribution,
|
||||
MenuModelRegistry,
|
||||
} from '@theia/core';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
||||
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import {
|
||||
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 URI from '@theia/core/lib/common/uri';
|
||||
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 { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser';
|
||||
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
|
||||
import { FileNavigatorContribution } 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 { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
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 { ElectronWindowPreferences } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
|
||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
||||
import { EditorMode } from './editor-mode';
|
||||
import { ArduinoMenus } from './menu/arduino-menus';
|
||||
import { MonitorConnection } from './monitor/monitor-connection';
|
||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||
import { WorkspaceService } from './theia/workspace/workspace-service';
|
||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { 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';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { SerialPlotterContribution } from './serial/plotter/plotter-frontend-contribution';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFrontendContribution implements FrontendApplicationContribution,
|
||||
TabBarToolbarContribution, CommandContribution, MenuContribution, ColorContribution {
|
||||
export class ArduinoFrontendContribution
|
||||
implements
|
||||
FrontendApplicationContribution,
|
||||
TabBarToolbarContribution,
|
||||
CommandContribution,
|
||||
MenuContribution,
|
||||
ColorContribution
|
||||
{
|
||||
@inject(MessageService)
|
||||
private readonly messageService: MessageService;
|
||||
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
@inject(CommandRegistry)
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
@inject(ElectronWindowPreferences)
|
||||
private readonly electronWindowPreferences: ElectronWindowPreferences;
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
if (!window.navigator.onLine) {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
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.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
protected 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)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
protected invalidConfigPopup: Promise<void | 'No' | 'Yes' | undefined> | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
if (!window.navigator.onLine) {
|
||||
// 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.');
|
||||
onStart(): void {
|
||||
this.electronWindowPreferences.onPreferenceChanged((event) => {
|
||||
if (event.newValue !== event.oldValue) {
|
||||
switch (event.preferenceName) {
|
||||
case 'window.zoomLevel':
|
||||
if (typeof event.newValue === 'number') {
|
||||
const webContents = remote.getCurrentWebContents();
|
||||
webContents.setZoomLevel(event.newValue || 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
const updateStatusBar = ({ selectedBoard, selectedPort }: BoardsConfig.Config) => {
|
||||
this.statusBar.setElement('arduino-selected-board', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: selectedBoard ? `$(microchip) ${selectedBoard.name}` : '$(close) no board selected',
|
||||
className: 'arduino-selected-board'
|
||||
});
|
||||
if (selectedBoard) {
|
||||
this.statusBar.setElement('arduino-selected-port', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: selectedPort ? `on ${Port.toString(selectedPort)}` : '[not connected]',
|
||||
className: 'arduino-selected-port'
|
||||
});
|
||||
}
|
||||
}
|
||||
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
|
||||
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
|
||||
}
|
||||
});
|
||||
this.appStateService.reachedState('ready').then(() =>
|
||||
this.electronWindowPreferences.ready.then(() => {
|
||||
const webContents = remote.getCurrentWebContents();
|
||||
const zoomLevel =
|
||||
this.electronWindowPreferences.get('window.zoomLevel');
|
||||
webContents.setZoomLevel(zoomLevel);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: BoardsToolBarItem.TOOLBAR_ID,
|
||||
render: () => (
|
||||
<BoardsToolBarItem
|
||||
key="boardsToolbarItem"
|
||||
commands={this.commandRegistry}
|
||||
boardsServiceProvider={this.boardsServiceProvider}
|
||||
/>
|
||||
),
|
||||
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({
|
||||
id: 'toggle-serial-monitor',
|
||||
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
|
||||
tooltip: nls.localize('arduino/common/serialMonitor', 'Serial Monitor'),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
for (const command of [
|
||||
EditorCommands.SPLIT_EDITOR_DOWN,
|
||||
EditorCommands.SPLIT_EDITOR_LEFT,
|
||||
EditorCommands.SPLIT_EDITOR_RIGHT,
|
||||
EditorCommands.SPLIT_EDITOR_UP,
|
||||
EditorCommands.SPLIT_EDITOR_VERTICAL,
|
||||
EditorCommands.SPLIT_EDITOR_HORIZONTAL,
|
||||
FileNavigatorCommands.REVEAL_IN_NAVIGATOR,
|
||||
]) {
|
||||
registry.unregisterCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
// Initialize all `pro-mode` widgets. This is a NOOP if in normal mode.
|
||||
for (const viewContribution of [
|
||||
this.fileNavigatorContributions,
|
||||
this.outputContribution,
|
||||
this.outlineContribution,
|
||||
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();
|
||||
webContents.setZoomLevel(event.newValue || 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
const menuId = (menuPath: string[]): string => {
|
||||
const index = menuPath.length - 1;
|
||||
const menuId = menuPath[index];
|
||||
return menuId;
|
||||
};
|
||||
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(TerminalMenus.TERMINAL));
|
||||
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
|
||||
|
||||
protected languageServerFqbn?: string;
|
||||
protected languageServerStartMutex = new Mutex();
|
||||
protected async startLanguageServer(fqbn: string, name: string | undefined): Promise<void> {
|
||||
const release = await this.languageServerStartMutex.acquire();
|
||||
try {
|
||||
await this.hostedPluginSupport.didStart;
|
||||
const details = await this.boardsService.getBoardDetails({ fqbn });
|
||||
if (!details) {
|
||||
// Core is not installed for the selected board.
|
||||
console.info(`Could not start language server for ${fqbn}. The core is not installed for the board.`);
|
||||
if (this.languageServerFqbn) {
|
||||
try {
|
||||
await this.commandRegistry.executeCommand('arduino.languageserver.stop');
|
||||
console.info(`Stopped language server process for ${this.languageServerFqbn}.`);
|
||||
this.languageServerFqbn = undefined;
|
||||
} catch (e) {
|
||||
console.error(`Failed to start language server process for ${this.languageServerFqbn}`, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (fqbn === this.languageServerFqbn) {
|
||||
// NOOP
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Starting language server: ${fqbn}`);
|
||||
const log = this.arduinoPreferences.get('arduino.language.log');
|
||||
let currentSketchPath: string | undefined = undefined;
|
||||
if (log) {
|
||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||
if (currentSketch) {
|
||||
currentSketchPath = await this.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) {
|
||||
console.log(`Failed to start language server for ${fqbn}`, e);
|
||||
this.languageServerFqbn = undefined;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.SKETCH,
|
||||
nls.localize('arduino/menu/sketch', 'Sketch')
|
||||
);
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.TOOLS,
|
||||
nls.localize('arduino/menu/tools', 'Tools')
|
||||
);
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: BoardsToolBarItem.TOOLBAR_ID,
|
||||
render: () => <BoardsToolBarItem
|
||||
key='boardsToolbarItem'
|
||||
commands={this.commandRegistry}
|
||||
boardsServiceClient={this.boardsServiceClientImpl} />,
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
priority: 7
|
||||
});
|
||||
registry.registerItem({
|
||||
id: 'toggle-serial-monitor',
|
||||
command: MonitorViewContribution.TOGGLE_SERIAL_MONITOR_TOOLBAR,
|
||||
tooltip: 'Serial Monitor'
|
||||
});
|
||||
}
|
||||
registerColors(colors: ColorRegistry): void {
|
||||
colors.register(
|
||||
{
|
||||
id: 'arduino.toolbar.button.background',
|
||||
defaults: {
|
||||
dark: 'button.background',
|
||||
light: 'button.background',
|
||||
hc: 'activityBar.inactiveForeground',
|
||||
},
|
||||
description:
|
||||
'Background color of the toolbar items. Such as Upload, Verify, etc.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.button.hoverBackground',
|
||||
defaults: {
|
||||
dark: 'button.hoverBackground',
|
||||
light: 'button.hoverBackground',
|
||||
hc: 'button.background',
|
||||
},
|
||||
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',
|
||||
defaults: {
|
||||
dark: 'editor.selectionBackground',
|
||||
light: 'editor.selectionBackground',
|
||||
hc: 'textPreformat.foreground',
|
||||
},
|
||||
description:
|
||||
'Toggle color of the toolbar items when they are currently toggled (the command is in progress)',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.dropdown.border',
|
||||
defaults: {
|
||||
dark: 'dropdown.border',
|
||||
light: 'dropdown.border',
|
||||
hc: 'dropdown.border',
|
||||
},
|
||||
description: 'Border color of the Board Selector.',
|
||||
},
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
|
||||
execute: () => this.editorMode.toggleCompileForDebug(),
|
||||
isToggled: () => this.editorMode.compileForDebug
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.OPEN_SKETCH_FILES, {
|
||||
execute: async (uri: URI) => {
|
||||
this.openSketchFiles(uri);
|
||||
}
|
||||
});
|
||||
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
|
||||
execute: async (query?: string | undefined) => {
|
||||
const boardsConfig = await this.boardsConfigDialog.open(query);
|
||||
if (boardsConfig) {
|
||||
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
{
|
||||
id: 'arduino.toolbar.dropdown.borderActive',
|
||||
defaults: {
|
||||
dark: 'focusBorder',
|
||||
light: 'focusBorder',
|
||||
hc: 'focusBorder',
|
||||
},
|
||||
description: "Border color of the Board Selector when it's active",
|
||||
},
|
||||
|
||||
registerMenus(registry: MenuModelRegistry) {
|
||||
const menuId = (menuPath: string[]): string => {
|
||||
const index = menuPath.length - 1;
|
||||
const menuId = menuPath[index];
|
||||
return menuId;
|
||||
}
|
||||
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(TerminalMenus.TERMINAL));
|
||||
registry.getMenu(MAIN_MENU_BAR).removeNode(menuId(CommonMenus.VIEW));
|
||||
|
||||
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
|
||||
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG.id,
|
||||
label: 'Optimize for Debugging',
|
||||
order: '4'
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
colors.register(
|
||||
{
|
||||
id: 'arduino.branding.primary',
|
||||
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: {
|
||||
dark: 'button.background',
|
||||
light: 'button.background',
|
||||
hc: 'activityBar.inactiveForeground'
|
||||
},
|
||||
description: 'Background color of the toolbar items. Such as Upload, Verify, etc.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.hoverBackground',
|
||||
defaults: {
|
||||
dark: 'button.hoverBackground',
|
||||
light: 'button.hoverBackground',
|
||||
hc: 'activityBar.inactiveForeground'
|
||||
},
|
||||
description: 'Background color of the toolbar items when hovering over them. Such as Upload, Verify, etc.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.foreground',
|
||||
defaults: {
|
||||
dark: 'editor.foreground',
|
||||
light: 'editor.foreground',
|
||||
hc: 'editor.foreground'
|
||||
},
|
||||
description: 'Color of the text in the Output view.'
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.background',
|
||||
defaults: {
|
||||
dark: 'editor.background',
|
||||
light: 'editor.background',
|
||||
hc: 'editor.background'
|
||||
},
|
||||
description: 'Background color of the Output view.'
|
||||
}
|
||||
);
|
||||
}
|
||||
{
|
||||
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.toolbar.dropdown.iconSelected',
|
||||
defaults: {
|
||||
dark: 'list.activeSelectionIconForeground',
|
||||
light: 'list.activeSelectionIconForeground',
|
||||
hc: 'list.activeSelectionIconForeground',
|
||||
},
|
||||
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 { createPreferenceProxy, PreferenceProxy, PreferenceService, PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser/preferences';
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
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';
|
||||
|
||||
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 = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'arduino.language.log': {
|
||||
'type': 'boolean',
|
||||
'description': "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
|
||||
'default': false
|
||||
},
|
||||
'arduino.compile.verbose': {
|
||||
'type': 'boolean',
|
||||
'description': 'True for verbose compile output. False by default',
|
||||
'default': false
|
||||
},
|
||||
'arduino.compile.warnings': {
|
||||
'enum': [...CompilerWarningLiterals],
|
||||
'description': "Tells gcc which warning level to use. It's 'None' by default",
|
||||
'default': 'None'
|
||||
},
|
||||
'arduino.upload.verbose': {
|
||||
'type': 'boolean',
|
||||
'description': 'True for verbose upload output. False by default.',
|
||||
'default': false
|
||||
},
|
||||
'arduino.upload.verify': {
|
||||
'type': 'boolean',
|
||||
'default': false
|
||||
},
|
||||
'arduino.window.autoScale': {
|
||||
'type': 'boolean',
|
||||
'description': 'True if the user interface automatically scales with the font size.',
|
||||
'default': true
|
||||
},
|
||||
'arduino.window.zoomLevel': {
|
||||
'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.',
|
||||
'default': 0
|
||||
},
|
||||
'arduino.ide.autoUpdate': {
|
||||
'type': 'boolean',
|
||||
'description': 'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
|
||||
'default': true
|
||||
}
|
||||
}
|
||||
type: 'object',
|
||||
properties: {
|
||||
'arduino.language.log': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'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': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'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': {
|
||||
enum: [...CompilerWarningLiterals],
|
||||
description: nls.localize(
|
||||
'arduino/preferences/compile.warnings',
|
||||
"Tells gcc which warning level to use. It's 'None' by default"
|
||||
),
|
||||
default: 'None',
|
||||
},
|
||||
'arduino.upload.verbose': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/upload.verbose',
|
||||
'True for verbose upload output. False by default.'
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
'arduino.upload.verify': {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
'arduino.window.autoScale': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/window.autoScale',
|
||||
'True if the user interface automatically scales with the font size.'
|
||||
),
|
||||
default: true,
|
||||
},
|
||||
'arduino.window.zoomLevel': {
|
||||
type: 'number',
|
||||
description: '',
|
||||
default: 0,
|
||||
deprecationMessage: nls.localize(
|
||||
'arduino/preferences/window.zoomLevel/deprecationMessage',
|
||||
"Deprecated. Use 'window.zoomLevel' instead."
|
||||
),
|
||||
},
|
||||
'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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface ArduinoConfiguration {
|
||||
'arduino.language.log': boolean;
|
||||
'arduino.compile.verbose': boolean;
|
||||
'arduino.compile.warnings': CompilerWarnings;
|
||||
'arduino.upload.verbose': boolean;
|
||||
'arduino.upload.verify': boolean;
|
||||
'arduino.window.autoScale': boolean;
|
||||
'arduino.window.zoomLevel': number;
|
||||
'arduino.ide.autoUpdate': boolean;
|
||||
'arduino.language.log': boolean;
|
||||
'arduino.language.realTimeDiagnostics': boolean;
|
||||
'arduino.compile.verbose': boolean;
|
||||
'arduino.compile.experimental': boolean;
|
||||
'arduino.compile.revealRange': ErrorRevealStrategy;
|
||||
'arduino.compile.warnings': CompilerWarnings;
|
||||
'arduino.upload.verbose': boolean;
|
||||
'arduino.upload.verify': boolean;
|
||||
'arduino.window.autoScale': 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 type ArduinoPreferences = PreferenceProxy<ArduinoConfiguration>;
|
||||
|
||||
export function createArduinoPreferences(preferences: PreferenceService): ArduinoPreferences {
|
||||
return createPreferenceProxy(preferences, ArduinoConfigSchema);
|
||||
}
|
||||
|
||||
export function bindArduinoPreferences(bind: interfaces.Bind): void {
|
||||
bind(ArduinoPreferences).toDynamicValue(ctx => {
|
||||
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
||||
return createArduinoPreferences(preferences);
|
||||
});
|
||||
bind(PreferenceContribution).toConstantValue({ schema: ArduinoConfigSchema });
|
||||
bind(ArduinoPreferences).toDynamicValue((ctx) => {
|
||||
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
||||
return createPreferenceProxy(preferences, ArduinoConfigSchema);
|
||||
});
|
||||
bind(PreferenceContribution).toConstantValue({
|
||||
schema: ArduinoConfigSchema,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { toUnix } from 'upath';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { isWindows } from '@theia/core/lib/common/os';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
|
||||
/**
|
||||
* Class for determining the default workspace location from the
|
||||
* `location.hash`, the historical workspace locations, and recent sketch files.
|
||||
*
|
||||
* The following logic is used for determining the default workspace location:
|
||||
* - `hash` points to an existing location?
|
||||
* - Yes
|
||||
* - `validate location`. Is valid sketch location?
|
||||
* - Yes
|
||||
* - Done.
|
||||
* - No
|
||||
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
|
||||
* - No
|
||||
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
|
||||
*/
|
||||
namespace ArduinoWorkspaceRootResolver {
|
||||
export interface InitOptions {
|
||||
readonly isValid: (uri: string) => MaybePromise<boolean>;
|
||||
}
|
||||
export interface ResolveOptions {
|
||||
readonly hash?: string
|
||||
readonly recentWorkspaces: string[];
|
||||
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
|
||||
readonly recentSketches: string[];
|
||||
}
|
||||
}
|
||||
export class ArduinoWorkspaceRootResolver {
|
||||
|
||||
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {
|
||||
}
|
||||
|
||||
async resolve(options: ArduinoWorkspaceRootResolver.ResolveOptions): Promise<{ uri: string } | undefined> {
|
||||
const { hash, recentWorkspaces, recentSketches } = options;
|
||||
for (const uri of [this.hashToUri(hash), ...recentWorkspaces, ...recentSketches].filter(notEmpty)) {
|
||||
const valid = await this.isValid(uri);
|
||||
if (valid) {
|
||||
return { uri };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected isValid(uri: string): MaybePromise<boolean> {
|
||||
return this.options.isValid(uri);
|
||||
}
|
||||
|
||||
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
|
||||
// This is important for Windows only and a NOOP on POSIX.
|
||||
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
|
||||
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
|
||||
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
|
||||
protected hashToUri(hash: string | undefined): string | undefined {
|
||||
if (hash
|
||||
&& hash.length > 1
|
||||
&& hash.startsWith('#')) {
|
||||
const path = hash.slice(1); // Trim the leading `#`.
|
||||
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
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(),
|
||||
isEnabled: () => !this._session,
|
||||
isVisible: () => !this._session,
|
||||
});
|
||||
registry.registerCommand(CloudUserCommands.LOGOUT, {
|
||||
execute: () => this.service.logout(),
|
||||
isEnabled: () => !!this._session,
|
||||
isVisible: () => !!this._session,
|
||||
});
|
||||
}
|
||||
|
||||
notifySessionDidChange(session: AuthenticationSession | undefined): void {
|
||||
this.updateSession(session);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
|
||||
export const LEARN_MORE_URL =
|
||||
'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync';
|
||||
|
||||
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'
|
||||
);
|
||||
}
|
||||
@@ -1,62 +1,281 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
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 { Installable, ResponseServiceClient } from '../../common/protocol';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
|
||||
import { InstallationProgressDialog } from '../widgets/progress-dialog';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
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
|
||||
* 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()
|
||||
export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsListWidgetFrontendContribution)
|
||||
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||
@inject(ResponseServiceClient)
|
||||
protected readonly responseService: ResponseServiceClient;
|
||||
|
||||
onStart(): void {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this));
|
||||
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
|
||||
}
|
||||
@inject(BoardsListWidgetFrontendContribution)
|
||||
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||
|
||||
protected ensureCoreExists(config: BoardsConfig.Config): void {
|
||||
const { selectedBoard } = config;
|
||||
if (selectedBoard) {
|
||||
this.boardsService.search({}).then(packages => {
|
||||
const candidates = packages
|
||||
.filter(pkg => BoardsPackage.contains(selectedBoard, pkg))
|
||||
.filter(({ installable, installedVersion }) => installable && !installedVersion);
|
||||
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') {
|
||||
const dialog = new InstallationProgressDialog(candidate.name, candidate.availableVersions[0]);
|
||||
dialog.open();
|
||||
try {
|
||||
await this.boardsService.install({ item: candidate });
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
if (answer) {
|
||||
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
// 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 {
|
||||
const setEventListeners = () => {
|
||||
this.boardsServiceClient.onBoardsConfigChanged((config) => {
|
||||
const { selectedBoard, selectedPort } = config;
|
||||
|
||||
const boardWasUnplugged =
|
||||
!selectedPort && this.portSelectedOnLastRefusal;
|
||||
|
||||
this.clearLastRefusedPromptInfo();
|
||||
|
||||
if (
|
||||
boardWasUnplugged ||
|
||||
!selectedBoard ||
|
||||
this.promptAlreadyShowingForBoard(selectedBoard)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureCoreExists(selectedBoard, selectedPort);
|
||||
});
|
||||
|
||||
// we "clearRefusedPackageInfo" if a "refused" package is eventually
|
||||
// installed, though this is not strictly necessary. It's more of a
|
||||
// cleanup, to ensure the related variables are representative of
|
||||
// current state.
|
||||
this.notificationCenter.onPlatformDidInstall((installed) => {
|
||||
if (this.lastRefusedPackageId === installed.item.id) {
|
||||
this.clearLastRefusedPromptInfo();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// we should invoke this.ensureCoreExists only once we're sure
|
||||
// everything has been reconciled
|
||||
this.boardsServiceClient.reconciled.then(() => {
|
||||
const { selectedBoard, selectedPort } =
|
||||
this.boardsServiceClient.boardsConfig;
|
||||
|
||||
if (selectedBoard) {
|
||||
this.ensureCoreExists(selectedBoard, selectedPort);
|
||||
}
|
||||
|
||||
setEventListeners();
|
||||
});
|
||||
}
|
||||
|
||||
private removeNotificationByBoard(selectedBoard: Board): void {
|
||||
const index = this.notifications.findIndex((notification) =>
|
||||
Board.sameAs(notification, selectedBoard)
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.notifications.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private clearLastRefusedPromptInfo(): void {
|
||||
this.lastRefusedPackageId = undefined;
|
||||
this.portSelectedOnLastRefusal = undefined;
|
||||
}
|
||||
|
||||
private setLastRefusedPromptInfo(
|
||||
packageId: string,
|
||||
selectedPort?: Port
|
||||
): void {
|
||||
this.lastRefusedPackageId = packageId;
|
||||
this.portSelectedOnLastRefusal = selectedPort;
|
||||
}
|
||||
|
||||
private promptAlreadyShowingForBoard(board: Board): boolean {
|
||||
return Boolean(
|
||||
this.notifications.find((notification) =>
|
||||
Board.sameAs(notification, board)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected ensureCoreExists(selectedBoard: Board, selectedPort?: Port): void {
|
||||
this.notifications.push(selectedBoard);
|
||||
this.boardsService.search({}).then((packages) => {
|
||||
const candidate = this.getInstallCandidate(packages, selectedBoard);
|
||||
|
||||
if (candidate) {
|
||||
this.showAutoInstallPrompt(candidate, selectedBoard, selectedPort);
|
||||
} else {
|
||||
this.removeNotificationByBoard(selectedBoard);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getInstallCandidate(
|
||||
packages: BoardsPackage[],
|
||||
selectedBoard: Board
|
||||
): BoardsPackage | undefined {
|
||||
// filter packagesForBoard selecting matches from the cli (installed packages)
|
||||
// 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 { injectable, inject } from 'inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ReactWidget, Message } from '@theia/core/lib/browser';
|
||||
import { BoardsService } from '../../common/protocol/boards-service';
|
||||
@@ -9,60 +9,63 @@ import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialogWidget extends ReactWidget {
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
|
||||
protected readonly onBoardConfigChangedEmitter =
|
||||
new Emitter<BoardsConfig.Config>();
|
||||
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
|
||||
|
||||
protected readonly onFilterTextDidChangeEmitter = new Emitter<string>();
|
||||
protected readonly onBoardConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
|
||||
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
|
||||
protected focusNode: HTMLElement | undefined;
|
||||
|
||||
protected focusNode: HTMLElement | undefined;
|
||||
constructor() {
|
||||
super();
|
||||
this.id = 'select-board-dialog';
|
||||
this.toDispose.pushAll([
|
||||
this.onBoardConfigChangedEmitter,
|
||||
this.onFilterTextDidChangeEmitter,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = 'select-board-dialog';
|
||||
this.toDispose.pushAll([
|
||||
this.onBoardConfigChangedEmitter,
|
||||
this.onFilterTextDidChangeEmitter
|
||||
]);
|
||||
search(query: string): void {
|
||||
this.onFilterTextDidChangeEmitter.fire(query);
|
||||
}
|
||||
|
||||
protected fireConfigChanged = (config: BoardsConfig.Config) => {
|
||||
this.onBoardConfigChangedEmitter.fire(config);
|
||||
};
|
||||
|
||||
protected setFocusNode = (element: HTMLElement | undefined) => {
|
||||
this.focusNode = element;
|
||||
};
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return (
|
||||
<div className="selectBoardContainer">
|
||||
<BoardsConfig
|
||||
boardsServiceProvider={this.boardsServiceClient}
|
||||
notificationCenter={this.notificationCenter}
|
||||
onConfigChange={this.fireConfigChanged}
|
||||
onFocusNodeSet={this.setFocusNode}
|
||||
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event}
|
||||
onAppStateDidChange={this.notificationCenter.onAppStateDidChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
if (this.focusNode instanceof HTMLInputElement) {
|
||||
this.focusNode.select();
|
||||
}
|
||||
|
||||
search(query: string): void {
|
||||
this.onFilterTextDidChangeEmitter.fire(query);
|
||||
}
|
||||
|
||||
protected fireConfigChanged = (config: BoardsConfig.Config) => {
|
||||
this.onBoardConfigChangedEmitter.fire(config);
|
||||
}
|
||||
|
||||
protected setFocusNode = (element: HTMLElement | undefined) => {
|
||||
this.focusNode = element;
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return <div className='selectBoardContainer'>
|
||||
<BoardsConfig
|
||||
boardsServiceProvider={this.boardsServiceClient}
|
||||
notificationCenter={this.notificationCenter}
|
||||
onConfigChange={this.fireConfigChanged}
|
||||
onFocusNodeSet={this.setFocusNode}
|
||||
onFilteredTextDidChangeEvent={this.onFilterTextDidChangeEmitter.event} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
if (this.focusNode instanceof HTMLInputElement) {
|
||||
this.focusNode.select();
|
||||
}
|
||||
(this.focusNode || this.node).focus();
|
||||
}
|
||||
|
||||
(this.focusNode || this.node).focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +1,142 @@
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
|
||||
import {
|
||||
injectable,
|
||||
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 { BoardsService } from '../../common/protocol/boards-service';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialogProps extends DialogProps {
|
||||
}
|
||||
export class BoardsConfigDialogProps extends DialogProps {}
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
@inject(BoardsConfigDialogWidget)
|
||||
protected readonly widget: BoardsConfigDialogWidget;
|
||||
|
||||
@inject(BoardsConfigDialogWidget)
|
||||
protected readonly widget: BoardsConfigDialogWidget;
|
||||
@inject(BoardsService)
|
||||
protected readonly boardService: BoardsService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardService: BoardsService;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
protected config: BoardsConfig.Config = {};
|
||||
|
||||
protected config: BoardsConfig.Config = {};
|
||||
constructor(
|
||||
@inject(BoardsConfigDialogProps)
|
||||
protected override readonly props: BoardsConfigDialogProps
|
||||
) {
|
||||
super({ ...props, maxWidth: 500 });
|
||||
|
||||
constructor(@inject(BoardsConfigDialogProps) protected readonly props: BoardsConfigDialogProps) {
|
||||
super(props);
|
||||
this.node.id = 'select-board-dialog-container';
|
||||
this.contentNode.classList.add('select-board-dialog');
|
||||
this.contentNode.appendChild(this.createDescription());
|
||||
|
||||
this.contentNode.classList.add('select-board-dialog');
|
||||
this.contentNode.appendChild(this.createDescription());
|
||||
this.appendCloseButton(
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel')
|
||||
);
|
||||
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
|
||||
}
|
||||
|
||||
this.appendCloseButton('CANCEL');
|
||||
this.appendAcceptButton('OK');
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.push(this.boardsServiceClient.onBoardsConfigChanged(config => {
|
||||
this.config = config;
|
||||
this.update();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
if (typeof query === 'string') {
|
||||
this.widget.search(query);
|
||||
}
|
||||
return super.open();
|
||||
}
|
||||
|
||||
protected createDescription(): HTMLElement {
|
||||
const head = document.createElement('div');
|
||||
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');
|
||||
text.classList.add('text');
|
||||
head.appendChild(text);
|
||||
|
||||
for (const paragraph of [
|
||||
'Select both a Board and a Port if you want to upload a sketch.',
|
||||
'If you only select a Board you will be able just to compile, but not to upload your sketch.'
|
||||
]) {
|
||||
const p = document.createElement('p');
|
||||
p.textContent = paragraph;
|
||||
text.appendChild(p);
|
||||
}
|
||||
|
||||
return head;
|
||||
}
|
||||
|
||||
protected onAfterAttach(msg: Message): void {
|
||||
if (this.widget.isAttached) {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
this.toDisposeOnDetach.push(this.widget.onBoardConfigChanged(config => {
|
||||
this.config = config;
|
||||
this.update();
|
||||
}));
|
||||
super.onAfterAttach(msg);
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.push(
|
||||
this.boardsServiceClient.onBoardsConfigChanged((config) => {
|
||||
this.config = config;
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass in an empty string if you want to reset the search term. Using `undefined` has no effect.
|
||||
*/
|
||||
override async open(
|
||||
query: string | undefined = undefined
|
||||
): Promise<BoardsConfig.Config | undefined> {
|
||||
if (typeof query === 'string') {
|
||||
this.widget.search(query);
|
||||
}
|
||||
return super.open();
|
||||
}
|
||||
|
||||
protected createDescription(): HTMLElement {
|
||||
const head = document.createElement('div');
|
||||
head.classList.add('head');
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.classList.add('text');
|
||||
head.appendChild(text);
|
||||
|
||||
for (const paragraph of [
|
||||
nls.localize(
|
||||
'arduino/board/configDialog1',
|
||||
'Select both a Board and a Port if you want to upload a sketch.'
|
||||
),
|
||||
nls.localize(
|
||||
'arduino/board/configDialog2',
|
||||
'If you only select a Board you will be able to compile, but not to upload your sketch.'
|
||||
),
|
||||
]) {
|
||||
const p = document.createElement('div');
|
||||
p.textContent = paragraph;
|
||||
text.appendChild(p);
|
||||
}
|
||||
|
||||
protected onUpdateRequest(msg: Message) {
|
||||
super.onUpdateRequest(msg);
|
||||
this.widget.update();
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.widget.activate();
|
||||
protected override onAfterAttach(msg: Message): void {
|
||||
if (this.widget.isAttached) {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
this.toDisposeOnDetach.push(
|
||||
this.widget.onBoardConfigChanged((config) => {
|
||||
this.config = config;
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected handleEnter(event: KeyboardEvent): boolean | void {
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
return false;
|
||||
}
|
||||
protected override onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.widget.update();
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.widget.activate();
|
||||
}
|
||||
|
||||
protected override handleEnter(event: KeyboardEvent): boolean | void {
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected isValid(value: BoardsConfig.Config): DialogError {
|
||||
if (!value.selectedBoard) {
|
||||
if (value.selectedPort) {
|
||||
return 'Please pick a board connected to the port you have selected.';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
get value(): BoardsConfig.Config {
|
||||
return this.config;
|
||||
protected override isValid(value: BoardsConfig.Config): DialogError {
|
||||
if (!value.selectedBoard) {
|
||||
if (value.selectedPort) {
|
||||
return nls.localize(
|
||||
'arduino/board/pleasePickBoard',
|
||||
'Please pick a board connected to the port you have selected.'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
get value(): BoardsConfig.Config {
|
||||
return this.config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,286 +1,432 @@
|
||||
import * as React from 'react';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { Event } from '@theia/core/lib/common/event';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Board, Port, AttachedBoardsChangeEvent, BoardWithPackage } from '../../common/protocol/boards-service';
|
||||
import {
|
||||
Board,
|
||||
Port,
|
||||
BoardConfig as ProtocolBoardConfig,
|
||||
BoardWithPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
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 type Config = ProtocolBoardConfig;
|
||||
|
||||
export interface Config {
|
||||
selectedBoard?: Board;
|
||||
selectedPort?: Port;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
readonly notificationCenter: NotificationCenter;
|
||||
readonly onConfigChange: (config: Config) => void;
|
||||
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
|
||||
readonly onFilteredTextDidChangeEvent: Event<string>;
|
||||
}
|
||||
|
||||
export interface State extends Config {
|
||||
searchResults: Array<BoardWithPackage>;
|
||||
knownPorts: Port[];
|
||||
showAllPorts: boolean;
|
||||
query: string;
|
||||
}
|
||||
export interface Props {
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
readonly notificationCenter: NotificationCenter;
|
||||
readonly onConfigChange: (config: Config) => void;
|
||||
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
|
||||
readonly onFilteredTextDidChangeEvent: Event<string>;
|
||||
readonly onAppStateDidChange: Event<FrontendApplicationState>;
|
||||
}
|
||||
|
||||
export interface State extends Config {
|
||||
searchResults: Array<BoardWithPackage>;
|
||||
knownPorts: Port[];
|
||||
showAllPorts: boolean;
|
||||
query: string;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Item<T> extends React.Component<{
|
||||
item: T,
|
||||
label: string,
|
||||
selected: boolean,
|
||||
onClick: (item: T) => void,
|
||||
missing?: boolean,
|
||||
details?: string
|
||||
item: T;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
onClick: (item: T) => void;
|
||||
missing?: boolean;
|
||||
details?: string;
|
||||
}> {
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { selected, label, missing, details } = this.props;
|
||||
const classNames = ['item'];
|
||||
if (selected) {
|
||||
classNames.push('selected');
|
||||
}
|
||||
if (missing === true) {
|
||||
classNames.push('missing')
|
||||
}
|
||||
return <div 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>;
|
||||
override render(): React.ReactNode {
|
||||
const { selected, label, missing, details } = this.props;
|
||||
const classNames = ['item'];
|
||||
if (selected) {
|
||||
classNames.push('selected');
|
||||
}
|
||||
|
||||
protected onClick = () => {
|
||||
this.props.onClick(this.props.item);
|
||||
if (missing === true) {
|
||||
classNames.push('missing');
|
||||
}
|
||||
return (
|
||||
<div
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
protected onClick = () => {
|
||||
this.props.onClick(this.props.item);
|
||||
};
|
||||
}
|
||||
|
||||
export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConfig.State> {
|
||||
export class BoardsConfig extends React.Component<
|
||||
BoardsConfig.Props,
|
||||
BoardsConfig.State
|
||||
> {
|
||||
protected toDispose = new DisposableCollection();
|
||||
|
||||
protected toDispose = new DisposableCollection();
|
||||
constructor(props: BoardsConfig.Props) {
|
||||
super(props);
|
||||
|
||||
constructor(props: BoardsConfig.Props) {
|
||||
super(props);
|
||||
const { boardsConfig } = props.boardsServiceProvider;
|
||||
this.state = {
|
||||
searchResults: [],
|
||||
knownPorts: [],
|
||||
showAllPorts: false,
|
||||
query: '',
|
||||
...boardsConfig,
|
||||
};
|
||||
}
|
||||
|
||||
const { boardsConfig } = props.boardsServiceProvider;
|
||||
this.state = {
|
||||
searchResults: [],
|
||||
knownPorts: [],
|
||||
showAllPorts: false,
|
||||
query: '',
|
||||
...boardsConfig
|
||||
override componentDidMount(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.props.onAppStateDidChange((state) => {
|
||||
if (state === 'ready') {
|
||||
this.updateBoards();
|
||||
this.updatePorts(
|
||||
this.props.boardsServiceProvider.availableBoards
|
||||
.map(({ port }) => port)
|
||||
.filter(notEmpty)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateBoards();
|
||||
this.updatePorts(this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty));
|
||||
this.toDispose.pushAll([
|
||||
this.props.notificationCenter.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
|
||||
this.props.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
|
||||
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
|
||||
}),
|
||||
this.props.notificationCenter.onPlatformInstalled(() => this.updateBoards(this.state.query)),
|
||||
this.props.notificationCenter.onPlatformUninstalled(() => this.updateBoards(this.state.query)),
|
||||
this.props.notificationCenter.onIndexUpdated(() => this.updateBoards(this.state.query)),
|
||||
this.props.notificationCenter.onDaemonStarted(() => this.updateBoards(this.state.query)),
|
||||
this.props.notificationCenter.onDaemonStopped(() => this.setState({ searchResults: [] })),
|
||||
this.props.onFilteredTextDidChangeEvent(query => this.setState({ query }, () => this.updateBoards(this.state.query)))
|
||||
]);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected fireConfigChanged() {
|
||||
const { selectedBoard, selectedPort } = this.state;
|
||||
this.props.onConfigChange({ selectedBoard, selectedPort });
|
||||
}
|
||||
|
||||
protected updateBoards = (eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = '') => {
|
||||
const query = typeof eventOrQuery === 'string'
|
||||
? eventOrQuery
|
||||
: eventOrQuery.target.value.toLowerCase();
|
||||
this.setState({ query });
|
||||
this.queryBoards({ query }).then(searchResults => this.setState({ searchResults }));
|
||||
}
|
||||
|
||||
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
|
||||
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
|
||||
let { selectedPort } = this.state;
|
||||
// If the currently selected port is not available anymore, unset the selected port.
|
||||
if (removedPorts.some(port => Port.equals(port, selectedPort))) {
|
||||
selectedPort = undefined;
|
||||
}
|
||||
this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged());
|
||||
});
|
||||
}
|
||||
|
||||
protected queryBoards = (options: { query?: string } = {}): Promise<Array<BoardWithPackage>> => {
|
||||
return this.props.boardsServiceProvider.searchBoards(options);
|
||||
}
|
||||
|
||||
protected get availablePorts(): MaybePromise<Port[]> {
|
||||
return this.props.boardsServiceProvider.availableBoards.map(({ port }) => port).filter(notEmpty);
|
||||
}
|
||||
|
||||
protected queryPorts = async (availablePorts: MaybePromise<Port[]> = this.availablePorts) => {
|
||||
const ports = await availablePorts;
|
||||
return { knownPorts: ports.sort(Port.compare) };
|
||||
}
|
||||
|
||||
protected toggleFilterPorts = () => {
|
||||
this.setState({ showAllPorts: !this.state.showAllPorts });
|
||||
}
|
||||
|
||||
protected selectPort = (selectedPort: Port | undefined) => {
|
||||
this.setState({ selectedPort }, () => this.fireConfigChanged());
|
||||
}
|
||||
|
||||
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
|
||||
this.setState({ selectedBoard }, () => this.fireConfigChanged());
|
||||
}
|
||||
|
||||
protected focusNodeSet = (element: HTMLElement | null) => {
|
||||
this.props.onFocusNodeSet(element || undefined);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return <div className='body'>
|
||||
{this.renderContainer('boards', this.renderBoards.bind(this))}
|
||||
{this.renderContainer('ports', this.renderPorts.bind(this), this.renderPortsFooter.bind(this))}
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected renderContainer(title: string, contentRenderer: () => React.ReactNode, footerRenderer?: () => React.ReactNode): React.ReactNode {
|
||||
return <div className='container'>
|
||||
<div className='content'>
|
||||
<div className='title'>
|
||||
{title}
|
||||
</div>
|
||||
{contentRenderer()}
|
||||
<div className='footer'>
|
||||
{(footerRenderer ? footerRenderer() : '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
protected renderBoards(): React.ReactNode {
|
||||
const { selectedBoard, searchResults, query } = this.state;
|
||||
// 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.
|
||||
const distinctBoards = new Map<string, Board.Detailed>();
|
||||
const toKey = ({ name, packageName, fqbn }: Board.Detailed) => !!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
|
||||
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
|
||||
const key = toKey(board);
|
||||
if (!distinctBoards.has(key)) {
|
||||
distinctBoards.set(key, board);
|
||||
}
|
||||
}),
|
||||
this.props.boardsServiceProvider.onAvailablePortsChanged(
|
||||
({ newState, oldState }) => {
|
||||
const removedPorts = oldState.filter(
|
||||
(oldPort) =>
|
||||
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
|
||||
);
|
||||
this.updatePorts(newState, removedPorts);
|
||||
}
|
||||
),
|
||||
this.props.boardsServiceProvider.onBoardsConfigChanged(
|
||||
({ selectedBoard, selectedPort }) => {
|
||||
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))
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<div className='search'>
|
||||
<input
|
||||
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}
|
||||
label={board.name}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={board.missing}
|
||||
/>)}
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
override componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected fireConfigChanged(): void {
|
||||
const { selectedBoard, selectedPort } = this.state;
|
||||
this.props.onConfigChange({ selectedBoard, selectedPort });
|
||||
}
|
||||
|
||||
protected updateBoards = (
|
||||
eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = ''
|
||||
) => {
|
||||
const query =
|
||||
typeof eventOrQuery === 'string'
|
||||
? eventOrQuery
|
||||
: eventOrQuery.target.value.toLowerCase();
|
||||
this.setState({ query });
|
||||
this.queryBoards({ query }).then((searchResults) =>
|
||||
this.setState({ searchResults })
|
||||
);
|
||||
};
|
||||
|
||||
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
|
||||
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
|
||||
let { selectedPort } = this.state;
|
||||
// If the currently selected port is not available anymore, unset the selected port.
|
||||
if (removedPorts.some((port) => Port.sameAs(port, selectedPort))) {
|
||||
selectedPort = undefined;
|
||||
}
|
||||
this.setState({ knownPorts, selectedPort }, () =>
|
||||
this.fireConfigChanged()
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
protected queryBoards = (
|
||||
options: { query?: string } = {}
|
||||
): Promise<Array<BoardWithPackage>> => {
|
||||
return this.props.boardsServiceProvider.searchBoards(options);
|
||||
};
|
||||
|
||||
protected get availablePorts(): MaybePromise<Port[]> {
|
||||
return this.props.boardsServiceProvider.availableBoards
|
||||
.map(({ port }) => port)
|
||||
.filter(notEmpty);
|
||||
}
|
||||
|
||||
protected get availableBoards(): AvailableBoard[] {
|
||||
return this.props.boardsServiceProvider.availableBoards;
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
this.setState({ showAllPorts: !this.state.showAllPorts });
|
||||
};
|
||||
|
||||
protected selectPort = (selectedPort: Port | undefined) => {
|
||||
this.setState({ selectedPort }, () => this.fireConfigChanged());
|
||||
};
|
||||
|
||||
protected selectBoard = (selectedBoard: BoardWithPackage | undefined) => {
|
||||
this.setState({ selectedBoard }, () => this.fireConfigChanged());
|
||||
};
|
||||
|
||||
protected focusNodeSet = (element: HTMLElement | null) => {
|
||||
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)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
protected renderContainer(
|
||||
title: string,
|
||||
contentRenderer: () => React.ReactNode,
|
||||
footerRenderer?: () => React.ReactNode
|
||||
): React.ReactNode {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="content">
|
||||
<div className="title">{title}</div>
|
||||
{contentRenderer()}
|
||||
<div className="footer">{footerRenderer ? footerRenderer() : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected renderBoards(): React.ReactNode {
|
||||
const { selectedBoard, searchResults, query } = this.state;
|
||||
// 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.
|
||||
const distinctBoards = new Map<string, Board.Detailed>();
|
||||
const toKey = ({ name, packageName, fqbn }: Board.Detailed) =>
|
||||
!!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`;
|
||||
for (const board of Board.decorateBoards(selectedBoard, searchResults)) {
|
||||
const key = toKey(board);
|
||||
if (!distinctBoards.has(key)) {
|
||||
distinctBoards.set(key, board);
|
||||
}
|
||||
}
|
||||
|
||||
protected renderPorts(): React.ReactNode {
|
||||
const filter = this.state.showAllPorts ? () => true : Port.isBoardPort;
|
||||
const ports = this.state.knownPorts.filter(filter);
|
||||
return !ports.length ?
|
||||
(
|
||||
<div className='loading noselect'>
|
||||
No ports discovered
|
||||
</div>
|
||||
) :
|
||||
(
|
||||
<div className='ports list'>
|
||||
{ports.map(port => <Item<Port>
|
||||
key={Port.toString(port)}
|
||||
item={port}
|
||||
label={Port.toString(port)}
|
||||
selected={Port.equals(this.state.selectedPort, port)}
|
||||
onClick={this.selectPort}
|
||||
/>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const boardsList = Array.from(distinctBoards.values()).map((board) => (
|
||||
<Item<BoardWithPackage>
|
||||
key={toKey(board)}
|
||||
item={board}
|
||||
label={board.name}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={board.missing}
|
||||
/>
|
||||
));
|
||||
|
||||
protected renderPortsFooter(): React.ReactNode {
|
||||
return <div className='noselect'>
|
||||
<label
|
||||
title='Shows all available ports when enabled'>
|
||||
<input
|
||||
type='checkbox'
|
||||
defaultChecked={this.state.showAllPorts}
|
||||
onChange={this.toggleFilterPorts}
|
||||
/>
|
||||
<span>Show all ports</span>
|
||||
</label>
|
||||
</div>;
|
||||
}
|
||||
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>
|
||||
{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 {
|
||||
let ports = [] as Port[];
|
||||
if (this.state.showAllPorts) {
|
||||
ports = this.state.knownPorts;
|
||||
} else {
|
||||
ports = this.state.knownPorts.filter(
|
||||
Port.visiblePorts(this.availableBoards)
|
||||
);
|
||||
}
|
||||
return !ports.length ? (
|
||||
<div className="no-result">
|
||||
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="ports list">
|
||||
{ports.map((port) => (
|
||||
<Item<Port>
|
||||
key={`${Port.keyOf(port)}`}
|
||||
item={port}
|
||||
label={Port.toString(port)}
|
||||
selected={Port.sameAs(this.state.selectedPort, port)}
|
||||
onClick={this.selectPort}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected renderPortsFooter(): React.ReactNode {
|
||||
return (
|
||||
<div className="noselect">
|
||||
<label
|
||||
title={nls.localize(
|
||||
'arduino/board/showAllAvailablePorts',
|
||||
'Shows all available ports when enabled'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={this.state.showAllPorts}
|
||||
onChange={this.toggleFilterPorts}
|
||||
/>
|
||||
<span>
|
||||
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace BoardsConfig {
|
||||
|
||||
export namespace Config {
|
||||
|
||||
export function sameAs(config: Config, other: Config | Board): boolean {
|
||||
const { selectedBoard, selectedPort } = config;
|
||||
if (Board.is(other)) {
|
||||
return !!selectedBoard
|
||||
&& Board.equals(other, selectedBoard)
|
||||
&& Port.sameAs(selectedPort, other.port);
|
||||
}
|
||||
return sameAs(config, other);
|
||||
}
|
||||
|
||||
export function equals(left: Config, right: Config): boolean {
|
||||
return left.selectedBoard === right.selectedBoard
|
||||
&& left.selectedPort === right.selectedPort;
|
||||
}
|
||||
|
||||
export function toString(config: Config, options: { default: string } = { default: '' }): string {
|
||||
const { selectedBoard, selectedPort: port } = config;
|
||||
if (!selectedBoard) {
|
||||
return options.default;
|
||||
}
|
||||
const { name } = selectedBoard;
|
||||
return `${name}${port ? ' at ' + Port.toString(port) : ''}`;
|
||||
}
|
||||
|
||||
export namespace Config {
|
||||
export function sameAs(config: Config, other: Config | Board): boolean {
|
||||
const { selectedBoard, selectedPort } = config;
|
||||
if (Board.is(other)) {
|
||||
return (
|
||||
!!selectedBoard &&
|
||||
Board.equals(other, selectedBoard) &&
|
||||
Port.sameAs(selectedPort, other.port)
|
||||
);
|
||||
}
|
||||
return sameAs(config, other);
|
||||
}
|
||||
|
||||
export function equals(left: Config, right: Config): boolean {
|
||||
return (
|
||||
left.selectedBoard === right.selectedBoard &&
|
||||
left.selectedPort === right.selectedPort
|
||||
);
|
||||
}
|
||||
|
||||
export function toString(
|
||||
config: Config,
|
||||
options: { default: string } = { default: '' }
|
||||
): string {
|
||||
const { selectedBoard, selectedPort: port } = config;
|
||||
if (!selectedBoard) {
|
||||
return options.default;
|
||||
}
|
||||
const { name } = selectedBoard;
|
||||
return `${name}${port ? ` at ${port.address}` : ''}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,180 @@
|
||||
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 { 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 { Board, ConfigOption, Programmer } from '../../common/protocol';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { BoardsDataStore } from './boards-data-store';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
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()
|
||||
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
protected readonly toDisposeOnBoardChange = new DisposableCollection();
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
protected readonly toDisposeOnBoardChange = new DisposableCollection();
|
||||
|
||||
async onStart(): Promise<void> {
|
||||
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
|
||||
this.boardsDataStore.onChanged(() => this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.updateMenuActions(selectedBoard));
|
||||
}
|
||||
async onStart(): Promise<void> {
|
||||
this.appStateService
|
||||
.reachedState('ready')
|
||||
.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> {
|
||||
return this.queue.add(async () => {
|
||||
this.toDisposeOnBoardChange.dispose();
|
||||
this.mainMenuManager.update();
|
||||
if (selectedBoard) {
|
||||
const { fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
const { configOptions, programmers, selectedProgrammer } = await this.boardsDataStore.getData(fqbn);
|
||||
if (configOptions.length) {
|
||||
const boardsConfigMenuPath = [...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 commands = new Map<string, Disposable & { label: string }>()
|
||||
for (const value of values) {
|
||||
const id = `${fqbn}-${option}--${value.value}`;
|
||||
const command = { id };
|
||||
const selectedValue = value.value;
|
||||
const handler = {
|
||||
execute: () => this.boardsDataStore.selectConfigOption({ fqbn, option, selectedValue }),
|
||||
isToggled: () => value.selected
|
||||
};
|
||||
commands.set(id, Object.assign(this.commandRegistry.registerCommand(command, handler), { label: value.label }));
|
||||
}
|
||||
this.menuRegistry.registerSubmenu(menuPath, label);
|
||||
this.toDisposeOnBoardChange.pushAll([
|
||||
...commands.values(),
|
||||
Disposable.create(() => unregisterSubmenu(menuPath, this.menuRegistry)),
|
||||
...Array.from(commands.keys()).map((commandId, i) => {
|
||||
const { label } = commands.get(commandId)!;
|
||||
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: `${i}`, label });
|
||||
return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId));
|
||||
})
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (programmers.length) {
|
||||
const programmersMenuPath = [...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, 'z02_programmers'];
|
||||
const label = selectedProgrammer ? `Programmer: "${selectedProgrammer.name}"` : 'Programmer'
|
||||
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
|
||||
this.toDisposeOnBoardChange.push(Disposable.create(() => unregisterSubmenu(programmersMenuPath, this.menuRegistry)));
|
||||
for (const programmer of programmers) {
|
||||
const { id, name } = programmer;
|
||||
const command = { id: `${fqbn}-programmer--${id}` };
|
||||
const handler = {
|
||||
execute: () => this.boardsDataStore.selectProgrammer({ fqbn, selectedProgrammer: programmer }),
|
||||
isToggled: () => Programmer.equals(programmer, selectedProgrammer)
|
||||
};
|
||||
this.menuRegistry.registerMenuAction(programmersMenuPath, { commandId: command.id, label: name });
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeOnBoardChange.pushAll([
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
|
||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command.id))
|
||||
]);
|
||||
}
|
||||
}
|
||||
this.mainMenuManager.update();
|
||||
protected async updateMenuActions(
|
||||
selectedBoard: Board | undefined
|
||||
): Promise<void> {
|
||||
return this.queue.add(async () => {
|
||||
this.toDisposeOnBoardChange.dispose();
|
||||
this.mainMenuManager.update();
|
||||
if (selectedBoard) {
|
||||
const { fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
const { configOptions, programmers, selectedProgrammer } =
|
||||
await this.boardsDataStore.getData(fqbn);
|
||||
if (configOptions.length) {
|
||||
const boardsConfigMenuPath = [
|
||||
...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 commands = new Map<
|
||||
string,
|
||||
Disposable & { label: string }
|
||||
>();
|
||||
let selectedValue = '';
|
||||
for (const value of values) {
|
||||
const id = `${fqbn}-${option}--${value.value}`;
|
||||
const command = { id };
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.boardsDataStore.selectConfigOption({
|
||||
fqbn,
|
||||
option,
|
||||
selectedValue: value.value,
|
||||
}),
|
||||
isToggled: () => value.selected,
|
||||
};
|
||||
commands.set(
|
||||
id,
|
||||
Object.assign(
|
||||
this.commandRegistry.registerCommand(command, handler),
|
||||
{ label: value.label }
|
||||
)
|
||||
);
|
||||
if (value.selected) {
|
||||
selectedValue = value.label;
|
||||
}
|
||||
}
|
||||
this.menuRegistry.registerSubmenu(
|
||||
menuPath,
|
||||
`${label}${selectedValue ? `: "${selectedValue}"` : ''}`
|
||||
);
|
||||
this.toDisposeOnBoardChange.pushAll([
|
||||
...commands.values(),
|
||||
Disposable.create(() =>
|
||||
unregisterSubmenu(menuPath, this.menuRegistry)
|
||||
),
|
||||
...Array.from(commands.keys()).map((commandId, i) => {
|
||||
const { label } = commands.get(commandId)!;
|
||||
this.menuRegistry.registerMenuAction(menuPath, {
|
||||
commandId,
|
||||
order: String(i).padStart(4),
|
||||
label,
|
||||
});
|
||||
return Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(commandId)
|
||||
);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
if (programmers.length) {
|
||||
const programmersMenuPath = [
|
||||
...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.toDisposeOnBoardChange.push(
|
||||
Disposable.create(() =>
|
||||
unregisterSubmenu(programmersMenuPath, this.menuRegistry)
|
||||
)
|
||||
);
|
||||
for (const programmer of programmers) {
|
||||
const { id, name } = programmer;
|
||||
const command = { id: `${fqbn}-programmer--${id}` };
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.boardsDataStore.selectProgrammer({
|
||||
fqbn,
|
||||
selectedProgrammer: programmer,
|
||||
}),
|
||||
isToggled: () =>
|
||||
Programmer.equals(programmer, selectedProgrammer),
|
||||
};
|
||||
this.menuRegistry.registerMenuAction(programmersMenuPath, {
|
||||
commandId: command.id,
|
||||
label: name,
|
||||
});
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeOnBoardChange.pushAll([
|
||||
Disposable.create(() =>
|
||||
this.commandRegistry.unregisterCommand(command)
|
||||
),
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(command.id)
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,211 +1,219 @@
|
||||
import { injectable, inject, named } from 'inversify';
|
||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
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 { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
|
||||
import {
|
||||
FrontendApplicationContribution,
|
||||
LocalStorageService,
|
||||
} from '@theia/core/lib/browser';
|
||||
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';
|
||||
|
||||
@injectable()
|
||||
export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
@inject(ILogger)
|
||||
@named('store')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(ILogger)
|
||||
@named('store')
|
||||
protected readonly logger: ILogger;
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
@inject(LocalStorageService)
|
||||
protected readonly storageService: LocalStorageService;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
protected readonly storageService: LocalStorageService;
|
||||
protected readonly onChangedEmitter = new Emitter<string[]>();
|
||||
|
||||
protected readonly onChangedEmitter = new Emitter<void>();
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onPlatformInstalled(async ({ item }) => {
|
||||
const { installedVersion: version } = item;
|
||||
if (!version) {
|
||||
return;
|
||||
onStart(): void {
|
||||
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
|
||||
const dataDidChangePerFqbn: string[] = [];
|
||||
for (const fqbn of item.boards
|
||||
.map(({ fqbn }) => fqbn)
|
||||
.filter(notEmpty)
|
||||
.filter((fqbn) => !!fqbn)) {
|
||||
const key = this.getStorageKey(fqbn);
|
||||
let data = await this.storageService.getData<
|
||||
ConfigOption[] | undefined
|
||||
>(key);
|
||||
if (!data || !data.length) {
|
||||
const details = await this.getBoardDetailsSafe(fqbn);
|
||||
if (details) {
|
||||
data = details.configOptions;
|
||||
if (data.length) {
|
||||
await this.storageService.setData(key, data);
|
||||
dataDidChangePerFqbn.push(fqbn);
|
||||
}
|
||||
let shouldFireChanged = false;
|
||||
for (const fqbn of item.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) {
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
let data = await this.storageService.getData<ConfigOption[] | undefined>(key);
|
||||
if (!data || !data.length) {
|
||||
const details = await this.getBoardDetailsSafe(fqbn);
|
||||
if (details) {
|
||||
data = details.configOptions;
|
||||
if (data.length) {
|
||||
await this.storageService.setData(key, data);
|
||||
shouldFireChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldFireChanged) {
|
||||
this.fireChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dataDidChangePerFqbn.length) {
|
||||
this.fireChanged(...dataDidChangePerFqbn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get onChanged(): Event<string[]> {
|
||||
return this.onChangedEmitter.event;
|
||||
}
|
||||
|
||||
async appendConfigToFqbn(
|
||||
fqbn: string | undefined,
|
||||
): Promise<string | undefined> {
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const { configOptions } = await this.getData(fqbn);
|
||||
return ConfigOption.decorate(fqbn, configOptions);
|
||||
}
|
||||
|
||||
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
|
||||
if (!fqbn) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
|
||||
get onChanged(): Event<void> {
|
||||
return this.onChangedEmitter.event;
|
||||
const key = this.getStorageKey(fqbn);
|
||||
let data = await this.storageService.getData<
|
||||
BoardsDataStore.Data | undefined
|
||||
>(key, undefined);
|
||||
if (BoardsDataStore.Data.is(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
async appendConfigToFqbn(
|
||||
fqbn: string | undefined,
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<string | undefined> {
|
||||
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
|
||||
return ConfigOption.decorate(fqbn, configOptions);
|
||||
const boardDetails = await this.getBoardDetailsSafe(fqbn);
|
||||
if (!boardDetails) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
|
||||
async getData(
|
||||
fqbn: string | undefined,
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<BoardsDataStore.Data> {
|
||||
data = {
|
||||
configOptions: boardDetails.configOptions,
|
||||
programmers: boardDetails.programmers,
|
||||
};
|
||||
await this.storageService.setData(key, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
if (!fqbn) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
let data = await this.storageService.getData<BoardsDataStore.Data | undefined>(key, undefined);
|
||||
if (BoardsDataStore.Data.is(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const boardDetails = await this.getBoardDetailsSafe(fqbn);
|
||||
if (!boardDetails) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
|
||||
data = { configOptions: boardDetails.configOptions, programmers: boardDetails.programmers };
|
||||
await this.storageService.setData(key, data);
|
||||
return data;
|
||||
async selectProgrammer(
|
||||
{
|
||||
fqbn,
|
||||
selectedProgrammer,
|
||||
}: { fqbn: string; selectedProgrammer: Programmer },
|
||||
): Promise<boolean> {
|
||||
const data = deepClone(await this.getData(fqbn));
|
||||
const { programmers } = data;
|
||||
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async selectProgrammer(
|
||||
{ fqbn, selectedProgrammer }: { fqbn: string, selectedProgrammer: Programmer },
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
|
||||
await this.setData({
|
||||
fqbn,
|
||||
data: { ...data, selectedProgrammer },
|
||||
});
|
||||
this.fireChanged(fqbn);
|
||||
return true;
|
||||
}
|
||||
|
||||
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
|
||||
const { programmers } = data;
|
||||
if (!programmers.find(p => Programmer.equals(selectedProgrammer, p))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.setData({ fqbn, data: { ...data, selectedProgrammer }, version });
|
||||
this.fireChanged();
|
||||
return true;
|
||||
async selectConfigOption(
|
||||
{
|
||||
fqbn,
|
||||
option,
|
||||
selectedValue,
|
||||
}: { fqbn: string; option: string; selectedValue: string }
|
||||
): Promise<boolean> {
|
||||
const data = deepClone(await this.getData(fqbn));
|
||||
const { configOptions } = data;
|
||||
const configOption = configOptions.find((c) => c.option === option);
|
||||
if (!configOption) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async selectConfigOption(
|
||||
{ fqbn, option, selectedValue }: { fqbn: string, option: string, selectedValue: string },
|
||||
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
|
||||
|
||||
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
|
||||
const { configOptions } = data;
|
||||
const configOption = configOptions.find(c => c.option === option);
|
||||
if (!configOption) {
|
||||
return false;
|
||||
}
|
||||
let updated = false;
|
||||
for (const value of configOption.values) {
|
||||
if (value.value === selectedValue) {
|
||||
(value as any).selected = true;
|
||||
updated = true;
|
||||
} else {
|
||||
(value as any).selected = false;
|
||||
}
|
||||
}
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.setData({ fqbn, data, version });
|
||||
this.fireChanged();
|
||||
return true;
|
||||
let updated = false;
|
||||
for (const value of configOption.values) {
|
||||
if (value.value === selectedValue) {
|
||||
(value as any).selected = true;
|
||||
updated = true;
|
||||
} else {
|
||||
(value as any).selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async setData(
|
||||
{ fqbn, data, version }: { fqbn: string, data: BoardsDataStore.Data, version: Installable.Version }): Promise<void> {
|
||||
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
return this.storageService.setData(key, data);
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
await this.setData({ fqbn, data });
|
||||
this.fireChanged(fqbn);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected getStorageKey(fqbn: string, version: Installable.Version): string {
|
||||
return `.arduinoIDE-configOptions-${version}-${fqbn}`;
|
||||
}
|
||||
|
||||
protected async getBoardDetailsSafe(fqbn: string): Promise<BoardDetails | undefined> {
|
||||
try {
|
||||
const details = this.boardsService.getBoardDetails({ fqbn });
|
||||
return details;
|
||||
} catch (err) {
|
||||
if (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 {
|
||||
this.logger.error(`An unexpected error occurred while retrieving the board details for ${fqbn}.`, err);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected fireChanged(): void {
|
||||
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;
|
||||
protected async setData({
|
||||
fqbn,
|
||||
data,
|
||||
}: {
|
||||
fqbn: string;
|
||||
data: BoardsDataStore.Data;
|
||||
}): Promise<void> {
|
||||
const key = this.getStorageKey(fqbn);
|
||||
return this.storageService.setData(key, data);
|
||||
}
|
||||
|
||||
protected getStorageKey(fqbn: string): string {
|
||||
return `.arduinoIDE-configOptions-${fqbn}`;
|
||||
}
|
||||
|
||||
protected async getBoardDetailsSafe(
|
||||
fqbn: string
|
||||
): Promise<BoardDetails | undefined> {
|
||||
try {
|
||||
const details = this.boardsService.getBoardDetails({ fqbn });
|
||||
return details;
|
||||
} catch (err) {
|
||||
if (
|
||||
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 {
|
||||
this.logger.error(
|
||||
`An unexpected error occurred while retrieving the board details for ${fqbn}.`,
|
||||
err
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected fireChanged(...fqbn: string[]): void {
|
||||
this.onChangedEmitter.fire(fqbn);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace BoardsDataStore {
|
||||
export interface Data {
|
||||
readonly configOptions: ConfigOption[];
|
||||
readonly programmers: Programmer[];
|
||||
readonly selectedProgrammer?: Programmer;
|
||||
}
|
||||
export namespace Data {
|
||||
export const EMPTY: Data = {
|
||||
configOptions: [],
|
||||
programmers: []
|
||||
};
|
||||
export function is(arg: any): arg is Data {
|
||||
return !!arg
|
||||
&& 'configOptions' in arg && Array.isArray(arg['configOptions'])
|
||||
&& 'programmers' in arg && Array.isArray(arg['programmers'])
|
||||
}
|
||||
export interface Data {
|
||||
readonly configOptions: ConfigOption[];
|
||||
readonly programmers: Programmer[];
|
||||
readonly selectedProgrammer?: Programmer;
|
||||
}
|
||||
export namespace Data {
|
||||
export const EMPTY: Data = {
|
||||
configOptions: [],
|
||||
programmers: [],
|
||||
};
|
||||
export function is(arg: any): arg is Data {
|
||||
return (
|
||||
!!arg &&
|
||||
'configOptions' in arg &&
|
||||
Array.isArray(arg['configOptions']) &&
|
||||
'programmers' in arg &&
|
||||
Array.isArray(arg['programmers'])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,91 @@
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { BoardsPackage, BoardsService } from '../../common/protocol/boards-service';
|
||||
import {
|
||||
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 { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { BoardsFilterRenderer } from '../widgets/component-list/filter-renderer';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
static WIDGET_ID = 'boards-list-widget';
|
||||
static WIDGET_LABEL = nls.localize('arduino/boardsManager', 'Boards Manager');
|
||||
|
||||
static WIDGET_ID = 'boards-list-widget';
|
||||
static WIDGET_LABEL = 'Boards Manager';
|
||||
constructor(
|
||||
@inject(BoardsService) service: BoardsService,
|
||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
|
||||
@inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
|
||||
) {
|
||||
super({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
label: BoardsListWidget.WIDGET_LABEL,
|
||||
iconClass: 'fa fa-arduino-boards',
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All' },
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
@inject(BoardsService) protected service: BoardsService,
|
||||
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardsPackage>) {
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onPlatformDidInstall(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
this.notificationCenter.onPlatformDidUninstall(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
super({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
label: BoardsListWidget.WIDGET_LABEL,
|
||||
iconClass: 'fa fa-microchip',
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemRenderer
|
||||
});
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
super.init();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onPlatformInstalled(() => this.refresh(undefined)),
|
||||
this.notificationCenter.onPlatformUninstalled(() => 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,193 +1,322 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import * as ReactDOM from '@theia/core/shared/react-dom';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
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 { ArduinoCommands } from '../arduino-commands';
|
||||
import { BoardsServiceProvider, AvailableBoard } from './boards-service-provider';
|
||||
|
||||
export interface BoardsDropDownListCoords {
|
||||
readonly top: number;
|
||||
readonly left: number;
|
||||
readonly width: number;
|
||||
readonly paddingTop: number;
|
||||
readonly top: number;
|
||||
readonly left: number;
|
||||
readonly width: number;
|
||||
readonly paddingTop: number;
|
||||
}
|
||||
|
||||
export namespace BoardsDropDown {
|
||||
export interface Props {
|
||||
readonly coords: BoardsDropDownListCoords | 'hidden';
|
||||
readonly items: Array<AvailableBoard & { onClick: () => void, port: Port }>;
|
||||
readonly openBoardsConfig: () => void;
|
||||
}
|
||||
export interface Props {
|
||||
readonly coords: BoardsDropDownListCoords | 'hidden';
|
||||
readonly items: Array<AvailableBoard & { onClick: () => void; port: Port }>;
|
||||
readonly openBoardsConfig: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
protected dropdownElement: HTMLElement;
|
||||
private listRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
protected dropdownElement: HTMLElement;
|
||||
constructor(props: BoardsDropDown.Props) {
|
||||
super(props);
|
||||
|
||||
constructor(props: BoardsDropDown.Props) {
|
||||
super(props);
|
||||
|
||||
let list = document.getElementById('boards-dropdown-container');
|
||||
if (!list) {
|
||||
list = document.createElement('div');
|
||||
list.id = 'boards-dropdown-container';
|
||||
document.body.appendChild(list);
|
||||
this.dropdownElement = list;
|
||||
}
|
||||
this.listRef = React.createRef();
|
||||
let list = document.getElementById('boards-dropdown-container');
|
||||
if (!list) {
|
||||
list = document.createElement('div');
|
||||
list.id = 'boards-dropdown-container';
|
||||
document.body.appendChild(list);
|
||||
this.dropdownElement = list;
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
|
||||
override componentDidUpdate(prevProps: BoardsDropDown.Props): void {
|
||||
if (prevProps.coords === 'hidden' && this.listRef.current) {
|
||||
this.listRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected renderNode(): React.ReactNode {
|
||||
const { coords, items } = this.props;
|
||||
if (coords === 'hidden') {
|
||||
return '';
|
||||
}
|
||||
return <div className='arduino-boards-dropdown-list'
|
||||
style={{
|
||||
position: 'absolute',
|
||||
...coords
|
||||
}}>
|
||||
{this.renderItem({
|
||||
label: 'Select Other Board & Port',
|
||||
onClick: () => this.props.openBoardsConfig()
|
||||
})}
|
||||
{items.map(({ name, port, selected, onClick }) => ({ label: `${name} at ${Port.toString(port)}`, selected, onClick })).map(this.renderItem)}
|
||||
override render(): React.ReactNode {
|
||||
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
|
||||
}
|
||||
|
||||
protected renderNode(): React.ReactNode {
|
||||
const { coords, items } = this.props;
|
||||
if (coords === 'hidden') {
|
||||
return '';
|
||||
}
|
||||
const footerLabel = nls.localize(
|
||||
'arduino/board/openBoardsConfig',
|
||||
'Select other board and port…'
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="arduino-boards-dropdown-list"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
...coords,
|
||||
}}
|
||||
ref={this.listRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="arduino-boards-dropdown-list--items-container">
|
||||
{items
|
||||
.map(({ name, port, selected, onClick }) => ({
|
||||
boardLabel: name,
|
||||
port,
|
||||
selected,
|
||||
onClick,
|
||||
}))
|
||||
.map(this.renderItem)}
|
||||
</div>
|
||||
}
|
||||
|
||||
protected renderItem({ label, selected, onClick }: { label: string, selected?: boolean, onClick: () => void }): React.ReactNode {
|
||||
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
|
||||
<div>
|
||||
{label}
|
||||
</div>
|
||||
{selected ? <span className='fa fa-check' /> : ''}
|
||||
<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,
|
||||
})}
|
||||
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 className="arduino-boards-dropdown-item--port-label noWrapInfo noselect">
|
||||
{port.addressLabel}
|
||||
</div>
|
||||
</div>
|
||||
{selected ? <div className="fa fa-check" /> : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
|
||||
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) {
|
||||
super(props);
|
||||
|
||||
constructor(props: BoardsToolBarItem.Props) {
|
||||
super(props);
|
||||
const { availableBoards } = props.boardsServiceProvider;
|
||||
this.state = {
|
||||
availableBoards,
|
||||
coords: 'hidden',
|
||||
};
|
||||
|
||||
const { availableBoards } = props.boardsServiceClient;
|
||||
this.state = {
|
||||
availableBoards,
|
||||
coords: 'hidden'
|
||||
};
|
||||
document.addEventListener('click', () => {
|
||||
this.setState({ coords: 'hidden' });
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
this.setState({ coords: 'hidden' });
|
||||
override componentDidMount(): void {
|
||||
this.props.boardsServiceProvider.onAvailableBoardsChanged(
|
||||
(availableBoards) => this.setState({ availableBoards })
|
||||
);
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected readonly show = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
const { currentTarget: element } = event;
|
||||
if (element instanceof HTMLElement) {
|
||||
if (this.state.coords === 'hidden') {
|
||||
const rect = element.getBoundingClientRect();
|
||||
this.setState({
|
||||
coords: {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
paddingTop: rect.height,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.setState({ availableBoards }));
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
|
||||
const { currentTarget: element } = event;
|
||||
if (element instanceof HTMLElement) {
|
||||
if (this.state.coords === 'hidden') {
|
||||
const rect = element.getBoundingClientRect();
|
||||
this.setState({
|
||||
coords: {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
paddingTop: rect.height
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setState({ coords: 'hidden' });
|
||||
}
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
const { coords, availableBoards } = this.state;
|
||||
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
|
||||
const title = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
|
||||
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>
|
||||
<div className='arduino-boards-toolbar-item-container'>
|
||||
<div className='arduino-boards-toolbar-item' title={title}>
|
||||
<div className='inner-container' onClick={this.show}>
|
||||
<span className={decorator} />
|
||||
<div className='label noWrapInfo'>
|
||||
<div className='noWrapInfo noselect'>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
<span className='fa fa-caret-down caret' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BoardsDropDown
|
||||
coords={coords}
|
||||
items={availableBoards.filter(AvailableBoard.hasPort).map(board => ({
|
||||
...board,
|
||||
onClick: () => {
|
||||
if (board.state === AvailableBoard.State.incomplete) {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
selectedPort: board.port
|
||||
};
|
||||
this.openDialog();
|
||||
} else {
|
||||
this.props.boardsServiceClient.boardsConfig = {
|
||||
selectedBoard: board,
|
||||
selectedPort: board.port
|
||||
}
|
||||
}
|
||||
}
|
||||
}))}
|
||||
openBoardsConfig={this.openDialog}>
|
||||
</BoardsDropDown>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
protected openDialog = () => {
|
||||
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
|
||||
} else {
|
||||
this.setState({ coords: 'hidden' });
|
||||
};
|
||||
}
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { coords, availableBoards } = this.state;
|
||||
const { selectedBoard, selectedPort } =
|
||||
this.props.boardsServiceProvider.boardsConfig;
|
||||
|
||||
const boardLabel =
|
||||
selectedBoard?.name ||
|
||||
nls.localize('arduino/board/selectBoard', 'Select Board');
|
||||
const selectedPortLabel = portLabel(selectedPort?.address);
|
||||
|
||||
const isConnected = Boolean(selectedBoard && selectedPort);
|
||||
const protocolIcon = isConnected
|
||||
? iconNameFromProtocol(selectedPort?.protocol || '')
|
||||
: null;
|
||||
const protocolIconClassNames = classNames(
|
||||
'arduino-boards-toolbar-item--protocol',
|
||||
'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 className="fa fa-caret-down caret" />
|
||||
</div>
|
||||
<BoardsDropDown
|
||||
coords={coords}
|
||||
items={availableBoards
|
||||
.filter(AvailableBoard.hasPort)
|
||||
.map((board) => ({
|
||||
...board,
|
||||
onClick: () => {
|
||||
if (!board.fqbn) {
|
||||
const previousBoardConfig =
|
||||
this.props.boardsServiceProvider.boardsConfig;
|
||||
this.props.boardsServiceProvider.boardsConfig = {
|
||||
selectedPort: board.port,
|
||||
};
|
||||
this.openDialog(previousBoardConfig);
|
||||
} else {
|
||||
this.props.boardsServiceProvider.boardsConfig = {
|
||||
selectedBoard: board,
|
||||
selectedPort: board.port,
|
||||
};
|
||||
}
|
||||
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 interface Props {
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
readonly commands: CommandRegistry;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
readonly boardsServiceClient: BoardsServiceProvider;
|
||||
readonly commands: CommandRegistry;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
availableBoards: AvailableBoard[];
|
||||
coords: BoardsDropDownListCoords | 'hidden';
|
||||
}
|
||||
|
||||
export interface State {
|
||||
availableBoards: AvailableBoard[];
|
||||
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 { BoardsListWidget } from './boards-list-widget';
|
||||
import { BoardsPackage } from '../../common/protocol/boards-service';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
BoardsPackage,
|
||||
BoardSearch
|
||||
> {
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: BoardsListWidget.WIDGET_ID,
|
||||
widgetName: BoardsListWidget.WIDGET_LABEL,
|
||||
defaultWidgetOptions: {
|
||||
area: 'left',
|
||||
rank: 2,
|
||||
},
|
||||
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
||||
toggleKeybinding: 'CtrlCmd+Shift+B',
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: BoardsListWidget.WIDGET_ID,
|
||||
widgetName: BoardsListWidget.WIDGET_LABEL,
|
||||
defaultWidgetOptions: {
|
||||
area: 'left',
|
||||
rank: 600
|
||||
},
|
||||
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
||||
toggleKeybinding: 'CtrlCmd+Shift+B'
|
||||
});
|
||||
}
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
protected canParse(uri: URI): boolean {
|
||||
try {
|
||||
BoardSearch.UriParser.parse(uri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): BoardSearch | undefined {
|
||||
return BoardSearch.UriParser.parse(uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||
import { QuickOpenContribution } from '@theia/core/lib/browser/quick-open';
|
||||
import { KeybindingContribution } from '@theia/core/lib/browser/keybinding';
|
||||
import { BoardsQuickOpenService } from './boards-quick-open-service';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(BoardsQuickOpenService).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(BoardsQuickOpenService);
|
||||
bind(KeybindingContribution).toService(BoardsQuickOpenService);
|
||||
bind(QuickOpenContribution).toService(BoardsQuickOpenService);
|
||||
bind(ILogger).toDynamicValue(({ container }) => container.get<ILogger>(ILogger).child('boards-quick-open'))
|
||||
.inSingletonScope()
|
||||
.whenTargetNamed('boards-quick-open');
|
||||
});
|
||||
@@ -1,309 +0,0 @@
|
||||
import * as fuzzy from 'fuzzy';
|
||||
import { inject, injectable, postConstruct, named } from 'inversify';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
|
||||
import { KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import { QuickOpenItem, QuickOpenModel, QuickOpenMode, QuickOpenGroupItem } from '@theia/core/lib/common/quick-open-model';
|
||||
import {
|
||||
QuickOpenService,
|
||||
QuickOpenHandler,
|
||||
QuickOpenOptions,
|
||||
QuickOpenItemOptions,
|
||||
QuickOpenContribution,
|
||||
QuickOpenActionProvider,
|
||||
QuickOpenHandlerRegistry,
|
||||
QuickOpenGroupItemOptions
|
||||
} from '@theia/core/lib/browser/quick-open';
|
||||
import { naturalCompare } from '../../../common/utils';
|
||||
import { BoardsService, Port, Board, ConfigOption, ConfigValue } from '../../../common/protocol';
|
||||
import { BoardsDataStore } from '../boards-data-store';
|
||||
import { BoardsServiceProvider, AvailableBoard } from '../boards-service-provider';
|
||||
import { NotificationCenter } from '../../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class BoardsQuickOpenService implements QuickOpenContribution, QuickOpenModel, QuickOpenHandler, CommandContribution, KeybindingContribution, Command {
|
||||
|
||||
readonly id = 'arduino-boards-quick-open';
|
||||
readonly prefix = '|';
|
||||
readonly description = 'Configure Available Boards';
|
||||
readonly label: 'Configure Available Boards';
|
||||
|
||||
@inject(ILogger)
|
||||
@named('boards-quick-open')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(QuickOpenService)
|
||||
protected readonly quickOpenService: QuickOpenService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected notificationCenter: NotificationCenter;
|
||||
|
||||
protected isOpen: boolean = false;
|
||||
protected currentQuery: string = '';
|
||||
// Attached boards plus the user's config.
|
||||
protected availableBoards: AvailableBoard[] = [];
|
||||
// Only for the `selected` one from the `availableBoards`. Note: the `port` of the `selected` is optional.
|
||||
protected data: BoardsDataStore.Data = BoardsDataStore.Data.EMPTY;
|
||||
protected allBoards: Board.Detailed[] = []
|
||||
protected selectedBoard?: (AvailableBoard & { port: Port });
|
||||
|
||||
// `init` name is used by the `QuickOpenHandler`.
|
||||
@postConstruct()
|
||||
protected postConstruct(): void {
|
||||
this.notificationCenter.onIndexUpdated(() => this.update(this.availableBoards));
|
||||
this.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.update(availableBoards));
|
||||
this.update(this.boardsServiceClient.availableBoards);
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(this, { execute: () => this.open() });
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({ command: this.id, keybinding: 'ctrlCmd+k ctrlCmd+b' });
|
||||
}
|
||||
|
||||
registerQuickOpenHandlers(registry: QuickOpenHandlerRegistry): void {
|
||||
registry.registerHandler(this);
|
||||
}
|
||||
|
||||
getModel(): QuickOpenModel {
|
||||
return this;
|
||||
}
|
||||
|
||||
getOptions(): QuickOpenOptions {
|
||||
let placeholder = '';
|
||||
if (!this.selectedBoard) {
|
||||
placeholder += 'No board selected.';
|
||||
}
|
||||
placeholder += 'Type to filter boards';
|
||||
if (this.data.configOptions.length) {
|
||||
placeholder += ' or use the ↓↑ keys to adjust the board settings...';
|
||||
} else {
|
||||
placeholder += '...';
|
||||
}
|
||||
return {
|
||||
placeholder,
|
||||
fuzzyMatchLabel: true,
|
||||
onClose: () => this.isOpen = false
|
||||
};
|
||||
}
|
||||
|
||||
open(): void {
|
||||
this.isOpen = true;
|
||||
this.quickOpenService.open(this, this.getOptions());
|
||||
}
|
||||
|
||||
onType(
|
||||
lookFor: string,
|
||||
acceptor: (items: QuickOpenItem<QuickOpenItemOptions>[], actionProvider?: QuickOpenActionProvider) => void): void {
|
||||
|
||||
this.currentQuery = lookFor;
|
||||
const fuzzyFilter = this.fuzzyFilter(lookFor);
|
||||
const availableBoards = this.availableBoards.filter(AvailableBoard.hasPort).filter(({ name }) => fuzzyFilter(name));
|
||||
const toAccept: QuickOpenItem<QuickOpenItemOptions>[] = [];
|
||||
|
||||
// Show the selected attached in a different group.
|
||||
if (this.selectedBoard && fuzzyFilter(this.selectedBoard.name)) {
|
||||
toAccept.push(this.toQuickItem(this.selectedBoard, { groupLabel: 'Selected Board' }));
|
||||
}
|
||||
|
||||
// Filter the selected from the attached ones.
|
||||
toAccept.push(...availableBoards.filter(board => board !== this.selectedBoard).map((board, i) => {
|
||||
let group: QuickOpenGroupItemOptions | undefined = undefined;
|
||||
if (i === 0) {
|
||||
// If no `selectedBoard`, then this item is the top one, no borders required.
|
||||
group = { groupLabel: 'Attached Boards', showBorder: !!this.selectedBoard };
|
||||
}
|
||||
return this.toQuickItem(board, group);
|
||||
}));
|
||||
|
||||
// Show the config only if the `input` is empty.
|
||||
if (!lookFor.trim().length) {
|
||||
toAccept.push(...this.data.configOptions.map((config, i) => {
|
||||
let group: QuickOpenGroupItemOptions | undefined = undefined;
|
||||
if (i === 0) {
|
||||
group = { groupLabel: 'Board Settings', showBorder: true };
|
||||
}
|
||||
return this.toQuickItem(config, group);
|
||||
}));
|
||||
} else {
|
||||
toAccept.push(...this.allBoards.filter(({ name }) => fuzzyFilter(name)).map((board, i) => {
|
||||
let group: QuickOpenGroupItemOptions | undefined = undefined;
|
||||
if (i === 0) {
|
||||
group = { groupLabel: 'Boards', showBorder: true };
|
||||
}
|
||||
return this.toQuickItem(board, group);
|
||||
}));
|
||||
}
|
||||
|
||||
acceptor(toAccept);
|
||||
}
|
||||
|
||||
private fuzzyFilter(lookFor: string): (inputString: string) => boolean {
|
||||
const shouldFilter = !!lookFor.trim().length;
|
||||
return (inputString: string) => shouldFilter ? fuzzy.test(lookFor.toLocaleLowerCase(), inputString.toLocaleLowerCase()) : true;
|
||||
}
|
||||
|
||||
protected async update(availableBoards: AvailableBoard[]): Promise<void> {
|
||||
// `selectedBoard` is not an attached board, we need to show the board settings for it (TODO: clarify!)
|
||||
const selectedBoard = availableBoards.filter(AvailableBoard.hasPort).find(({ selected }) => selected);
|
||||
const [data, boards] = await Promise.all([
|
||||
selectedBoard && selectedBoard.fqbn ? this.boardsDataStore.getData(selectedBoard.fqbn) : Promise.resolve(BoardsDataStore.Data.EMPTY),
|
||||
this.boardsService.allBoards({})
|
||||
]);
|
||||
this.allBoards = Board.decorateBoards(selectedBoard, boards)
|
||||
.filter(board => !availableBoards.some(availableBoard => Board.sameAs(availableBoard, board)));
|
||||
this.availableBoards = availableBoards;
|
||||
this.data = data;
|
||||
this.selectedBoard = selectedBoard;
|
||||
|
||||
if (this.isOpen) {
|
||||
// Hack, to update the state without closing and reopening the quick open widget.
|
||||
(this.quickOpenService as any).onType(this.currentQuery);
|
||||
}
|
||||
}
|
||||
|
||||
protected toQuickItem(item: BoardsQuickOpenService.Item, group?: QuickOpenGroupItemOptions): QuickOpenItem<QuickOpenItemOptions> {
|
||||
let options: QuickOpenItemOptions;
|
||||
if (AvailableBoard.is(item)) {
|
||||
const description = `on ${Port.toString(item.port)}`
|
||||
options = {
|
||||
label: `${item.name}`,
|
||||
description,
|
||||
descriptionHighlights: [
|
||||
{
|
||||
start: 0,
|
||||
end: description.length
|
||||
}
|
||||
],
|
||||
run: this.toRun(() => this.boardsServiceClient.boardsConfig = ({ selectedBoard: item, selectedPort: item.port }))
|
||||
};
|
||||
} else if (ConfigOption.is(item)) {
|
||||
const selected = item.values.find(({ selected }) => selected);
|
||||
const value = selected ? selected.label : 'Not set';
|
||||
const label = `${item.label}: ${value}`;
|
||||
options = {
|
||||
label,
|
||||
// Intended to match the value part of a board setting.
|
||||
// NOTE: this does not work, as `fuzzyMatchLabel: true` is set. Manual highlighting is ignored, apparently.
|
||||
labelHighlights: [
|
||||
{
|
||||
start: label.length - value.length,
|
||||
end: label.length
|
||||
}
|
||||
],
|
||||
run: (mode) => {
|
||||
if (mode === QuickOpenMode.OPEN) {
|
||||
this.setConfig(item);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
if (!selected) {
|
||||
options.description = 'Not set';
|
||||
};
|
||||
} else {
|
||||
options = {
|
||||
label: `${item.name}`,
|
||||
description: `${item.missing ? '' : `[installed with '${item.packageName}']`}`,
|
||||
run: (mode) => {
|
||||
if (mode === QuickOpenMode.OPEN) {
|
||||
this.selectBoard(item);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
if (group) {
|
||||
return new QuickOpenGroupItem<QuickOpenGroupItemOptions>({ ...options, ...group });
|
||||
} else {
|
||||
return new QuickOpenItem<QuickOpenItemOptions>(options);
|
||||
}
|
||||
}
|
||||
|
||||
protected toRun(run: (() => void)): ((mode: QuickOpenMode) => boolean) {
|
||||
return (mode) => {
|
||||
if (mode !== QuickOpenMode.OPEN) {
|
||||
return false;
|
||||
}
|
||||
run();
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
protected async selectBoard(board: Board): Promise<void> {
|
||||
const allPorts = this.availableBoards.filter(AvailableBoard.hasPort).map(({ port }) => port).sort(Port.compare);
|
||||
const toItem = (port: Port) => new QuickOpenItem<QuickOpenItemOptions>({
|
||||
label: Port.toString(port, { useLabel: true }),
|
||||
run: this.toRun(() => {
|
||||
this.boardsServiceClient.boardsConfig = {
|
||||
selectedBoard: board,
|
||||
selectedPort: port
|
||||
};
|
||||
})
|
||||
});
|
||||
const options = {
|
||||
placeholder: `Select a port for '${board.name}'. Press 'Enter' to confirm or 'Escape' to cancel.`,
|
||||
fuzzyMatchLabel: true
|
||||
}
|
||||
this.quickOpenService.open({
|
||||
onType: (lookFor, acceptor) => {
|
||||
const fuzzyFilter = this.fuzzyFilter(lookFor);
|
||||
acceptor(allPorts.filter(({ address }) => fuzzyFilter(address)).map(toItem));
|
||||
}
|
||||
}, options);
|
||||
}
|
||||
|
||||
protected async setConfig(config: ConfigOption): Promise<void> {
|
||||
const toItem = (value: ConfigValue) => new QuickOpenItem<QuickOpenItemOptions>({
|
||||
label: value.label,
|
||||
iconClass: value.selected ? 'fa fa-check' : '',
|
||||
run: this.toRun(() => {
|
||||
if (!this.selectedBoard) {
|
||||
this.logger.warn(`Could not alter the boards settings. No board selected. ${JSON.stringify(config)}`);
|
||||
return;
|
||||
}
|
||||
if (!this.selectedBoard.fqbn) {
|
||||
this.logger.warn(`Could not alter the boards settings. The selected board does not have a FQBN. ${JSON.stringify(this.selectedBoard)}`);
|
||||
return;
|
||||
}
|
||||
const { fqbn } = this.selectedBoard;
|
||||
this.boardsDataStore.selectConfigOption({
|
||||
fqbn,
|
||||
option: config.option,
|
||||
selectedValue: value.value
|
||||
});
|
||||
})
|
||||
});
|
||||
const options = {
|
||||
placeholder: `Configure '${config.label}'. Press 'Enter' to confirm or 'Escape' to cancel.`,
|
||||
fuzzyMatchLabel: true
|
||||
}
|
||||
this.quickOpenService.open({
|
||||
onType: (lookFor, acceptor) => {
|
||||
const fuzzyFilter = this.fuzzyFilter(lookFor);
|
||||
acceptor(config.values
|
||||
.filter(({ label }) => fuzzyFilter(label))
|
||||
.sort((left, right) => naturalCompare(left.label, right.label))
|
||||
.map(toItem));
|
||||
}
|
||||
}, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace BoardsQuickOpenService {
|
||||
export type Item = AvailableBoard & { port: Port } | Board.Detailed | ConfigOption;
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { ConfigService, ConfigState } from '../../common/protocol';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class ConfigServiceClient implements FrontendApplicationContribution {
|
||||
@inject(ConfigService)
|
||||
private readonly delegate: ConfigService;
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
@inject(MessageService)
|
||||
private readonly messageService: MessageService;
|
||||
|
||||
private readonly didChangeSketchDirUriEmitter = new Emitter<
|
||||
URI | undefined
|
||||
>();
|
||||
private readonly didChangeDataDirUriEmitter = new Emitter<URI | undefined>();
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.didChangeSketchDirUriEmitter,
|
||||
this.didChangeDataDirUriEmitter
|
||||
);
|
||||
|
||||
private config: ConfigState | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.appStateService.reachedState('ready').then(async () => {
|
||||
const config = await this.delegate.getConfiguration();
|
||||
this.use(config);
|
||||
});
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onConfigDidChange((config) => this.use(config));
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get onDidChangeSketchDirUri(): Event<URI | undefined> {
|
||||
return this.didChangeSketchDirUriEmitter.event;
|
||||
}
|
||||
|
||||
get onDidChangeDataDirUri(): Event<URI | undefined> {
|
||||
return this.didChangeDataDirUriEmitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI config related error messages if any.
|
||||
*/
|
||||
tryGetMessages(): string[] | undefined {
|
||||
return this.config?.messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* `directories.user`
|
||||
*/
|
||||
tryGetSketchDirUri(): URI | undefined {
|
||||
return this.config?.config?.sketchDirUri
|
||||
? new URI(this.config?.config?.sketchDirUri)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* `directories.data`
|
||||
*/
|
||||
tryGetDataDirUri(): URI | undefined {
|
||||
return this.config?.config?.dataDirUri
|
||||
? new URI(this.config?.config?.dataDirUri)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private use(config: ConfigState): void {
|
||||
const oldConfig = deepClone(this.config);
|
||||
this.config = config;
|
||||
if (oldConfig?.config?.sketchDirUri !== this.config?.config?.sketchDirUri) {
|
||||
this.didChangeSketchDirUriEmitter.fire(this.tryGetSketchDirUri());
|
||||
}
|
||||
if (oldConfig?.config?.dataDirUri !== this.config?.config?.dataDirUri) {
|
||||
this.didChangeDataDirUriEmitter.fire(this.tryGetDataDirUri());
|
||||
}
|
||||
if (this.config.messages?.length) {
|
||||
const message = this.config.messages.join(' ');
|
||||
// toast the error later otherwise it might not show up in IDE2
|
||||
setTimeout(() => this.messageService.error(message), 1_000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,184 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
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 { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
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 { ConfigService } from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class About extends Contribution {
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(About.Commands.ABOUT_APP, {
|
||||
execute: () => this.showAbout(),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(About.Commands.ABOUT_APP, {
|
||||
execute: () => this.showAbout()
|
||||
});
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
|
||||
commandId: About.Commands.ABOUT_APP.id,
|
||||
label: nls.localize(
|
||||
'arduino/about/label',
|
||||
'About {0}',
|
||||
this.applicationName
|
||||
),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
|
||||
async showAbout(): Promise<void> {
|
||||
const version = await this.configService.getVersion();
|
||||
const buildDate = this.buildDate;
|
||||
const detail = (showAll: boolean) =>
|
||||
nls.localize(
|
||||
'arduino/about/detail',
|
||||
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}',
|
||||
remote.app.getVersion(),
|
||||
buildDate ? buildDate : nls.localize('', 'dev build'),
|
||||
buildDate && showAll ? ` (${this.ago(buildDate)})` : '',
|
||||
version,
|
||||
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 { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
message: `${this.applicationName}`,
|
||||
title: `${this.applicationName}`,
|
||||
type: 'info',
|
||||
detail: detail(true),
|
||||
buttons,
|
||||
noLink: true,
|
||||
defaultId: buttons.indexOf(ok),
|
||||
cancelId: buttons.indexOf(ok),
|
||||
}
|
||||
);
|
||||
|
||||
if (buttons[response] === copy) {
|
||||
await this.clipboardService.writeText(detail(false).trim());
|
||||
}
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
|
||||
commandId: About.Commands.ABOUT_APP.id,
|
||||
label: `About ${this.applicationName}`,
|
||||
order: '0'
|
||||
});
|
||||
protected get applicationName(): string {
|
||||
return FrontendApplicationConfigProvider.get().applicationName;
|
||||
}
|
||||
|
||||
protected get buildDate(): string | undefined {
|
||||
return FrontendApplicationConfigProvider.get().buildDate;
|
||||
}
|
||||
|
||||
protected ago(isoTime: string): string {
|
||||
const now = moment(Date.now());
|
||||
const other = moment(isoTime);
|
||||
let result = now.diff(other, 'minute');
|
||||
if (result < 60) {
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
async showAbout(): Promise<void> {
|
||||
const { version, commit, status: cliStatus } = await this.configService.getVersion();
|
||||
const buildDate = this.buildDate;
|
||||
const detail = (showAll: boolean) => `Version: ${remote.app.getVersion()}
|
||||
Date: ${buildDate ? buildDate : 'dev build'}${buildDate && showAll ? ` (${this.ago(buildDate)})` : ''}
|
||||
CLI Version: ${version}${cliStatus ? ` ${cliStatus}` : ''} [${commit}]
|
||||
|
||||
${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
|
||||
`;
|
||||
const ok = 'OK';
|
||||
const copy = 'Copy';
|
||||
const buttons = !isWindows && !isOSX ? [copy, ok] : [ok, copy];
|
||||
const { response } = await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
message: `${this.applicationName}`,
|
||||
title: `${this.applicationName}`,
|
||||
type: 'info',
|
||||
detail: detail(true),
|
||||
buttons,
|
||||
noLink: true,
|
||||
defaultId: buttons.indexOf(ok),
|
||||
cancelId: buttons.indexOf(ok)
|
||||
});
|
||||
|
||||
if (buttons[response] === copy) {
|
||||
await this.clipboardService.writeText(detail(false).trim());
|
||||
}
|
||||
result = now.diff(other, 'hour');
|
||||
if (result < 25) {
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
protected get applicationName(): string {
|
||||
return FrontendApplicationConfigProvider.get().applicationName;
|
||||
result = now.diff(other, 'day');
|
||||
if (result < 8) {
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
protected get buildDate(): string | undefined {
|
||||
return FrontendApplicationConfigProvider.get().buildDate;
|
||||
result = now.diff(other, 'week');
|
||||
if (result < 5) {
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
protected ago(isoTime: string): string {
|
||||
const now = moment(Date.now());
|
||||
const other = moment(isoTime);
|
||||
let result = now.diff(other, 'minute');
|
||||
if (result < 60) {
|
||||
return result === 1 ? `${result} minute ago` : `${result} minute ago`;
|
||||
}
|
||||
result = now.diff(other, 'hour');
|
||||
if (result < 25) {
|
||||
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
|
||||
}
|
||||
result = now.diff(other, 'day');
|
||||
if (result < 8) {
|
||||
return result === 1 ? `${result} day ago` : `${result} days ago`;
|
||||
}
|
||||
result = now.diff(other, 'week');
|
||||
if (result < 5) {
|
||||
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
|
||||
}
|
||||
result = now.diff(other, 'month');
|
||||
if (result < 13) {
|
||||
return result === 1 ? `${result} month ago` : `${result} months ago`;
|
||||
}
|
||||
result = now.diff(other, 'year');
|
||||
return result === 1 ? `${result} year ago` : `${result} years ago`;
|
||||
result = now.diff(other, 'month');
|
||||
if (result < 13) {
|
||||
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');
|
||||
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 Commands {
|
||||
export const ABOUT_APP: Command = {
|
||||
id: 'arduino-about'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const ABOUT_APP: Command = {
|
||||
id: 'arduino-about',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
145
arduino-ide-extension/src/browser/contributions/account.ts
Normal file
145
arduino-ide-extension/src/browser/contributions/account.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MenuPath } from '@theia/core/lib/common/menu';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CloudUserCommands, LEARN_MORE_URL } from '../auth/cloud-user-commands';
|
||||
import { CreateFeatures } from '../create/create-features';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
Contribution,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
|
||||
export const accountMenu: SidebarMenu = {
|
||||
id: 'arduino-accounts-menu',
|
||||
iconClass: 'codicon codicon-account',
|
||||
title: nls.localize('arduino/account/menuTitle', 'Arduino Cloud'),
|
||||
menuPath: ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT,
|
||||
order: 0,
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class Account extends Contribution {
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
@inject(CreateFeatures)
|
||||
private readonly createFeatures: CreateFeatures;
|
||||
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private app: FrontendApplication;
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.app = app;
|
||||
this.updateSidebarCommand();
|
||||
this.toDispose.push(
|
||||
this.createFeatures.onDidChangeEnabled((enabled) =>
|
||||
this.updateSidebarCommand(enabled)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
const openExternal = (url: string) =>
|
||||
this.windowService.openNewWindow(url, { external: true });
|
||||
registry.registerCommand(Account.Commands.LEARN_MORE, {
|
||||
execute: () => openExternal(LEARN_MORE_URL),
|
||||
isEnabled: () => !Boolean(this.createFeatures.session),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_PROFILE, {
|
||||
execute: () => openExternal('https://id.arduino.cc/'),
|
||||
isEnabled: () => Boolean(this.createFeatures.session),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, {
|
||||
execute: () => openExternal('https://create.arduino.cc/editor'),
|
||||
isEnabled: () => Boolean(this.createFeatures.session),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, {
|
||||
execute: () => openExternal('https://create.arduino.cc/iot/'),
|
||||
isEnabled: () => Boolean(this.createFeatures.session),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
const register = (
|
||||
menuPath: MenuPath,
|
||||
...commands: (Command | [command: Command, menuLabel: string])[]
|
||||
) =>
|
||||
commands.forEach((command, index) => {
|
||||
const commandId = Array.isArray(command) ? command[0].id : command.id;
|
||||
const label = Array.isArray(command) ? command[1] : command.label;
|
||||
registry.registerMenuAction(menuPath, {
|
||||
label,
|
||||
commandId,
|
||||
order: String(index),
|
||||
});
|
||||
});
|
||||
|
||||
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP, [
|
||||
CloudUserCommands.LOGIN,
|
||||
nls.localize('arduino/cloud/signInToCloud', 'Sign in to Arduino Cloud'),
|
||||
]);
|
||||
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP, [
|
||||
Account.Commands.LEARN_MORE,
|
||||
nls.localize('arduino/cloud/learnMore', 'Learn more'),
|
||||
]);
|
||||
register(
|
||||
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP,
|
||||
[
|
||||
Account.Commands.GO_TO_PROFILE,
|
||||
nls.localize('arduino/account/goToProfile', 'Go to Profile'),
|
||||
],
|
||||
[
|
||||
Account.Commands.GO_TO_CLOUD_EDITOR,
|
||||
nls.localize('arduino/account/goToCloudEditor', 'Go to Cloud Editor'),
|
||||
],
|
||||
[
|
||||
Account.Commands.GO_TO_IOT_CLOUD,
|
||||
nls.localize('arduino/account/goToIoTCloud', 'Go to IoT Cloud'),
|
||||
]
|
||||
);
|
||||
register(
|
||||
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP,
|
||||
CloudUserCommands.LOGOUT
|
||||
);
|
||||
}
|
||||
|
||||
private updateSidebarCommand(
|
||||
visible: boolean = this.preferences['arduino.cloud.enabled']
|
||||
): void {
|
||||
if (!this.app) {
|
||||
return;
|
||||
}
|
||||
const handler = this.app.shell.leftPanelHandler;
|
||||
if (visible) {
|
||||
handler.addBottomMenu(accountMenu);
|
||||
} else {
|
||||
handler.removeBottomMenu(accountMenu.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Account {
|
||||
export namespace Commands {
|
||||
export const GO_TO_PROFILE: Command = {
|
||||
id: 'arduino-go-to-profile',
|
||||
};
|
||||
export const GO_TO_CLOUD_EDITOR: Command = {
|
||||
id: 'arduino-go-to-cloud-editor',
|
||||
};
|
||||
export const GO_TO_IOT_CLOUD: Command = {
|
||||
id: 'arduino-go-to-iot-cloud',
|
||||
};
|
||||
export const LEARN_MORE: Command = {
|
||||
id: 'arduino-learn-more',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,106 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, URI } from './contribution';
|
||||
import {
|
||||
SketchContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
URI,
|
||||
Sketch,
|
||||
} from './contribution';
|
||||
import { FileDialogService } from '@theia/filesystem/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class AddFile extends SketchContribution {
|
||||
@inject(FileDialogService)
|
||||
private readonly fileDialogService: FileDialogService;
|
||||
|
||||
@inject(FileDialogService)
|
||||
protected readonly fileDialogService: FileDialogService;
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddFile.Commands.ADD_FILE, {
|
||||
execute: () => this.addFile(),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddFile.Commands.ADD_FILE, {
|
||||
execute: () => this.addFile()
|
||||
});
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
||||
commandId: AddFile.Commands.ADD_FILE.id,
|
||||
label: nls.localize('arduino/contributions/addFile', 'Add File') + '...',
|
||||
order: '2',
|
||||
});
|
||||
}
|
||||
|
||||
private async addFile(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
||||
commandId: AddFile.Commands.ADD_FILE.id,
|
||||
label: 'Add File...',
|
||||
order: '2'
|
||||
});
|
||||
const toAddUri = await this.fileDialogService.showOpenDialog({
|
||||
title: nls.localize('arduino/contributions/addFile', 'Add File'),
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
modal: true,
|
||||
});
|
||||
if (!toAddUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected async addFile(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
const toAddUri = await this.fileDialogService.showOpenDialog({
|
||||
title: 'Add File',
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false
|
||||
});
|
||||
if (!toAddUri) {
|
||||
return;
|
||||
}
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const filename = toAddUri.path.base;
|
||||
const targetUri = sketchUri.resolve('data').resolve(filename);
|
||||
const exists = await this.fileService.exists(targetUri);
|
||||
if (exists) {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
type: 'question',
|
||||
title: 'Replace',
|
||||
buttons: ['Cancel', 'OK'],
|
||||
message: `Replace the existing version of ${filename}?`
|
||||
});
|
||||
if (response === 0) { // Cancel
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
|
||||
this.messageService.info('One file added to the sketch.', { timeout: 2000 });
|
||||
const { uri: targetUri, filename } = this.resolveTarget(sketch, toAddUri);
|
||||
const exists = await this.fileService.exists(targetUri);
|
||||
if (exists) {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
type: 'question',
|
||||
title: nls.localize('arduino/contributions/replaceTitle', 'Replace'),
|
||||
buttons: [
|
||||
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
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/contributions/fileAdded',
|
||||
'One file added to the sketch.'
|
||||
),
|
||||
{
|
||||
timeout: 2000,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/arduino/arduino-ide/issues/284#issuecomment-1364533662
|
||||
// File the file to add has one of the following extension, it goes to the sketch folder root: .ino, .h, .cpp, .c, .S
|
||||
// Otherwise, the files goes to the `data` folder inside the sketch folder root.
|
||||
private resolveTarget(
|
||||
sketch: Sketch,
|
||||
toAddUri: URI
|
||||
): { uri: URI; filename: string } {
|
||||
const path = toAddUri.path;
|
||||
const filename = path.base;
|
||||
let root = new URI(sketch.uri);
|
||||
if (!Sketch.Extensions.CODE_FILES.includes(path.ext)) {
|
||||
root = root.resolve('data');
|
||||
}
|
||||
return { uri: root.resolve(filename), filename: filename };
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AddFile {
|
||||
export namespace Commands {
|
||||
export const ADD_FILE: Command = {
|
||||
id: 'arduino-add-file'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const ADD_FILE: Command = {
|
||||
id: 'arduino-add-file',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,146 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { InstallationProgressDialog } from '../widgets/progress-dialog';
|
||||
import { LibraryService } from '../../common/protocol';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||
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()
|
||||
export class AddZipLibrary extends SketchContribution {
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
@inject(LibraryService)
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
|
||||
execute: () => this.addZipLibrary(),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
|
||||
execute: () => this.addZipLibrary()
|
||||
});
|
||||
}
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
const includeLibMenuPath = [
|
||||
...ArduinoMenus.SKETCH__UTILS_GROUP,
|
||||
'0_include',
|
||||
];
|
||||
registry.registerMenuAction([...includeLibMenuPath, '1_install'], {
|
||||
commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id,
|
||||
label: nls.localize('arduino/library/addZip', 'Add .ZIP Library...'),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include'];
|
||||
// TODO: do we need it? calling `registerSubmenu` multiple times is noop, so it does not hurt.
|
||||
registry.registerSubmenu(includeLibMenuPath, 'Include Library', { order: '1' });
|
||||
registry.registerMenuAction([...includeLibMenuPath, '1_install'], {
|
||||
commandId: AddZipLibrary.Commands.ADD_ZIP_LIBRARY.id,
|
||||
label: 'Add .ZIP Library...',
|
||||
order: '1'
|
||||
});
|
||||
}
|
||||
|
||||
async addZipLibrary(): Promise<void> {
|
||||
const homeUri = await this.envVariableServer.getHomeDirUri();
|
||||
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
|
||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
|
||||
title: "Select a zip file containing the library you'd like to add",
|
||||
defaultPath,
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: 'Library',
|
||||
extensions: ['zip']
|
||||
}
|
||||
]
|
||||
});
|
||||
if (!canceled && filePaths.length) {
|
||||
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
|
||||
const dialog = new InstallationProgressDialog('Installing library', 'zip');
|
||||
try {
|
||||
this.outputChannelManager.getChannel('Arduino').clear();
|
||||
dialog.open();
|
||||
await this.libraryService.installZip({ zipUri });
|
||||
} catch (e) {
|
||||
this.messageService.error(e.toString());
|
||||
} finally {
|
||||
dialog.close();
|
||||
}
|
||||
private async addZipLibrary(): Promise<void> {
|
||||
const homeUri = await this.envVariableServer.getHomeDirUri();
|
||||
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
|
||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/selectZip',
|
||||
"Select a zip file containing the library you'd like to add"
|
||||
),
|
||||
defaultPath,
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/library/zipLibrary', 'Library'),
|
||||
extensions: ['zip'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
if (!canceled && filePaths.length) {
|
||||
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
|
||||
try {
|
||||
await this.doInstall(zipUri);
|
||||
} catch (error) {
|
||||
if (error instanceof AlreadyInstalledError) {
|
||||
const result = await new ConfirmDialog({
|
||||
msg: error.message,
|
||||
title: nls.localize(
|
||||
'arduino/library/overwriteExistingLibrary',
|
||||
'Do you want to overwrite the existing library?'
|
||||
),
|
||||
ok: nls.localize('vscode/extensionsUtils/yes', 'Yes'),
|
||||
cancel: nls.localize('vscode/extensionsUtils/no', 'No'),
|
||||
}).open();
|
||||
if (result) {
|
||||
await this.doInstall(zipUri, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
|
||||
try {
|
||||
await ExecuteWithProgress.doWithProgress({
|
||||
messageService: this.messageService,
|
||||
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) {
|
||||
if (error instanceof Error) {
|
||||
const match = error.message.match(/library (.*?) already installed/);
|
||||
if (match && match.length >= 2) {
|
||||
const name = match[1].trim();
|
||||
if (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 {
|
||||
throw new AlreadyInstalledError(
|
||||
nls.localize(
|
||||
'arduino/library/libraryAlreadyExists',
|
||||
'A library already exists. Do you want to overwrite it?'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.messageService.error(error.toString());
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AlreadyInstalledError extends Error {
|
||||
constructor(message: string, readonly libraryName?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, AlreadyInstalledError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AddZipLibrary {
|
||||
export namespace Commands {
|
||||
export const ADD_ZIP_LIBRARY: Command = {
|
||||
id: 'arduino-add-zip-library'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const ADD_ZIP_LIBRARY: Command = {
|
||||
id: 'arduino-add-zip-library',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,79 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
import {
|
||||
SketchContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class ArchiveSketch extends SketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
|
||||
execute: () => this.archiveSketch(),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
|
||||
execute: () => this.archiveSketch()
|
||||
});
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/archiveSketch', 'Archive Sketch'),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
private async archiveSketch(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
|
||||
label: 'Archive Sketch',
|
||||
order: '1'
|
||||
});
|
||||
const archiveBasename = `${sketch.name}-${dateFormat(
|
||||
new Date(),
|
||||
'yymmdd'
|
||||
)}a.zip`;
|
||||
const defaultContainerUri = await this.defaultUri();
|
||||
const defaultUri = defaultContainerUri.resolve(archiveBasename);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveSketchAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
);
|
||||
if (!filePath || canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected async archiveSketch(): Promise<void> {
|
||||
const [sketch, config] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration()
|
||||
]);
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
const archiveBasename = `${sketch.name}-${dateFormat(new Date(), 'yymmdd')}a.zip`;
|
||||
const defaultPath = await this.fileService.fsPath(new URI(config.sketchDirUri).resolve(archiveBasename));
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath });
|
||||
if (!filePath || canceled) {
|
||||
return;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
if (!destinationUri) {
|
||||
return;
|
||||
}
|
||||
await this.sketchService.archive(sketch, destinationUri.toString());
|
||||
this.messageService.info(`Created archive '${archiveBasename}'.`, { timeout: 2000 });
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
if (!destinationUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sketchesService.archive(sketch, destinationUri.toString());
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/sketch/createdArchive',
|
||||
"Created archive '{0}'.",
|
||||
archiveBasename
|
||||
),
|
||||
{
|
||||
timeout: 2000,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ArchiveSketch {
|
||||
export namespace Commands {
|
||||
export const ARCHIVE_SKETCH: Command = {
|
||||
id: 'arduino-archive-sketch'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const ARCHIVE_SKETCH: Command = {
|
||||
id: 'arduino-archive-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,222 +1,346 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { firstToUpperCase } from '../../common/utils';
|
||||
import {
|
||||
DisposableCollection,
|
||||
Disposable,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { BoardsListWidget } from '../boards/boards-list-widget';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { ArduinoMenus, PlaceholderMenuNode, unregisterSubmenu } from '../menu/arduino-menus';
|
||||
import { BoardsService, InstalledBoardWithPackage, AvailablePorts, Port } from '../../common/protocol';
|
||||
import {
|
||||
ArduinoMenus,
|
||||
PlaceholderMenuNode,
|
||||
unregisterSubmenu,
|
||||
} from '../menu/arduino-menus';
|
||||
import {
|
||||
BoardsService,
|
||||
InstalledBoardWithPackage,
|
||||
AvailablePorts,
|
||||
Port,
|
||||
getBoardInfo,
|
||||
} from '../../common/protocol';
|
||||
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class BoardSelection extends SketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuModelRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuModelRegistry: MenuModelRegistry;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
|
||||
|
||||
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
|
||||
execute: async () => {
|
||||
const { selectedBoard, selectedPort } = this.boardsServiceProvider.boardsConfig;
|
||||
if (!selectedBoard) {
|
||||
this.messageService.info('Please select a board to obtain board info.');
|
||||
return;
|
||||
}
|
||||
if (!selectedBoard.fqbn) {
|
||||
this.messageService.info(`The platform for the selected '${selectedBoard.name}' board is not installed.`);
|
||||
return;
|
||||
}
|
||||
if (!selectedPort) {
|
||||
this.messageService.info('Please select a port to obtain board info.');
|
||||
return;
|
||||
}
|
||||
const boardDetails = await this.boardsService.getBoardDetails({ fqbn: selectedBoard.fqbn });
|
||||
if (boardDetails) {
|
||||
const { VID, PID } = boardDetails;
|
||||
const detail = `BN: ${selectedBoard.name}
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
|
||||
execute: async () => {
|
||||
const boardInfo = await getBoardInfo(
|
||||
this.boardsServiceProvider.boardsConfig.selectedPort,
|
||||
this.boardsService.getState()
|
||||
);
|
||||
if (typeof boardInfo === 'string') {
|
||||
this.messageService.info(boardInfo);
|
||||
return;
|
||||
}
|
||||
const { BN, VID, PID, SN } = boardInfo;
|
||||
const detail = `
|
||||
BN: ${BN}
|
||||
VID: ${VID}
|
||||
PID: ${PID}`;
|
||||
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
message: 'Board Info',
|
||||
title: 'Board Info',
|
||||
type: 'info',
|
||||
detail,
|
||||
buttons: ['OK']
|
||||
});
|
||||
}
|
||||
}
|
||||
PID: ${PID}
|
||||
SN: ${SN}
|
||||
`.trim();
|
||||
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
type: 'info',
|
||||
detail,
|
||||
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
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));
|
||||
}
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
protected async updateMenus(): Promise<void> {
|
||||
const [installedBoards, availablePorts, config] = await Promise.all([
|
||||
this.installedBoards(),
|
||||
this.boardsService.getState(),
|
||||
this.boardsServiceProvider.boardsConfig
|
||||
]);
|
||||
this.rebuildMenus(installedBoards, availablePorts, config);
|
||||
}
|
||||
override async onReady(): Promise<void> {
|
||||
this.updateMenus();
|
||||
}
|
||||
|
||||
protected rebuildMenus(installedBoards: InstalledBoardWithPackage[], availablePorts: AvailablePorts, config: BoardsConfig.Config): void {
|
||||
this.toDisposeBeforeMenuRebuild.dispose();
|
||||
protected async updateMenus(): Promise<void> {
|
||||
const [installedBoards, availablePorts, config] = await Promise.all([
|
||||
this.installedBoards(),
|
||||
this.boardsService.getState(),
|
||||
this.boardsServiceProvider.boardsConfig,
|
||||
]);
|
||||
this.rebuildMenus(installedBoards, availablePorts, config);
|
||||
}
|
||||
|
||||
// Boards submenu
|
||||
const boardsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '1_boards'];
|
||||
const boardsSubmenuLabel = config.selectedBoard?.name;
|
||||
// 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.
|
||||
this.menuModelRegistry.registerSubmenu(boardsSubmenuPath, `Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`, { order: '100' });
|
||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)));
|
||||
protected rebuildMenus(
|
||||
installedBoards: InstalledBoardWithPackage[],
|
||||
availablePorts: AvailablePorts,
|
||||
config: BoardsConfig.Config
|
||||
): void {
|
||||
this.toDisposeBeforeMenuRebuild.dispose();
|
||||
|
||||
// Ports submenu
|
||||
const portsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '2_ports'];
|
||||
const portsSubmenuLabel = config.selectedPort?.address;
|
||||
this.menuModelRegistry.registerSubmenu(portsSubmenuPath, `Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`, { order: '101' });
|
||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)));
|
||||
// Boards submenu
|
||||
const boardsSubmenuPath = [
|
||||
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||
'1_boards',
|
||||
];
|
||||
const boardsSubmenuLabel = config.selectedBoard?.name;
|
||||
// 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.
|
||||
this.menuModelRegistry.registerSubmenu(
|
||||
boardsSubmenuPath,
|
||||
nls.localize(
|
||||
'arduino/board/board',
|
||||
'Board{0}',
|
||||
!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''
|
||||
),
|
||||
{ order: '100' }
|
||||
);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
Disposable.create(() =>
|
||||
unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)
|
||||
)
|
||||
);
|
||||
|
||||
const getBoardInfo = { commandId: BoardSelection.Commands.GET_BOARD_INFO.id, label: 'Get Board Info', order: '103' };
|
||||
this.menuModelRegistry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, getBoardInfo);
|
||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuAction(getBoardInfo)));
|
||||
// Ports submenu
|
||||
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
|
||||
const portsSubmenuLabel = config.selectedPort?.address;
|
||||
this.menuModelRegistry.registerSubmenu(
|
||||
portsSubmenuPath,
|
||||
nls.localize(
|
||||
'arduino/board/port',
|
||||
'Port{0}',
|
||||
portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''
|
||||
),
|
||||
{ order: '101' }
|
||||
);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
Disposable.create(() =>
|
||||
unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)
|
||||
)
|
||||
);
|
||||
|
||||
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
|
||||
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
|
||||
const getBoardInfo = {
|
||||
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
|
||||
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)
|
||||
)
|
||||
);
|
||||
|
||||
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
|
||||
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
||||
label: 'Boards Manager...'
|
||||
});
|
||||
const boardsManagerGroup = [...boardsSubmenuPath, '0_manager'];
|
||||
const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages'];
|
||||
|
||||
// Installed boards
|
||||
for (const board of installedBoards) {
|
||||
const { packageId, packageName, fqbn, name } = board;
|
||||
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
|
||||
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
||||
label: `${BoardsListWidget.WIDGET_LABEL}...`,
|
||||
});
|
||||
|
||||
// Platform submenu
|
||||
const platformMenuPath = [...boardsPackagesGroup, packageId];
|
||||
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
|
||||
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageName);
|
||||
// Installed boards
|
||||
installedBoards.forEach((board, index) => {
|
||||
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
|
||||
|
||||
const id = `arduino-select-board--${fqbn}`;
|
||||
const command = { id };
|
||||
const handler = {
|
||||
execute: () => {
|
||||
if (fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn) {
|
||||
this.boardsServiceProvider.boardsConfig = {
|
||||
selectedBoard: {
|
||||
name,
|
||||
fqbn,
|
||||
port: this.boardsServiceProvider.boardsConfig.selectedBoard?.port // TODO: verify!
|
||||
},
|
||||
selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort
|
||||
}
|
||||
}
|
||||
},
|
||||
isToggled: () => fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
|
||||
const packageLabel =
|
||||
packageName +
|
||||
`${
|
||||
manuallyInstalled
|
||||
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||
: ''
|
||||
}`;
|
||||
// Platform submenu
|
||||
const platformMenuPath = [...boardsPackagesGroup, packageId];
|
||||
// Note: Registering the same submenu twice is a noop. No need to group the boards per platform.
|
||||
this.menuModelRegistry.registerSubmenu(platformMenuPath, packageLabel, {
|
||||
order: packageName.toLowerCase(),
|
||||
});
|
||||
|
||||
const id = `arduino-select-board--${fqbn}`;
|
||||
const command = { id };
|
||||
const handler = {
|
||||
execute: () => {
|
||||
if (
|
||||
fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn
|
||||
) {
|
||||
this.boardsServiceProvider.boardsConfig = {
|
||||
selectedBoard: {
|
||||
name,
|
||||
fqbn,
|
||||
port: this.boardsServiceProvider.boardsConfig.selectedBoard
|
||||
?.port, // TODO: verify!
|
||||
},
|
||||
selectedPort:
|
||||
this.boardsServiceProvider.boardsConfig.selectedPort,
|
||||
};
|
||||
}
|
||||
},
|
||||
isToggled: () =>
|
||||
fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn,
|
||||
};
|
||||
|
||||
// Board menu
|
||||
const menuAction = { commandId: id, label: name };
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
|
||||
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
|
||||
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
|
||||
// Board menu
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label: name,
|
||||
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
|
||||
);
|
||||
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
|
||||
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
|
||||
});
|
||||
|
||||
// Installed ports
|
||||
const registerPorts = (
|
||||
protocol: string,
|
||||
protocolOrder: number,
|
||||
ports: AvailablePorts
|
||||
) => {
|
||||
const portIDs = Object.keys(ports);
|
||||
if (!portIDs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register placeholder for protocol
|
||||
const menuPath = [
|
||||
...portsSubmenuPath,
|
||||
`${protocolOrder.toString()}_${protocol}`,
|
||||
];
|
||||
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.toDisposeBeforeMenuRebuild.push(
|
||||
Disposable.create(() =>
|
||||
this.menuModelRegistry.unregisterMenuNode(placeholder.id)
|
||||
)
|
||||
);
|
||||
|
||||
// First we show addresses with recognized boards connected,
|
||||
// then all the rest.
|
||||
const sortedIDs = Object.keys(ports).sort(
|
||||
(left: string, right: string): number => {
|
||||
const [, leftBoards] = ports[left];
|
||||
const [, rightBoards] = ports[right];
|
||||
return rightBoards.length - leftBoards.length;
|
||||
}
|
||||
);
|
||||
|
||||
// Installed ports
|
||||
const registerPorts = (ports: AvailablePorts) => {
|
||||
const addresses = Object.keys(ports);
|
||||
if (!addresses.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register placeholder for protocol
|
||||
const [port] = ports[addresses[0]];
|
||||
const protocol = port.protocol;
|
||||
const menuPath = [...portsSubmenuPath, protocol];
|
||||
const placeholder = new PlaceholderMenuNode(menuPath, `${firstToUpperCase(port.protocol)} ports`);
|
||||
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
|
||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuNode(placeholder.id)));
|
||||
|
||||
for (const address of addresses) {
|
||||
if (!!ports[address]) {
|
||||
const [port, boards] = ports[address];
|
||||
if (!boards.length) {
|
||||
boards.push({
|
||||
name: ''
|
||||
});
|
||||
}
|
||||
for (const { name, fqbn } of boards) {
|
||||
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''}`;
|
||||
const command = { id };
|
||||
const handler = {
|
||||
execute: () => {
|
||||
if (!Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)) {
|
||||
this.boardsServiceProvider.boardsConfig = {
|
||||
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
|
||||
selectedPort: port
|
||||
}
|
||||
}
|
||||
},
|
||||
isToggled: () => Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)
|
||||
};
|
||||
const label = `${address}${name ? ` (${name})` : ''}`;
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label,
|
||||
order: `1${label}` // `1` comes after the placeholder which has order `0`
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
|
||||
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 handler = {
|
||||
execute: () => {
|
||||
if (
|
||||
!Port.sameAs(
|
||||
port,
|
||||
this.boardsServiceProvider.boardsConfig.selectedPort
|
||||
)
|
||||
) {
|
||||
this.boardsServiceProvider.boardsConfig = {
|
||||
selectedBoard:
|
||||
this.boardsServiceProvider.boardsConfig.selectedBoard,
|
||||
selectedPort: port,
|
||||
};
|
||||
}
|
||||
},
|
||||
isToggled: () =>
|
||||
Port.sameAs(
|
||||
port,
|
||||
this.boardsServiceProvider.boardsConfig.selectedPort
|
||||
),
|
||||
};
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label,
|
||||
order: String(protocolOrder + i + 1).padStart(4),
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
Disposable.create(() =>
|
||||
this.commandRegistry.unregisterCommand(command)
|
||||
)
|
||||
);
|
||||
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
|
||||
}
|
||||
};
|
||||
|
||||
const { serial, network, unknown } = AvailablePorts.groupByProtocol(availablePorts);
|
||||
registerPorts(serial);
|
||||
registerPorts(network);
|
||||
registerPorts(unknown);
|
||||
const grouped = AvailablePorts.groupByProtocol(availablePorts);
|
||||
let protocolOrder = 100;
|
||||
// We first show serial and network ports, then all the rest
|
||||
['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();
|
||||
}
|
||||
|
||||
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
|
||||
const allBoards = await this.boardsService.allBoards({});
|
||||
return allBoards.filter(InstalledBoardWithPackage.is);
|
||||
}
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
|
||||
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
|
||||
const allBoards = await this.boardsService.getInstalledBoards();
|
||||
return allBoards.filter(InstalledBoardWithPackage.is);
|
||||
}
|
||||
}
|
||||
export namespace BoardSelection {
|
||||
export namespace Commands {
|
||||
export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' };
|
||||
}
|
||||
export namespace Commands {
|
||||
export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +1,88 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { OutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { MonitorConnection } from '../monitor/monitor-connection';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
CoreServiceContribution,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class BurnBootloader extends SketchContribution {
|
||||
export class BurnBootloader extends CoreServiceContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
|
||||
execute: () => this.burnBootloader(),
|
||||
});
|
||||
}
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
|
||||
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
|
||||
label: nls.localize(
|
||||
'arduino/bootloader/burnBootloader',
|
||||
'Burn Bootloader'
|
||||
),
|
||||
order: 'z99',
|
||||
});
|
||||
}
|
||||
|
||||
@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, {
|
||||
execute: () => this.burnBootloader()
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
|
||||
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
|
||||
label: 'Burn Bootloader',
|
||||
order: 'z99'
|
||||
});
|
||||
}
|
||||
|
||||
async burnBootloader(): Promise<void> {
|
||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.disconnect();
|
||||
}
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const port = boardsConfig.selectedPort?.address;
|
||||
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = 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.outputChannelManager.getChannel('Arduino').clear();
|
||||
await this.coreService.burnBootloader({
|
||||
fqbn,
|
||||
programmer,
|
||||
port,
|
||||
verify,
|
||||
verbose
|
||||
});
|
||||
this.messageService.info('Done burning bootloader.', { timeout: 1000 });
|
||||
} catch (e) {
|
||||
this.messageService.error(e.toString());
|
||||
} finally {
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.connect(monitorConfig);
|
||||
}
|
||||
private async burnBootloader(): Promise<void> {
|
||||
this.clearVisibleNotification();
|
||||
const options = await this.options();
|
||||
try {
|
||||
await this.doWithProgress({
|
||||
progressText: nls.localize(
|
||||
'arduino/bootloader/burningBootloader',
|
||||
'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.preferences.get('arduino.upload.verify'),
|
||||
this.preferences.get('arduino.upload.verbose'),
|
||||
]);
|
||||
return {
|
||||
fqbn,
|
||||
programmer,
|
||||
port,
|
||||
verify,
|
||||
verbose,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace BurnBootloader {
|
||||
export namespace Commands {
|
||||
export const BURN_BOOTLOADER: Command = {
|
||||
id: 'arduino-burn-bootloader'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const BURN_BOOTLOADER: Command = {
|
||||
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,119 +1,230 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { toArray } from '@phosphor/algorithm';
|
||||
import { remote } from 'electron';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
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 { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
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 '../sketches-service-client-impl';
|
||||
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.
|
||||
*/
|
||||
@injectable()
|
||||
export class Close extends SketchContribution {
|
||||
private shell: ApplicationShell | undefined;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
override onStart(app: FrontendApplication): MaybePromise<void> {
|
||||
this.shell = app.shell;
|
||||
}
|
||||
|
||||
protected shell: ApplicationShell;
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
this.shell = app.shell;
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Close.Commands.CLOSE, {
|
||||
execute: async () => {
|
||||
|
||||
// Close current editor if closeable.
|
||||
const { currentEditor } = this.editorManager;
|
||||
if (currentEditor && currentEditor.title.closable) {
|
||||
currentEditor.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close current widget from the main area if possible.
|
||||
const { currentWidget } = this.shell;
|
||||
if (currentWidget) {
|
||||
const currentWidgetInMain = toArray(this.shell.mainPanel.widgets()).find(widget => widget === currentWidget);
|
||||
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
|
||||
return currentWidgetInMain.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Close the sketch (window).
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
if (isTemp && await this.wasTouched(uri)) {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ["Don't Save", 'Cancel', 'Save'],
|
||||
message: 'Do you want to save changes to this sketch before closing?',
|
||||
detail: "If you don't save, your changes will be lost."
|
||||
});
|
||||
if (response === 1) { // Cancel
|
||||
return;
|
||||
}
|
||||
if (response === 2) { // Save
|
||||
const saved = await this.commandService.executeCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH.id, { openAfterMove: false, execOnlyIfTemp: true });
|
||||
if (!saved) { // If it was not saved, do bail the close.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: Close.Commands.CLOSE.id,
|
||||
label: 'Close',
|
||||
order: '5'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: Close.Commands.CLOSE.id,
|
||||
keybinding: 'CtrlCmd+W'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
|
||||
*/
|
||||
protected async wasTouched(uri: string): Promise<boolean> {
|
||||
const editorWidget = await this.editorManager.getByUri(new URI(uri));
|
||||
if (editorWidget) {
|
||||
const { editor } = editorWidget;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
const versionId = editor.getControl().getModel()?.getVersionId();
|
||||
if (Number.isInteger(versionId) && versionId! > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Close.Commands.CLOSE, {
|
||||
execute: () => {
|
||||
// Close current editor if closeable.
|
||||
const { currentEditor } = this.editorManager;
|
||||
if (currentEditor && currentEditor.title.closable) {
|
||||
currentEditor.close();
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
|
||||
if (this.shell) {
|
||||
// Close current widget from the main area if possible.
|
||||
const { currentWidget } = this.shell;
|
||||
if (currentWidget) {
|
||||
const currentWidgetInMain = toArray(
|
||||
this.shell.mainPanel.widgets()
|
||||
).find((widget) => widget === currentWidget);
|
||||
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
|
||||
return currentWidgetInMain.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
return remote.getCurrentWindow().close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: Close.Commands.CLOSE.id,
|
||||
label: nls.localize('vscode/editor.contribution/close', 'Close'),
|
||||
order: '6',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: Close.Commands.CLOSE.id,
|
||||
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.sketchesService.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.
|
||||
*/
|
||||
protected async wasTouched(uri: string): Promise<boolean> {
|
||||
const editorWidget = await this.editorManager.getByUri(new URI(uri));
|
||||
if (editorWidget) {
|
||||
const { editor } = editorWidget;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
const versionId = editor.getControl().getModel()?.getVersionId();
|
||||
if (this.isInteger(versionId) && versionId > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isInteger(arg: unknown): arg is number {
|
||||
return Number.isInteger(arg);
|
||||
}
|
||||
}
|
||||
|
||||
enum Prompt {
|
||||
Save,
|
||||
DoNotSave,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
export namespace Close {
|
||||
export namespace Commands {
|
||||
export const CLOSE: Command = {
|
||||
id: 'arduino-close'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const CLOSE: Command = {
|
||||
id: 'arduino-close',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateApi } from '../create/create-api';
|
||||
import { CreateFeatures } from '../create/create-features';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { Create, isNotFound } from '../create/typings';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
|
||||
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
|
||||
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
|
||||
import { SketchContribution } from './contribution';
|
||||
|
||||
export function sketchAlreadyExists(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/alreadyExists',
|
||||
"Cloud sketch '{0}' already exists.",
|
||||
input
|
||||
);
|
||||
}
|
||||
export function sketchNotFound(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/notFound',
|
||||
"Could not pull the cloud sketch '{0}'. It does not exist.",
|
||||
input
|
||||
);
|
||||
}
|
||||
export const synchronizingSketchbook = nls.localize(
|
||||
'arduino/cloudSketch/synchronizingSketchbook',
|
||||
'Synchronizing sketchbook...'
|
||||
);
|
||||
export function pullingSketch(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/pulling',
|
||||
"Synchronizing sketchbook, pulling '{0}'...",
|
||||
input
|
||||
);
|
||||
}
|
||||
export function pushingSketch(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/pushing',
|
||||
"Synchronizing sketchbook, pushing '{0}'...",
|
||||
input
|
||||
);
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class CloudSketchContribution extends SketchContribution {
|
||||
@inject(SketchbookWidgetContribution)
|
||||
private readonly widgetContribution: SketchbookWidgetContribution;
|
||||
@inject(CreateApi)
|
||||
protected readonly createApi: CreateApi;
|
||||
@inject(CreateFeatures)
|
||||
protected readonly createFeatures: CreateFeatures;
|
||||
|
||||
protected async treeModel(): Promise<
|
||||
(CloudSketchbookTreeModel & { root: CompositeTreeNode }) | undefined
|
||||
> {
|
||||
const { enabled, session } = this.createFeatures;
|
||||
if (enabled && session) {
|
||||
const widget = await this.widgetContribution.widget;
|
||||
const treeModel = this.treeModelFrom(widget);
|
||||
if (treeModel) {
|
||||
const root = treeModel.root;
|
||||
if (CompositeTreeNode.is(root)) {
|
||||
return treeModel as CloudSketchbookTreeModel & {
|
||||
root: CompositeTreeNode;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async pull(
|
||||
sketch: Create.Sketch
|
||||
): Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (!treeModel) {
|
||||
return undefined;
|
||||
}
|
||||
const id = CreateUri.toUri(sketch).path.toString();
|
||||
const node = treeModel.getNode(id);
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
`Could not find cloud sketchbook tree node with ID: ${id}.`
|
||||
);
|
||||
}
|
||||
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||
throw new Error(
|
||||
`Cloud sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
|
||||
);
|
||||
}
|
||||
try {
|
||||
await treeModel.sketchbookTree().pull({ node });
|
||||
return node;
|
||||
} catch (err) {
|
||||
if (isNotFound(err)) {
|
||||
await treeModel.refresh();
|
||||
this.messageService.error(sketchNotFound(sketch.name));
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private treeModelFrom(
|
||||
widget: SketchbookWidget
|
||||
): CloudSketchbookTreeModel | undefined {
|
||||
for (const treeWidget of widget.getTreeWidgets()) {
|
||||
if (treeWidget instanceof CloudSketchbookTreeWidget) {
|
||||
const model = treeWidget.model;
|
||||
if (model instanceof CloudSketchbookTreeModel) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -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 { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { Saveable } from '@theia/core/lib/browser/saveable';
|
||||
@@ -7,116 +12,307 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
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 '../sketches-service-client-impl';
|
||||
import {
|
||||
SketchesService,
|
||||
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';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { ConfigServiceClient } from '../config/config-service-client';
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
URI,
|
||||
Sketch,
|
||||
open,
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export abstract class Contribution implements CommandContribution, MenuContribution, KeybindingContribution, TabBarToolbarContribution, FrontendApplicationContribution {
|
||||
export abstract class Contribution
|
||||
implements
|
||||
CommandContribution,
|
||||
MenuContribution,
|
||||
KeybindingContribution,
|
||||
TabBarToolbarContribution,
|
||||
FrontendApplicationContribution
|
||||
{
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
@inject(SettingsService)
|
||||
protected readonly settingsService: SettingsService;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly preferences: ArduinoPreferences;
|
||||
|
||||
@inject(SettingsService)
|
||||
protected readonly settingsService: SettingsService;
|
||||
@inject(FrontendApplicationStateService)
|
||||
protected readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
onStart(app: FrontendApplication): MaybePromise<void> {
|
||||
}
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
}
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.appStateService.reachedState('ready').then(() => this.onReady());
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): 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> {}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
|
||||
registerCommands(registry: CommandRegistry): void {}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
|
||||
registerMenus(registry: MenuModelRegistry): 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()
|
||||
export abstract class SketchContribution extends Contribution {
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
@inject(FileSystemExt)
|
||||
protected readonly fileSystemExt: FileSystemExt;
|
||||
|
||||
@inject(FileSystemExt)
|
||||
protected readonly fileSystemExt: FileSystemExt;
|
||||
@inject(ConfigServiceClient)
|
||||
protected readonly configService: ConfigServiceClient;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchesService: SketchesService;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly preferences: ArduinoPreferences;
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
protected async sourceOverride(): Promise<Record<string, string>> {
|
||||
const override: Record<string, string> = {};
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (sketch) {
|
||||
for (const editor of this.editorManager.all) {
|
||||
const uri = editor.editor.uri;
|
||||
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
|
||||
override[uri.toString()] = editor.editor.document.getText();
|
||||
}
|
||||
}
|
||||
protected async sourceOverride(): Promise<Record<string, string>> {
|
||||
const override: Record<string, string> = {};
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (CurrentSketch.isValid(sketch)) {
|
||||
for (const editor of this.editorManager.all) {
|
||||
const uri = editor.editor.uri;
|
||||
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
|
||||
override[uri.toString()] = editor.editor.document.getText();
|
||||
}
|
||||
return override;
|
||||
}
|
||||
}
|
||||
return override;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults to `directories.user` if defined and not CLI config errors were detected.
|
||||
* Otherwise, the URI of the user home directory.
|
||||
*/
|
||||
protected async defaultUri(): Promise<URI> {
|
||||
const errors = this.configService.tryGetMessages();
|
||||
let defaultUri = this.configService.tryGetSketchDirUri();
|
||||
if (!defaultUri || errors?.length) {
|
||||
// Fall back to user home when the `directories.user` is not available or there are known CLI config errors
|
||||
defaultUri = new URI(await this.envVariableServer.getHomeDirUri());
|
||||
}
|
||||
return defaultUri;
|
||||
}
|
||||
|
||||
protected async defaultPath(): Promise<string> {
|
||||
const defaultUri = await this.defaultUri();
|
||||
return this.fileService.fsPath(defaultUri);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
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 function configure<T>(bind: interfaces.Bind, serviceIdentifier: typeof Contribution): void {
|
||||
bind(serviceIdentifier).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(serviceIdentifier);
|
||||
bind(MenuContribution).toService(serviceIdentifier);
|
||||
bind(KeybindingContribution).toService(serviceIdentifier);
|
||||
bind(TabBarToolbarContribution).toService(serviceIdentifier);
|
||||
bind(FrontendApplicationContribution).toService(serviceIdentifier);
|
||||
}
|
||||
export function configure(
|
||||
bind: interfaces.Bind,
|
||||
serviceIdentifier: typeof Contribution
|
||||
): void {
|
||||
bind(serviceIdentifier).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(serviceIdentifier);
|
||||
bind(MenuContribution).toService(serviceIdentifier);
|
||||
bind(KeybindingContribution).toService(serviceIdentifier);
|
||||
bind(TabBarToolbarContribution).toService(serviceIdentifier);
|
||||
bind(FrontendApplicationContribution).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,134 +1,282 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board, BoardsService, ExecutableService } from '../../common/protocol';
|
||||
import {
|
||||
Board,
|
||||
BoardsService,
|
||||
ExecutableService,
|
||||
Sketch,
|
||||
} from '../../common/protocol';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { URI, 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 '../sketches-service-client-impl';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
|
||||
|
||||
@injectable()
|
||||
export class Debug extends SketchContribution {
|
||||
@inject(HostedPluginSupport)
|
||||
private readonly hostedPluginSupport: HostedPluginSupport;
|
||||
|
||||
@inject(HostedPluginSupport)
|
||||
protected hostedPluginSupport: HostedPluginSupport;
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
@inject(ExecutableService)
|
||||
private readonly executableService: ExecutableService;
|
||||
|
||||
@inject(ExecutableService)
|
||||
protected readonly executableService: ExecutableService;
|
||||
@inject(BoardsService)
|
||||
private readonly boardService: BoardsService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardService: BoardsService;
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
/**
|
||||
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
|
||||
*/
|
||||
private _disabledMessages?: string = nls.localize(
|
||||
'arduino/common/noBoardSelected',
|
||||
'No board selected'
|
||||
); // Initial pessimism.
|
||||
private disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
|
||||
private onDisabledMessageDidChange =
|
||||
this.disabledMessageDidChangeEmitter.event;
|
||||
|
||||
/**
|
||||
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
|
||||
*/
|
||||
protected _disabledMessages?: string = 'No board selected'; // Initial pessimism.
|
||||
protected disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
|
||||
protected onDisabledMessageDidChange = this.disabledMessageDidChangeEmitter.event;
|
||||
private get disabledMessage(): string | undefined {
|
||||
return this._disabledMessages;
|
||||
}
|
||||
private set disabledMessage(message: string | undefined) {
|
||||
this._disabledMessages = message;
|
||||
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
|
||||
}
|
||||
|
||||
protected get disabledMessage(): string | undefined {
|
||||
return this._disabledMessages;
|
||||
private readonly debugToolbarItem = {
|
||||
id: Debug.Commands.START_DEBUGGING.id,
|
||||
command: Debug.Commands.START_DEBUGGING.id,
|
||||
tooltip: `${
|
||||
this.disabledMessage
|
||||
? nls.localize(
|
||||
'arduino/debug/debugWithMessage',
|
||||
'Debug - {0}',
|
||||
this.disabledMessage
|
||||
)
|
||||
: Debug.Commands.START_DEBUGGING.label
|
||||
}`,
|
||||
priority: 3,
|
||||
onDidChange: this.onDisabledMessageDidChange as Event<void>,
|
||||
};
|
||||
|
||||
override onStart(): void {
|
||||
this.onDisabledMessageDidChange(
|
||||
() =>
|
||||
(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) {
|
||||
this.disabledMessage = nls.localize(
|
||||
'arduino/common/noBoardSelected',
|
||||
'No board selected'
|
||||
);
|
||||
return;
|
||||
}
|
||||
protected set disabledMessage(message: string | undefined) {
|
||||
this._disabledMessages = message;
|
||||
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
|
||||
const fqbn = board.fqbn;
|
||||
if (!fqbn) {
|
||||
this.disabledMessage = nls.localize(
|
||||
'arduino/debug/noPlatformInstalledFor',
|
||||
"Platform is not installed for '{0}'",
|
||||
board.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
const details = await this.boardService.getBoardDetails({ fqbn });
|
||||
if (!details) {
|
||||
this.disabledMessage = nls.localize(
|
||||
'arduino/debug/noPlatformInstalledFor',
|
||||
"Platform is not installed for '{0}'",
|
||||
board.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { debuggingSupported } = details;
|
||||
if (!debuggingSupported) {
|
||||
this.disabledMessage = nls.localize(
|
||||
'arduino/debug/debuggingNotSupported',
|
||||
"Debugging is not supported by '{0}'",
|
||||
board.name
|
||||
);
|
||||
} else {
|
||||
this.disabledMessage = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly debugToolbarItem = {
|
||||
id: Debug.Commands.START_DEBUGGING.id,
|
||||
command: Debug.Commands.START_DEBUGGING.id,
|
||||
tooltip: `${this.disabledMessage ? `Debug - ${this.disabledMessage}` : 'Start Debugging'}`,
|
||||
priority: 3,
|
||||
onDidChange: this.onDisabledMessageDidChange as Event<void>
|
||||
private async startDebug(
|
||||
board: Board | undefined = this.boardsServiceProvider.boardsConfig
|
||||
.selectedBoard
|
||||
): Promise<void> {
|
||||
if (!board) {
|
||||
return;
|
||||
}
|
||||
const { name, fqbn } = board;
|
||||
if (!fqbn) {
|
||||
return;
|
||||
}
|
||||
await this.hostedPluginSupport.didStart;
|
||||
const [sketch, executables] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.executableService.list(),
|
||||
]);
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
|
||||
sketch
|
||||
);
|
||||
const [cliPath, sketchPath, configPath] = await Promise.all([
|
||||
this.fileService.fsPath(new URI(executables.cliUri)),
|
||||
this.fileService.fsPath(new URI(sketch.uri)),
|
||||
this.fileService.fsPath(new URI(ideTempFolderUri)),
|
||||
]);
|
||||
const config = {
|
||||
cliPath,
|
||||
board: {
|
||||
fqbn,
|
||||
name,
|
||||
},
|
||||
sketchPath,
|
||||
configPath,
|
||||
};
|
||||
|
||||
onStart(): void {
|
||||
this.onDisabledMessageDidChange(() => this.debugToolbarItem.tooltip = `${this.disabledMessage ? `Debug - ${this.disabledMessage}` : 'Start Debugging'}`);
|
||||
const refreshState = async (board: Board | undefined = this.boardsServiceProvider.boardsConfig.selectedBoard) => {
|
||||
if (!board) {
|
||||
this.disabledMessage = 'No board selected';
|
||||
return;
|
||||
}
|
||||
const fqbn = board.fqbn;
|
||||
if (!fqbn) {
|
||||
this.disabledMessage = `Platform is not installed for '${board.name}'`;
|
||||
return;
|
||||
}
|
||||
const details = await this.boardService.getBoardDetails({ fqbn });
|
||||
if (!details) {
|
||||
this.disabledMessage = `Platform is not installed for '${board.name}'`;
|
||||
return;
|
||||
}
|
||||
const { debuggingSupported } = details;
|
||||
if (!debuggingSupported) {
|
||||
this.disabledMessage = `Debugging is not supported by '${board.name}'`;
|
||||
} else {
|
||||
this.disabledMessage = undefined;
|
||||
}
|
||||
try {
|
||||
await this.commandService.executeCommand('arduino.debug.start', config);
|
||||
} catch (err) {
|
||||
if (await this.isSketchNotVerifiedError(err, sketch)) {
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
const answer = await this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/debug/sketchIsNotCompiled',
|
||||
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
|
||||
sketch.name
|
||||
),
|
||||
yes
|
||||
);
|
||||
if (answer === yes) {
|
||||
this.commandService.executeCommand('arduino-verify-sketch');
|
||||
}
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) => refreshState(selectedBoard));
|
||||
this.notificationCenter.onPlatformInstalled(() => refreshState());
|
||||
this.notificationCenter.onPlatformUninstalled(() => refreshState());
|
||||
refreshState();
|
||||
} else {
|
||||
this.messageService.error(
|
||||
err instanceof Error ? err.message : String(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Debug.Commands.START_DEBUGGING, {
|
||||
execute: () => this.startDebug(),
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: () => !this.disabledMessage
|
||||
});
|
||||
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.menuManager.update();
|
||||
}
|
||||
|
||||
private async isSketchNotVerifiedError(
|
||||
err: unknown,
|
||||
sketch: Sketch
|
||||
): Promise<boolean> {
|
||||
if (err instanceof Error) {
|
||||
try {
|
||||
const tempBuildPaths = await this.sketchesService.tempBuildPath(sketch);
|
||||
return tempBuildPaths.some((tempBuildPath) =>
|
||||
err.message.includes(tempBuildPath)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem(this.debugToolbarItem);
|
||||
}
|
||||
|
||||
protected async startDebug(board: Board | undefined = this.boardsServiceProvider.boardsConfig.selectedBoard): Promise<void> {
|
||||
if (!board) {
|
||||
return;
|
||||
}
|
||||
const { name, fqbn } = board;
|
||||
if (!fqbn) {
|
||||
return;
|
||||
}
|
||||
await this.hostedPluginSupport.didStart;
|
||||
const [sketch, executables] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.executableService.list()
|
||||
]);
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
const [cliPath, sketchPath] = await Promise.all([
|
||||
this.fileService.fsPath(new URI(executables.cliUri)),
|
||||
this.fileService.fsPath(new URI(sketch.uri))
|
||||
])
|
||||
const config = {
|
||||
cliPath,
|
||||
board: {
|
||||
fqbn,
|
||||
name
|
||||
},
|
||||
sketchPath
|
||||
};
|
||||
return this.commandService.executeCommand('arduino.debug.start', config);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Debug {
|
||||
export namespace Commands {
|
||||
export const START_DEBUGGING: Command = {
|
||||
id: 'arduino-start-debug',
|
||||
label: 'Start Debugging',
|
||||
category: 'Arduino'
|
||||
}
|
||||
}
|
||||
export namespace Commands {
|
||||
export const START_DEBUGGING = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-start-debug',
|
||||
label: 'Start Debugging',
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
168
arduino-ide-extension/src/browser/contributions/delete-sketch.ts
Normal file
168
arduino-ide-extension/src/browser/contributions/delete-sketch.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { ipcRenderer } from '@theia/core/electron-shared/electron';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import type { MaybeArray } from '@theia/core/lib/common/types';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import type { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
import { SCHEDULE_DELETION_SIGNAL } from '../../electron-common/electron-messages';
|
||||
import { Sketch } from '../contributions/contribution';
|
||||
import { isNotFound } from '../create/typings';
|
||||
import { Command, CommandRegistry } from './contribution';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
|
||||
export interface DeleteSketchParams {
|
||||
/**
|
||||
* Either the URI of the sketch folder or the sketch to delete.
|
||||
*/
|
||||
readonly toDelete: string | Sketch;
|
||||
/**
|
||||
* If `true`, the currently opened sketch is expected to be deleted.
|
||||
* Hence, the editors must be closed, the sketch will be scheduled
|
||||
* for deletion, and the browser window will close or navigate away.
|
||||
* If `false`, the sketch will be scheduled for deletion,
|
||||
* but the current window remains open. If `force`, the window will
|
||||
* navigate away, but IDE2 won't open any confirmation dialogs.
|
||||
*/
|
||||
readonly willNavigateAway?: boolean | 'force';
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DeleteSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
|
||||
execute: (params: DeleteSketchParams) => this.deleteSketch(params),
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteSketch(params: DeleteSketchParams): Promise<void> {
|
||||
const { toDelete, willNavigateAway } = params;
|
||||
let sketch: Sketch;
|
||||
if (typeof toDelete === 'string') {
|
||||
const resolvedSketch = await this.loadSketch(toDelete);
|
||||
if (!resolvedSketch) {
|
||||
console.info(
|
||||
`Failed to load the sketch. It was not found at '${toDelete}'. Skipping deletion.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
sketch = resolvedSketch;
|
||||
} else {
|
||||
sketch = toDelete;
|
||||
}
|
||||
if (!willNavigateAway) {
|
||||
this.scheduleDeletion(sketch);
|
||||
return;
|
||||
}
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (willNavigateAway !== 'force') {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localizeByDefault('Delete'),
|
||||
type: 'question',
|
||||
buttons: [Dialog.CANCEL, Dialog.OK],
|
||||
message: cloudUri
|
||||
? nls.localize(
|
||||
'theia/workspace/deleteCloudSketch',
|
||||
"The cloud sketch '{0}' will be permanently deleted from the Arduino servers and the local caches. This action is irreversible. Do you want to delete the current sketch?",
|
||||
sketch.name
|
||||
)
|
||||
: nls.localize(
|
||||
'theia/workspace/deleteCurrentSketch',
|
||||
"The sketch '{0}' will be permanently deleted. This action is irreversible. Do you want to delete the current sketch?",
|
||||
sketch.name
|
||||
),
|
||||
});
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cloudUri) {
|
||||
const posixPath = cloudUri.path.toString();
|
||||
const cloudSketch = this.createApi.sketchCache.getSketch(posixPath);
|
||||
if (!cloudSketch) {
|
||||
throw new Error(
|
||||
`Cloud sketch with path '${posixPath}' was not cached. Cache: ${this.createApi.sketchCache.toString()}`
|
||||
);
|
||||
}
|
||||
try {
|
||||
// IDE2 cannot use DELETE directory as the server responses with HTTP 500 if it's missing.
|
||||
// https://github.com/arduino/arduino-ide/issues/1825#issuecomment-1406301406
|
||||
await this.createApi.deleteSketch(cloudSketch.path);
|
||||
} catch (err) {
|
||||
if (!isNotFound(err)) {
|
||||
throw err;
|
||||
} else {
|
||||
console.info(
|
||||
`Could not delete the cloud sketch with path '${posixPath}'. It does not exist.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all([
|
||||
...Sketch.uris(sketch).map((uri) =>
|
||||
this.closeWithoutSaving(new URI(uri))
|
||||
),
|
||||
]);
|
||||
this.windowService.setSafeToShutDown();
|
||||
this.scheduleDeletion(sketch);
|
||||
return window.close();
|
||||
}
|
||||
|
||||
private scheduleDeletion(sketch: Sketch): void {
|
||||
ipcRenderer.send(SCHEDULE_DELETION_SIGNAL, sketch);
|
||||
}
|
||||
|
||||
private async loadSketch(uri: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchesService.loadSketch(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// fix: https://github.com/eclipse-theia/theia/issues/12107
|
||||
private async closeWithoutSaving(uri: URI): Promise<void> {
|
||||
const affected = getAffected(this.shell.widgets, uri);
|
||||
const toClose = [...affected].map(([, widget]) => widget);
|
||||
await this.shell.closeMany(toClose, { save: false });
|
||||
}
|
||||
}
|
||||
export namespace DeleteSketch {
|
||||
export namespace Commands {
|
||||
export const DELETE_SKETCH: Command = {
|
||||
id: 'arduino-delete-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getAffected<T extends Widget>(
|
||||
widgets: Iterable<T>,
|
||||
context: MaybeArray<URI>
|
||||
): [URI, T & NavigatableWidget][] {
|
||||
const uris = Array.isArray(context) ? context : [context];
|
||||
const result: [URI, T & NavigatableWidget][] = [];
|
||||
for (const widget of widgets) {
|
||||
if (NavigatableWidget.is(widget)) {
|
||||
const resourceUri = widget.getResourceUri();
|
||||
if (resourceUri && uris.some((uri) => uri.isEqualOrParent(resourceUri))) {
|
||||
result.push([resourceUri, widget]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,294 +1,274 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
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 { Contribution, Command, MenuModelRegistry, KeybindingRegistry, CommandRegistry } from './contribution';
|
||||
import {
|
||||
Contribution,
|
||||
Command,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
CommandRegistry,
|
||||
} from './contribution';
|
||||
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
|
||||
// Depends on https://github.com/eclipse-theia/theia/pull/7964
|
||||
@injectable()
|
||||
export class EditContributions extends Contribution {
|
||||
@inject(MonacoEditorService)
|
||||
private readonly codeEditorService: MonacoEditorService;
|
||||
|
||||
@inject(MonacoEditorService)
|
||||
protected readonly codeEditorService: MonacoEditorService;
|
||||
@inject(ClipboardService)
|
||||
private readonly clipboardService: ClipboardService;
|
||||
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferences: PreferenceService;
|
||||
|
||||
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, {
|
||||
execute: async () => {
|
||||
const settings = await this.settingsService.settings();
|
||||
if (settings.autoScaleInterface) {
|
||||
settings.interfaceScale = settings.interfaceScale - 1;
|
||||
} else {
|
||||
settings.editorFontSize = settings.editorFontSize - 1;
|
||||
}
|
||||
await this.settingsService.update(settings);
|
||||
await this.settingsService.save();
|
||||
}
|
||||
});
|
||||
/* Tools */registry.registerCommand(EditContributions.Commands.AUTO_FORMAT, { execute: () => this.run('editor.action.formatDocument') });
|
||||
registry.registerCommand(EditContributions.Commands.COPY_FOR_FORUM, {
|
||||
execute: async () => {
|
||||
const value = await this.currentValue();
|
||||
if (value !== undefined) {
|
||||
this.clipboardService.writeText(`[code]
|
||||
override 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('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, {
|
||||
execute: async () => {
|
||||
const value = await this.currentValue();
|
||||
if (value !== undefined) {
|
||||
this.clipboardService.writeText(`
|
||||
\`\`\`cpp
|
||||
${value}
|
||||
[/code]`)
|
||||
}
|
||||
}
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.COPY_FOR_GITHUB, {
|
||||
execute: async () => {
|
||||
const value = await this.currentValue();
|
||||
if (value !== undefined) {
|
||||
this.clipboardService.writeText(`\`\`\`cpp
|
||||
${value}
|
||||
\`\`\``)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.CUT.id,
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.COPY.id,
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||
label: 'Copy for Forum',
|
||||
order: '2'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
|
||||
label: 'Copy for GitHub',
|
||||
order: '3'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.PASTE.id,
|
||||
order: '4'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.SELECT_ALL.id,
|
||||
order: '5'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.GO_TO_LINE.id,
|
||||
label: 'Go to Line...',
|
||||
order: '6'
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
|
||||
label: 'Comment/Uncomment',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INDENT_LINES.id,
|
||||
label: 'Increase Indent',
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.OUTDENT_LINES.id,
|
||||
label: 'Decrease Indent',
|
||||
order: '2'
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: 'Increase Font Size',
|
||||
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, {
|
||||
commandId: EditContributions.Commands.FIND.id,
|
||||
label: 'Find',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND_NEXT.id,
|
||||
label: 'Find Next',
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
|
||||
label: 'Find Previous',
|
||||
order: '2'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.USE_FOR_FIND.id,
|
||||
label: 'Use Selection for Find', // XXX: The Java IDE uses `Use Selection For Find`.
|
||||
order: '3'
|
||||
});
|
||||
|
||||
// `Tools`
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: EditContributions.Commands.AUTO_FORMAT.id,
|
||||
label: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||
keybinding: 'CtrlCmd+Shift+C',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.COPY_FOR_GITHUB.id,
|
||||
keybinding: 'CtrlCmd+Alt+C',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.GO_TO_LINE.id,
|
||||
keybinding: 'CtrlCmd+L',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.TOGGLE_COMMENT.id,
|
||||
keybinding: 'CtrlCmd+/',
|
||||
when: 'editorFocus'
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+='
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+-'
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND.id,
|
||||
keybinding: 'CtrlCmd+F'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND_NEXT.id,
|
||||
keybinding: 'CtrlCmd+G'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND_PREVIOUS.id,
|
||||
keybinding: 'CtrlCmd+Shift+G'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.USE_FOR_FIND.id,
|
||||
keybinding: 'CtrlCmd+E'
|
||||
});
|
||||
|
||||
// `Tools`
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.AUTO_FORMAT.id,
|
||||
keybinding: 'CtrlCmd+T'
|
||||
});
|
||||
}
|
||||
|
||||
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
|
||||
return this.codeEditorService.getFocusedCodeEditor() || this.codeEditorService.getActiveCodeEditor();
|
||||
}
|
||||
|
||||
protected async currentValue(): Promise<string | undefined> {
|
||||
const currentEditor = await this.current();
|
||||
if (currentEditor) {
|
||||
const selection = currentEditor.getSelection();
|
||||
if (!selection || selection.isEmpty()) {
|
||||
return currentEditor.getValue();
|
||||
}
|
||||
return currentEditor.getModel()?.getValueInRange(selection);
|
||||
\`\`\`
|
||||
`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected async run(commandId: string): Promise<any> {
|
||||
const editor = await this.current();
|
||||
if (editor) {
|
||||
const action = editor.getAction(commandId);
|
||||
if (action) {
|
||||
return action.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.CUT.id,
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.COPY.id,
|
||||
order: '1',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/copyForForum',
|
||||
'Copy for Forum (Markdown)'
|
||||
),
|
||||
order: '2',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.PASTE.id,
|
||||
order: '3',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.SELECT_ALL.id,
|
||||
order: '4',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.GO_TO_LINE.id,
|
||||
label: nls.localize(
|
||||
'vscode/standaloneStrings/gotoLineActionLabel',
|
||||
'Go to Line...'
|
||||
),
|
||||
order: '5',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.TOGGLE_COMMENT.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/commentUncomment',
|
||||
'Comment/Uncomment'
|
||||
),
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INDENT_LINES.id,
|
||||
label: nls.localize('arduino/editor/increaseIndent', 'Increase Indent'),
|
||||
order: '1',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.OUTDENT_LINES.id,
|
||||
label: nls.localize('arduino/editor/decreaseIndent', 'Decrease Indent'),
|
||||
order: '2',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.AUTO_FORMAT.id,
|
||||
label: nls.localize('arduino/editor/autoFormat', 'Auto Format'),
|
||||
order: '3',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND.id,
|
||||
label: nls.localize('vscode/findController/startFindAction', 'Find'),
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND_NEXT.id,
|
||||
label: nls.localize(
|
||||
'vscode/findController/findNextMatchAction',
|
||||
'Find Next'
|
||||
),
|
||||
order: '1',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
|
||||
label: nls.localize(
|
||||
'vscode/findController/findPreviousMatchAction',
|
||||
'Find Previous'
|
||||
),
|
||||
order: '2',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.USE_FOR_FIND.id,
|
||||
label: nls.localize(
|
||||
'vscode/findController/startFindWithSelectionAction',
|
||||
'Use Selection for Find'
|
||||
), // XXX: The Java IDE uses `Use Selection For Find`.
|
||||
order: '3',
|
||||
});
|
||||
|
||||
// `Tools`
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: EditContributions.Commands.AUTO_FORMAT.id,
|
||||
label: nls.localize('arduino/editor/autoFormat', 'Auto Format'), // XXX: The Java IDE uses `Use Selection For Find`.
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||
keybinding: 'CtrlCmd+Shift+C',
|
||||
when: 'editorFocus',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.GO_TO_LINE.id,
|
||||
keybinding: 'CtrlCmd+L',
|
||||
when: 'editorFocus',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.TOGGLE_COMMENT.id,
|
||||
keybinding: 'CtrlCmd+/',
|
||||
when: 'editorFocus',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND.id,
|
||||
keybinding: 'CtrlCmd+F',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND_NEXT.id,
|
||||
keybinding: 'CtrlCmd+G',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND_PREVIOUS.id,
|
||||
keybinding: 'CtrlCmd+Shift+G',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.USE_FOR_FIND.id,
|
||||
keybinding: 'CtrlCmd+E',
|
||||
});
|
||||
|
||||
// `Tools`
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.AUTO_FORMAT.id,
|
||||
keybinding: 'CtrlCmd+T',
|
||||
});
|
||||
}
|
||||
|
||||
protected async current(): Promise<
|
||||
ICodeEditor | StandaloneCodeEditor | undefined
|
||||
> {
|
||||
return (
|
||||
this.codeEditorService.getFocusedCodeEditor() ||
|
||||
this.codeEditorService.getActiveCodeEditor() ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
protected async currentValue(): Promise<string | undefined> {
|
||||
const currentEditor = await this.current();
|
||||
if (currentEditor) {
|
||||
const selection = currentEditor.getSelection();
|
||||
if (!selection || selection.isEmpty()) {
|
||||
return currentEditor.getValue();
|
||||
}
|
||||
return currentEditor.getModel()?.getValueInRange(selection);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async run(commandId: string): Promise<any> {
|
||||
const editor = await this.current();
|
||||
if (editor) {
|
||||
const action = editor.getAction(commandId);
|
||||
if (action) {
|
||||
return action.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace EditContributions {
|
||||
export namespace Commands {
|
||||
export const COPY_FOR_FORUM: Command = {
|
||||
id: 'arduino-copy-for-forum'
|
||||
};
|
||||
export const COPY_FOR_GITHUB: Command = {
|
||||
id: 'arduino-copy-for-github'
|
||||
};
|
||||
export const GO_TO_LINE: Command = {
|
||||
id: 'arduino-go-to-line'
|
||||
};
|
||||
export const TOGGLE_COMMENT: Command = {
|
||||
id: 'arduino-toggle-comment'
|
||||
};
|
||||
export const INDENT_LINES: Command = {
|
||||
id: 'arduino-indent-lines'
|
||||
};
|
||||
export const OUTDENT_LINES: Command = {
|
||||
id: 'arduino-outdent-lines'
|
||||
};
|
||||
export const FIND: Command = {
|
||||
id: 'arduino-find'
|
||||
};
|
||||
export const FIND_NEXT: Command = {
|
||||
id: 'arduino-find-next'
|
||||
};
|
||||
export const FIND_PREVIOUS: Command = {
|
||||
id: 'arduino-find-previous'
|
||||
};
|
||||
export const USE_FOR_FIND: Command = {
|
||||
id: 'arduino-for-find'
|
||||
};
|
||||
export const INCREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-increase-font-size'
|
||||
};
|
||||
export const DECREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-decrease-font-size'
|
||||
};
|
||||
export const AUTO_FORMAT: Command = {
|
||||
id: 'arduino-auto-format' // `Auto Format` should belong to `Tool`.
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const COPY_FOR_FORUM: Command = {
|
||||
id: 'arduino-copy-for-forum',
|
||||
};
|
||||
export const GO_TO_LINE: Command = {
|
||||
id: 'arduino-go-to-line',
|
||||
};
|
||||
export const TOGGLE_COMMENT: Command = {
|
||||
id: 'arduino-toggle-comment',
|
||||
};
|
||||
export const INDENT_LINES: Command = {
|
||||
id: 'arduino-indent-lines',
|
||||
};
|
||||
export const OUTDENT_LINES: Command = {
|
||||
id: 'arduino-outdent-lines',
|
||||
};
|
||||
export const FIND: Command = {
|
||||
id: 'arduino-find',
|
||||
};
|
||||
export const FIND_NEXT: Command = {
|
||||
id: 'arduino-find-next',
|
||||
};
|
||||
export const FIND_PREVIOUS: Command = {
|
||||
id: 'arduino-find-previous',
|
||||
};
|
||||
export const USE_FOR_FIND: Command = {
|
||||
id: 'arduino-for-find',
|
||||
};
|
||||
export const AUTO_FORMAT: Command = {
|
||||
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,169 +1,331 @@
|
||||
import * as PQueue from 'p-queue';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||
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 { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service';
|
||||
import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import {
|
||||
SketchContribution,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board } from '../../common/protocol';
|
||||
import {
|
||||
Board,
|
||||
SketchRef,
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
Sketch,
|
||||
CoreService,
|
||||
} from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { unregisterSubmenu } from '../menu/arduino-menus';
|
||||
|
||||
@injectable()
|
||||
export abstract class Examples extends SketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
@postConstruct()
|
||||
init(): void {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard));
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||
this.handleBoardChanged(selectedBoard)
|
||||
);
|
||||
this.notificationCenter.onDidReinitialize(() =>
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
// No force refresh. The core client was already refreshed.
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
||||
protected handleBoardChanged(board: Board | undefined): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
protected abstract update(options?: {
|
||||
board?: Board | undefined;
|
||||
forceRefresh?: boolean;
|
||||
}): void;
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
try {
|
||||
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
||||
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
|
||||
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
|
||||
const groupPath =
|
||||
index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
|
||||
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
|
||||
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
|
||||
parent.addNode(examples);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.warn('Could not patch menu ordering.');
|
||||
}
|
||||
// Registering the same submenu multiple times has no side-effect.
|
||||
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
|
||||
nls.localize('arduino/examples/menu', 'Examples'),
|
||||
{
|
||||
order: '4',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected handleBoardChanged(board: Board | undefined): void {
|
||||
// NOOP
|
||||
}
|
||||
registerRecursively(
|
||||
sketchContainerOrPlaceholder:
|
||||
| SketchContainer
|
||||
| (SketchRef | SketchContainer)[]
|
||||
| string,
|
||||
menuPath: MenuPath,
|
||||
pushToDispose: DisposableCollection = new DisposableCollection(),
|
||||
subMenuOptions?: SubMenuOptions | undefined
|
||||
): void {
|
||||
if (typeof sketchContainerOrPlaceholder === 'string') {
|
||||
const placeholder = new PlaceholderMenuNode(
|
||||
menuPath,
|
||||
sketchContainerOrPlaceholder
|
||||
);
|
||||
this.menuRegistry.registerMenuNode(menuPath, placeholder);
|
||||
pushToDispose.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuNode(placeholder.id)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const sketches: SketchRef[] = [];
|
||||
const children: SketchContainer[] = [];
|
||||
let submenuPath = menuPath;
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
try {
|
||||
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
||||
const index = ArduinoMenus.FILE__EXAMPLES_SUBMENU.length - 1;
|
||||
const menuId = ArduinoMenus.FILE__EXAMPLES_SUBMENU[index];
|
||||
const groupPath = index === 0 ? [] : ArduinoMenus.FILE__EXAMPLES_SUBMENU.slice(0, index);
|
||||
const parent: CompositeMenuNode = (registry as any).findGroup(groupPath);
|
||||
const examples = new CompositeMenuNode(menuId, '', { order: '4' });
|
||||
parent.addNode(examples);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.warn('Could not patch menu ordering.');
|
||||
if (SketchContainer.is(sketchContainerOrPlaceholder)) {
|
||||
const { label } = sketchContainerOrPlaceholder;
|
||||
submenuPath = [...menuPath, label];
|
||||
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
|
||||
this.toDispose.push(
|
||||
Disposable.create(() =>
|
||||
unregisterSubmenu(submenuPath, this.menuRegistry)
|
||||
)
|
||||
);
|
||||
sketches.push(...sketchContainerOrPlaceholder.sketches);
|
||||
children.push(...sketchContainerOrPlaceholder.children);
|
||||
} else {
|
||||
for (const sketchOrContainer of sketchContainerOrPlaceholder) {
|
||||
if (SketchContainer.is(sketchOrContainer)) {
|
||||
children.push(sketchOrContainer);
|
||||
} else {
|
||||
sketches.push(sketchOrContainer);
|
||||
}
|
||||
}
|
||||
// Registering the same submenu multiple times has no side-effect.
|
||||
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
|
||||
registry.registerSubmenu(ArduinoMenus.FILE__EXAMPLES_SUBMENU, 'Examples', { order: '4' });
|
||||
}
|
||||
children.forEach((child) =>
|
||||
this.registerRecursively(child, submenuPath, pushToDispose)
|
||||
);
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const commandId = `arduino-open-example-${submenuPath.join(
|
||||
':'
|
||||
)}--${uri}`;
|
||||
const command = { id: commandId };
|
||||
const handler = this.createHandler(uri);
|
||||
pushToDispose.push(
|
||||
this.commandRegistry.registerCommand(command, handler)
|
||||
);
|
||||
this.menuRegistry.registerMenuAction(submenuPath, {
|
||||
commandId,
|
||||
label: sketch.name,
|
||||
order: sketch.name.toLocaleLowerCase(),
|
||||
});
|
||||
pushToDispose.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(command)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerRecursively(
|
||||
exampleContainerOrPlaceholder: ExampleContainer | string,
|
||||
menuPath: MenuPath,
|
||||
pushToDispose: DisposableCollection = new DisposableCollection()): void {
|
||||
|
||||
if (typeof exampleContainerOrPlaceholder === 'string') {
|
||||
const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder);
|
||||
this.menuRegistry.registerMenuNode(menuPath, placeholder);
|
||||
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
|
||||
} else {
|
||||
const { label, sketches, children } = exampleContainerOrPlaceholder;
|
||||
const submenuPath = [...menuPath, label];
|
||||
this.menuRegistry.registerSubmenu(submenuPath, label);
|
||||
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`;
|
||||
const command = { id: commandId };
|
||||
const handler = {
|
||||
execute: async () => {
|
||||
const sketch = await this.sketchService.cloneExample(uri);
|
||||
this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
|
||||
}
|
||||
};
|
||||
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
|
||||
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name });
|
||||
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.sketchesService.cloneExample(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BuiltInExamples extends Examples {
|
||||
override async onReady(): Promise<void> {
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.register(); // no `await`
|
||||
protected override async update(): Promise<void> {
|
||||
let sketchContainers: SketchContainer[] | undefined;
|
||||
try {
|
||||
sketchContainers = await this.examplesService.builtIns();
|
||||
} catch (e) {
|
||||
console.error('Could not initialize built-in examples.', e);
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/examples/couldNotInitializeExamples',
|
||||
'Could not initialize built-in examples.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
protected async register() {
|
||||
let exampleContainers: ExampleContainer[] | undefined;
|
||||
try {
|
||||
exampleContainers = await this.examplesService.builtIns();
|
||||
} catch (e) {
|
||||
console.error('Could not initialize built-in examples.', e);
|
||||
this.messageService.error('Could not initialize built-in examples.');
|
||||
return;
|
||||
}
|
||||
this.toDispose.dispose();
|
||||
for (const container of ['Built-in examples', ...exampleContainers]) {
|
||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
|
||||
}
|
||||
this.menuManager.update();
|
||||
// TODO: remove
|
||||
console.log(typeof this.menuRegistry);
|
||||
this.toDispose.dispose();
|
||||
for (const container of [
|
||||
nls.localize('arduino/examples/builtInExamples', 'Built-in examples'),
|
||||
...sketchContainers,
|
||||
]) {
|
||||
this.registerRecursively(
|
||||
container,
|
||||
ArduinoMenus.EXAMPLES__BUILT_IN_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
}
|
||||
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LibraryExamples extends Examples {
|
||||
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.update());
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.update());
|
||||
}
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
override async onReady(): Promise<void> {
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.register(); // no `await`
|
||||
this.notificationCenter.onLibraryInstalled(() => this.register());
|
||||
this.notificationCenter.onLibraryUninstalled(() => this.register());
|
||||
protected override handleBoardChanged(board: Board | undefined): void {
|
||||
this.update({ board });
|
||||
}
|
||||
|
||||
protected override async update(
|
||||
options: { board?: Board; forceRefresh?: boolean } = {
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
}
|
||||
|
||||
protected handleBoardChanged(board: Board | undefined): void {
|
||||
this.register(board);
|
||||
}
|
||||
|
||||
protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard) {
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
if (!board || !board.fqbn) {
|
||||
return;
|
||||
}
|
||||
const { fqbn, name } = board;
|
||||
const { user, current, any } = await this.examplesService.installed({ fqbn });
|
||||
if (user.length) {
|
||||
(user as any).unshift('Examples from Custom Libraries');
|
||||
}
|
||||
if (current.length) {
|
||||
(current as any).unshift(`Examples for ${name}`);
|
||||
}
|
||||
if (any.length) {
|
||||
(any as any).unshift('Examples for any board');
|
||||
}
|
||||
for (const container of user) {
|
||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, this.toDispose);
|
||||
}
|
||||
for (const container of current) {
|
||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP, this.toDispose);
|
||||
}
|
||||
for (const container of any) {
|
||||
this.registerRecursively(container, ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP, this.toDispose);
|
||||
}
|
||||
this.menuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
): Promise<void> {
|
||||
const { board, forceRefresh } = options;
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
if (forceRefresh) {
|
||||
await this.coreService.refresh();
|
||||
}
|
||||
const fqbn = board?.fqbn;
|
||||
const name = board?.name;
|
||||
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
|
||||
const { user, current, any } = await this.examplesService.installed({
|
||||
fqbn,
|
||||
});
|
||||
if (user.length) {
|
||||
(user as any).unshift(
|
||||
nls.localize(
|
||||
'arduino/examples/customLibrary',
|
||||
'Examples from Custom Libraries'
|
||||
)
|
||||
);
|
||||
}
|
||||
if (name && fqbn && current.length) {
|
||||
(current as any).unshift(
|
||||
nls.localize('arduino/examples/for', 'Examples for {0}', name)
|
||||
);
|
||||
}
|
||||
if (any.length) {
|
||||
(any as any).unshift(
|
||||
nls.localize('arduino/examples/forAny', 'Examples for any board')
|
||||
);
|
||||
}
|
||||
for (const container of user) {
|
||||
this.registerRecursively(
|
||||
container,
|
||||
ArduinoMenus.EXAMPLES__USER_LIBS_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
}
|
||||
for (const container of current) {
|
||||
this.registerRecursively(
|
||||
container,
|
||||
ArduinoMenus.EXAMPLES__CURRENT_BOARD_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
}
|
||||
for (const container of any) {
|
||||
this.registerRecursively(
|
||||
container,
|
||||
ArduinoMenus.EXAMPLES__ANY_BOARD_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
}
|
||||
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,137 +1,192 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
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 { 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()
|
||||
export class Help extends Contribution {
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService;
|
||||
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService;
|
||||
@inject(QuickInputService)
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
@inject(QuickInputService)
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
const open = (url: string) =>
|
||||
this.windowService.openNewWindow(url, { external: true });
|
||||
const createOpenHandler = (url: string) =>
|
||||
<CommandHandler>{
|
||||
execute: () => open(url),
|
||||
};
|
||||
registry.registerCommand(
|
||||
Help.Commands.GETTING_STARTED,
|
||||
createOpenHandler('https://www.arduino.cc/en/Guide')
|
||||
);
|
||||
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, {
|
||||
execute: async () => {
|
||||
let searchFor: string | undefined = undefined;
|
||||
const { currentEditor } = this.editorManager;
|
||||
if (currentEditor && currentEditor.editor instanceof MonacoEditor) {
|
||||
const codeEditor = currentEditor.editor.getControl();
|
||||
const selection = codeEditor.getSelection();
|
||||
const model = codeEditor.getModel();
|
||||
if (model && selection && !monaco.Range.isEmpty(selection)) {
|
||||
searchFor = model.getValueInRange(selection);
|
||||
}
|
||||
}
|
||||
if (!searchFor) {
|
||||
searchFor = await this.quickInputService.input({
|
||||
prompt: nls.localize('arduino/help/search', 'Search on Arduino.cc'),
|
||||
placeHolder: nls.localize('arduino/help/keyword', 'Type a keyword'),
|
||||
});
|
||||
}
|
||||
if (searchFor) {
|
||||
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(
|
||||
Help.Commands.VISIT_ARDUINO,
|
||||
createOpenHandler('https://www.arduino.cc/')
|
||||
);
|
||||
registry.registerCommand(
|
||||
Help.Commands.PRIVACY_POLICY,
|
||||
createOpenHandler('https://www.arduino.cc/en/privacy-policy')
|
||||
);
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
const open = (url: string) => this.windowService.openNewWindow(url, { external: true });
|
||||
const createOpenHandler = (url: string) => <CommandHandler>{
|
||||
execute: () => open(url)
|
||||
};
|
||||
registry.registerCommand(Help.Commands.GETTING_STARTED, createOpenHandler('https://www.arduino.cc/en/Guide'));
|
||||
registry.registerCommand(Help.Commands.ENVIRONMENT, createOpenHandler('https://www.arduino.cc/en/Guide/Environment'));
|
||||
registry.registerCommand(Help.Commands.TROUBLESHOOTING, createOpenHandler('https://support.arduino.cc/hc/en-us'));
|
||||
registry.registerCommand(Help.Commands.REFERENCE, createOpenHandler('https://www.arduino.cc/reference/en/'));
|
||||
registry.registerCommand(Help.Commands.FIND_IN_REFERENCE, {
|
||||
execute: async () => {
|
||||
let searchFor: string | undefined = undefined;
|
||||
const { currentEditor } = this.editorManager;
|
||||
if (currentEditor && currentEditor.editor instanceof MonacoEditor) {
|
||||
const codeEditor = currentEditor.editor.getControl();
|
||||
const selection = codeEditor.getSelection();
|
||||
const model = codeEditor.getModel();
|
||||
if (model && selection && !monaco.Range.isEmpty(selection)) {
|
||||
searchFor = model.getValueInRange(selection);
|
||||
}
|
||||
}
|
||||
if (!searchFor) {
|
||||
searchFor = await this.quickInputService.open({
|
||||
prompt: 'Search on Arduino.cc',
|
||||
placeHolder: 'Type a keyword'
|
||||
});
|
||||
}
|
||||
if (searchFor) {
|
||||
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(Help.Commands.VISIT_ARDUINO, createOpenHandler('https://www.arduino.cc/'));
|
||||
}
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.unregisterMenuAction({
|
||||
commandId: ElectronCommands.TOGGLE_DEVELOPER_TOOLS.id,
|
||||
});
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||
commandId: Help.Commands.GETTING_STARTED.id,
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||
commandId: Help.Commands.ENVIRONMENT.id,
|
||||
order: '1'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||
commandId: Help.Commands.TROUBLESHOOTING.id,
|
||||
order: '2'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||
commandId: Help.Commands.REFERENCE.id,
|
||||
order: '3'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||
commandId: Help.Commands.GETTING_STARTED.id,
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||
commandId: Help.Commands.ENVIRONMENT.id,
|
||||
order: '1',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||
commandId: Help.Commands.TROUBLESHOOTING.id,
|
||||
order: '2',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||
commandId: Help.Commands.REFERENCE.id,
|
||||
order: '3',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||
commandId: Help.Commands.FIND_IN_REFERENCE.id,
|
||||
order: '4'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||
commandId: Help.Commands.FAQ.id,
|
||||
order: '5'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||
commandId: Help.Commands.VISIT_ARDUINO.id,
|
||||
order: '6'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: Help.Commands.FIND_IN_REFERENCE.id,
|
||||
keybinding: 'CtrlCmd+Shift+F'
|
||||
});
|
||||
}
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||
commandId: Help.Commands.FIND_IN_REFERENCE.id,
|
||||
order: '4',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||
commandId: Help.Commands.FAQ.id,
|
||||
order: '5',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__FIND_GROUP, {
|
||||
commandId: Help.Commands.VISIT_ARDUINO.id,
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: Help.Commands.FIND_IN_REFERENCE.id,
|
||||
keybinding: 'CtrlCmd+Shift+F',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Help {
|
||||
export namespace Commands {
|
||||
export const GETTING_STARTED: Command = {
|
||||
id: 'arduino-getting-started',
|
||||
label: 'Getting Started',
|
||||
category: 'Arduino'
|
||||
};
|
||||
export const ENVIRONMENT: Command = {
|
||||
id: 'arduino-environment',
|
||||
label: 'Environment',
|
||||
category: 'Arduino'
|
||||
};
|
||||
export const TROUBLESHOOTING: Command = {
|
||||
id: 'arduino-troubleshooting',
|
||||
label: 'Troubleshooting',
|
||||
category: 'Arduino'
|
||||
};
|
||||
export const REFERENCE: Command = {
|
||||
id: 'arduino-reference',
|
||||
label: 'Reference',
|
||||
category: 'Arduino'
|
||||
};
|
||||
export const FIND_IN_REFERENCE: Command = {
|
||||
id: 'arduino-find-in-reference',
|
||||
label: 'Find in Reference',
|
||||
category: 'Arduino'
|
||||
};
|
||||
export const FAQ: Command = {
|
||||
id: 'arduino-faq',
|
||||
label: 'Frequently Asked Questions',
|
||||
category: 'Arduino'
|
||||
};
|
||||
export const VISIT_ARDUINO: Command = {
|
||||
id: 'arduino-visit-arduino',
|
||||
label: 'Visit Arduino.cc',
|
||||
category: 'Arduino'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const GETTING_STARTED: Command = {
|
||||
id: 'arduino-getting-started',
|
||||
label: nls.localize('arduino/help/gettingStarted', 'Getting Started'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const ENVIRONMENT: Command = {
|
||||
id: 'arduino-environment',
|
||||
label: nls.localize('arduino/help/environment', 'Environment'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const TROUBLESHOOTING: Command = {
|
||||
id: 'arduino-troubleshooting',
|
||||
label: nls.localize('arduino/help/troubleshooting', 'Troubleshooting'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const REFERENCE: Command = {
|
||||
id: 'arduino-reference',
|
||||
label: nls.localize('arduino/help/reference', 'Reference'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const FIND_IN_REFERENCE: Command = {
|
||||
id: 'arduino-find-in-reference',
|
||||
label: nls.localize('arduino/help/findInReference', 'Find in Reference'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const FAQ: Command = {
|
||||
id: 'arduino-faq',
|
||||
label: nls.localize('arduino/help/faq', 'Frequently Asked Questions'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const VISIT_ARDUINO: Command = {
|
||||
id: 'arduino-visit-arduino',
|
||||
label: nls.localize('arduino/help/visit', 'Visit Arduino.cc'),
|
||||
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 { inject, injectable } from 'inversify';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
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 { LibraryPackage, LibraryService } from '../../common/protocol';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
@@ -12,166 +15,222 @@ import { LibraryListWidget } from '../library/library-list-widget';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class IncludeLibrary extends SketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
@inject(EditorManager)
|
||||
protected override readonly editorManager: EditorManager;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
override onStart(): void {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(() =>
|
||||
this.updateMenuActions()
|
||||
);
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.updateMenuActions());
|
||||
this.notificationCenter.onLibraryDidUninstall(() =>
|
||||
this.updateMenuActions()
|
||||
);
|
||||
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.updateMenuActions();
|
||||
this.boardsServiceClient.onBoardsConfigChanged(() => this.updateMenuActions())
|
||||
this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions());
|
||||
this.notificationCenter.onLibraryUninstalled(() => this.updateMenuActions());
|
||||
}
|
||||
override async onReady(): Promise<void> {
|
||||
this.updateMenuActions();
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
// `Include Library` submenu
|
||||
const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include'];
|
||||
registry.registerSubmenu(includeLibMenuPath, 'Include Library', { order: '1' });
|
||||
// `Manage Libraries...` group.
|
||||
registry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
|
||||
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
|
||||
label: 'Manage Libraries...'
|
||||
});
|
||||
}
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
// `Include Library` submenu
|
||||
const includeLibMenuPath = [
|
||||
...ArduinoMenus.SKETCH__UTILS_GROUP,
|
||||
'0_include',
|
||||
];
|
||||
registry.registerSubmenu(
|
||||
includeLibMenuPath,
|
||||
nls.localize('arduino/library/include', 'Include Library'),
|
||||
{
|
||||
order: '1',
|
||||
}
|
||||
);
|
||||
// `Manage Libraries...` group.
|
||||
registry.registerMenuAction([...includeLibMenuPath, '0_manage'], {
|
||||
commandId: `${LibraryListWidget.WIDGET_ID}:toggle`,
|
||||
label: nls.localize(
|
||||
'arduino/library/manageLibraries',
|
||||
'Manage Libraries...'
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
|
||||
execute: async arg => {
|
||||
if (LibraryPackage.is(arg)) {
|
||||
this.includeLibrary(arg);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async updateMenuActions(): Promise<void> {
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
this.mainMenuManager.update();
|
||||
const libraries: LibraryPackage[] = []
|
||||
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
|
||||
// Do not show board specific examples, when no board is selected.
|
||||
if (fqbn) {
|
||||
libraries.push(...await this.libraryService.list({ fqbn }));
|
||||
}
|
||||
|
||||
const includeLibMenuPath = [...ArduinoMenus.SKETCH__UTILS_GROUP, '0_include'];
|
||||
// `Add .ZIP Library...`
|
||||
// TODO: implement it
|
||||
|
||||
// `Arduino libraries`
|
||||
const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
|
||||
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
|
||||
const { user, rest } = LibraryPackage.groupByLocation(libraries);
|
||||
if (rest.length) {
|
||||
(rest as any).unshift('Arduino libraries');
|
||||
}
|
||||
if (user.length) {
|
||||
(user as any).unshift('Contributed libraries');
|
||||
}
|
||||
|
||||
for (const library of user) {
|
||||
this.toDispose.push(this.registerLibrary(library, userMenuPath));
|
||||
}
|
||||
for (const library of rest) {
|
||||
this.toDispose.push(this.registerLibrary(library, packageMenuPath));
|
||||
}
|
||||
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
protected registerLibrary(libraryOrPlaceholder: LibraryPackage | string, menuPath: MenuPath): Disposable {
|
||||
if (typeof libraryOrPlaceholder === 'string') {
|
||||
const placeholder = new PlaceholderMenuNode(menuPath, libraryOrPlaceholder);
|
||||
this.menuRegistry.registerMenuNode(menuPath, placeholder);
|
||||
return Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id));
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
|
||||
execute: async (arg) => {
|
||||
if (LibraryPackage.is(arg)) {
|
||||
this.includeLibrary(arg);
|
||||
}
|
||||
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
|
||||
const command = { id: commandId };
|
||||
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, libraryOrPlaceholder) };
|
||||
const menuAction = { commandId, label: libraryOrPlaceholder.name };
|
||||
this.menuRegistry.registerMenuAction(menuPath, menuAction);
|
||||
return new DisposableCollection(
|
||||
this.commandRegistry.registerCommand(command, handler),
|
||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(menuAction)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected async updateMenuActions(): Promise<void> {
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
this.mainMenuManager.update();
|
||||
const libraries: LibraryPackage[] = [];
|
||||
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
|
||||
// Show all libraries, when no board is selected.
|
||||
// Otherwise, show libraries only for the selected board.
|
||||
libraries.push(...(await this.libraryService.list({ fqbn })));
|
||||
|
||||
const includeLibMenuPath = [
|
||||
...ArduinoMenus.SKETCH__UTILS_GROUP,
|
||||
'0_include',
|
||||
];
|
||||
// `Add .ZIP Library...`
|
||||
// TODO: implement it
|
||||
|
||||
// `Arduino libraries`
|
||||
const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
|
||||
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
|
||||
const { user, rest } = LibraryPackage.groupByLocation(libraries);
|
||||
if (rest.length) {
|
||||
(rest as any).unshift(
|
||||
nls.localize('arduino/library/arduinoLibraries', 'Arduino libraries')
|
||||
);
|
||||
}
|
||||
if (user.length) {
|
||||
(user as any).unshift(
|
||||
nls.localize(
|
||||
'arduino/library/contributedLibraries',
|
||||
'Contributed libraries'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (const library of user) {
|
||||
this.toDispose.push(this.registerLibrary(library, userMenuPath));
|
||||
}
|
||||
for (const library of rest) {
|
||||
this.toDispose.push(this.registerLibrary(library, packageMenuPath));
|
||||
}
|
||||
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
protected registerLibrary(
|
||||
libraryOrPlaceholder: LibraryPackage | string,
|
||||
menuPath: MenuPath
|
||||
): Disposable {
|
||||
if (typeof libraryOrPlaceholder === 'string') {
|
||||
const placeholder = new PlaceholderMenuNode(
|
||||
menuPath,
|
||||
libraryOrPlaceholder
|
||||
);
|
||||
this.menuRegistry.registerMenuNode(menuPath, placeholder);
|
||||
return Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuNode(placeholder.id)
|
||||
);
|
||||
}
|
||||
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
|
||||
const command = { id: commandId };
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.commandRegistry.executeCommand(
|
||||
IncludeLibrary.Commands.INCLUDE_LIBRARY.id,
|
||||
libraryOrPlaceholder
|
||||
),
|
||||
};
|
||||
const menuAction = { commandId, label: libraryOrPlaceholder.name };
|
||||
this.menuRegistry.registerMenuAction(menuPath, menuAction);
|
||||
return new DisposableCollection(
|
||||
this.commandRegistry.registerCommand(command, handler),
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(menuAction)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected async includeLibrary(library: LibraryPackage): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
// If the current editor is one of the additional files from the sketch, we use that.
|
||||
// Otherwise, we pick the editor of the main sketch file.
|
||||
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
const editor = this.editorManager.currentEditor?.editor;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
if (
|
||||
sketch.additionalFileUris.some((uri) => uri === editor.uri.toString())
|
||||
) {
|
||||
codeEditor = editor.getControl();
|
||||
}
|
||||
}
|
||||
|
||||
protected async includeLibrary(library: LibraryPackage): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
// If the current editor is one of the additional files from the sketch, we use that.
|
||||
// Otherwise, we pick the editor of the main sketch file.
|
||||
let codeEditor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
const editor = this.editorManager.currentEditor?.editor;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
if (sketch.additionalFileUris.some(uri => uri === editor.uri.toString())) {
|
||||
codeEditor = editor.getControl();
|
||||
}
|
||||
}
|
||||
|
||||
if (!codeEditor) {
|
||||
const widget = await this.editorManager.open(new URI(sketch.mainFileUri));
|
||||
if (widget.editor instanceof MonacoEditor) {
|
||||
codeEditor = widget.editor.getControl();
|
||||
}
|
||||
}
|
||||
|
||||
if (!codeEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textModel = codeEditor.getModel();
|
||||
if (!textModel) {
|
||||
return;
|
||||
}
|
||||
const cursorState = codeEditor.getSelections() || [];
|
||||
const eol = textModel.getEOL();
|
||||
const includes = library.includes.slice();
|
||||
includes.push(''); // For the trailing new line.
|
||||
const text = includes.map(include => include ? `#include <${include}>` : eol).join(eol);
|
||||
textModel.pushStackElement(); // Start a fresh operation.
|
||||
textModel.pushEditOperations(cursorState, [{
|
||||
range: new monaco.Range(1, 1, 1, 1),
|
||||
text,
|
||||
forceMoveMarkers: true
|
||||
}], () => cursorState);
|
||||
textModel.pushStackElement(); // Make it undoable.
|
||||
if (!codeEditor) {
|
||||
const widget = await this.editorManager.open(new URI(sketch.mainFileUri));
|
||||
if (widget.editor instanceof MonacoEditor) {
|
||||
codeEditor = widget.editor.getControl();
|
||||
}
|
||||
}
|
||||
|
||||
if (!codeEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textModel = codeEditor.getModel();
|
||||
if (!textModel) {
|
||||
return;
|
||||
}
|
||||
const cursorState = codeEditor.getSelections() || [];
|
||||
const eol = textModel.getEOL();
|
||||
const includes = library.includes.slice();
|
||||
includes.push(''); // For the trailing new line.
|
||||
const text = includes
|
||||
.map((include) => (include ? `#include <${include}>` : eol))
|
||||
.join(eol);
|
||||
textModel.pushStackElement(); // Start a fresh operation.
|
||||
textModel.pushEditOperations(
|
||||
cursorState,
|
||||
[
|
||||
{
|
||||
range: new monaco.Range(1, 1, 1, 1),
|
||||
text,
|
||||
forceMoveMarkers: true,
|
||||
},
|
||||
],
|
||||
() => cursorState
|
||||
);
|
||||
textModel.pushStackElement(); // Make it undoable.
|
||||
}
|
||||
}
|
||||
|
||||
export namespace IncludeLibrary {
|
||||
export namespace Commands {
|
||||
export const INCLUDE_LIBRARY: Command = {
|
||||
id: 'arduino-include-library'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const INCLUDE_LIBRARY: Command = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
212
arduino-ide-extension/src/browser/contributions/ino-language.ts
Normal file
212
arduino-ide-extension/src/browser/contributions/ino-language.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import {
|
||||
ArduinoDaemon,
|
||||
assertSanitizedFqbn,
|
||||
BoardsService,
|
||||
ExecutableService,
|
||||
sanitizeFqbn,
|
||||
} from '../../common/protocol';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { HostedPluginEvents } from '../hosted-plugin-events';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { SketchContribution, URI } from './contribution';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
|
||||
@injectable()
|
||||
export class InoLanguage extends SketchContribution {
|
||||
@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;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
private readonly boardDataStore: BoardsDataStore;
|
||||
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private readonly languageServerStartMutex = new Mutex();
|
||||
private languageServerFqbn?: string;
|
||||
|
||||
override onReady(): void {
|
||||
const start = (
|
||||
{ selectedBoard }: BoardsConfig.Config,
|
||||
forceStart = false
|
||||
) => {
|
||||
if (selectedBoard) {
|
||||
const { name, fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
this.startLanguageServer(fqbn, name, forceStart);
|
||||
}
|
||||
}
|
||||
};
|
||||
const forceRestart = () => {
|
||||
start(this.boardsServiceProvider.boardsConfig, true);
|
||||
};
|
||||
this.toDispose.pushAll([
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(start),
|
||||
this.hostedPluginEvents.onPluginsDidStart(() =>
|
||||
start(this.boardsServiceProvider.boardsConfig)
|
||||
),
|
||||
this.hostedPluginEvents.onPluginsWillUnload(
|
||||
() => (this.languageServerFqbn = undefined)
|
||||
),
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, oldValue, newValue }) => {
|
||||
if (oldValue !== newValue) {
|
||||
switch (preferenceName) {
|
||||
case 'arduino.language.log':
|
||||
case 'arduino.language.realTimeDiagnostics':
|
||||
forceRestart();
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
this.notificationCenter.onLibraryDidInstall(() => forceRestart()),
|
||||
this.notificationCenter.onLibraryDidUninstall(() => forceRestart()),
|
||||
this.notificationCenter.onPlatformDidInstall(() => forceRestart()),
|
||||
this.notificationCenter.onPlatformDidUninstall(() => forceRestart()),
|
||||
this.notificationCenter.onDidReinitialize(() => forceRestart()),
|
||||
this.boardDataStore.onChanged((dataChangePerFqbn) => {
|
||||
if (this.languageServerFqbn) {
|
||||
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
|
||||
if (!sanitizeFqbn) {
|
||||
throw new Error(
|
||||
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
|
||||
);
|
||||
}
|
||||
const matchingFqbn = dataChangePerFqbn.find(
|
||||
(fqbn) => sanitizedFqbn === fqbn
|
||||
);
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
if (
|
||||
matchingFqbn &&
|
||||
boardsConfig.selectedBoard?.fqbn === matchingFqbn
|
||||
) {
|
||||
start(boardsConfig);
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
start(this.boardsServiceProvider.boardsConfig);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private async startLanguageServer(
|
||||
fqbn: string,
|
||||
name: string | undefined,
|
||||
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;
|
||||
}
|
||||
assertSanitizedFqbn(fqbn);
|
||||
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
|
||||
if (!fqbnWithConfig) {
|
||||
throw new Error(
|
||||
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
|
||||
);
|
||||
}
|
||||
if (!forceStart && fqbnWithConfig === this.languageServerFqbn) {
|
||||
// NOOP
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Starting language server: ${fqbnWithConfig}`);
|
||||
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: fqbnWithConfig,
|
||||
name: name ? `"${name}"` : undefined,
|
||||
},
|
||||
realTimeDiagnostics,
|
||||
silentOutput: true,
|
||||
}
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
|
||||
this.languageServerFqbn = undefined;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
Contribution,
|
||||
Command,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CommandRegistry, MaybePromise, nls } from '@theia/core/lib/common';
|
||||
import { Settings } from '../dialogs/settings/settings';
|
||||
import debounce = require('lodash.debounce');
|
||||
|
||||
@injectable()
|
||||
export class InterfaceScale extends Contribution {
|
||||
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 {
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/increaseFontSize',
|
||||
'Increase Font Size'
|
||||
),
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/decreaseFontSize',
|
||||
'Decrease Font Size'
|
||||
),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
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.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
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,165 @@
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { isConflict } from '../create/typings';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
TaskFactoryImpl,
|
||||
WorkspaceInputDialogWithProgress,
|
||||
} from '../theia/workspace/workspace-input-dialog';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
|
||||
import { Command, CommandRegistry, Sketch } from './contribution';
|
||||
import {
|
||||
CloudSketchContribution,
|
||||
pullingSketch,
|
||||
sketchAlreadyExists,
|
||||
synchronizingSketchbook,
|
||||
} from './cloud-contribution';
|
||||
|
||||
@injectable()
|
||||
export class NewCloudSketch extends CloudSketchContribution {
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
|
||||
override onReady(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.createFeatures.onDidChangeEnabled(() => this.menuManager.update()),
|
||||
this.createFeatures.onDidChangeSession(() => this.menuManager.update()),
|
||||
]);
|
||||
if (this.createFeatures.session) {
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
|
||||
execute: () => this.createNewSketch(true),
|
||||
isEnabled: () => Boolean(this.createFeatures.session),
|
||||
isVisible: () => this.createFeatures.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
label: nls.localize('arduino/cloudSketch/new', 'New Cloud Sketch'),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+Alt+N',
|
||||
});
|
||||
}
|
||||
|
||||
private async createNewSketch(
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined
|
||||
): Promise<void> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (treeModel) {
|
||||
const rootNode = treeModel.root;
|
||||
return this.openWizard(
|
||||
rootNode,
|
||||
treeModel,
|
||||
skipShowErrorMessageOnOpen,
|
||||
initialValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async openWizard(
|
||||
rootNode: CompositeTreeNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined
|
||||
): Promise<void> {
|
||||
const existingNames = rootNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
const taskFactory = new TaskFactoryImpl((value) =>
|
||||
this.createNewSketchWithProgress(treeModel, value)
|
||||
);
|
||||
try {
|
||||
const dialog = new WorkspaceInputDialogWithProgress(
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/newCloudSketch/newSketchTitle',
|
||||
'Name of the new Cloud Sketch'
|
||||
),
|
||||
parentUri: CreateUri.root,
|
||||
initialValue,
|
||||
validate: (input) => {
|
||||
if (existingNames.includes(input)) {
|
||||
return sketchAlreadyExists(input);
|
||||
}
|
||||
return Sketch.validateCloudSketchFolderName(input) ?? '';
|
||||
},
|
||||
},
|
||||
this.labelProvider,
|
||||
taskFactory
|
||||
);
|
||||
await dialog.open(skipShowErrorMessageOnOpen);
|
||||
if (dialog.taskResult) {
|
||||
this.openInNewWindow(dialog.taskResult);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
await treeModel.refresh();
|
||||
return this.createNewSketch(false, taskFactory.value ?? initialValue);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private createNewSketchWithProgress(
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
value: string
|
||||
): (
|
||||
progress: Progress
|
||||
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
|
||||
return async (progress: Progress) => {
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/creating',
|
||||
"Creating cloud sketch '{0}'...",
|
||||
value
|
||||
),
|
||||
});
|
||||
const sketch = await this.createApi.createSketch(value);
|
||||
progress.report({ message: synchronizingSketchbook });
|
||||
await treeModel.refresh();
|
||||
progress.report({ message: pullingSketch(sketch.name) });
|
||||
const node = await this.pull(sketch);
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
private openInNewWindow(
|
||||
node: CloudSketchbookTree.CloudSketchDirNode
|
||||
): Promise<void> {
|
||||
return this.commandService.executeCommand(
|
||||
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||
{ node }
|
||||
);
|
||||
}
|
||||
}
|
||||
export namespace NewCloudSketch {
|
||||
export namespace Commands {
|
||||
export const NEW_CLOUD_SKETCH: Command = {
|
||||
id: 'arduino-new-cloud-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,52 @@
|
||||
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 { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
||||
import {
|
||||
SketchContribution,
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class NewSketch extends SketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
||||
execute: () => this.newSketch(),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
||||
execute: () => this.newSketch()
|
||||
});
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id)
|
||||
});
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/new', 'New Sketch'),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: NewSketch.Commands.NEW_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+N',
|
||||
});
|
||||
}
|
||||
|
||||
async newSketch(): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchesService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
||||
label: 'New',
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: NewSketch.Commands.NEW_SKETCH.id,
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
async newSketch(): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export namespace NewSketch {
|
||||
export namespace Commands {
|
||||
export const NEW_SKETCH: Command = {
|
||||
id: 'arduino-new-sketch'
|
||||
};
|
||||
export const NEW_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-new-sketch--toolbar'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const NEW_SKETCH: Command = {
|
||||
id: 'arduino-new-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,62 +1,111 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
SketchContribution,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class OpenRecentSketch extends SketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
@inject(WorkspaceServer)
|
||||
protected readonly workspaceServer: WorkspaceServer;
|
||||
|
||||
@inject(WorkspaceServer)
|
||||
protected readonly workspaceServer: WorkspaceServer;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
protected toDispose = new DisposableCollection();
|
||||
|
||||
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||
this.refreshMenu(sketches)
|
||||
);
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
const refreshMenu = (sketches: Sketch[]) => {
|
||||
this.register(sketches);
|
||||
this.mainMenuManager.update();
|
||||
};
|
||||
this.notificationCenter.onRecentSketchesChanged(({ sketches }) => refreshMenu(sketches));
|
||||
this.sketchService.recentlyOpenedSketches().then(refreshMenu);
|
||||
}
|
||||
override async onReady(): Promise<void> {
|
||||
this.update();
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerSubmenu(ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, 'Open Recent', { order: '2' });
|
||||
}
|
||||
private update(forceUpdate?: boolean): void {
|
||||
this.sketchesService
|
||||
.recentlyOpenedSketches(forceUpdate)
|
||||
.then((sketches) => this.refreshMenu(sketches));
|
||||
}
|
||||
|
||||
protected register(sketches: Sketch[]): void {
|
||||
let order = 0;
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const toDispose = this.toDisposeBeforeRegister.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
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 {
|
||||
const order = 0;
|
||||
this.toDispose.dispose();
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const command = { id: `arduino-open-recent--${uri}` };
|
||||
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;
|
||||
}
|
||||
const command = { id: `arduino-open-recent--${uri}` };
|
||||
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__OPEN_RECENT_SUBMENU, { commandId: command.id, label: sketch.name, order: String(order) });
|
||||
this.toDisposeBeforeRegister.set(sketch.uri, new DisposableCollection(
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
|
||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
|
||||
));
|
||||
}
|
||||
},
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
||||
{
|
||||
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)
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import type { Settings } from '../dialogs/settings/settings';
|
||||
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
SketchContribution,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class OpenSettings extends SketchContribution {
|
||||
@inject(SettingsDialog)
|
||||
private readonly settingsDialog: SettingsDialog;
|
||||
|
||||
private settingsOpened = false;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSettings.Commands.OPEN, {
|
||||
execute: async () => {
|
||||
let settings: Settings | undefined = undefined;
|
||||
try {
|
||||
this.settingsOpened = true;
|
||||
this.menuManager.update();
|
||||
settings = await this.settingsDialog.open();
|
||||
} finally {
|
||||
this.settingsOpened = false;
|
||||
this.menuManager.update();
|
||||
}
|
||||
if (settings) {
|
||||
await this.settingsService.update(settings);
|
||||
await this.settingsService.save();
|
||||
} else {
|
||||
await this.settingsService.reset();
|
||||
}
|
||||
},
|
||||
isEnabled: () => !this.settingsOpened,
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
|
||||
commandId: OpenSettings.Commands.OPEN.id,
|
||||
label:
|
||||
nls.localize(
|
||||
'vscode/preferences.contribution/preferences',
|
||||
'Preferences'
|
||||
) + '...',
|
||||
order: '0',
|
||||
});
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.FILE__ADVANCED_SUBMENU,
|
||||
nls.localize('arduino/menu/advanced', 'Advanced')
|
||||
);
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: OpenSettings.Commands.OPEN.id,
|
||||
keybinding: 'CtrlCmd+,',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace OpenSettings {
|
||||
export namespace Commands {
|
||||
export const OPEN: Command = {
|
||||
id: 'arduino-settings-open',
|
||||
label:
|
||||
nls.localize(
|
||||
'vscode/preferences.contribution/openSettings2',
|
||||
'Open Preferences'
|
||||
) + '...',
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,57 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { 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()
|
||||
export class OpenSketchExternal extends SketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
|
||||
execute: () => this.openExternal(),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
|
||||
execute: () => this.openExternal()
|
||||
});
|
||||
}
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
||||
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||
label: nls.localize('arduino/sketch/showFolder', 'Show Sketch Folder'),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
||||
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||
label: 'Show Sketch Folder',
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||
keybinding: 'CtrlCmd+Alt+K',
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||
keybinding: 'CtrlCmd+Alt+K'
|
||||
});
|
||||
}
|
||||
|
||||
protected async openExternal(): Promise<void> {
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (uri) {
|
||||
const exists = this.fileService.exists(new URI(uri));
|
||||
if (exists) {
|
||||
const fsPath = await this.fileService.fsPath(new URI(uri));
|
||||
if (fsPath) {
|
||||
remote.shell.showItemInFolder(fsPath);
|
||||
}
|
||||
}
|
||||
protected async openExternal(): Promise<void> {
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (uri) {
|
||||
const exists = await this.fileService.exists(new URI(uri));
|
||||
if (exists) {
|
||||
const fsPath = await this.fileService.fsPath(new URI(uri));
|
||||
if (fsPath) {
|
||||
remote.shell.showItemInFolder(fsPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export namespace OpenSketchExternal {
|
||||
export namespace Commands {
|
||||
export const OPEN_EXTERNAL: Command = {
|
||||
id: 'arduino-open-sketch-external'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const OPEN_EXTERNAL: Command = {
|
||||
id: 'arduino-open-sketch-external',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
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, focusMainSketchFile) =>
|
||||
this.openSketchFiles(uri, focusMainSketchFile),
|
||||
});
|
||||
registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, {
|
||||
execute: (
|
||||
uri: string,
|
||||
forceOpen?: boolean,
|
||||
options?: EditorOpenerOptions
|
||||
) => {
|
||||
this.ensureOpened(uri, forceOpen, options);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async openSketchFiles(
|
||||
uri: URI,
|
||||
focusMainSketchFile = false
|
||||
): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchesService.loadSketch(uri.toString());
|
||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
|
||||
await this.ensureOpened(uri);
|
||||
}
|
||||
if (focusMainSketchFile) {
|
||||
await this.ensureOpened(mainFileUri, true, { mode: 'activate' });
|
||||
}
|
||||
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,
|
||||
sketchesService: this.sketchesService,
|
||||
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.sketchesService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
||||
}
|
||||
|
||||
private async ensureOpened(
|
||||
uri: string,
|
||||
forceOpen = false,
|
||||
options?: EditorOpenerOptions
|
||||
): Promise<EditorWidget | undefined> {
|
||||
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: EditorWidget | undefined | 'timeout' = await Promise.race([
|
||||
deferred.promise,
|
||||
wait(timeout).then(() => {
|
||||
disposables.dispose();
|
||||
return 'timeout' as const;
|
||||
}),
|
||||
]);
|
||||
if (result === 'timeout') {
|
||||
console.warn(
|
||||
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
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,179 +1,181 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import {
|
||||
SketchesError,
|
||||
SketchesService,
|
||||
SketchRef,
|
||||
} from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SketchContribution, Sketch, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import { BuiltInExamples } from './examples';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
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()
|
||||
export class OpenSketch extends SketchContribution {
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(BuiltInExamples)
|
||||
protected readonly builtInExamples: BuiltInExamples;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
||||
execute: arg => Sketch.is(arg) ? this.openSketch(arg) : this.openSketch()
|
||||
});
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (_: Widget, target: EventTarget) => {
|
||||
const sketches = await this.sketchService.getSketches();
|
||||
if (!sketches.length) {
|
||||
this.openSketch();
|
||||
} else {
|
||||
this.toDisposeBeforeCreateNewContextMenu.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.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(OpenSketch.Commands.OPEN_SKETCH)));
|
||||
for (const sketch of sketches) {
|
||||
const command = { id: `arduino-open-sketch--${sketch.uri}` };
|
||||
const handler = { execute: () => this.openSketch(sketch) };
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(registry.registerCommand(command, handler));
|
||||
this.menuRegistry.registerMenuAction(ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP, {
|
||||
commandId: command.id,
|
||||
label: sketch.name
|
||||
});
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
|
||||
}
|
||||
try {
|
||||
const containers = await this.examplesService.builtIns();
|
||||
for (const container of containers) {
|
||||
this.builtInExamples.registerRecursively(container, ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP, this.toDisposeBeforeCreateNewContextMenu);
|
||||
}
|
||||
} 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 {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: 'Open...',
|
||||
order: '1'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+O'
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
||||
command: OpenSketch.Commands.OPEN_SKETCH__TOOLBAR.id,
|
||||
tooltip: 'Open',
|
||||
priority: 4
|
||||
});
|
||||
}
|
||||
|
||||
async openSketch(toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()): Promise<void> {
|
||||
const sketch = await toOpen;
|
||||
if (sketch) {
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
||||
execute: async (arg) => {
|
||||
const toOpen = !SketchLocation.is(arg)
|
||||
? await this.selectSketch()
|
||||
: arg;
|
||||
if (toOpen) {
|
||||
return this.openSketch(toOpen);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected async selectSketch(): Promise<Sketch | undefined> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
const defaultPath = await this.fileService.fsPath(new URI(config.sketchDirUri));
|
||||
const { filePaths } = await remote.dialog.showOpenDialog({
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: 'Sketch',
|
||||
extensions: ['ino', 'pde']
|
||||
}
|
||||
]
|
||||
});
|
||||
if (!filePaths.length) {
|
||||
return undefined;
|
||||
}
|
||||
if (filePaths.length > 1) {
|
||||
this.logger.warn(`Multiple sketches were selected: ${filePaths}. Using the first one.`);
|
||||
}
|
||||
const sketchFilePath = filePaths[0];
|
||||
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
|
||||
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
|
||||
if (sketch) {
|
||||
return sketch;
|
||||
}
|
||||
if (Sketch.isSketchFile(sketchFileUri)) {
|
||||
const name = new URI(sketchFileUri).path.name;
|
||||
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: 'Moving',
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
|
||||
order: '2',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+O',
|
||||
});
|
||||
}
|
||||
|
||||
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> {
|
||||
if (!toOpen) {
|
||||
return;
|
||||
}
|
||||
const uri = SketchLocation.toUri(toOpen);
|
||||
try {
|
||||
await this.sketchesService.loadSketch(uri.toString());
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
this.workspaceService.open(uri);
|
||||
}
|
||||
|
||||
private async selectSketch(): Promise<Sketch | undefined> {
|
||||
const defaultPath = await this.defaultPath();
|
||||
const { filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
|
||||
extensions: ['ino', 'pde'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
if (!filePaths.length) {
|
||||
return undefined;
|
||||
}
|
||||
if (filePaths.length > 1) {
|
||||
this.logger.warn(
|
||||
`Multiple sketches were selected: ${filePaths}. Using the first one.`
|
||||
);
|
||||
}
|
||||
const sketchFilePath = filePaths[0];
|
||||
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
|
||||
const sketch = await this.sketchesService.getSketchFolder(sketchFileUri);
|
||||
if (sketch) {
|
||||
return sketch;
|
||||
}
|
||||
if (Sketch.isSketchFile(sketchFileUri)) {
|
||||
return promptMoveSketch(sketchFileUri, {
|
||||
fileService: this.fileService,
|
||||
sketchesService: this.sketchesService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace OpenSketch {
|
||||
export namespace Commands {
|
||||
export const OPEN_SKETCH: Command = {
|
||||
id: 'arduino-open-sketch'
|
||||
};
|
||||
export const OPEN_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch--toolbar'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const OPEN_SKETCH: Command = {
|
||||
id: 'arduino-open-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function promptMoveSketch(
|
||||
sketchFileUri: string | URI,
|
||||
options: {
|
||||
fileService: FileService;
|
||||
sketchesService: SketchesService;
|
||||
labelProvider: LabelProvider;
|
||||
}
|
||||
): Promise<Sketch | undefined> {
|
||||
const { fileService, sketchesService, 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 sketchesService.getSketchFolder(newSketchUri.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
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 { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class QuitApp extends Contribution {
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
if (!isOSX) {
|
||||
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
|
||||
execute: () => remote.app.quit()
|
||||
});
|
||||
}
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
if (!isOSX) {
|
||||
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
|
||||
execute: () => remote.app.quit(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
|
||||
if (!isOSX) {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
|
||||
commandId: QuitApp.Commands.QUIT_APP.id,
|
||||
label: 'Quit',
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
|
||||
if (!isOSX) {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
|
||||
commandId: QuitApp.Commands.QUIT_APP.id,
|
||||
label: nls.localize('vscode/bulkEditService/quit', 'Quit'),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
if (!isOSX) {
|
||||
registry.registerKeybinding({
|
||||
command: QuitApp.Commands.QUIT_APP.id,
|
||||
keybinding: 'CtrlCmd+Q'
|
||||
});
|
||||
}
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
if (!isOSX) {
|
||||
registry.registerKeybinding({
|
||||
command: QuitApp.Commands.QUIT_APP.id,
|
||||
keybinding: 'CtrlCmd+Q',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export namespace QuitApp {
|
||||
export namespace Commands {
|
||||
export const QUIT_APP: Command = {
|
||||
id: 'arduino-quit-app'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const QUIT_APP: Command = {
|
||||
id: 'arduino-quit-app',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { isConflict } from '../create/typings';
|
||||
import {
|
||||
TaskFactoryImpl,
|
||||
WorkspaceInputDialogWithProgress,
|
||||
} from '../theia/workspace/workspace-input-dialog';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import {
|
||||
CloudSketchContribution,
|
||||
pullingSketch,
|
||||
pushingSketch,
|
||||
sketchAlreadyExists,
|
||||
synchronizingSketchbook,
|
||||
} from './cloud-contribution';
|
||||
import { Command, CommandRegistry, Sketch, URI } from './contribution';
|
||||
|
||||
export interface RenameCloudSketchParams {
|
||||
readonly cloudUri: URI;
|
||||
readonly sketch: Sketch;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class RenameCloudSketch extends CloudSketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH, {
|
||||
execute: (params: RenameCloudSketchParams) =>
|
||||
this.renameSketch(params, true),
|
||||
});
|
||||
}
|
||||
|
||||
private async renameSketch(
|
||||
params: RenameCloudSketchParams,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initValue: string = params.sketch.name
|
||||
): Promise<string | undefined> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (treeModel) {
|
||||
const posixPath = params.cloudUri.path.toString();
|
||||
const node = treeModel.getNode(posixPath);
|
||||
const parentNode = node?.parent;
|
||||
if (
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(node) &&
|
||||
CompositeTreeNode.is(parentNode)
|
||||
) {
|
||||
return this.openWizard(
|
||||
params,
|
||||
node,
|
||||
parentNode,
|
||||
treeModel,
|
||||
skipShowErrorMessageOnOpen,
|
||||
initValue
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async openWizard(
|
||||
params: RenameCloudSketchParams,
|
||||
node: CloudSketchbookTree.CloudSketchDirNode,
|
||||
parentNode: CompositeTreeNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined
|
||||
): Promise<string | undefined> {
|
||||
const parentUri = CloudSketchbookTree.CloudSketchDirNode.is(parentNode)
|
||||
? parentNode.uri
|
||||
: CreateUri.root;
|
||||
const existingNames = parentNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
const taskFactory = new TaskFactoryImpl((value) =>
|
||||
this.renameSketchWithProgress(params, node, treeModel, value)
|
||||
);
|
||||
try {
|
||||
const dialog = new WorkspaceInputDialogWithProgress(
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/renameCloudSketch/renameSketchTitle',
|
||||
'New name of the Cloud Sketch'
|
||||
),
|
||||
parentUri,
|
||||
initialValue,
|
||||
validate: (input) => {
|
||||
if (existingNames.includes(input)) {
|
||||
return sketchAlreadyExists(input);
|
||||
}
|
||||
return Sketch.validateCloudSketchFolderName(input) ?? '';
|
||||
},
|
||||
},
|
||||
this.labelProvider,
|
||||
taskFactory
|
||||
);
|
||||
await dialog.open(skipShowErrorMessageOnOpen);
|
||||
return dialog.taskResult;
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
await treeModel.refresh();
|
||||
return this.renameSketch(
|
||||
params,
|
||||
false,
|
||||
taskFactory.value ?? initialValue
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private renameSketchWithProgress(
|
||||
params: RenameCloudSketchParams,
|
||||
node: CloudSketchbookTree.CloudSketchDirNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
value: string
|
||||
): (progress: Progress) => Promise<string | undefined> {
|
||||
return async (progress: Progress) => {
|
||||
const fromName = params.cloudUri.path.name;
|
||||
const fromPosixPath = params.cloudUri.path.toString();
|
||||
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
|
||||
// push
|
||||
progress.report({ message: pushingSketch(params.sketch.name) });
|
||||
await treeModel.sketchbookTree().push(node);
|
||||
|
||||
// rename
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/renaming',
|
||||
"Renaming cloud sketch from '{0}' to '{1}'...",
|
||||
fromName,
|
||||
value
|
||||
),
|
||||
});
|
||||
await this.createApi.rename(fromPosixPath, toPosixPath);
|
||||
|
||||
// sync
|
||||
progress.report({
|
||||
message: synchronizingSketchbook,
|
||||
});
|
||||
this.createApi.sketchCache.init(); // invalidate the cache
|
||||
await this.createApi.sketches(); // IDE2 must pull all sketches to find the new one
|
||||
const sketch = this.createApi.sketchCache.getSketch(toPosixPath);
|
||||
if (!sketch) {
|
||||
return undefined;
|
||||
}
|
||||
await treeModel.refresh();
|
||||
|
||||
// pull
|
||||
progress.report({ message: pullingSketch(sketch.name) });
|
||||
const pulledNode = await this.pull(sketch);
|
||||
return pulledNode
|
||||
? node.uri.parent.resolve(sketch.name).toString()
|
||||
: undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
export namespace RenameCloudSketch {
|
||||
export namespace Commands {
|
||||
export const RENAME_CLOUD_SKETCH: Command = {
|
||||
id: 'arduino-rename-cloud-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,319 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
|
||||
import { Saveable } from '@theia/core/lib/browser/saveable';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { SketchContribution, URI, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry } from './contribution';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { DeleteSketch } from './delete-sketch';
|
||||
import {
|
||||
RenameCloudSketch,
|
||||
RenameCloudSketchParams,
|
||||
} from './rename-cloud-sketch';
|
||||
|
||||
@injectable()
|
||||
export class SaveAsSketch extends SketchContribution {
|
||||
export class SaveAsSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly applicationShell: ApplicationShell;
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
|
||||
execute: args => this.saveAs(args)
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
|
||||
execute: (args) => this.saveAs(args),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
label: nls.localizeByDefault('Save As...'),
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+Shift+S',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves `true` if the sketch was successfully saved as something.
|
||||
*/
|
||||
private async saveAs(
|
||||
{
|
||||
execOnlyIfTemp,
|
||||
openAfterMove,
|
||||
wipeOriginal,
|
||||
markAsRecentlyOpened,
|
||||
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||
): Promise<boolean> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let destinationUri: string | undefined;
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (cloudUri) {
|
||||
destinationUri = await this.createCloudCopy({ cloudUri, sketch });
|
||||
} else {
|
||||
destinationUri = await this.createLocalCopy(sketch, execOnlyIfTemp);
|
||||
}
|
||||
if (!destinationUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newWorkspaceUri = await this.sketchesService.copy(sketch, {
|
||||
destinationUri,
|
||||
});
|
||||
if (!newWorkspaceUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
|
||||
}
|
||||
const options: WorkspaceInput & StartupTask.Owner = {
|
||||
preserveWindow: true,
|
||||
tasks: [],
|
||||
};
|
||||
if (openAfterMove) {
|
||||
this.windowService.setSafeToShutDown();
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
options.tasks.push({
|
||||
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
args: [{ toDelete: sketch.uri }],
|
||||
});
|
||||
}
|
||||
this.workspaceService.open(new URI(newWorkspaceUri), options);
|
||||
}
|
||||
return !!newWorkspaceUri;
|
||||
}
|
||||
|
||||
private async createCloudCopy(
|
||||
params: RenameCloudSketchParams
|
||||
): Promise<string | undefined> {
|
||||
return this.commandService.executeCommand<string>(
|
||||
RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH.id,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
private async createLocalCopy(
|
||||
sketch: Sketch,
|
||||
execOnlyIfTemp?: boolean
|
||||
): Promise<string | undefined> {
|
||||
const isTemp = await this.sketchesService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
label: 'Save As...',
|
||||
order: '7'
|
||||
});
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const sketchbookDirUri = await this.defaultUri();
|
||||
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
|
||||
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
|
||||
// Otherwise, it proposes the parent folder of the current sketch.
|
||||
const containerDirUri = isTemp
|
||||
? sketchbookDirUri
|
||||
: !sketchbookDirUri.isEqualOrParent(sketchUri)
|
||||
? sketchbookDirUri
|
||||
: sketchUri.parent;
|
||||
const exists = await this.fileService.exists(
|
||||
containerDirUri.resolve(sketch.name)
|
||||
);
|
||||
|
||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||
// IDE2 must never prompt an invalid sketch folder name (https://github.com/arduino/arduino-ide/pull/1833#issuecomment-1412569252)
|
||||
const defaultUri = containerDirUri.resolve(
|
||||
Sketch.toValidSketchFolderName(sketch.name, exists)
|
||||
);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
return await this.promptLocalSketchFolderDestination(sketch, defaultPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts for the new sketch folder name until a valid one is give,
|
||||
* then resolves with the destination sketch folder URI string,
|
||||
* or `undefined` if the operation was canceled.
|
||||
*/
|
||||
private async promptLocalSketchFolderDestination(
|
||||
sketch: Sketch,
|
||||
defaultPath: string
|
||||
): Promise<string | undefined> {
|
||||
let sketchFolderDestinationUri: string | undefined;
|
||||
while (!sketchFolderDestinationUri) {
|
||||
const { filePath } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveFolderAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
);
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
// The new location of the sketch cannot be inside the location of current sketch.
|
||||
// https://github.com/arduino/arduino-ide/issues/1882
|
||||
let dialogContent: InvalidSketchFolderDialogContent | undefined;
|
||||
if (new URI(sketch.uri).isEqualOrParent(new URI(destinationUri))) {
|
||||
dialogContent = {
|
||||
message: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderLocationMessage',
|
||||
"Invalid sketch folder location: '{0}'",
|
||||
filePath
|
||||
),
|
||||
details: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderLocationDetails',
|
||||
'You cannot save a sketch into a folder inside itself.'
|
||||
),
|
||||
question: nls.localize(
|
||||
'arduino/sketch/editInvalidSketchFolderLocationQuestion',
|
||||
'Do you want to try saving the sketch to a different location?'
|
||||
),
|
||||
};
|
||||
}
|
||||
if (!dialogContent) {
|
||||
const sketchFolderName = new URI(destinationUri).path.base;
|
||||
const errorMessage = Sketch.validateSketchFolderName(sketchFolderName);
|
||||
if (errorMessage) {
|
||||
dialogContent = {
|
||||
message: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderNameMessage',
|
||||
"Invalid sketch folder name: '{0}'",
|
||||
sketchFolderName
|
||||
),
|
||||
details: errorMessage,
|
||||
question: nls.localize(
|
||||
'arduino/sketch/editInvalidSketchFolderQuestion',
|
||||
'Do you want to try saving the sketch with a different name?'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (dialogContent) {
|
||||
const message = `
|
||||
${dialogContent.message}
|
||||
|
||||
${dialogContent.details}
|
||||
|
||||
${dialogContent.question}`.trim();
|
||||
defaultPath = filePath;
|
||||
const { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
message,
|
||||
buttons: [Dialog.CANCEL, Dialog.YES],
|
||||
}
|
||||
);
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
sketchFolderDestinationUri = destinationUri;
|
||||
}
|
||||
}
|
||||
return sketchFolderDestinationUri;
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+Shift+S'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves `true` if the sketch was successfully saved as something.
|
||||
*/
|
||||
async saveAs({ execOnlyIfTemp, openAfterMove, wipeOriginal }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT): Promise<boolean> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||
const sketchDirUri = new URI((await this.configService.getConfiguration()).sketchDirUri);
|
||||
const exists = await this.fileService.exists(sketchDirUri.resolve(sketch.name));
|
||||
const defaultUri = exists
|
||||
? sketchDirUri.resolve(sketchDirUri.resolve(`${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`).toString())
|
||||
: sketchDirUri.resolve(sketch.name);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({ title: 'Save sketch folder as...', defaultPath });
|
||||
if (!filePath || canceled) {
|
||||
return false;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
if (!destinationUri) {
|
||||
return false;
|
||||
}
|
||||
const workspaceUri = await this.sketchService.copy(sketch, { destinationUri });
|
||||
if (workspaceUri && openAfterMove) {
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
try {
|
||||
await this.fileService.delete(new URI(sketch.uri), { recursive: true });
|
||||
} catch { /* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */ }
|
||||
}
|
||||
this.workspaceService.open(new URI(workspaceUri), { preserveWindow: true });
|
||||
}
|
||||
return !!workspaceUri;
|
||||
private async saveOntoCopiedSketch(
|
||||
sketch: Sketch,
|
||||
newSketchFolderUri: string
|
||||
): Promise<void> {
|
||||
const widgets = this.applicationShell.widgets;
|
||||
const snapshots = new Map<string, Saveable.Snapshot>();
|
||||
for (const widget of widgets) {
|
||||
const saveable = Saveable.getDirty(widget);
|
||||
const uri = NavigatableWidget.getUri(widget);
|
||||
if (!uri) {
|
||||
continue;
|
||||
}
|
||||
const uriString = uri.toString();
|
||||
let relativePath: string;
|
||||
if (
|
||||
uriString.includes(sketch.uri) &&
|
||||
saveable &&
|
||||
saveable.createSnapshot
|
||||
) {
|
||||
// The main file will change its name during the copy process
|
||||
// We need to store the new name in the map
|
||||
if (sketch.mainFileUri === uriString) {
|
||||
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
|
||||
relativePath = '/' + lastPart;
|
||||
} else {
|
||||
relativePath = uri.toString().substring(sketch.uri.length);
|
||||
}
|
||||
snapshots.set(relativePath, saveable.createSnapshot());
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
|
||||
const widgetUri = new URI(newSketchFolderUri + path);
|
||||
try {
|
||||
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface InvalidSketchFolderDialogContent {
|
||||
readonly message: string;
|
||||
readonly details: string;
|
||||
readonly question: string;
|
||||
}
|
||||
|
||||
export namespace SaveAsSketch {
|
||||
export namespace Commands {
|
||||
export const SAVE_AS_SKETCH: Command = {
|
||||
id: 'arduino-save-as-sketch'
|
||||
};
|
||||
}
|
||||
export interface Options {
|
||||
readonly execOnlyIfTemp?: boolean;
|
||||
readonly openAfterMove?: boolean;
|
||||
/**
|
||||
* Ignored if `openAfterMove` is `false`.
|
||||
*/
|
||||
readonly wipeOriginal?: boolean;
|
||||
}
|
||||
export namespace Options {
|
||||
export const DEFAULT: Options = {
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: false
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const SAVE_AS_SKETCH: Command = {
|
||||
id: 'arduino-save-as-sketch',
|
||||
};
|
||||
}
|
||||
export interface Options {
|
||||
readonly execOnlyIfTemp?: boolean;
|
||||
readonly openAfterMove?: boolean;
|
||||
/**
|
||||
* Ignored if `openAfterMove` is `false`.
|
||||
*/
|
||||
readonly wipeOriginal?: boolean;
|
||||
readonly markAsRecentlyOpened?: boolean;
|
||||
}
|
||||
export namespace Options {
|
||||
export const DEFAULT: Options = {
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: true,
|
||||
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 { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry } from './contribution';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
import {
|
||||
SketchContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class SaveSketch extends SketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
||||
execute: () => this.saveSketch(),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
||||
execute: () => this.saveSketch()
|
||||
});
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
|
||||
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id)
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
label: 'Save',
|
||||
order: '6'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
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> {
|
||||
return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id);
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
label: nls.localize('vscode/fileCommands/save', 'Save'),
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+S',
|
||||
});
|
||||
}
|
||||
|
||||
async saveSketch(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const isTemp = await this.sketchesService.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 Commands {
|
||||
export const SAVE_SKETCH: Command = {
|
||||
id: 'arduino-save-sketch'
|
||||
};
|
||||
export const SAVE_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-save-sketch--toolbar'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const SAVE_SKETCH: Command = {
|
||||
id: 'arduino-save-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,60 +0,0 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Command, MenuModelRegistry, CommandRegistry, SketchContribution, KeybindingRegistry } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { Settings as Preferences, SettingsDialog } from '../settings';
|
||||
|
||||
@injectable()
|
||||
export class Settings extends SketchContribution {
|
||||
|
||||
@inject(SettingsDialog)
|
||||
protected readonly settingsDialog: SettingsDialog;
|
||||
|
||||
protected settingsOpened = false;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Settings.Commands.OPEN, {
|
||||
execute: async () => {
|
||||
let settings: Preferences | undefined = undefined;
|
||||
try {
|
||||
this.settingsOpened = true;
|
||||
settings = await this.settingsDialog.open();
|
||||
} finally {
|
||||
this.settingsOpened = false;
|
||||
}
|
||||
if (settings) {
|
||||
await this.settingsService.update(settings);
|
||||
await this.settingsService.save();
|
||||
} else {
|
||||
await this.settingsService.reset();
|
||||
}
|
||||
},
|
||||
isEnabled: () => !this.settingsOpened
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SETTINGS_GROUP, {
|
||||
commandId: Settings.Commands.OPEN.id,
|
||||
label: 'Preferences...',
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: Settings.Commands.OPEN.id,
|
||||
keybinding: 'CtrlCmd+,',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace Settings {
|
||||
export namespace Commands {
|
||||
export const OPEN: Command = {
|
||||
id: 'arduino-settings-open',
|
||||
label: 'Open Preferences...',
|
||||
category: 'Arduino'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +1,196 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
|
||||
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { URI, SketchContribution, Command, CommandRegistry, MenuModelRegistry, KeybindingRegistry, TabBarToolbarRegistry, open } from './contribution';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
open,
|
||||
SketchContribution,
|
||||
TabBarToolbarRegistry,
|
||||
URI,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class SketchControl extends SketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(ContextMenuRenderer)
|
||||
private readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
protected readonly toDisposeBeforeCreateNewContextMenu =
|
||||
new DisposableCollection();
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(
|
||||
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR,
|
||||
{
|
||||
isVisible: (widget) =>
|
||||
this.shell.getWidgets('main').indexOf(widget) !== -1,
|
||||
execute: async () => {
|
||||
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
let parentElement: HTMLElement | undefined = undefined;
|
||||
const target = document.getElementById(
|
||||
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
||||
);
|
||||
if (target instanceof HTMLElement) {
|
||||
parentElement = target.parentElement ?? undefined;
|
||||
}
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected readonly toDisposeBeforeCreateNewContextMenu = new DisposableCollection();
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR, {
|
||||
isVisible: widget => this.shell.getWidgets('main').indexOf(widget) !== -1,
|
||||
execute: async () => {
|
||||
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = document.getElementById(SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id);
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainFileUri, rootFolderFileUris } = await this.sketchService.loadSketch(sketch.uri);
|
||||
const uris = [mainFileUri, ...rootFolderFileUris];
|
||||
for (let i = 0; i < uris.length; i++) {
|
||||
const uri = new URI(uris[i]);
|
||||
const command = { 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,
|
||||
label: this.labelProvider.getName(uri),
|
||||
order: `${i}`
|
||||
});
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
|
||||
}
|
||||
const options = {
|
||||
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
y: parentElement.getBoundingClientRect().top + parentElement.offsetHeight
|
||||
}
|
||||
}
|
||||
this.contextMenuRenderer.render(options);
|
||||
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
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP, {
|
||||
commandId: WorkspaceCommands.NEW_FILE.id,
|
||||
label: 'New Tab',
|
||||
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'
|
||||
});
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_DELETE.id,
|
||||
label: nls.localize('vscode/fileActions/delete', 'Delete'),
|
||||
order: '2',
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
WorkspaceCommands.FILE_DELETE
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, {
|
||||
commandId: CommonCommands.PREVIOUS_TAB.id,
|
||||
label: 'Previous Tab',
|
||||
order: '0'
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP, {
|
||||
commandId: CommonCommands.NEXT_TAB.id,
|
||||
label: 'Next Tab',
|
||||
order: '0'
|
||||
});
|
||||
}
|
||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||
const uris = [mainFileUri, ...rootFolderFileUris];
|
||||
for (let i = 0; i < uris.length; i++) {
|
||||
const uri = new URI(uris[i]);
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: WorkspaceCommands.NEW_FILE.id,
|
||||
keybinding: 'CtrlCmd+Shift+N'
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: CommonCommands.PREVIOUS_TAB.id,
|
||||
keybinding: 'CtrlCmd+Alt+Left' // TODO: check why electron does not show the keybindings in the UI.
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: CommonCommands.NEXT_TAB.id,
|
||||
keybinding: 'CtrlCmd+Alt+Right'
|
||||
});
|
||||
}
|
||||
// focus on the opened sketch
|
||||
const command = {
|
||||
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,
|
||||
label: this.labelProvider.getName(uri),
|
||||
order: String(i).padStart(4),
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(command)
|
||||
)
|
||||
);
|
||||
}
|
||||
const options = {
|
||||
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
y:
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
showDisabled: true,
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
||||
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
||||
});
|
||||
}
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.NEW_FILE.id,
|
||||
label: nls.localize('vscode/menubar/mNewTab', 'New Tab'),
|
||||
order: '0',
|
||||
}
|
||||
);
|
||||
|
||||
registry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
|
||||
{
|
||||
commandId: CommonCommands.PREVIOUS_TAB.id,
|
||||
label: nls.localize('vscode/menubar/mShowPreviousTab', 'Previous Tab'),
|
||||
order: '0',
|
||||
}
|
||||
);
|
||||
registry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
|
||||
{
|
||||
commandId: CommonCommands.NEXT_TAB.id,
|
||||
label: nls.localize('vscode/menubar/mShowNextTab', 'Next Tab'),
|
||||
order: '0',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: WorkspaceCommands.NEW_FILE.id,
|
||||
keybinding: 'CtrlCmd+Shift+N',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: CommonCommands.PREVIOUS_TAB.id,
|
||||
keybinding: 'CtrlCmd+Alt+Left',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: CommonCommands.NEXT_TAB.id,
|
||||
keybinding: 'CtrlCmd+Alt+Right',
|
||||
});
|
||||
}
|
||||
|
||||
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
||||
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SketchControl {
|
||||
export namespace Commands {
|
||||
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch-control--toolbar',
|
||||
iconClass: 'fa fa-caret-down'
|
||||
};
|
||||
}
|
||||
export namespace Commands {
|
||||
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch-control--toolbar',
|
||||
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 '../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.sketchesService.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,69 +1,64 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { SketchContribution, CommandRegistry, MenuModelRegistry, Sketch } from './contribution';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||
import { MenuModelRegistry } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Examples } from './examples';
|
||||
import { SketchContainer, SketchesError } from '../../common/protocol';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class Sketchbook extends SketchContribution {
|
||||
export class Sketchbook extends Examples {
|
||||
override onStart(): void {
|
||||
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||
this.configService.onDidChangeSketchDirUri(() => this.update());
|
||||
}
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
override async onReady(): Promise<void> {
|
||||
this.update();
|
||||
}
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
protected override update(): void {
|
||||
this.sketchesService.getSketches({}).then((container) => {
|
||||
this.register(container);
|
||||
this.menuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
|
||||
nls.localize('arduino/sketch/sketchbook', 'Sketchbook'),
|
||||
{ order: '3' }
|
||||
);
|
||||
}
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
private register(container: SketchContainer): void {
|
||||
this.toDispose.dispose();
|
||||
this.registerRecursively(
|
||||
[...container.children, ...container.sketches],
|
||||
ArduinoMenus.FILE__SKETCHBOOK_SUBMENU,
|
||||
this.toDispose
|
||||
);
|
||||
}
|
||||
|
||||
protected toDisposePerSketch = new Map<string, DisposableCollection>();
|
||||
|
||||
onStart(): void {
|
||||
this.sketchService.getSketches().then(sketches => {
|
||||
this.register(sketches);
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
this.sketchServiceClient.onSketchbookDidChange(({ created, removed }) => {
|
||||
this.unregister(removed);
|
||||
this.register(created);
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerSubmenu(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, 'Sketchbook', { order: '3' });
|
||||
}
|
||||
|
||||
protected register(sketches: Sketch[]): void {
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const toDispose = this.toDisposePerSketch.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
const command = { id: `arduino-sketchbook-open--${uri}` };
|
||||
const handler = { execute: () => this.commandRegistry.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch) };
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.menuRegistry.registerMenuAction(ArduinoMenus.FILE__SKETCHBOOK_SUBMENU, { commandId: command.id, label: sketch.name });
|
||||
this.toDisposePerSketch.set(sketch.uri, new DisposableCollection(
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command)),
|
||||
Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))
|
||||
));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected unregister(sketches: Sketch[]): void {
|
||||
for (const { uri } of sketches) {
|
||||
const toDispose = this.toDisposePerSketch.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user