mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-26 03:28:32 +00:00
Compare commits
563 Commits
2.0.0-beta
...
2.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06ad7d143c | ||
|
|
b78c4621fc | ||
|
|
e08439b6a4 | ||
|
|
e03e5eb603 | ||
|
|
b723951d78 | ||
|
|
ea2c54bff0 | ||
|
|
5fd02b9fd7 | ||
|
|
10b38820bc | ||
|
|
ea91904f00 | ||
|
|
57fa18b940 | ||
|
|
2aae9e0a07 | ||
|
|
9a99957e73 | ||
|
|
b25665561e | ||
|
|
420d31ff4b | ||
|
|
db01efead3 | ||
|
|
5a76be306a | ||
|
|
69ae38effa | ||
|
|
9a6a457bc4 | ||
|
|
144df893d0 | ||
|
|
e17472ef07 | ||
|
|
f6a43254f5 | ||
|
|
94d2962985 | ||
|
|
95fdc593ed | ||
|
|
954fee41a0 | ||
|
|
0bcb182ec0 | ||
|
|
42d017e876 | ||
|
|
c66b720509 | ||
|
|
a5c9735619 | ||
|
|
6e18dca644 | ||
|
|
f3b99b08da | ||
|
|
d0bce15f8b | ||
|
|
d79bc0d083 | ||
|
|
8f8b46fa6f | ||
|
|
d1fa6d4f3b | ||
|
|
4af488b05e | ||
|
|
db0049d635 | ||
|
|
0fbd1fbe85 | ||
|
|
9d2297c684 | ||
|
|
e47fb2e651 | ||
|
|
ee43a12eb7 | ||
|
|
31deeebb49 | ||
|
|
117b2a4fc7 | ||
|
|
278dd4ba87 | ||
|
|
36e2092398 | ||
|
|
33ab2a6955 | ||
|
|
192aac5a81 | ||
|
|
964ea3bc0c | ||
|
|
e6828f86d7 | ||
|
|
51f69f6a59 | ||
|
|
097c92d904 | ||
|
|
b451e2db8c | ||
|
|
b3b94948a3 | ||
|
|
80afd382ad | ||
|
|
2c74ad6437 | ||
|
|
9fff553f1a | ||
|
|
4c2f0fb841 | ||
|
|
54f210d4de | ||
|
|
5540170341 | ||
|
|
7cc252fc36 | ||
|
|
cb2a371263 | ||
|
|
96da5bb5ea | ||
|
|
ef5762599a | ||
|
|
3aee575a35 | ||
|
|
80d5b5afa7 | ||
|
|
ab5c63c4b7 | ||
|
|
0a53550b0b | ||
|
|
f5c98c8400 | ||
|
|
eb1f247296 | ||
|
|
6e72be1b4c | ||
|
|
e4beb03a40 | ||
|
|
39ab836880 | ||
|
|
dafb2454fd | ||
|
|
9b49712669 | ||
|
|
0ab28266df | ||
|
|
b09ae48536 | ||
|
|
2aad0e3b16 | ||
|
|
58aac236bf | ||
|
|
ec24b6813d | ||
|
|
d398ed1345 | ||
|
|
fb10de1446 | ||
|
|
24dc0bbc88 | ||
|
|
fa9777e529 | ||
|
|
77213507fb | ||
|
|
bfec85c352 | ||
|
|
f3d3d40c75 | ||
|
|
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 |
@@ -15,9 +15,10 @@ module.exports = {
|
||||
'.browser_modules/*',
|
||||
'docs/*',
|
||||
'scripts/*',
|
||||
'electron/*',
|
||||
'electron-app/*',
|
||||
'browser-app/*',
|
||||
'electron-app/lib/*',
|
||||
'electron-app/src-gen/*',
|
||||
'electron-app/gen-webpack*.js',
|
||||
'!electron-app/webpack.config.js',
|
||||
'plugins/*',
|
||||
'arduino-ide-extension/src/node/cli-protocol',
|
||||
],
|
||||
|
||||
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"
|
||||
27
.github/label-configuration-files/labels.yml
vendored
Normal file
27
.github/label-configuration-files/labels.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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: cloud"
|
||||
color: "00ffff"
|
||||
description: Related to Arduino Cloud and cloud sketches
|
||||
- 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
|
||||
206
.github/workflows/build.yml
vendored
206
.github/workflows/build.yml
vendored
@@ -1,48 +1,112 @@
|
||||
name: Arduino IDE
|
||||
|
||||
on:
|
||||
create:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '[0-9]+.[0-9]+.x'
|
||||
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)
|
||||
|
||||
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:
|
||||
run-determination:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
result: ${{ steps.determination.outputs.result }}
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Determine if the rest of the workflow should run
|
||||
id: determination
|
||||
run: |
|
||||
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
|
||||
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
|
||||
if [[
|
||||
"${{ github.event_name }}" != "create" ||
|
||||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
|
||||
]]; then
|
||||
# Run the other jobs.
|
||||
RESULT="true"
|
||||
else
|
||||
# There is no need to run the other jobs.
|
||||
RESULT="false"
|
||||
fi
|
||||
|
||||
echo "result=$RESULT" >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
name: build (${{ matrix.config.os }})
|
||||
needs: run-determination
|
||||
if: needs.run-determination.outputs.result == 'true'
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- os: windows-latest
|
||||
- os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259
|
||||
- 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-20.04
|
||||
- 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.14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '12.14.1'
|
||||
node-version: '16.14'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'yarn'
|
||||
|
||||
- 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@v4
|
||||
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
|
||||
@@ -50,40 +114,44 @@ 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/') }}
|
||||
IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }}
|
||||
CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }}
|
||||
run: |
|
||||
# See: https://www.electron.build/code-signing
|
||||
if [ $IS_FORK = true ]; then
|
||||
echo "Skipping the app signing: building from a fork."
|
||||
if [ $CAN_SIGN = false ]; then
|
||||
echo "Skipping the app signing: certificate not provided."
|
||||
else
|
||||
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"
|
||||
|
||||
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
|
||||
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] }}"
|
||||
export CSC_FOR_PULL_REQUEST=true
|
||||
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 install --immutable
|
||||
|
||||
yarn --cwd arduino-ide-extension build
|
||||
yarn test
|
||||
yarn --cwd arduino-ide-extension test:slow
|
||||
yarn --cwd arduino-ide-extension lint
|
||||
|
||||
yarn --cwd electron-app rebuild
|
||||
yarn --cwd electron-app build
|
||||
yarn --cwd electron-app package
|
||||
|
||||
- name: Upload [GitHub Actions]
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: electron/build/dist/build-artifacts/
|
||||
path: electron-app/dist/build-artifacts
|
||||
|
||||
artifacts:
|
||||
name: ${{ matrix.artifact.name }} artifact
|
||||
@@ -94,26 +162,30 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
artifact:
|
||||
- path: "*Linux_64bit.zip"
|
||||
name: Linux_X86-64
|
||||
- path: "*macOS_64bit.dmg"
|
||||
name: macOS
|
||||
- path: "*Windows_64bit.exe"
|
||||
- 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"
|
||||
- path: '*Windows_64bit.msi'
|
||||
name: Windows_X86-64_MSI
|
||||
- path: "*Windows_64bit.zip"
|
||||
- path: '*Windows_64bit.zip'
|
||||
name: Windows_X86-64_zip
|
||||
|
||||
steps:
|
||||
- name: Download job transfer artifact
|
||||
uses: actions/download-artifact@v2
|
||||
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@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact.name }}
|
||||
path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }}
|
||||
@@ -125,7 +197,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.
|
||||
|
||||
@@ -147,15 +219,19 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
echo -e "$BODY"
|
||||
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
|
||||
echo "::set-output name=BODY::$OUTPUT_SAFE_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: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: CHANGELOG.txt
|
||||
@@ -166,7 +242,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download [GitHub Actions]
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
@@ -174,20 +250,20 @@ jobs:
|
||||
- name: Publish Nightly [S3]
|
||||
uses: docker://plugins/s3
|
||||
env:
|
||||
PLUGIN_SOURCE: "${{ env.JOB_TRANSFER_ARTIFACT }}/*"
|
||||
PLUGIN_STRIP_PREFIX: "${{ env.JOB_TRANSFER_ARTIFACT }}/"
|
||||
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 }}
|
||||
|
||||
release:
|
||||
needs: changelog
|
||||
if: github.repository == 'arduino/arduino-ide' && startsWith(github.ref, 'refs/tags/')
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download [GitHub Actions]
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
@@ -195,10 +271,10 @@ jobs:
|
||||
- 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.7.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
|
||||
@@ -207,12 +283,22 @@ jobs:
|
||||
file_glob: true
|
||||
body: ${{ needs.changelog.outputs.BODY }}
|
||||
|
||||
# Temporary measure to prevent release update offers before the manually produced builds are uploaded.
|
||||
# The step must be removed once fully automated builds are regained.
|
||||
- name: Remove "channel update info files" related to manual builds
|
||||
run: |
|
||||
# See: https://github.com/arduino/arduino-ide/issues/2018
|
||||
rm "${{ env.JOB_TRANSFER_ARTIFACT }}/stable-linux.yml"
|
||||
# See: https://github.com/arduino/arduino-ide/issues/408
|
||||
rm "${{ env.JOB_TRANSFER_ARTIFACT }}/stable-mac.yml"
|
||||
|
||||
- name: Publish Release [S3]
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
uses: docker://plugins/s3
|
||||
env:
|
||||
PLUGIN_SOURCE: "${{ env.JOB_TRANSFER_ARTIFACT }}/*"
|
||||
PLUGIN_STRIP_PREFIX: "${{ env.JOB_TRANSFER_ARTIFACT }}/"
|
||||
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 }}
|
||||
@@ -229,6 +315,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Remove unneeded job transfer artifact
|
||||
uses: geekyeggo/delete-artifact@v1
|
||||
uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
|
||||
53
.github/workflows/check-certificates.yml
vendored
53
.github/workflows/check-certificates.yml
vendored
@@ -3,6 +3,7 @@ name: Check Certificates
|
||||
|
||||
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on:
|
||||
create:
|
||||
push:
|
||||
paths:
|
||||
- '.github/workflows/check-certificates.ya?ml'
|
||||
@@ -20,12 +21,49 @@ env:
|
||||
EXPIRATION_WARNING_PERIOD: 30
|
||||
|
||||
jobs:
|
||||
run-determination:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
result: ${{ steps.determination.outputs.result }}
|
||||
steps:
|
||||
- name: Determine if the rest of the workflow should run
|
||||
id: determination
|
||||
run: |
|
||||
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
|
||||
REPO_SLUG="arduino/arduino-ide"
|
||||
if [[
|
||||
(
|
||||
# Only run on branch creation when it is a release branch.
|
||||
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
|
||||
"${{ github.event_name }}" != "create" ||
|
||||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX
|
||||
) &&
|
||||
(
|
||||
# Only run when the workflow will have access to the certificate secrets.
|
||||
# This could be done via a GitHub Actions workflow conditional, but makes more sense to do it here as well.
|
||||
(
|
||||
"${{ github.event_name }}" != "pull_request" &&
|
||||
"${{ github.repository }}" == "$REPO_SLUG"
|
||||
) ||
|
||||
(
|
||||
"${{ github.event_name }}" == "pull_request" &&
|
||||
"${{ github.event.pull_request.head.repo.full_name }}" == "$REPO_SLUG"
|
||||
)
|
||||
)
|
||||
]]; then
|
||||
# Run the other jobs.
|
||||
RESULT="true"
|
||||
else
|
||||
# There is no need to run the other jobs.
|
||||
RESULT="false"
|
||||
fi
|
||||
|
||||
echo "result=$RESULT" >> $GITHUB_OUTPUT
|
||||
|
||||
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')
|
||||
needs: run-determination
|
||||
if: needs.run-determination.outputs.result == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -59,7 +97,9 @@ jobs:
|
||||
(
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-noout -passin env:CERTIFICATE_PASSWORD
|
||||
-legacy \
|
||||
-noout \
|
||||
-passin env:CERTIFICATE_PASSWORD
|
||||
) || (
|
||||
echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
|
||||
exit 1
|
||||
@@ -87,6 +127,7 @@ jobs:
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-clcerts \
|
||||
-legacy \
|
||||
-nodes \
|
||||
-passin env:CERTIFICATE_PASSWORD
|
||||
) | (
|
||||
@@ -108,7 +149,7 @@ jobs:
|
||||
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
|
||||
|
||||
85
.github/workflows/check-i18n-task.yml
vendored
Normal file
85
.github/workflows/check-i18n-task.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
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:
|
||||
create:
|
||||
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:
|
||||
run-determination:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
result: ${{ steps.determination.outputs.result }}
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Determine if the rest of the workflow should run
|
||||
id: determination
|
||||
run: |
|
||||
RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x"
|
||||
TAG_REGEX="refs/tags/.*"
|
||||
# The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead.
|
||||
if [[
|
||||
("${{ github.event_name }}" != "create" ||
|
||||
"${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX) &&
|
||||
! "${{ github.ref }}" =~ $TAG_REGEX
|
||||
]]; then
|
||||
# Run the other jobs.
|
||||
RESULT="true"
|
||||
else
|
||||
# There is no need to run the other jobs.
|
||||
RESULT="false"
|
||||
fi
|
||||
|
||||
echo "result=$RESULT" >> $GITHUB_OUTPUT
|
||||
|
||||
check:
|
||||
needs: run-determination
|
||||
if: needs.run-determination.outputs.result == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js 16.14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.14'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
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 install --immutable
|
||||
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 }}
|
||||
46
.github/workflows/i18n-nightly-push.yml
vendored
Normal file
46
.github/workflows/i18n-nightly-push.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
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.14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.14'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
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 install --immutable
|
||||
|
||||
- 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 }}
|
||||
54
.github/workflows/i18n-weekly-pull.yml
vendored
Normal file
54
.github/workflows/i18n-weekly-pull.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
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.14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16.14'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
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 install --immutable
|
||||
|
||||
- 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@v5
|
||||
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 }}
|
||||
63
.github/workflows/themes-weekly-pull.yml
vendored
Normal file
63
.github/workflows/themes-weekly-pull.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
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'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
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 install --immutable
|
||||
|
||||
- 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@v5
|
||||
with:
|
||||
commit-message: Updated themes
|
||||
title: Update themes
|
||||
branch: themes/themes-update
|
||||
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -1,18 +1,22 @@
|
||||
node_modules/
|
||||
# .node_modules is a hack for the electron builder.
|
||||
.node_modules/
|
||||
lib/
|
||||
downloads/
|
||||
build/
|
||||
Examples/
|
||||
!electron/build/
|
||||
resources/
|
||||
arduino-ide-extension/Examples/
|
||||
src-gen/
|
||||
*webpack.config.js
|
||||
gen-webpack.config.js
|
||||
gen-webpack.node.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
|
||||
electron-app/plugins
|
||||
# the tokens folder for the themes
|
||||
scripts/themes/tokens
|
||||
# content trace files for electron
|
||||
electron-app/traces
|
||||
# any Arduino LS generated log files
|
||||
inols*.log
|
||||
# The electron-builder output.
|
||||
electron-app/dist
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"printWidth": 80
|
||||
"printWidth": 80,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
|
||||
99
.vscode/launch.json
vendored
99
.vscode/launch.json
vendored
@@ -4,36 +4,33 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "App (Electron)",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
|
||||
"name": "App",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd",
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"NODE_PRESERVE_SYMLINKS": "1"
|
||||
}
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd",
|
||||
},
|
||||
"cwd": "${workspaceFolder}/electron-app",
|
||||
"protocol": "inspector",
|
||||
"args": [
|
||||
".",
|
||||
"--log-level=debug",
|
||||
"--hostname=localhost",
|
||||
"--no-cluster",
|
||||
"--app-project-path=${workspaceRoot}/electron-app",
|
||||
"--app-project-path=${workspaceFolder}/electron-app",
|
||||
"--remote-debugging-port=9222",
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:../plugins"
|
||||
"--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"
|
||||
"${workspaceFolder}/electron-app/lib/backend/electron-main.js",
|
||||
"${workspaceFolder}/electron-app/lib/backend/main.js",
|
||||
"${workspaceFolder}/electron-app/lib/**/*.js",
|
||||
"${workspaceFolder}/arduino-ide-extension/lib/**/*.js",
|
||||
"${workspaceFolder}/node_modules/@theia/**/*.js"
|
||||
],
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
@@ -42,49 +39,71 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"name": "App [Dev]",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
||||
"windows": {
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"NODE_PRESERVE_SYMLINKS": "1"
|
||||
}
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd",
|
||||
},
|
||||
"cwd": "${workspaceFolder}/electron-app",
|
||||
"args": [
|
||||
".",
|
||||
"--log-level=debug",
|
||||
"--hostname=localhost",
|
||||
"--app-project-path=${workspaceFolder}/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}/browser-app/src-gen/backend/*.js",
|
||||
"${workspaceRoot}/browser-app/lib/**/*.js",
|
||||
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
|
||||
"${workspaceFolder}/electron-app/lib/backend/electron-main.js",
|
||||
"${workspaceFolder}/electron-app/lib/backend/main.js",
|
||||
"${workspaceFolder}/electron-app/lib/**/*.js",
|
||||
"${workspaceFolder}/arduino-ide-extension/lib/**/*.js",
|
||||
"${workspaceFolder}/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",
|
||||
"protocol": "inspector",
|
||||
"name": "Run Test [current]",
|
||||
"program": "${workspaceRoot}/node_modules/mocha/bin/_mocha",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"args": [
|
||||
"--require",
|
||||
"reflect-metadata/Reflect",
|
||||
"--require",
|
||||
"ignore-styles",
|
||||
"--no-timeouts",
|
||||
"--colors",
|
||||
"**/${fileBasenameNoExtension}.js"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "${workspaceRoot}/tsconfig.json"
|
||||
"TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.json",
|
||||
"IDE2_TEST": "true"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"smartStep": true,
|
||||
@@ -97,12 +116,14 @@
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}"
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Electron Packager",
|
||||
"program": "${workspaceRoot}/electron/packager/index.js",
|
||||
"cwd": "${workspaceFolder}/electron/packager"
|
||||
"name": "Launch Electron Backend & Frontend",
|
||||
"configurations": [
|
||||
"App",
|
||||
"Attach to Electron Frontend"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -2,6 +2,9 @@
|
||||
"files.exclude": {
|
||||
"**/lib": false
|
||||
},
|
||||
"search.exclude": {
|
||||
"arduino-ide-extension/src/test/node/__test_sketchbook__": true
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
|
||||
42
.vscode/tasks.json
vendored
42
.vscode/tasks.json
vendored
@@ -2,7 +2,7 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Arduino IDE - Rebuild Electron App",
|
||||
"label": "Rebuild App",
|
||||
"type": "shell",
|
||||
"command": "yarn rebuild:browser && yarn rebuild:electron",
|
||||
"group": "build",
|
||||
@@ -13,18 +13,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Start Browser App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app start",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch IDE Extension",
|
||||
"label": "Watch Extension",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./arduino-ide-extension watch",
|
||||
"group": "build",
|
||||
@@ -35,18 +24,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch Browser App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app watch",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch Electron App",
|
||||
"label": "Watch App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./electron-app watch",
|
||||
"group": "build",
|
||||
@@ -57,19 +35,11 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch All [Browser]",
|
||||
"label": "Watch All",
|
||||
"type": "shell",
|
||||
"dependsOn": [
|
||||
"Arduino IDE - Watch IDE Extension",
|
||||
"Arduino IDE - Watch Browser App"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch All [Electron]",
|
||||
"type": "shell",
|
||||
"dependsOn": [
|
||||
"Arduino IDE - Watch IDE Extension",
|
||||
"Arduino IDE - Watch Electron App"
|
||||
"Watch Extension",
|
||||
"Watch App"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
137
BUILDING.md
137
BUILDING.md
@@ -1,136 +1,3 @@
|
||||
# Development
|
||||
|
||||
This page includes technical documentation for developers who want to build the IDE locally and contribute to the project.
|
||||
|
||||
## Architecture overview
|
||||
|
||||
The IDE consists of three major parts:
|
||||
- the _Electron main_ process,
|
||||
- the _backend_, and
|
||||
- the _frontend_.
|
||||
|
||||
The _Electron main_ process is responsible for:
|
||||
- creating the application,
|
||||
- managing the application lifecycle via listeners, and
|
||||
- creating and managing the web pages for the app.
|
||||
|
||||
In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly one main process.
|
||||
|
||||
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>
|
||||
|
||||
In normal browsers, web pages usually run in a sandboxed environment, and accessing native resources are disallowed. However, Electron has the power to use Node.js APIs in the web pages allowing lower-level OS interactions. Due to security reasons, accessing native resources is an undesired behavior in the IDE. So by convention, we do not use Node.js APIs. (Note: the Node.js integration is [not yet disabled](https://github.com/eclipse-theia/theia/issues/2018) although it is not used). In the IDE, only the _backend_ allows OS interaction.
|
||||
|
||||
The _backend_ process is responsible for:
|
||||
- providing access to the filesystem,
|
||||
- communicating with the [Arduino CLI](https://github.com/arduino/arduino-cli) via gRPC,
|
||||
- running your terminal,
|
||||
- exposing additional RESTful APIs,
|
||||
- performing the Git commands in the local repositories,
|
||||
- hosting and running any VS Code extensions, or
|
||||
- executing VS Code tasks<sup>[[2]]</sup>.
|
||||
|
||||
The _Electron main_ process spawns the _backend_ process. There is always exactly one _backend_ process. However, due to performance considerations, the _backend_ spawns several sub-processes for the filesystem watching, Git repository discovery, etc. The communication between the _backend_ process and its sub-processes is established via IPC. Besides spawning sub-processes, the _backend_ will start an HTTP server on a random available port, and serves the web application as static content. When the sub-processes are up and running, and the HTTP server is also listening, the _backend_ process sends the HTTP server port to the _Electron main_ process via IPC. The _Electron main_ process will load the _backend_'s endpoint in the `BrowserWindow`.
|
||||
|
||||
The _frontend_ is running as an Electron renderer process and can invoke services implemented on the _backend_. The communication between the _backend_ and the _frontend_ is done via JSON-RPC over a websocket connection. This means, the services running in the _frontend_ are all proxies, and will ask the corresponding service implementation on the _backend_.
|
||||
|
||||
[1]: https://www.electronjs.org/docs/tutorial/application-architecture#differences-between-main-process-and-renderer-process
|
||||
[2]: https://code.visualstudio.com/Docs/editor/tasks
|
||||
|
||||
|
||||
## Build from source
|
||||
|
||||
If you’re familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the
|
||||
project, you should be able to build the Arduino IDE locally. Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
|
||||
|
||||
### Build
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
### Rebuild the native dependencies
|
||||
```sh
|
||||
yarn rebuild:electron
|
||||
```
|
||||
|
||||
### Start
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
### CI
|
||||
|
||||
This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions).
|
||||
|
||||
- _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts for each operating system can be downloaded from the GitHub Actions page.
|
||||
- _Nightly_ builds run every day at 03:00 GMT from the `main` branch.
|
||||
- _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build:
|
||||
- Create a local tag:
|
||||
```sh
|
||||
git tag -a 1.2.3 -m "Creating a new tag for the `1.2.3` release."
|
||||
```
|
||||
- Push it to the remote:
|
||||
```sh
|
||||
git push origin 1.2.3
|
||||
```
|
||||
|
||||
## Notes for macOS contributors
|
||||
Beginning in macOS 10.14.5, the software [must be notarized to run](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution). The signing and notarization processes for the Arduino IDE are managed by our Continuous Integration (CI) workflows, implemented with GitHub Actions. On every push and pull request, the Arduino IDE is built and saved to a workflow artifact. These artifacts can be used by contributors and beta testers who don't want to set up a build system locally.
|
||||
For security reasons, signing and notarization are disabled for workflow runs for pull requests from forks of this repository. This means that macOS will block you from running those artifacts.
|
||||
Due to this limitation, Mac users have two options for testing contributions from forks:
|
||||
|
||||
### The Safe approach (recommended)
|
||||
|
||||
Follow [the instructions above](#build-from-source) to create the build environment locally, then build the code you want to test.
|
||||
|
||||
### The Risky approach
|
||||
|
||||
*Please note that this approach is risky as you are lowering the security on your system, therefore we strongly discourage you from following it.*
|
||||
1. Use [this guide](https://help.apple.com/xcode/mac/10.2/index.html?localePath=en.lproj#/dev9b7736b0e), in order to disable Gatekeeper (at your own risk!).
|
||||
1. Download the unsigned artifact provided by the CI workflow run related to the Pull Request at each push.
|
||||
1. Re-enable Gatekeeper after tests are done, following the guide linked above.
|
||||
|
||||
### Creating a release
|
||||
|
||||
You will not need to create a new release yourself as the Arduino team takes care of this on a regular basis, but we are documenting the process here. Let's assume the current version is `0.1.3` and you want to release `0.2.0`.
|
||||
|
||||
- Make sure the `main` state represents what you want to release and you're on `main`.
|
||||
- Prepare a release-candidate build on a branch:
|
||||
```bash
|
||||
git branch 0.2.0-rc \
|
||||
&& git checkout 0.2.0-rc
|
||||
```
|
||||
- Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one:
|
||||
```bash
|
||||
yarn update:version 0.2.0
|
||||
```
|
||||
- This should generate multiple outgoing changes with the version update.
|
||||
- Commit your changes and push to the remote:
|
||||
```bash
|
||||
git add . \
|
||||
&& git commit -s -m "Updated versions to 0.2.0" \
|
||||
&& git push
|
||||
```
|
||||
- Create the GH PR the workflow starts automatically.
|
||||
- Once you're happy with the RC, merge the changes to the `main`.
|
||||
- Create a tag and push it:
|
||||
```bash
|
||||
git tag -a 0.2.0 -m "0.2.0" \
|
||||
&& git push origin 0.2.0
|
||||
```
|
||||
- The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases).
|
||||
- If you do not want to release the `EXE` and `MSI` installers, wipe them manually.
|
||||
- If you do not like the generated changelog, modify it and update the GH release.
|
||||
|
||||
## FAQ
|
||||
|
||||
* *Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?*
|
||||
|
||||
Yes. It is possible but not recommended. The CLI exposes a set of functionality via [gRPC](https://github.com/arduino/arduino-cli/tree/master/rpc) and the IDE uses this API to communicate with the CLI. Before we build a new version of IDE, we pin a specific version of CLI and use the corresponding `proto` files to generate TypeScript modules for gRPC. This means, a particular version of IDE is compliant only with the pinned version of CLI. Mismatching IDE and CLI versions might not be able to communicate with each other. This could cause unpredictable IDE behavior.
|
||||
|
||||
* *I have understood that not all versions of the CLI are compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?*
|
||||
|
||||
[Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at:
|
||||
- Windows: `C:\path\to\Arduino IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
|
||||
- macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
|
||||
- Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
|
||||
# Development Guide
|
||||
|
||||
This documentation has been moved [**here**](docs/development.md#development-guide).
|
||||
|
||||
48
README.md
48
README.md
@@ -1,43 +1,18 @@
|
||||
<img src="https://content.arduino.cc/website/Arduino_logo_teal.svg" height="100" align="right" />
|
||||
|
||||
# Arduino IDE 2.x (beta)
|
||||
# Arduino IDE 2.x
|
||||
|
||||
[](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
|
||||
|
||||
This repository contains the source code of the Arduino IDE 2.x, which is currently in beta stage. If you're looking for the stable IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
|
||||
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
|
||||
|
||||
The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is based on the [Theia IDE](https://theia-ide.org/) framework and built with [Electron](https://www.electronjs.org/). The backend operations such as compilation and uploading are offloaded to an [arduino-cli](https://github.com/arduino/arduino-cli) instance running in daemon mode. This new IDE was developed with the goal of preserving the same interface and user experience of the previous major version in order to provide a frictionless upgrade.
|
||||
|
||||
> ⚠️ This is **beta** software. Help us test it!
|
||||
|
||||

|
||||
|
||||
## Download
|
||||
|
||||
You can download the latest version from the [software download page on the Arduino website](https://www.arduino.cc/en/software#experimental-software).
|
||||
### Nightly builds
|
||||
|
||||
These builds are generated every day at 03:00 GMT from the `main` branch and
|
||||
should be considered unstable:
|
||||
|
||||
Platform | 32 bit | 64 bit |
|
||||
--------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
Linux | | [Nightly Linux 64 bit] |
|
||||
Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
|
||||
Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
|
||||
macOS | | [Nightly macOS 64 bit] |
|
||||
|
||||
[🚧 Work in progress...]: https://github.com/arduino/arduino-ide/issues/107
|
||||
[Nightly Linux 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
|
||||
[Nightly Windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
|
||||
[Nightly Windows 64 bit MSI]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
|
||||
[Nightly Windows 64 bit ZIP]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
|
||||
[Nightly macOS 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
|
||||
|
||||
> These links return an HTTP `302: Found` response, redirecting to latest
|
||||
generated builds by replacing `latest` with the latest available build
|
||||
date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
|
||||
replaced with `20190806`)
|
||||
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).
|
||||
|
||||
## Support
|
||||
|
||||
@@ -45,10 +20,9 @@ If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-u
|
||||
|
||||
## Bugs & Issues
|
||||
|
||||
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository. A few rules apply:
|
||||
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository.
|
||||
|
||||
* Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
|
||||
* Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board.
|
||||
See [**the issue report guide**](docs/contributor-guide/issues.md#issue-report-guide) for instructions.
|
||||
|
||||
### Security
|
||||
|
||||
@@ -60,14 +34,16 @@ e-mail contact: security@arduino.cc
|
||||
|
||||
## Contributions and development
|
||||
|
||||
Contributions are very welcome! You can browse the list of open issues to see what's needed and then you can submit your code using a Pull Request. Please provide detailed descriptions. We also appreciate any help in testing issues and patches contributed by other users.
|
||||
Contributions are very welcome! There are several ways to participate in this project, including:
|
||||
|
||||
This repository contains the main code, but two more repositories are included during the build process:
|
||||
- Fixing bugs
|
||||
- Beta testing
|
||||
- Translation
|
||||
|
||||
* [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
|
||||
* [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
|
||||
See [**the contributor guide**](docs/CONTRIBUTING.md#contributor-guide) for more information.
|
||||
|
||||
See the [**development guide**](docs/development.md) for a technical overview of the application and instructions for building the code.
|
||||
|
||||
See the [BUILDING.md](BUILDING.md) for a technical overview of the application and instructions for building the code.
|
||||
## Donations
|
||||
|
||||
This open source code was written by the Arduino team and is maintained on a daily basis with the help of the community. We invest a considerable amount of time in development, testing and optimization. Please consider [donating](https://www.arduino.cc/en/donate/) or [sponsoring](https://github.com/sponsors/arduino) to support our work, as well as [buying original Arduino boards](https://store.arduino.cc/) which is the best way to make sure our effort can continue in the long term.
|
||||
|
||||
@@ -30,17 +30,20 @@ The Core Service is responsible for building your sketches and uploading them to
|
||||
- compiling a sketch for a selected board type
|
||||
- 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
|
||||
|
||||
@@ -59,6 +62,15 @@ The Config Service knows about your system, like for example the default sketch
|
||||
- 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:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,115 +1,141 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "2.0.0-beta.11",
|
||||
"version": "2.2.0",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn clean && yarn download-examples && yarn build",
|
||||
"prepare": "yarn download-cli && yarn download-fwuploader && yarn download-ls && yarn copy-i18n && yarn download-examples",
|
||||
"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": "ncp ../i18n ./src/node/resources/i18n",
|
||||
"download-ls": "node ./scripts/download-ls.js",
|
||||
"download-examples": "node ./scripts/download-examples.js",
|
||||
"generate-protocol": "node ./scripts/generate-protocol.js",
|
||||
"lint": "eslint",
|
||||
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
|
||||
"prebuild": "rimraf lib",
|
||||
"build": "tsc",
|
||||
"build:dev": "yarn build",
|
||||
"postbuild": "ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/",
|
||||
"watch": "tsc -w",
|
||||
"test": "mocha \"./lib/test/**/*.test.js\"",
|
||||
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
|
||||
"test": "cross-env IDE2_TEST=true mocha \"./lib/test/**/*.test.js\"",
|
||||
"test:slow": "cross-env IDE2_TEST=true mocha \"./lib/test/**/*.slow-test.js\" --slow 5000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.1.1",
|
||||
"@theia/application-package": "next",
|
||||
"@theia/core": "next",
|
||||
"@theia/editor": "next",
|
||||
"@theia/filesystem": "next",
|
||||
"@theia/git": "next",
|
||||
"@theia/keymaps": "next",
|
||||
"@theia/markers": "next",
|
||||
"@theia/monaco": "next",
|
||||
"@theia/navigator": "next",
|
||||
"@theia/outline-view": "next",
|
||||
"@theia/output": "next",
|
||||
"@theia/preferences": "next",
|
||||
"@theia/search-in-workspace": "next",
|
||||
"@theia/terminal": "next",
|
||||
"@theia/workspace": "next",
|
||||
"@grpc/grpc-js": "^1.8.14",
|
||||
"@theia/application-package": "1.39.0",
|
||||
"@theia/core": "1.39.0",
|
||||
"@theia/debug": "1.39.0",
|
||||
"@theia/editor": "1.39.0",
|
||||
"@theia/electron": "1.39.0",
|
||||
"@theia/filesystem": "1.39.0",
|
||||
"@theia/keymaps": "1.39.0",
|
||||
"@theia/markers": "1.39.0",
|
||||
"@theia/messages": "1.39.0",
|
||||
"@theia/monaco": "1.39.0",
|
||||
"@theia/monaco-editor-core": "1.72.3",
|
||||
"@theia/navigator": "1.39.0",
|
||||
"@theia/outline-view": "1.39.0",
|
||||
"@theia/output": "1.39.0",
|
||||
"@theia/plugin-ext": "1.39.0",
|
||||
"@theia/preferences": "1.39.0",
|
||||
"@theia/scm": "1.39.0",
|
||||
"@theia/search-in-workspace": "1.39.0",
|
||||
"@theia/terminal": "1.39.0",
|
||||
"@theia/typehierarchy": "1.39.0",
|
||||
"@theia/workspace": "1.39.0",
|
||||
"@tippyjs/react": "^4.2.5",
|
||||
"@types/atob": "^2.1.2",
|
||||
"@types/auth0-js": "^9.14.0",
|
||||
"@types/btoa": "^1.2.3",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/deepmerge": "^2.2.0",
|
||||
"@types/glob": "^5.0.35",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/js-yaml": "^3.12.2",
|
||||
"@types/keytar": "^4.4.0",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@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/temp": "^0.8.34",
|
||||
"@types/which": "^1.3.1",
|
||||
"ajv": "^6.5.3",
|
||||
"arduino-serial-plotter-webapp": "0.2.0",
|
||||
"async-mutex": "^0.3.0",
|
||||
"atob": "^2.1.2",
|
||||
"auth0-js": "^9.14.0",
|
||||
"btoa": "^1.2.1",
|
||||
"css-element-queries": "^1.2.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cpy": "^10.0.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"dateformat": "^3.0.3",
|
||||
"deepmerge": "2.0.1",
|
||||
"fuzzy": "^0.1.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"drivelist": "^9.2.4",
|
||||
"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",
|
||||
"google-protobuf": "^3.20.1",
|
||||
"hash.js": "^1.1.7",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"is-online": "^10.0.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"jsdom": "^21.1.1",
|
||||
"jsonc-parser": "^2.2.0",
|
||||
"just-diff": "^5.1.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"keytar": "7.2.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"ncp": "^2.0.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-log-rotate": "^0.1.5",
|
||||
"open": "^8.0.6",
|
||||
"p-queue": "^5.0.0",
|
||||
"p-debounce": "^2.1.0",
|
||||
"p-queue": "^2.4.2",
|
||||
"process": "^0.11.10",
|
||||
"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-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"
|
||||
"util": "^0.12.5",
|
||||
"vscode-arduino-api": "^0.1.2"
|
||||
},
|
||||
"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",
|
||||
"@xhmikosr/downloader": "^13.0.1",
|
||||
"chai": "^4.2.0",
|
||||
"chai-string": "^1.5.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"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",
|
||||
"ncp": "^2.0.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"shelljs": "^0.8.3",
|
||||
"sinon": "^9.0.1",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^11.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"grpc-tools": "^1.9.0"
|
||||
"grpc-tools": "^1.9.0",
|
||||
"protoc": "^1.0.4"
|
||||
},
|
||||
"mocha": {
|
||||
"require": [
|
||||
"reflect-metadata/Reflect"
|
||||
"reflect-metadata/Reflect",
|
||||
"ignore-styles"
|
||||
],
|
||||
"reporter": "spec",
|
||||
"colors": true,
|
||||
@@ -124,24 +150,38 @@
|
||||
"examples"
|
||||
],
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"preload": "lib/electron-browser/preload"
|
||||
},
|
||||
{
|
||||
"backend": "lib/node/arduino-ide-backend-module",
|
||||
"frontend": "lib/browser/arduino-ide-frontend-module"
|
||||
},
|
||||
{
|
||||
"frontend": "lib/browser/theia/core/browser-menu-module",
|
||||
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
|
||||
},
|
||||
{
|
||||
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
|
||||
},
|
||||
{
|
||||
"frontendElectron": "lib/electron-browser/electron-arduino-module"
|
||||
},
|
||||
{
|
||||
"electronMain": "lib/electron-main/arduino-electron-main-module"
|
||||
}
|
||||
],
|
||||
"arduino": {
|
||||
"cli": {
|
||||
"version": "0.18.3"
|
||||
"arduino-cli": {
|
||||
"version": "0.34.0"
|
||||
},
|
||||
"fwuploader": {
|
||||
"version": "2.0.0"
|
||||
"arduino-fwuploader": {
|
||||
"version": "2.4.0"
|
||||
},
|
||||
"arduino-language-server": {
|
||||
"version": "0.7.4"
|
||||
},
|
||||
"clangd": {
|
||||
"version": "14.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
116
arduino-ide-extension/scripts/compose-changelog.js
Executable file
116
arduino-ide-extension/scripts/compose-changelog.js
Executable file
@@ -0,0 +1,116 @@
|
||||
// @ts-check
|
||||
|
||||
(async () => {
|
||||
const { Octokit } = require('@octokit/rest');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const octokit = new Octokit({
|
||||
userAgent: 'Arduino IDE compose-changelog.js',
|
||||
});
|
||||
|
||||
const response = await octokit.rest.repos
|
||||
.listReleases({
|
||||
owner: 'arduino',
|
||||
repo: 'arduino-ide',
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const releases = response.data;
|
||||
|
||||
let fullChangelog = releases.reduce((acc, item, index) => {
|
||||
// Process each line separately
|
||||
const body = item.body.split('\n').map(processLine).join('\n');
|
||||
// item.name is the name of the release changelog
|
||||
return (
|
||||
acc +
|
||||
`## ${item.name}\n\n${body}${
|
||||
index !== releases.length - 1 ? '\n\n---\n\n' : '\n'
|
||||
}`
|
||||
);
|
||||
}, '');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length == 0) {
|
||||
console.error('Missing argument to destination file');
|
||||
process.exit(1);
|
||||
}
|
||||
const changelogFile = path.resolve(args[0]);
|
||||
|
||||
await fs.writeFile(
|
||||
changelogFile,
|
||||
fullChangelog,
|
||||
{
|
||||
flag: 'w+',
|
||||
},
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Changelog written to', changelogFile);
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
||||
// processLine applies different substitutions to line string.
|
||||
// We're assuming that there are no more than one substitution
|
||||
// per line to be applied.
|
||||
const processLine = (line) => {
|
||||
// Check if a link with one of the following format exists:
|
||||
// * [#123](https://github.com/arduino/arduino-ide/pull/123)
|
||||
// * [#123](https://github.com/arduino/arduino-ide/issues/123)
|
||||
// * [#123](https://github.com/arduino/arduino-ide/pull/123/)
|
||||
// * [#123](https://github.com/arduino/arduino-ide/issues/123/)
|
||||
// If it does return the line as is.
|
||||
let r =
|
||||
/(\(|\[)#\d+(\)|\])(\(|\[)https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?(\)|\])/gm;
|
||||
if (r.test(line)) {
|
||||
return line;
|
||||
}
|
||||
|
||||
// Check if a issue or PR link with the following format exists:
|
||||
// * #123
|
||||
// If it does it's changed to:
|
||||
// * [#123](https://github.com/arduino/arduino-ide/pull/123)
|
||||
r = /(?<![\w\d\/_]{1})#((\d)+)(?![\w\d\/_]{1})/gm;
|
||||
if (r.test(line)) {
|
||||
return line.replace(
|
||||
r,
|
||||
`[#$1](https://github.com/arduino/arduino-ide/pull/$1)`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if a link with one of the following format exists:
|
||||
// * https://github.com/arduino/arduino-ide/pull/123
|
||||
// * https://github.com/arduino/arduino-ide/issues/123
|
||||
// * https://github.com/arduino/arduino-ide/pull/123/
|
||||
// * https://github.com/arduino/arduino-ide/issues/123/
|
||||
// If it does it's changed respectively to:
|
||||
// * [#123](https://github.com/arduino/arduino-ide/pull/123)
|
||||
// * [#123](https://github.com/arduino/arduino-ide/issues/123)
|
||||
// * [#123](https://github.com/arduino/arduino-ide/pull/123/)
|
||||
// * [#123](https://github.com/arduino/arduino-ide/issues/123/)
|
||||
r =
|
||||
/(https:\/\/github\.com\/arduino\/arduino-ide\/(pull|issues)\/(\d+)\/?)/gm;
|
||||
if (r.test(line)) {
|
||||
return line.replace(r, `[#$3]($1)`);
|
||||
}
|
||||
|
||||
// Check if a link with the following format exists:
|
||||
// * https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3
|
||||
// * https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3/
|
||||
// If it does it's changed to:
|
||||
// * [`2.0.0-rc2...2.0.0-rc3`](https://github.com/arduino/arduino-ide/compare/2.0.0-rc2...2.0.0-rc3)
|
||||
r =
|
||||
/(https:\/\/github\.com\/arduino\/arduino-ide\/compare\/([^\/]*))\/?\s?/gm;
|
||||
if (r.test(line)) {
|
||||
return line.replace(r, '[`$2`]($1)');
|
||||
}
|
||||
|
||||
// If nothing matches just return the line as is
|
||||
return line;
|
||||
};
|
||||
@@ -1,14 +1,12 @@
|
||||
// @ts-check
|
||||
|
||||
(async () => {
|
||||
|
||||
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 { taskBuildFromGit } = require('./utils');
|
||||
|
||||
const version = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
@@ -21,7 +19,7 @@
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { cli } = arduino;
|
||||
const cli = arduino['arduino-cli'];
|
||||
if (!cli) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -36,24 +34,40 @@
|
||||
}
|
||||
|
||||
const { platform, arch } = process;
|
||||
const buildFolder = path.join(__dirname, '..', 'build');
|
||||
const resourcesFolder = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'node',
|
||||
'resources'
|
||||
);
|
||||
const cliName = `arduino-cli${platform === 'win32' ? '.exe' : ''}`;
|
||||
const destinationPath = path.join(buildFolder, cliName);
|
||||
const destinationPath = path.join(resourcesFolder, cliName);
|
||||
|
||||
if (typeof version === 'string') {
|
||||
const suffix = (() => {
|
||||
switch (platform) {
|
||||
case 'darwin': return 'macOS_64bit.tar.gz';
|
||||
case 'win32': return 'Windows_64bit.zip';
|
||||
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;
|
||||
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;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
if (!suffix) {
|
||||
@@ -62,80 +76,21 @@
|
||||
}
|
||||
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}'`);
|
||||
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}'`);
|
||||
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);
|
||||
}
|
||||
} 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);
|
||||
taskBuildFromGit(version, destinationPath, 'CLI');
|
||||
}
|
||||
if (!repo) {
|
||||
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
|
||||
shell.exit(1);
|
||||
}
|
||||
const url = `https://github.com/${owner}/${repo}.git`;
|
||||
shell.echo(`Building CLI from ${url}. Commitish: ${commitish ? commitish : 'HEAD'}`);
|
||||
|
||||
if (fs.existsSync(destinationPath)) {
|
||||
shell.echo(`Skipping the CLI build because it already exists: ${destinationPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shell.mkdir('-p', buildFolder).code !== 0) {
|
||||
shell.echo('Could not create build folder.');
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const tempRepoPath = temp.mkdirSync();
|
||||
shell.echo(`>>> Cloning CLI source to ${tempRepoPath}...`);
|
||||
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('<<< Cloned CLI repo.')
|
||||
|
||||
if (commitish) {
|
||||
shell.echo(`>>> Checking out ${commitish}...`);
|
||||
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Checked out ${commitish}.`);
|
||||
}
|
||||
|
||||
shell.echo(`>>> Building the CLI...`);
|
||||
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('<<< CLI build done.')
|
||||
|
||||
if (!fs.existsSync(path.join(tempRepoPath, cliName))) {
|
||||
shell.echo(`Could not find the CLI at ${path.join(tempRepoPath, cliName)}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const builtCliPath = path.join(tempRepoPath, cliName);
|
||||
shell.echo(`>>> Copying CLI from ${builtCliPath} to ${destinationPath}...`);
|
||||
if (shell.cp(builtCliPath, destinationPath).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Copied the CLI.`);
|
||||
|
||||
shell.echo('<<< Verifying CLI...');
|
||||
if (!fs.existsSync(destinationPath)) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo('>>> Verified CLI.');
|
||||
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -1,33 +1,108 @@
|
||||
// @ts-check
|
||||
|
||||
// The version to use.
|
||||
const version = '1.9.1';
|
||||
const version = '1.10.0';
|
||||
|
||||
(async () => {
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const os = require('node:os');
|
||||
const { existsSync, promises: fs } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const shell = require('shelljs');
|
||||
const { v4 } = require('uuid');
|
||||
const { exec } = require('./utils');
|
||||
|
||||
const destination = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'node',
|
||||
'resources',
|
||||
'Examples'
|
||||
);
|
||||
if (existsSync(destination)) {
|
||||
shell.echo(
|
||||
`Skipping Git checkout of the examples because the repository already exists: ${destination}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
process.exit(1);
|
||||
}
|
||||
exec(
|
||||
'git',
|
||||
['clone', 'https://github.com/arduino/arduino-examples.git', repository],
|
||||
shell
|
||||
);
|
||||
|
||||
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
process.exit(1);
|
||||
}
|
||||
exec(
|
||||
'git',
|
||||
['-C', repository, 'checkout', `tags/${version}`, '-b', version],
|
||||
shell
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
const examples = [];
|
||||
const categories = await fs.readdir(destination);
|
||||
const visit = async (pathLike, container) => {
|
||||
const stat = await fs.lstat(pathLike);
|
||||
if (stat.isDirectory()) {
|
||||
if (await isSketch(pathLike)) {
|
||||
container.sketches.push({
|
||||
name: path.basename(pathLike),
|
||||
relativePath: path.relative(destination, pathLike),
|
||||
});
|
||||
} else {
|
||||
const names = await fs.readdir(pathLike);
|
||||
for (const name of names) {
|
||||
const childPath = path.join(pathLike, name);
|
||||
if (await isSketch(childPath)) {
|
||||
container.sketches.push({
|
||||
name,
|
||||
relativePath: path.relative(destination, childPath),
|
||||
});
|
||||
} else {
|
||||
const child = {
|
||||
label: name,
|
||||
children: [],
|
||||
sketches: [],
|
||||
};
|
||||
container.children.push(child);
|
||||
await visit(childPath, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const category of categories) {
|
||||
const example = {
|
||||
label: category,
|
||||
children: [],
|
||||
sketches: [],
|
||||
};
|
||||
await visit(path.join(destination, category), example);
|
||||
examples.push(example);
|
||||
}
|
||||
await fs.writeFile(
|
||||
path.join(destination, 'examples.json'),
|
||||
JSON.stringify(examples, null, 2),
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
shell.echo(`Generated output to ${path.join(destination, 'examples.json')}`);
|
||||
})();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// @ts-check
|
||||
|
||||
(async () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const temp = require('temp');
|
||||
const path = require('node:path');
|
||||
const shell = require('shelljs');
|
||||
const semver = require('semver');
|
||||
const downloader = require('./downloader');
|
||||
const { taskBuildFromGit } = require('./utils');
|
||||
|
||||
const version = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
@@ -19,7 +18,7 @@
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { fwuploader } = arduino;
|
||||
const fwuploader = arduino['arduino-fwuploader'];
|
||||
if (!fwuploader) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -36,11 +35,17 @@
|
||||
}
|
||||
|
||||
const { platform, arch } = process;
|
||||
const buildFolder = path.join(__dirname, '..', 'build');
|
||||
const resourcesFolder = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'node',
|
||||
'resources'
|
||||
);
|
||||
const fwuploderName = `arduino-fwuploader${
|
||||
platform === 'win32' ? '.exe' : ''
|
||||
}`;
|
||||
const destinationPath = path.join(buildFolder, fwuploderName);
|
||||
const destinationPath = path.join(resourcesFolder, fwuploderName);
|
||||
|
||||
if (typeof version === 'string') {
|
||||
const suffix = (() => {
|
||||
@@ -86,81 +91,6 @@
|
||||
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.');
|
||||
taskBuildFromGit(version, destinationPath, 'Firmware Uploader');
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -4,69 +4,157 @@
|
||||
// - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX}
|
||||
|
||||
(() => {
|
||||
|
||||
const DEFAULT_ALS_VERSION = 'nightly';
|
||||
const DEFAULT_CLANGD_VERSION = 'snapshot_20210124';
|
||||
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
const downloader = require('./downloader');
|
||||
const { goBuildFromGit } = require('./utils');
|
||||
|
||||
const [DEFAULT_LS_VERSION, DEFAULT_CLANGD_VERSION] = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
if (!pkg) return [undefined, undefined];
|
||||
|
||||
const { arduino } = pkg;
|
||||
if (!arduino) return [undefined, undefined];
|
||||
|
||||
const { clangd } = arduino;
|
||||
const languageServer = arduino['arduino-language-server'];
|
||||
if (!languageServer) return [undefined, undefined];
|
||||
if (!clangd) return [undefined, undefined];
|
||||
|
||||
return [languageServer.version, clangd.version];
|
||||
})();
|
||||
|
||||
if (!DEFAULT_LS_VERSION) {
|
||||
shell.echo(
|
||||
`Could not retrieve Arduino Language Server version info from the 'package.json'.`
|
||||
);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
if (!DEFAULT_CLANGD_VERSION) {
|
||||
shell.echo(
|
||||
`Could not retrieve clangd version info from the 'package.json'.`
|
||||
);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const yargs = require('yargs')
|
||||
.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}.`
|
||||
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: ['snapshot_20210124'],
|
||||
describe: `The version of 'clangd' to download. Defaults to ${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.`
|
||||
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`,
|
||||
})
|
||||
.version(false).parse();
|
||||
.version(false)
|
||||
.parse();
|
||||
|
||||
const alsVersion = yargs['ls-version'];
|
||||
const clangdVersion = yargs['clangd-version']
|
||||
const lsVersion = yargs['ls-version'];
|
||||
const clangdVersion = yargs['clangd-version'];
|
||||
const force = yargs['force-download'];
|
||||
const { platform, arch } = process;
|
||||
const platformArch = platform + '-' + arch;
|
||||
const resourcesFolder = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'node',
|
||||
'resources'
|
||||
);
|
||||
const lsExecutablePath = path.join(
|
||||
resourcesFolder,
|
||||
`arduino-language-server${platform === 'win32' ? '.exe' : ''}`
|
||||
);
|
||||
let clangdExecutablePath, clangFormatExecutablePath, lsSuffix, clangdSuffix;
|
||||
|
||||
const build = path.join(__dirname, '..', 'build');
|
||||
const lsExecutablePath = path.join(build, `arduino-language-server${platform === 'win32' ? '.exe' : ''}`);
|
||||
|
||||
let clangdExecutablePath, lsSuffix, clangdPrefix;
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
clangdExecutablePath = path.join(build, 'bin', 'clangd')
|
||||
lsSuffix = 'macOS_amd64.zip';
|
||||
clangdPrefix = 'mac';
|
||||
switch (platformArch) {
|
||||
case 'darwin-x64':
|
||||
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
|
||||
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
|
||||
lsSuffix = 'macOS_64bit.tar.gz';
|
||||
clangdSuffix = 'macOS_64bit';
|
||||
break;
|
||||
case 'linux':
|
||||
clangdExecutablePath = path.join(build, 'bin', 'clangd')
|
||||
lsSuffix = 'Linux_amd64.zip';
|
||||
clangdPrefix = 'linux'
|
||||
case 'darwin-arm64':
|
||||
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
|
||||
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
|
||||
lsSuffix = 'macOS_ARM64.tar.gz';
|
||||
clangdSuffix = 'macOS_ARM64';
|
||||
break;
|
||||
case 'win32':
|
||||
clangdExecutablePath = path.join(build, 'bin', 'clangd.exe')
|
||||
lsSuffix = 'Windows_amd64.zip';
|
||||
clangdPrefix = 'windows';
|
||||
case 'linux-x64':
|
||||
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
|
||||
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
|
||||
lsSuffix = 'Linux_64bit.tar.gz';
|
||||
clangdSuffix = 'Linux_64bit';
|
||||
break;
|
||||
case 'linux-arm64':
|
||||
clangdExecutablePath = path.join(resourcesFolder, 'clangd');
|
||||
clangFormatExecutablePath = path.join(resourcesFolder, 'clang-format');
|
||||
lsSuffix = 'Linux_ARM64.tar.gz';
|
||||
clangdSuffix = 'Linux_ARM64';
|
||||
break;
|
||||
case 'win32-x64':
|
||||
clangdExecutablePath = path.join(resourcesFolder, 'clangd.exe');
|
||||
clangFormatExecutablePath = path.join(
|
||||
resourcesFolder,
|
||||
'clang-format.exe'
|
||||
);
|
||||
lsSuffix = 'Windows_64bit.zip';
|
||||
clangdSuffix = 'Windows_64bit';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported platform/arch: ${platformArch}.`);
|
||||
}
|
||||
if (!lsSuffix) {
|
||||
shell.echo(`The arduino-language-server is not available for ${platform} ${arch}.`);
|
||||
if (!lsSuffix || !clangdSuffix) {
|
||||
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);
|
||||
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,
|
||||
resourcesFolder,
|
||||
lsExecutablePath,
|
||||
force
|
||||
);
|
||||
} else {
|
||||
goBuildFromGit(lsVersion, lsExecutablePath, 'language-server');
|
||||
}
|
||||
|
||||
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.
|
||||
const clangdUrl = `https://downloads.arduino.cc/tools/clangd_${clangdVersion}_${clangdSuffix}.tar.bz2`;
|
||||
downloader.downloadUnzipAll(
|
||||
clangdUrl,
|
||||
resourcesFolder,
|
||||
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,
|
||||
resourcesFolder,
|
||||
clangFormatExecutablePath,
|
||||
force,
|
||||
{
|
||||
strip: 1,
|
||||
}
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const shell = require('shelljs');
|
||||
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;
|
||||
});
|
||||
process.on('uncaughtException', error => {
|
||||
process.on('uncaughtException', (error) => {
|
||||
shell.echo(String(error));
|
||||
shell.exit(1);
|
||||
throw error;
|
||||
@@ -23,7 +23,12 @@ 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) => {
|
||||
exports.downloadUnzipFile = async (
|
||||
url,
|
||||
targetFile,
|
||||
filePrefix,
|
||||
force = false
|
||||
) => {
|
||||
if (fs.existsSync(targetFile) && !force) {
|
||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||
return;
|
||||
@@ -41,28 +46,31 @@ exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) =
|
||||
}
|
||||
|
||||
shell.echo(`>>> Downloading from '${url}'...`);
|
||||
const { default: download } = await import('@xhmikosr/downloader');
|
||||
const data = await download(url);
|
||||
shell.echo(`<<< Download succeeded.`);
|
||||
|
||||
shell.echo('>>> Decompressing...');
|
||||
const files = await decompress(data, downloads, {
|
||||
plugins: [
|
||||
unzip(),
|
||||
untargz()
|
||||
]
|
||||
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));
|
||||
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.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) {
|
||||
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);
|
||||
}
|
||||
@@ -71,15 +79,22 @@ exports.downloadUnzipFile = async (url, targetFile, filePrefix, force = false) =
|
||||
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) => {
|
||||
exports.downloadUnzipAll = async (
|
||||
url,
|
||||
targetDir,
|
||||
targetFile,
|
||||
force,
|
||||
decompressOptions = undefined
|
||||
) => {
|
||||
if (fs.existsSync(targetFile) && !force) {
|
||||
shell.echo(`Skipping download because file already exists: ${targetFile}`);
|
||||
return;
|
||||
@@ -92,18 +107,16 @@ exports.downloadUnzipAll = async (url, targetDir, targetFile, force, decompressO
|
||||
}
|
||||
|
||||
shell.echo(`>>> Downloading from '${url}'...`);
|
||||
const { default: download } = await import('@xhmikosr/downloader');
|
||||
const data = await download(url);
|
||||
shell.echo(`<<< Download succeeded.`);
|
||||
|
||||
shell.echo('>>> Decompressing...');
|
||||
let options = {
|
||||
plugins: [
|
||||
unzip(),
|
||||
untargz()
|
||||
]
|
||||
plugins: [unzip(), untargz(), untarbz2()],
|
||||
};
|
||||
if (decompressOptions) {
|
||||
options = Object.assign(options, decompressOptions)
|
||||
options = Object.assign(options, decompressOptions);
|
||||
}
|
||||
const files = await decompress(data, targetDir, options);
|
||||
if (files.length === 0) {
|
||||
@@ -117,4 +130,4 @@ exports.downloadUnzipAll = async (url, targetDir, targetFile, force, decompressO
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`Done: ${targetFile}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
// @ts-check
|
||||
|
||||
(async () => {
|
||||
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { exec } = require('./utils');
|
||||
const glob = require('glob');
|
||||
const { v4 } = require('uuid');
|
||||
const shell = require('shelljs');
|
||||
const protoc = path.dirname(require('protoc/protoc'));
|
||||
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${protoc}`;
|
||||
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${path.join(__dirname, '..', 'node_modules', '.bin')}`;
|
||||
|
||||
const repository = path.join(os.tmpdir(), `${v4()}-arduino-cli`);
|
||||
if (shell.mkdir('-p', repository).code !== 0) {
|
||||
@@ -23,23 +21,28 @@
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const defaultVersion = {
|
||||
owner: 'arduino',
|
||||
repo: 'arduino-cli',
|
||||
commitish: undefined,
|
||||
};
|
||||
const { arduino } = pkg;
|
||||
if (!arduino) {
|
||||
return { owner: 'arduino', repo: 'arduino-cli' };
|
||||
return defaultVersion;
|
||||
}
|
||||
|
||||
const { cli } = arduino;
|
||||
const cli = arduino['arduino-cli'];
|
||||
if (!cli) {
|
||||
return { owner: 'arduino', repo: 'arduino-cli' };
|
||||
return defaultVersion;
|
||||
}
|
||||
|
||||
const { version } = cli;
|
||||
if (!version) {
|
||||
return { owner: 'arduino', repo: 'arduino-cli' };
|
||||
return defaultVersion;
|
||||
}
|
||||
|
||||
if (typeof version === 'string') {
|
||||
return { owner: 'arduino', repo: 'arduino-cli' };
|
||||
return defaultVersion;
|
||||
}
|
||||
|
||||
// We assume an object with `owner`, `repo`, commitish?` properties.
|
||||
@@ -58,15 +61,22 @@
|
||||
|
||||
const url = `https://github.com/${owner}/${repo}.git`;
|
||||
shell.echo(`>>> Cloning repository from '${url}'...`);
|
||||
if (shell.exec(`git clone ${url} ${repository}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
exec('git', ['clone', url, repository], shell);
|
||||
shell.echo(`<<< Repository cloned.`);
|
||||
|
||||
const { platform } = process;
|
||||
const build = path.join(__dirname, '..', 'build');
|
||||
const cli = path.join(build, `arduino-cli${platform === 'win32' ? '.exe' : ''}`);
|
||||
const versionJson = shell.exec(`${cli} version --format json`).trim();
|
||||
const resourcesFolder = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'node',
|
||||
'resources'
|
||||
);
|
||||
const cli = path.join(
|
||||
resourcesFolder,
|
||||
`arduino-cli${platform === 'win32' ? '.exe' : ''}`
|
||||
);
|
||||
const versionJson = exec(cli, ['version', '--format', 'json'], shell).trim();
|
||||
if (!versionJson) {
|
||||
shell.echo(`Could not retrieve the CLI version from ${cli}.`);
|
||||
shell.exit(1);
|
||||
@@ -87,70 +97,94 @@
|
||||
*/
|
||||
const versionObject = JSON.parse(versionJson);
|
||||
const version = versionObject.VersionString;
|
||||
if (version && !version.startsWith('nightly-') && version !== '0.0.0-git' && version !== 'git-snapshot') {
|
||||
if (
|
||||
version &&
|
||||
!version.startsWith('nightly-') &&
|
||||
version !== '0.0.0-git' &&
|
||||
version !== 'git-snapshot'
|
||||
) {
|
||||
shell.echo(`>>> Checking out tagged version: '${version}'...`);
|
||||
shell.exec(`git -C ${repository} fetch --all --tags`);
|
||||
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Checked out tagged version: '${commitish}'.`);
|
||||
exec('git', ['-C', repository, 'fetch', '--all', '--tags'], shell);
|
||||
exec(
|
||||
'git',
|
||||
['-C', repository, 'checkout', `tags/${version}`, '-b', version],
|
||||
shell
|
||||
);
|
||||
shell.echo(`<<< Checked out tagged version: '${version}'.`);
|
||||
} else if (commitish) {
|
||||
shell.echo(`>>> Checking out commitish from 'package.json': '${commitish}'...`);
|
||||
if (shell.exec(`git -C ${repository} checkout ${commitish}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Checked out commitish from 'package.json': '${commitish}'.`);
|
||||
shell.echo(
|
||||
`>>> Checking out commitish from 'package.json': '${commitish}'...`
|
||||
);
|
||||
exec('git', ['-C', repository, 'checkout', commitish], shell);
|
||||
shell.echo(
|
||||
`<<< Checked out commitish from 'package.json': '${commitish}'.`
|
||||
);
|
||||
} else if (versionObject.Commit) {
|
||||
shell.echo(`>>> Checking out commitish from the CLI: '${versionObject.Commit}'...`);
|
||||
if (shell.exec(`git -C ${repository} checkout ${versionObject.Commit}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Checked out commitish from the CLI: '${versionObject.Commit}'.`);
|
||||
shell.echo(
|
||||
`>>> Checking out commitish from the CLI: '${versionObject.Commit}'...`
|
||||
);
|
||||
exec('git', ['-C', repository, 'checkout', versionObject.Commit], shell);
|
||||
shell.echo(
|
||||
`<<< Checked out commitish from the CLI: '${versionObject.Commit}'.`
|
||||
);
|
||||
} else {
|
||||
shell.echo(`WARN: no 'git checkout'. Generating from the HEAD revision.`);
|
||||
}
|
||||
|
||||
shell.echo('>>> Generating TS/JS API from:');
|
||||
if (shell.exec(`git -C ${repository} rev-parse --abbrev-ref HEAD`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
exec('git', ['-C', repository, 'rev-parse', '--abbrev-ref', 'HEAD'], shell);
|
||||
|
||||
const rpc = path.join(repository, 'rpc');
|
||||
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
|
||||
shell.mkdir('-p', out);
|
||||
|
||||
const protos = await new Promise(resolve =>
|
||||
const protos = await new Promise((resolve) =>
|
||||
glob('**/*.proto', { cwd: rpc }, (error, matches) => {
|
||||
if (error) {
|
||||
shell.echo(error.stack);
|
||||
shell.echo(error.stack ?? error.message);
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
resolve(matches.map(filename => path.join(rpc, filename)));
|
||||
}));
|
||||
resolve(matches.map((filename) => path.join(rpc, filename)));
|
||||
})
|
||||
);
|
||||
if (!protos || protos.length === 0) {
|
||||
shell.echo(`Could not find any .proto files under ${rpc}.`);
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
// Generate JS code from the `.proto` files.
|
||||
if (shell.exec(`grpc_tools_node_protoc \
|
||||
--js_out=import_style=commonjs,binary:${out} \
|
||||
--grpc_out=generate_package_definition:${out} \
|
||||
-I ${rpc} \
|
||||
${protos.join(' ')}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
exec(
|
||||
'grpc_tools_node_protoc',
|
||||
[
|
||||
`--js_out=import_style=commonjs,binary:${out}`,
|
||||
`--grpc_out=generate_package_definition:${out}`,
|
||||
'-I',
|
||||
rpc,
|
||||
...protos,
|
||||
],
|
||||
shell
|
||||
);
|
||||
|
||||
// Generate the `.d.ts` files for JS.
|
||||
if (shell.exec(`protoc \
|
||||
--plugin=protoc-gen-ts=${path.resolve(__dirname, '..', 'node_modules', '.bin', `protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`)} \
|
||||
--ts_out=generate_package_definition:${out} \
|
||||
-I ${rpc} \
|
||||
${protos.join(' ')}`).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
exec(
|
||||
path.join(protoc, `protoc${platform === 'win32' ? '.exe' : ''}`),
|
||||
[
|
||||
`--plugin=protoc-gen-ts=${path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'node_modules',
|
||||
'.bin',
|
||||
`protoc-gen-ts${platform === 'win32' ? '.cmd' : ''}`
|
||||
)}`,
|
||||
`--ts_out=generate_package_definition:${out}`,
|
||||
'-I',
|
||||
rpc,
|
||||
...protos,
|
||||
],
|
||||
shell
|
||||
);
|
||||
|
||||
shell.echo('<<< Generation was successful.');
|
||||
|
||||
})();
|
||||
|
||||
144
arduino-ide-extension/scripts/utils.js
Normal file
144
arduino-ide-extension/scripts/utils.js
Normal file
@@ -0,0 +1,144 @@
|
||||
// @ts-check
|
||||
|
||||
const exec = (
|
||||
/** @type {string} */ command,
|
||||
/** @type {readonly string[]} */ args,
|
||||
/** @type {import('shelljs')|undefined}*/ shell = undefined,
|
||||
/** @type {import('node:child_process').ExecFileSyncOptionsWithStringEncoding|undefined} */ options = undefined
|
||||
) => {
|
||||
try {
|
||||
const stdout = require('node:child_process').execFileSync(
|
||||
command,
|
||||
args,
|
||||
options ? options : { encoding: 'utf8' }
|
||||
);
|
||||
if (shell) {
|
||||
shell.echo(stdout.trim());
|
||||
}
|
||||
return stdout;
|
||||
} catch (err) {
|
||||
if (shell) {
|
||||
shell.echo(err instanceof Error ? err.message : String(err));
|
||||
shell.exit(1);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
exports.exec = exec;
|
||||
|
||||
/**
|
||||
* 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` must be either `'go'` or `'task'`.
|
||||
* @param {string} command
|
||||
* @param {{ owner: any; repo: any; commitish: any; }} version
|
||||
* @param {string} destinationPath
|
||||
* @param {string} taskName
|
||||
*/
|
||||
function buildFromGit(command, version, destinationPath, taskName) {
|
||||
const fs = require('node:fs');
|
||||
const path = require('node: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 resourcesFolder = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'node',
|
||||
'resources'
|
||||
);
|
||||
if (shell.mkdir('-p', resourcesFolder).code !== 0) {
|
||||
shell.echo('Could not create resources folder.');
|
||||
shell.exit(1);
|
||||
}
|
||||
|
||||
const tempRepoPath = temp.mkdirSync();
|
||||
shell.echo(`>>> Cloning ${taskName} source to ${tempRepoPath}...`);
|
||||
exec('git', ['clone', url, tempRepoPath], shell);
|
||||
shell.echo(`<<< Cloned ${taskName} repo.`);
|
||||
|
||||
if (commitish) {
|
||||
shell.echo(`>>> Checking out ${commitish}...`);
|
||||
exec('git', ['-C', tempRepoPath, 'checkout', commitish], shell);
|
||||
shell.echo(`<<< Checked out ${commitish}.`);
|
||||
}
|
||||
|
||||
exec('git', ['-C', tempRepoPath, 'rev-parse', '--short', 'HEAD'], shell);
|
||||
|
||||
shell.echo(`>>> Building the ${taskName}...`);
|
||||
exec(command, ['build'], shell, { cwd: tempRepoPath, encoding: 'utf8' });
|
||||
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}.`);
|
||||
}
|
||||
16
arduino-ide-extension/src/browser/app-service.ts
Normal file
16
arduino-ide-extension/src/browser/app-service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import type { AppInfo } from '../electron-common/electron-arduino';
|
||||
import type { StartupTasks } from '../electron-common/startup-task';
|
||||
import type { Sketch } from './contributions/contribution';
|
||||
|
||||
export type { AppInfo };
|
||||
|
||||
export const AppService = Symbol('AppService');
|
||||
export interface AppService {
|
||||
quit(): void;
|
||||
info(): Promise<AppInfo>;
|
||||
registerStartupTasksHandler(
|
||||
handler: (tasks: StartupTasks) => void
|
||||
): Disposable;
|
||||
scheduleDeletion(sketch: Sketch): void; // TODO: find a better place
|
||||
}
|
||||
@@ -1,21 +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,83 +1,47 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import {
|
||||
MAIN_MENU_BAR,
|
||||
MenuContribution,
|
||||
MenuModelRegistry,
|
||||
SelectionService,
|
||||
ILogger,
|
||||
DisposableCollection,
|
||||
} from '@theia/core';
|
||||
import {
|
||||
ContextMenuRenderer,
|
||||
FrontendApplication,
|
||||
FrontendApplicationContribution,
|
||||
OpenerService,
|
||||
StatusBar,
|
||||
StatusBarAlignment,
|
||||
} 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 { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import {
|
||||
TabBarToolbarContribution,
|
||||
TabBarToolbarRegistry,
|
||||
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import {
|
||||
ColorTheme,
|
||||
CssStyleCollector,
|
||||
StylingParticipant,
|
||||
} from '@theia/core/lib/browser/styling-service';
|
||||
import {
|
||||
CommandContribution,
|
||||
CommandRegistry,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import {
|
||||
MAIN_MENU_BAR,
|
||||
MenuContribution,
|
||||
MenuModelRegistry,
|
||||
} from '@theia/core/lib/common/menu';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { isHighContrast } from '@theia/core/lib/common/theme';
|
||||
import { ElectronWindowPreferences } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
|
||||
import {
|
||||
EditorMainMenu,
|
||||
EditorManager,
|
||||
EditorOpenerOptions,
|
||||
} from '@theia/editor/lib/browser';
|
||||
import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
|
||||
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import React from '@theia/core/shared/react';
|
||||
import { EditorCommands } from '@theia/editor/lib/browser/editor-command';
|
||||
import { EditorMainMenu } from '@theia/editor/lib/browser/editor-menu';
|
||||
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,
|
||||
Sketch,
|
||||
} from '../common/protocol';
|
||||
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
|
||||
import { ConfigService } from '../common/protocol/config-service';
|
||||
import { FileSystemExt } from '../common/protocol/filesystem-ext';
|
||||
import { ArduinoCommands } from './arduino-commands';
|
||||
import { BoardsConfig } from './boards/boards-config';
|
||||
import { BoardsConfigDialog } from './boards/boards-config-dialog';
|
||||
import { BoardsDataStore } from './boards/boards-data-store';
|
||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||
import { 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 { SerialPlotterContribution } from './serial/plotter/plotter-frontend-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 { ResponseService } from '../common/protocol/response-service';
|
||||
import { ArduinoPreferences } from './arduino-preferences';
|
||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
||||
import { SaveAsSketch } from './contributions/save-as-sketch';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/browser';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { SketchbookWidgetContribution } from './widgets/sketchbook/sketchbook-widget-contribution';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoFrontendContribution
|
||||
@@ -86,323 +50,57 @@ export class ArduinoFrontendContribution
|
||||
TabBarToolbarContribution,
|
||||
CommandContribution,
|
||||
MenuContribution,
|
||||
ColorContribution
|
||||
ColorContribution,
|
||||
StylingParticipant
|
||||
{
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
private readonly messageService: MessageService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
@inject(SelectionService)
|
||||
protected readonly selectionService: SelectionService;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(FileDialogService)
|
||||
protected readonly fileDialogService: FileDialogService;
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
|
||||
@inject(BoardsConfigDialog)
|
||||
protected readonly boardsConfigDialog: BoardsConfigDialog;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(StatusBar)
|
||||
protected readonly statusBar: StatusBar;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
|
||||
@inject(FileNavigatorContribution)
|
||||
protected readonly fileNavigatorContributions: FileNavigatorContribution;
|
||||
|
||||
@inject(OutputContribution)
|
||||
protected readonly outputContribution: OutputContribution;
|
||||
|
||||
@inject(OutlineViewContribution)
|
||||
protected readonly outlineContribution: OutlineViewContribution;
|
||||
|
||||
@inject(ProblemContribution)
|
||||
protected readonly problemContribution: ProblemContribution;
|
||||
|
||||
@inject(ScmContribution)
|
||||
protected readonly scmContribution: ScmContribution;
|
||||
|
||||
@inject(SearchInWorkspaceFrontendContribution)
|
||||
protected readonly siwContribution: SearchInWorkspaceFrontendContribution;
|
||||
|
||||
@inject(SketchbookWidgetContribution)
|
||||
protected readonly sketchbookWidgetContribution: SketchbookWidgetContribution;
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@inject(ArduinoDaemon)
|
||||
protected readonly daemon: ArduinoDaemon;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(FileSystemExt)
|
||||
protected readonly fileSystemExt: FileSystemExt;
|
||||
|
||||
@inject(HostedPluginSupport)
|
||||
protected hostedPluginSupport: HostedPluginSupport;
|
||||
|
||||
@inject(ExecutableService)
|
||||
protected executableService: ExecutableService;
|
||||
|
||||
@inject(ResponseService)
|
||||
protected readonly responseService: ResponseService;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
@inject(ElectronWindowPreferences)
|
||||
private readonly electronWindowPreferences: ElectronWindowPreferences;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
protected readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
protected invalidConfigPopup:
|
||||
| Promise<void | 'No' | 'Yes' | undefined>
|
||||
| undefined;
|
||||
protected toDisposeOnStop = new DisposableCollection();
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
protected init(): 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.'
|
||||
)
|
||||
);
|
||||
}
|
||||
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',
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.electronWindowPreferences.onPreferenceChanged((event) => {
|
||||
if (event.newValue !== event.oldValue) {
|
||||
switch (event.preferenceName) {
|
||||
case 'window.zoomLevel':
|
||||
if (typeof event.newValue === 'number') {
|
||||
window.electronTheiaCore.setZoomLevel(event.newValue || 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (selectedBoard) {
|
||||
this.statusBar.setElement('arduino-selected-port', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: selectedPort
|
||||
? `on ${Port.toString(selectedPort)}`
|
||||
: '[not connected]',
|
||||
className: 'arduino-selected-port',
|
||||
});
|
||||
}
|
||||
};
|
||||
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
|
||||
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
|
||||
this.appStateService.reachedState('ready').then(async () => {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (sketch && !(await this.sketchService.isTemp(sketch))) {
|
||||
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
|
||||
this.toDisposeOnStop.push(
|
||||
this.fileService.onDidFilesChange(async (event) => {
|
||||
for (const { type, resource } of event.changes) {
|
||||
if (
|
||||
type === FileChangeType.ADDED &&
|
||||
resource.parent.toString() === sketch.uri
|
||||
) {
|
||||
const reloadedSketch = await this.sketchService.loadSketch(
|
||||
sketch.uri
|
||||
);
|
||||
if (Sketch.isInSketch(resource, reloadedSketch)) {
|
||||
this.ensureOpened(resource.toString(), true, {
|
||||
mode: 'open',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
this.appStateService.reachedState('ready').then(() =>
|
||||
this.electronWindowPreferences.ready.then(() => {
|
||||
const zoomLevel =
|
||||
this.electronWindowPreferences.get('window.zoomLevel');
|
||||
window.electronTheiaCore.setZoomLevel(zoomLevel);
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
this.sketchbookWidgetContribution,
|
||||
] 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);
|
||||
}
|
||||
});
|
||||
app.shell.leftPanelHandler.removeMenu('settings-menu');
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDisposeOnStop.dispose();
|
||||
}
|
||||
|
||||
protected languageServerFqbn?: string;
|
||||
protected languageServerStartMutex = new Mutex();
|
||||
protected async startLanguageServer(
|
||||
fqbn: string,
|
||||
name: string | undefined
|
||||
): Promise<void> {
|
||||
const release = await this.languageServerStartMutex.acquire();
|
||||
try {
|
||||
await this.hostedPluginSupport.didStart;
|
||||
const details = await this.boardsService.getBoardDetails({ fqbn });
|
||||
if (!details) {
|
||||
// Core is not installed for the selected board.
|
||||
console.info(
|
||||
`Could not start language server for ${fqbn}. The core is not installed for the board.`
|
||||
);
|
||||
if (this.languageServerFqbn) {
|
||||
try {
|
||||
await this.commandRegistry.executeCommand(
|
||||
'arduino.languageserver.stop'
|
||||
);
|
||||
console.info(
|
||||
`Stopped language server process for ${this.languageServerFqbn}.`
|
||||
);
|
||||
this.languageServerFqbn = undefined;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Failed to start language server process for ${this.languageServerFqbn}`,
|
||||
e
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (fqbn === this.languageServerFqbn) {
|
||||
// NOOP
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Starting language server: ${fqbn}`);
|
||||
const log = this.arduinoPreferences.get('arduino.language.log');
|
||||
let currentSketchPath: string | undefined = undefined;
|
||||
if (log) {
|
||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||
if (currentSketch) {
|
||||
currentSketchPath = await this.fileService.fsPath(
|
||||
new URI(currentSketch.uri)
|
||||
);
|
||||
}
|
||||
}
|
||||
const { clangdUri, cliUri, lsUri } = await this.executableService.list();
|
||||
const [clangdPath, cliPath, lsPath, cliConfigPath] = await Promise.all([
|
||||
this.fileService.fsPath(new URI(clangdUri)),
|
||||
this.fileService.fsPath(new URI(cliUri)),
|
||||
this.fileService.fsPath(new URI(lsUri)),
|
||||
this.fileService.fsPath(
|
||||
new URI(await this.configService.getCliConfigFileUri())
|
||||
),
|
||||
]);
|
||||
this.languageServerFqbn = await Promise.race([
|
||||
new Promise<undefined>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(`Timeout after ${20_000} ms.`)),
|
||||
20_000
|
||||
)
|
||||
),
|
||||
this.commandRegistry.executeCommand<string>(
|
||||
'arduino.languageserver.start',
|
||||
{
|
||||
lsPath,
|
||||
cliPath,
|
||||
clangdPath,
|
||||
log: currentSketchPath ? currentSketchPath : log,
|
||||
cliConfigPath,
|
||||
board: {
|
||||
fqbn,
|
||||
name: name ? `"${name}"` : undefined,
|
||||
},
|
||||
}
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(`Failed to start language server for ${fqbn}`, e);
|
||||
this.languageServerFqbn = undefined;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
@@ -411,41 +109,43 @@ export class ArduinoFrontendContribution
|
||||
<BoardsToolBarItem
|
||||
key="boardsToolbarItem"
|
||||
commands={this.commandRegistry}
|
||||
boardsServiceClient={this.boardsServiceClientImpl}
|
||||
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: 'Serial Monitor',
|
||||
tooltip: nls.localize('arduino/common/serialMonitor', 'Serial Monitor'),
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry) {
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
const menuId = (menuPath: string[]): string => {
|
||||
const index = menuPath.length - 1;
|
||||
const menuId = menuPath[index];
|
||||
@@ -456,138 +156,324 @@ export class ArduinoFrontendContribution
|
||||
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,
|
||||
}
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.SKETCH,
|
||||
nls.localize('arduino/menu/sketch', 'Sketch')
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} 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 = false,
|
||||
options?: EditorOpenerOptions | undefined
|
||||
): Promise<any> {
|
||||
const widget = this.editorManager.all.find(
|
||||
(widget) => widget.editor.uri.toString() === uri
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.TOOLS,
|
||||
nls.localize('arduino/menu/tools', 'Tools')
|
||||
);
|
||||
if (!widget || forceOpen) {
|
||||
return this.editorManager.open(new URI(uri), options);
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
id: 'arduino.toolbar.button.background',
|
||||
defaults: {
|
||||
dark: 'button.background',
|
||||
light: 'button.background',
|
||||
hc: 'activityBar.inactiveForeground',
|
||||
hcDark: 'activityBar.inactiveForeground',
|
||||
hcLight: 'activityBar.inactiveForeground',
|
||||
},
|
||||
description:
|
||||
'Background color of the toolbar items. Such as Upload, Verify, etc.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.hoverBackground',
|
||||
id: 'arduino.toolbar.button.hoverBackground',
|
||||
defaults: {
|
||||
dark: 'button.hoverBackground',
|
||||
light: 'button.foreground',
|
||||
hc: 'textLink.foreground',
|
||||
light: 'button.hoverBackground',
|
||||
hcDark: 'button.background',
|
||||
hcLight: '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',
|
||||
hcDark: 'activityBar.inactiveForeground',
|
||||
hcLight: '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',
|
||||
hcDark: 'textLink.foreground',
|
||||
hcLight: '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',
|
||||
hcDark: 'textPreformat.foreground',
|
||||
hcLight: 'textPreformat.foreground',
|
||||
},
|
||||
description:
|
||||
'Toggle color of the toolbar items when they are currently toggled (the command is in progress)',
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.foreground',
|
||||
id: 'arduino.toolbar.dropdown.border',
|
||||
defaults: {
|
||||
dark: 'editor.foreground',
|
||||
light: 'editor.foreground',
|
||||
hc: 'editor.foreground',
|
||||
dark: 'dropdown.border',
|
||||
light: 'dropdown.border',
|
||||
hcDark: 'dropdown.border',
|
||||
hcLight: 'dropdown.border',
|
||||
},
|
||||
description: 'Color of the text in the Output view.',
|
||||
description: 'Border color of the Board Selector.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.output.background',
|
||||
id: 'arduino.toolbar.dropdown.borderActive',
|
||||
defaults: {
|
||||
dark: 'editor.background',
|
||||
light: 'editor.background',
|
||||
hc: 'editor.background',
|
||||
dark: 'focusBorder',
|
||||
light: 'focusBorder',
|
||||
hcDark: 'focusBorder',
|
||||
hcLight: 'focusBorder',
|
||||
},
|
||||
description: 'Background color of the Output view.',
|
||||
description: "Border color of the Board Selector when it's active",
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.dropdown.background',
|
||||
defaults: {
|
||||
dark: 'tab.unfocusedActiveBackground',
|
||||
light: 'dropdown.background',
|
||||
hcDark: 'dropdown.background',
|
||||
hcLight: 'dropdown.background',
|
||||
},
|
||||
description: 'Background color of the Board Selector.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.dropdown.label',
|
||||
defaults: {
|
||||
dark: 'dropdown.foreground',
|
||||
light: 'dropdown.foreground',
|
||||
hcDark: 'dropdown.foreground',
|
||||
hcLight: 'dropdown.foreground',
|
||||
},
|
||||
description: 'Font color of the Board Selector.',
|
||||
},
|
||||
{
|
||||
id: 'arduino.toolbar.dropdown.iconSelected',
|
||||
defaults: {
|
||||
dark: 'list.activeSelectionIconForeground',
|
||||
light: 'list.activeSelectionIconForeground',
|
||||
hcDark: 'list.activeSelectionIconForeground',
|
||||
hcLight: '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',
|
||||
hcDark: 'list.hoverBackground',
|
||||
hcLight: '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',
|
||||
hcDark: 'list.activeSelectionBackground',
|
||||
hcLight: 'list.activeSelectionBackground',
|
||||
},
|
||||
description:
|
||||
'Background color of the selected board in the Board Selector.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registerThemeStyle(theme: ColorTheme, collector: CssStyleCollector): void {
|
||||
const warningForeground = theme.getColor('warningForeground');
|
||||
const warningBackground = theme.getColor('warningBackground');
|
||||
const focusBorder = theme.getColor('focusBorder');
|
||||
const contrastBorder = theme.getColor('contrastBorder');
|
||||
const notificationsBackground = theme.getColor('notifications.background');
|
||||
const buttonBorder = theme.getColor('button.border');
|
||||
const buttonBackground = theme.getColor('button.background') || 'none';
|
||||
const dropdownBackground = theme.getColor('dropdown.background');
|
||||
const arduinoToolbarButtonBackground = theme.getColor(
|
||||
'arduino.toolbar.button.background'
|
||||
);
|
||||
if (isHighContrast(theme.type)) {
|
||||
// toolbar items
|
||||
collector.addRule(`
|
||||
.p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div.toggle-serial-monitor,
|
||||
.p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div.toggle-serial-plotter {
|
||||
background: transparent;
|
||||
}
|
||||
`);
|
||||
if (contrastBorder) {
|
||||
collector.addRule(`
|
||||
.quick-input-widget {
|
||||
outline: 1px solid ${contrastBorder};
|
||||
outline-offset: -1px;
|
||||
}
|
||||
`);
|
||||
}
|
||||
if (focusBorder) {
|
||||
// customized react-select widget
|
||||
collector.addRule(`
|
||||
.arduino-select__option--is-selected {
|
||||
outline: 1px solid ${focusBorder};
|
||||
}
|
||||
`);
|
||||
collector.addRule(`
|
||||
.arduino-select__option--is-focused {
|
||||
outline: 1px dashed ${focusBorder};
|
||||
}
|
||||
`);
|
||||
// boards selector dropdown
|
||||
collector.addRule(`
|
||||
#select-board-dialog .selectBoardContainer .list .item:hover {
|
||||
outline: 1px dashed ${focusBorder};
|
||||
}
|
||||
`);
|
||||
// button hover
|
||||
collector.addRule(`
|
||||
.theia-button:hover,
|
||||
button.theia-button:hover {
|
||||
outline: 1px dashed ${focusBorder};
|
||||
}
|
||||
`);
|
||||
collector.addRule(`
|
||||
.theia-button {
|
||||
border: 1px solid ${focusBorder};
|
||||
}
|
||||
`);
|
||||
collector.addRule(`
|
||||
.component-list-item .header .installed-version:hover:before {
|
||||
background-color: transparent;
|
||||
outline: 1px dashed ${focusBorder};
|
||||
}
|
||||
`);
|
||||
// tree node
|
||||
collector.addRule(`
|
||||
.theia-TreeNode:hover {
|
||||
outline: 1px dashed ${focusBorder};
|
||||
}
|
||||
`);
|
||||
collector.addRule(`
|
||||
.quick-input-list .monaco-list-row.focused,
|
||||
.theia-Tree .theia-TreeNode.theia-mod-selected {
|
||||
outline: 1px dotted ${focusBorder};
|
||||
}
|
||||
`);
|
||||
collector.addRule(`
|
||||
div#select-board-dialog .selectBoardContainer .list .item.selected,
|
||||
.theia-Tree:focus .theia-TreeNode.theia-mod-selected,
|
||||
.theia-Tree .ReactVirtualized__List:focus .theia-TreeNode.theia-mod-selected {
|
||||
outline: 1px solid ${focusBorder};
|
||||
}
|
||||
`);
|
||||
// quick input
|
||||
collector.addRule(`
|
||||
.quick-input-list .monaco-list-row:hover {
|
||||
outline: 1px dashed ${focusBorder};
|
||||
}
|
||||
`);
|
||||
// editor tab-bar
|
||||
collector.addRule(`
|
||||
.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon:hover {
|
||||
outline: 1px dashed ${focusBorder};
|
||||
}
|
||||
`);
|
||||
collector.addRule(`
|
||||
#theia-main-content-panel .p-TabBar .p-TabBar-tab:hover {
|
||||
outline: 1px dashed ${focusBorder};
|
||||
outline-offset: -4px;
|
||||
}
|
||||
`);
|
||||
collector.addRule(`
|
||||
#theia-main-content-panel .p-TabBar .p-TabBar-tab.p-mod-current {
|
||||
outline: 1px solid ${focusBorder};
|
||||
outline-offset: -4px;
|
||||
}
|
||||
`);
|
||||
// boards selector dropdown
|
||||
collector.addRule(`
|
||||
.arduino-boards-dropdown-item:hover {
|
||||
outline: 1px dashed ${focusBorder};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
`);
|
||||
if (notificationsBackground) {
|
||||
// notification
|
||||
collector.addRule(`
|
||||
.theia-notification-list-item:hover:not(:focus) {
|
||||
background-color: ${notificationsBackground};
|
||||
outline: 1px dashed ${focusBorder};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
`);
|
||||
}
|
||||
if (arduinoToolbarButtonBackground) {
|
||||
// toolbar item
|
||||
collector.addRule(`
|
||||
.item.arduino-tool-item.toggled .arduino-upload-sketch--toolbar,
|
||||
.item.arduino-tool-item.toggled .arduino-verify-sketch--toolbar {
|
||||
background-color: ${arduinoToolbarButtonBackground} !important;
|
||||
outline: 1px solid ${focusBorder};
|
||||
}
|
||||
`);
|
||||
collector.addRule(`
|
||||
.p-TabBar-toolbar .item.arduino-tool-item.enabled:hover > div {
|
||||
background: ${arduinoToolbarButtonBackground};
|
||||
outline: 1px dashed ${focusBorder};
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
if (dropdownBackground) {
|
||||
// boards selector dropdown
|
||||
collector.addRule(`
|
||||
.arduino-boards-dropdown-item:hover {
|
||||
background: ${dropdownBackground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
if (warningForeground && warningBackground) {
|
||||
// <input> widget with inverted foreground and background colors
|
||||
collector.addRule(`
|
||||
.theia-input.warning:focus,
|
||||
.theia-input.warning::placeholder,
|
||||
.theia-input.warning {
|
||||
color: ${warningBackground};
|
||||
background-color: ${warningForeground};
|
||||
}
|
||||
`);
|
||||
}
|
||||
if (buttonBorder) {
|
||||
collector.addRule(`
|
||||
button.theia-button,
|
||||
button.theia-button.secondary,
|
||||
.component-list-item .theia-button.secondary.no-border,
|
||||
.component-list-item .theia-button.secondary.no-border:hover {
|
||||
border: 1px solid ${buttonBorder};
|
||||
}
|
||||
`);
|
||||
collector.addRule(`
|
||||
.component-list-item .header .installed-version:before {
|
||||
color: ${buttonBackground};
|
||||
border: 1px solid ${buttonBorder};
|
||||
}
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import '../../src/browser/style/index.css';
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
|
||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import {
|
||||
TabBarToolbarContribution,
|
||||
TabBarToolbarFactory,
|
||||
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
|
||||
import {
|
||||
FrontendApplicationContribution,
|
||||
@@ -26,11 +23,14 @@ import {
|
||||
SketchesService,
|
||||
SketchesServicePath,
|
||||
} from '../common/protocol/sketches-service';
|
||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
||||
import { SketchesServiceClientImpl } from './sketches-service-client-impl';
|
||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||
import {
|
||||
BoardListDumper,
|
||||
BoardsServiceProvider,
|
||||
} from './boards/boards-service-provider';
|
||||
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { WorkspaceService } from './theia/workspace/workspace-service';
|
||||
import { OutlineViewContribution as TheiaOutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
|
||||
@@ -50,49 +50,43 @@ import {
|
||||
ApplicationShell as TheiaApplicationShell,
|
||||
ShellLayoutRestorer as TheiaShellLayoutRestorer,
|
||||
CommonFrontendContribution as TheiaCommonFrontendContribution,
|
||||
KeybindingRegistry as TheiaKeybindingRegistry,
|
||||
DockPanelRenderer as TheiaDockPanelRenderer,
|
||||
TabBarRendererFactory,
|
||||
ContextMenuRenderer,
|
||||
createTreeContainer,
|
||||
TreeWidget,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||
import { ApplicationShell } from './theia/core/application-shell';
|
||||
import {
|
||||
ApplicationShell,
|
||||
DockPanelRenderer,
|
||||
} from './theia/core/application-shell';
|
||||
import { FrontendApplication } from './theia/core/frontend-application';
|
||||
import {
|
||||
BoardsConfigDialog,
|
||||
BoardsConfigDialogProps,
|
||||
} from './boards/boards-config-dialog';
|
||||
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
|
||||
import { ScmContribution as TheiaScmContribution } from '@theia/scm/lib/browser/scm-contribution';
|
||||
import { ScmContribution } from './theia/scm/scm-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution as TheiaSearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
|
||||
import { SearchInWorkspaceFrontendContribution } from './theia/search-in-workspace/search-in-workspace-frontend-contribution';
|
||||
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
|
||||
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
|
||||
import {
|
||||
MonitorServicePath,
|
||||
MonitorService,
|
||||
MonitorServiceClient,
|
||||
} from '../common/protocol/monitor-service';
|
||||
import {
|
||||
ConfigService,
|
||||
ConfigServicePath,
|
||||
} from '../common/protocol/config-service';
|
||||
import { MonitorWidget } from './monitor/monitor-widget';
|
||||
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
|
||||
import { MonitorConnection } from './monitor/monitor-connection';
|
||||
import { MonitorModel } from './monitor/monitor-model';
|
||||
import { MonitorWidget } from './serial/monitor/monitor-widget';
|
||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
||||
import { ProblemManager } from './theia/markers/problem-manager';
|
||||
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
||||
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
|
||||
import { EditorMode } from './editor-mode';
|
||||
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
|
||||
import {
|
||||
ArduinoComponentContextMenuRenderer,
|
||||
ListItemRenderer,
|
||||
} from './widgets/component-list/list-item-renderer';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
|
||||
import {
|
||||
ArduinoDaemonPath,
|
||||
ArduinoDaemon,
|
||||
@@ -101,14 +95,17 @@ import { EditorCommandContribution as TheiaEditorCommandContribution } from '@th
|
||||
import {
|
||||
FrontendConnectionStatusService,
|
||||
ApplicationConnectionStatusContribution,
|
||||
DaemonPort,
|
||||
IsOnline,
|
||||
} from './theia/core/connection-status-service';
|
||||
import {
|
||||
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
||||
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
|
||||
} from '@theia/core/lib/browser/connection-status-service';
|
||||
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
|
||||
import { BoardsDataMenuUpdater } from './contributions/boards-data-menu-updater';
|
||||
import { BoardsDataStore } from './boards/boards-data-store';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import {
|
||||
FileSystemExt,
|
||||
FileSystemExtPath,
|
||||
@@ -137,16 +134,12 @@ import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/
|
||||
import { PreferencesContribution } from './theia/preferences/preferences-contribution';
|
||||
import { QuitApp } from './contributions/quit-app';
|
||||
import { SketchControl } from './contributions/sketch-control';
|
||||
import { Settings } from './contributions/settings';
|
||||
import { KeybindingRegistry } from './theia/core/keybindings';
|
||||
import { OpenSettings } from './contributions/open-settings';
|
||||
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
|
||||
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
|
||||
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
|
||||
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
|
||||
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
|
||||
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
|
||||
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||
import { OutputWidget } from './theia/output/output-widget';
|
||||
import { BurnBootloader } from './contributions/burn-bootloader';
|
||||
import {
|
||||
ExamplesServicePath,
|
||||
@@ -154,19 +147,27 @@ import {
|
||||
} from '../common/protocol/examples-service';
|
||||
import { BuiltInExamples, LibraryExamples } from './contributions/examples';
|
||||
import { IncludeLibrary } from './contributions/include-library';
|
||||
import { OutputChannelManager as TheiaOutputChannelManager } from '@theia/output/lib/common/output-channel';
|
||||
import { OutputChannelManager as TheiaOutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
||||
import { OutputChannelManager } from './theia/output/output-channel';
|
||||
import {
|
||||
OutputChannelRegistryMainImpl as TheiaOutputChannelRegistryMainImpl,
|
||||
OutputChannelRegistryMainImpl,
|
||||
} from './theia/plugin-ext/output-channel-registry-main';
|
||||
import { ExecutableService, ExecutableServicePath } from '../common/protocol';
|
||||
import {
|
||||
ExecutableService,
|
||||
ExecutableServicePath,
|
||||
MonitorManagerProxy,
|
||||
MonitorManagerProxyClient,
|
||||
MonitorManagerProxyFactory,
|
||||
MonitorManagerProxyPath,
|
||||
} from '../common/protocol';
|
||||
import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
||||
import { MonacoTextModelService } from './theia/monaco/monaco-text-model-service';
|
||||
import { ResponseServiceImpl } from './response-service-impl';
|
||||
import {
|
||||
ResponseServicePath,
|
||||
ResponseService,
|
||||
ResponseServiceClient,
|
||||
ResponseServicePath,
|
||||
} from '../common/protocol/response-service';
|
||||
import { NotificationCenter } from './notification-center';
|
||||
import {
|
||||
@@ -180,8 +181,6 @@ import { EditorCommandContribution } from './theia/editor/editor-command';
|
||||
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
|
||||
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
|
||||
import { Debug } from './contributions/debug';
|
||||
import { DebugSessionManager } from './theia/debug/debug-session-manager';
|
||||
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
|
||||
import { Sketchbook } from './contributions/sketchbook';
|
||||
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
|
||||
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
|
||||
@@ -189,12 +188,12 @@ import { BoardSelection } from './contributions/board-selection';
|
||||
import { OpenRecentSketch } from './contributions/open-recent-sketch';
|
||||
import { Help } from './contributions/help';
|
||||
import { bindArduinoPreferences } from './arduino-preferences';
|
||||
import { SettingsService } from './dialogs/settings/settings';
|
||||
import {
|
||||
SettingsService,
|
||||
SettingsDialog,
|
||||
SettingsWidget,
|
||||
SettingsDialogProps,
|
||||
} from './settings';
|
||||
} from './dialogs/settings/settings-dialog';
|
||||
import { AddFile } from './contributions/add-file';
|
||||
import { ArchiveSketch } from './contributions/archive-sketch';
|
||||
import { OutputToolbarContribution as TheiaOutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
|
||||
@@ -204,15 +203,13 @@ import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } f
|
||||
import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution';
|
||||
import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager';
|
||||
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
|
||||
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
|
||||
import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget';
|
||||
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
|
||||
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
|
||||
import { SearchInWorkspaceFactory as TheiaSearchInWorkspaceFactory } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';
|
||||
import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory';
|
||||
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
|
||||
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { DebugEditorModel } from './theia/debug/debug-editor-model';
|
||||
import { DebugEditorModelFactory } from '@theia/debug/lib/browser/editor/debug-editor-model';
|
||||
import { StorageWrapper } from './storage-wrapper';
|
||||
import {
|
||||
MonacoEditorFactory,
|
||||
MonacoEditorProvider as TheiaMonacoEditorProvider,
|
||||
} from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { NotificationManager } from './theia/messages/notifications-manager';
|
||||
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||
import { NotificationsRenderer as TheiaNotificationsRenderer } from '@theia/messages/lib/browser/notifications-renderer';
|
||||
@@ -241,9 +238,7 @@ import { UploadFirmware } from './contributions/upload-firmware';
|
||||
import {
|
||||
UploadFirmwareDialog,
|
||||
UploadFirmwareDialogProps,
|
||||
UploadFirmwareDialogWidget,
|
||||
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
|
||||
|
||||
import { UploadCertificate } from './contributions/upload-certificate';
|
||||
import {
|
||||
ArduinoFirmwareUploader,
|
||||
@@ -254,33 +249,143 @@ import {
|
||||
UploadCertificateDialogProps,
|
||||
UploadCertificateDialogWidget,
|
||||
} from './dialogs/certificate-uploader/certificate-uploader-dialog';
|
||||
import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-contribution';
|
||||
import {
|
||||
UserFieldsDialog,
|
||||
UserFieldsDialogProps,
|
||||
} from './dialogs/user-fields/user-fields-dialog';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands';
|
||||
import {
|
||||
IDEUpdater,
|
||||
IDEUpdaterClient,
|
||||
IDEUpdaterPath,
|
||||
} from '../common/protocol/ide-updater';
|
||||
import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl';
|
||||
import {
|
||||
IDEUpdaterDialog,
|
||||
IDEUpdaterDialogProps,
|
||||
} from './dialogs/ide-updater/ide-updater-dialog';
|
||||
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
import { MonitorManagerProxyClientImpl } from './monitor-manager-proxy-client-impl';
|
||||
import { EditorManager as TheiaEditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { EditorManager } from './theia/editor/editor-manager';
|
||||
import { HostedPluginEvents } from './hosted-plugin-events';
|
||||
import { HostedPluginSupport } from './theia/plugin-ext/hosted-plugin';
|
||||
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||
import { Formatter, FormatterPath } from '../common/protocol/formatter';
|
||||
import { Format } from './contributions/format';
|
||||
import { MonacoFormattingConflictsContribution } from './theia/monaco/monaco-formatting-conflicts';
|
||||
import { MonacoFormattingConflictsContribution as TheiaMonacoFormattingConflictsContribution } from '@theia/monaco/lib/browser/monaco-formatting-conflicts';
|
||||
import { DefaultJsonSchemaContribution } from './theia/core/json-schema-store';
|
||||
import { DefaultJsonSchemaContribution as TheiaDefaultJsonSchemaContribution } from '@theia/core/lib/browser/json-schema-store';
|
||||
import { EditorNavigationContribution } from './theia/editor/editor-navigation-contribution';
|
||||
import { EditorNavigationContribution as TheiaEditorNavigationContribution } from '@theia/editor/lib/browser/editor-navigation-contribution';
|
||||
import { PreferenceTreeGenerator } from './theia/preferences/preference-tree-generator';
|
||||
import { PreferenceTreeGenerator as TheiaPreferenceTreeGenerator } from '@theia/preferences/lib/browser/util/preference-tree-generator';
|
||||
import { AboutDialog } from './theia/core/about-dialog';
|
||||
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
|
||||
import {
|
||||
SurveyNotificationService,
|
||||
SurveyNotificationServicePath,
|
||||
} from '../common/protocol/survey-service';
|
||||
import { WindowContribution } from './theia/core/window-contribution';
|
||||
import { WindowContribution as TheiaWindowContribution } from '@theia/core/lib/browser/window-contribution';
|
||||
import { CoreErrorHandler } from './contributions/core-error-handler';
|
||||
import { CompilerErrors } from './contributions/compiler-errors';
|
||||
import { WidgetManager } from './theia/core/widget-manager';
|
||||
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
|
||||
import { StartupTasksExecutor } from './contributions/startup-tasks-executor';
|
||||
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
|
||||
import { Daemon } from './contributions/daemon';
|
||||
import { FirstStartupInstaller } from './contributions/first-startup-installer';
|
||||
import { OpenSketchFiles } from './contributions/open-sketch-files';
|
||||
import { InoLanguage } from './contributions/ino-language';
|
||||
import { SelectedBoard } from './contributions/selected-board';
|
||||
import { CheckForIDEUpdates } from './contributions/check-for-ide-updates';
|
||||
import { OpenBoardsConfig } from './contributions/open-boards-config';
|
||||
import { SketchFilesTracker } from './contributions/sketch-files-tracker';
|
||||
import { EditorMenuContribution } from './theia/editor/editor-file';
|
||||
import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu';
|
||||
import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget';
|
||||
import { PreferencesEditorWidget } from './theia/preferences/preference-editor-widget';
|
||||
import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
|
||||
import { createPreferencesWidgetContainer } from '@theia/preferences/lib/browser/views/preference-widget-bindings';
|
||||
import {
|
||||
BoardsFilterRenderer,
|
||||
LibraryFilterRenderer,
|
||||
} from './widgets/component-list/filter-renderer';
|
||||
import { CheckForUpdates } from './contributions/check-for-updates';
|
||||
import { OutputEditorFactory } from './theia/output/output-editor-factory';
|
||||
import { StartupTaskProvider } from '../electron-common/startup-task';
|
||||
import { DeleteSketch } from './contributions/delete-sketch';
|
||||
import { UserFields } from './contributions/user-fields';
|
||||
import { UpdateIndexes } from './contributions/update-indexes';
|
||||
import { InterfaceScale } from './contributions/interface-scale';
|
||||
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
|
||||
import { NewCloudSketch } from './contributions/new-cloud-sketch';
|
||||
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
|
||||
import { WindowTitleUpdater } from './theia/core/window-title-updater';
|
||||
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
|
||||
import {
|
||||
MonacoThemingService,
|
||||
CleanupObsoleteThemes,
|
||||
ThemesRegistrationSummary,
|
||||
MonacoThemeRegistry,
|
||||
} from './theia/monaco/monaco-theming-service';
|
||||
import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
|
||||
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service';
|
||||
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
|
||||
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution';
|
||||
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
|
||||
import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution';
|
||||
import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
|
||||
import { ConfigServiceClient } from './config/config-service-client';
|
||||
import { ValidateSketch } from './contributions/validate-sketch';
|
||||
import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
|
||||
import { CreateFeatures } from './create/create-features';
|
||||
import { Account } from './contributions/account';
|
||||
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
|
||||
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
|
||||
import { CreateCloudCopy } from './contributions/create-cloud-copy';
|
||||
import { FileResourceResolver } from './theia/filesystem/file-resource';
|
||||
import { FileResourceResolver as TheiaFileResourceResolver } from '@theia/filesystem/lib/browser/file-resource';
|
||||
import { StylingParticipant } from '@theia/core/lib/browser/styling-service';
|
||||
import { MonacoEditorMenuContribution } from './theia/monaco/monaco-menu';
|
||||
import { MonacoEditorMenuContribution as TheiaMonacoEditorMenuContribution } from '@theia/monaco/lib/browser/monaco-menu';
|
||||
import { UpdateArduinoState } from './contributions/update-arduino-state';
|
||||
import { TerminalFrontendContribution } from './theia/terminal/terminal-frontend-contribution';
|
||||
import { TerminalFrontendContribution as TheiaTerminalFrontendContribution } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
||||
|
||||
const ElementQueries = require('css-element-queries/src/ElementQueries');
|
||||
|
||||
MonacoThemingService.register({
|
||||
id: 'arduino-theme',
|
||||
label: 'Light (Arduino)',
|
||||
uiTheme: 'vs',
|
||||
json: require('../../src/browser/data/arduino.color-theme.json'),
|
||||
});
|
||||
// Hack to fix copy/cut/paste issue after electron version update in Theia.
|
||||
// https://github.com/eclipse-theia/theia/issues/12487
|
||||
import('@theia/core/lib/browser/common-frontend-contribution.js').then(
|
||||
(theiaCommonContribution) => {
|
||||
theiaCommonContribution['supportCopy'] = true;
|
||||
theiaCommonContribution['supportCut'] = true;
|
||||
theiaCommonContribution['supportPaste'] = true;
|
||||
}
|
||||
);
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
ElementQueries.listen();
|
||||
ElementQueries.init();
|
||||
|
||||
// Commands and toolbar items
|
||||
// Commands, colors, theme adjustments, and toolbar items
|
||||
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(ArduinoFrontendContribution);
|
||||
bind(MenuContribution).toService(ArduinoFrontendContribution);
|
||||
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
|
||||
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
|
||||
bind(ColorContribution).toService(ArduinoFrontendContribution);
|
||||
bind(StylingParticipant).toService(ArduinoFrontendContribution);
|
||||
|
||||
bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
|
||||
|
||||
// Renderer for both the library and the core widgets.
|
||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||
bind(LibraryFilterRenderer).toSelf().inSingletonScope();
|
||||
bind(BoardsFilterRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// Library service
|
||||
bind(LibraryService)
|
||||
@@ -302,6 +407,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
LibraryListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService)
|
||||
@@ -324,6 +430,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
bind(ConfigServiceClient).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
|
||||
|
||||
// Boards service
|
||||
bind(BoardsService)
|
||||
@@ -337,11 +445,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Boards service client to receive and delegate notifications from the backend.
|
||||
bind(BoardsServiceProvider).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
|
||||
bind(CommandContribution).toService(BoardsServiceProvider);
|
||||
bind(BoardListDumper).toSelf().inSingletonScope();
|
||||
|
||||
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
|
||||
bind(FrontendApplicationContribution)
|
||||
.to(BoardsDataMenuUpdater)
|
||||
.inSingletonScope();
|
||||
bind(BoardsDataStore).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsDataStore);
|
||||
// Logger for the Arduino daemon
|
||||
@@ -367,12 +474,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
BoardsListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialog).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialogProps).toConstantValue({
|
||||
title: 'Select Board',
|
||||
title: nls.localize(
|
||||
'arduino/board/boardConfigDialogTitle',
|
||||
'Select Other Board and Port'
|
||||
),
|
||||
});
|
||||
|
||||
// Core service
|
||||
@@ -384,38 +494,38 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
bind(CoreErrorHandler).toSelf().inSingletonScope();
|
||||
|
||||
// Serial monitor
|
||||
bind(MonitorModel).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(MonitorModel);
|
||||
bind(MonitorWidget).toSelf();
|
||||
bind(FrontendApplicationContribution).toService(MonitorModel);
|
||||
bind(MonitorModel).toSelf().inSingletonScope();
|
||||
bindViewContribution(bind, MonitorViewContribution);
|
||||
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
||||
bind(WidgetFactory).toDynamicValue((context) => ({
|
||||
id: MonitorWidget.ID,
|
||||
createWidget: () => context.container.get(MonitorWidget),
|
||||
}));
|
||||
// Frontend binding for the serial monitor service
|
||||
bind(MonitorService)
|
||||
.toDynamicValue((context) => {
|
||||
const connection = context.container.get(WebSocketConnectionProvider);
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
return connection.createProxy(MonitorServicePath, client);
|
||||
})
|
||||
.inSingletonScope();
|
||||
bind(MonitorConnection).toSelf().inSingletonScope();
|
||||
// Serial monitor service client to receive and delegate notifications from the backend.
|
||||
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
|
||||
bind(MonitorServiceClient)
|
||||
.toDynamicValue((context) => {
|
||||
const client = context.container.get(MonitorServiceClientImpl);
|
||||
|
||||
bind(MonitorManagerProxyFactory).toFactory(
|
||||
(context) => () =>
|
||||
context.container.get<MonitorManagerProxy>(MonitorManagerProxy)
|
||||
);
|
||||
|
||||
bind(MonitorManagerProxy)
|
||||
.toDynamicValue((context) =>
|
||||
WebSocketConnectionProvider.createProxy(
|
||||
context.container,
|
||||
MonitorServicePath,
|
||||
client
|
||||
);
|
||||
return client;
|
||||
})
|
||||
MonitorManagerProxyPath,
|
||||
context.container.get(MonitorManagerProxyClient)
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
// Monitor manager proxy client to receive and delegate pluggable monitors
|
||||
// notifications from the backend
|
||||
bind(MonitorManagerProxyClient)
|
||||
.to(MonitorManagerProxyClientImpl)
|
||||
.inSingletonScope();
|
||||
|
||||
bind(WorkspaceService).toSelf().inSingletonScope();
|
||||
@@ -425,9 +535,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
WorkspaceVariableContribution
|
||||
);
|
||||
|
||||
// Customizing default Theia layout based on the editor mode: `pro-mode` or `classic`.
|
||||
bind(EditorMode).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(EditorMode);
|
||||
bind(SurveyNotificationService)
|
||||
.toDynamicValue((context) => {
|
||||
return ElectronIpcConnectionProvider.createProxy(
|
||||
context.container,
|
||||
SurveyNotificationServicePath
|
||||
);
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
// Layout and shell customizations.
|
||||
rebind(TheiaOutlineViewContribution)
|
||||
@@ -462,7 +577,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
rebind(TheiaPreferencesContribution)
|
||||
.to(PreferencesContribution)
|
||||
.inSingletonScope();
|
||||
rebind(TheiaKeybindingRegistry).to(KeybindingRegistry).inSingletonScope();
|
||||
rebind(TheiaWorkspaceCommandContribution)
|
||||
.to(WorkspaceCommandContribution)
|
||||
.inSingletonScope();
|
||||
@@ -470,16 +584,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.to(WorkspaceDeleteHandler)
|
||||
.inSingletonScope();
|
||||
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
|
||||
rebind(TabBarToolbarFactory).toFactory(
|
||||
({ container: parentContainer }) =>
|
||||
() => {
|
||||
const container = parentContainer.createChild();
|
||||
container.bind(TabBarToolbar).toSelf().inSingletonScope();
|
||||
return container.get(TabBarToolbar);
|
||||
}
|
||||
);
|
||||
bind(OutputWidget).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputWidget).toService(OutputWidget);
|
||||
bind(OutputChannelManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
|
||||
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
|
||||
@@ -491,18 +595,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(MonacoEditorProvider).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoEditorProvider).toService(MonacoEditorProvider);
|
||||
|
||||
bind(SearchInWorkspaceWidget).toSelf();
|
||||
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
|
||||
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(
|
||||
({ container }) => {
|
||||
const childContainer = createTreeContainer(container);
|
||||
childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf();
|
||||
childContainer
|
||||
.rebind(TreeWidget)
|
||||
.toService(SearchInWorkspaceResultTreeWidget);
|
||||
return childContainer.get(SearchInWorkspaceResultTreeWidget);
|
||||
}
|
||||
);
|
||||
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
|
||||
bind(EditorManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorManager).toService(EditorManager);
|
||||
|
||||
// replace search icon
|
||||
rebind(TheiaSearchInWorkspaceFactory)
|
||||
.to(SearchInWorkspaceFactory)
|
||||
.inSingletonScope();
|
||||
|
||||
// Show a disconnected status bar, when the daemon is not available
|
||||
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
|
||||
@@ -530,6 +630,19 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(OutputToolbarContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputToolbarContribution).toService(OutputToolbarContribution);
|
||||
|
||||
// To remove `New Window` from the `File` menu
|
||||
bind(WindowContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaWindowContribution).toService(WindowContribution);
|
||||
|
||||
// To remove `File` > `Close Editor`.
|
||||
bind(EditorMenuContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorMenuContribution).toService(EditorMenuContribution);
|
||||
|
||||
// To disable the highlighting of non-unicode characters in the _Output_ view
|
||||
bind(OutputEditorFactory).toSelf().inSingletonScope();
|
||||
// Rebind to `TheiaOutputEditorFactory` when https://github.com/eclipse-theia/theia/pull/11615 is available.
|
||||
rebind(MonacoEditorFactory).toService(OutputEditorFactory);
|
||||
|
||||
bind(ArduinoDaemon)
|
||||
.toDynamicValue((context) =>
|
||||
WebSocketConnectionProvider.createProxy(
|
||||
@@ -539,6 +652,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
bind(Formatter)
|
||||
.toDynamicValue(({ container }) =>
|
||||
WebSocketConnectionProvider.createProxy(container, FormatterPath)
|
||||
)
|
||||
.inSingletonScope();
|
||||
|
||||
bind(ArduinoFirmwareUploader)
|
||||
.toDynamicValue((context) =>
|
||||
WebSocketConnectionProvider.createProxy(
|
||||
@@ -589,7 +708,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, EditContributions);
|
||||
Contribution.configure(bind, QuitApp);
|
||||
Contribution.configure(bind, SketchControl);
|
||||
Contribution.configure(bind, Settings);
|
||||
Contribution.configure(bind, OpenSettings);
|
||||
Contribution.configure(bind, BurnBootloader);
|
||||
Contribution.configure(bind, BuiltInExamples);
|
||||
Contribution.configure(bind, LibraryExamples);
|
||||
@@ -605,6 +724,42 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, AddFile);
|
||||
Contribution.configure(bind, ArchiveSketch);
|
||||
Contribution.configure(bind, AddZipLibrary);
|
||||
Contribution.configure(bind, PlotterFrontendContribution);
|
||||
Contribution.configure(bind, Format);
|
||||
Contribution.configure(bind, CompilerErrors);
|
||||
Contribution.configure(bind, StartupTasksExecutor);
|
||||
Contribution.configure(bind, IndexesUpdateProgress);
|
||||
Contribution.configure(bind, Daemon);
|
||||
Contribution.configure(bind, FirstStartupInstaller);
|
||||
Contribution.configure(bind, OpenSketchFiles);
|
||||
Contribution.configure(bind, InoLanguage);
|
||||
Contribution.configure(bind, SelectedBoard);
|
||||
Contribution.configure(bind, CheckForIDEUpdates);
|
||||
Contribution.configure(bind, OpenBoardsConfig);
|
||||
Contribution.configure(bind, SketchFilesTracker);
|
||||
Contribution.configure(bind, CheckForUpdates);
|
||||
Contribution.configure(bind, UserFields);
|
||||
Contribution.configure(bind, DeleteSketch);
|
||||
Contribution.configure(bind, UpdateIndexes);
|
||||
Contribution.configure(bind, InterfaceScale);
|
||||
Contribution.configure(bind, NewCloudSketch);
|
||||
Contribution.configure(bind, ValidateSketch);
|
||||
Contribution.configure(bind, RenameCloudSketch);
|
||||
Contribution.configure(bind, Account);
|
||||
Contribution.configure(bind, CloudSketchbookContribution);
|
||||
Contribution.configure(bind, CreateCloudCopy);
|
||||
Contribution.configure(bind, UpdateArduinoState);
|
||||
Contribution.configure(bind, BoardsDataMenuUpdater);
|
||||
|
||||
bindContributionProvider(bind, StartupTaskProvider);
|
||||
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
|
||||
|
||||
// Disabled the quick-pick customization from Theia when multiple formatters are available.
|
||||
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
|
||||
bind(MonacoFormattingConflictsContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoFormattingConflictsContribution).toService(
|
||||
MonacoFormattingConflictsContribution
|
||||
);
|
||||
|
||||
bind(ResponseServiceImpl)
|
||||
.toSelf()
|
||||
@@ -617,7 +772,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
);
|
||||
return responseService;
|
||||
});
|
||||
|
||||
bind(ResponseService).toService(ResponseServiceImpl);
|
||||
bind(ResponseServiceClient).toService(ResponseServiceImpl);
|
||||
|
||||
bind(NotificationCenter).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(NotificationCenter);
|
||||
@@ -648,6 +805,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
|
||||
// Workaround for https://github.com/eclipse-theia/theia/issues/8722
|
||||
// Do not trigger a save on IDE startup if `"editor.autoSave": "on"` was set as a preference.
|
||||
// Note: `"editor.autoSave" was renamed to `"files.autoSave" and `"on"` was replaced with three
|
||||
// different cases, but we treat `!== 'off'` as auto save enabled. (https://github.com/eclipse-theia/theia/issues/10812)
|
||||
bind(EditorCommandContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorCommandContribution).toService(EditorCommandContribution);
|
||||
|
||||
@@ -655,9 +814,26 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
|
||||
rebind(TheiaNavigatorTabBarDecorator).toService(NavigatorTabBarDecorator);
|
||||
|
||||
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
|
||||
bind(DebugSessionManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
|
||||
// Do not fetch the `catalog.json` from Azure on FE load.
|
||||
bind(DefaultJsonSchemaContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaDefaultJsonSchemaContribution).toService(
|
||||
DefaultJsonSchemaContribution
|
||||
);
|
||||
|
||||
// Do not block the app startup when initializing the editor navigation history.
|
||||
bind(EditorNavigationContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorNavigationContribution).toService(
|
||||
EditorNavigationContribution
|
||||
);
|
||||
|
||||
// IDE2 does not use the Theia preferences widget, no need to create and sync the underlying tree model.
|
||||
bind(PreferenceTreeGenerator).toSelf().inSingletonScope();
|
||||
rebind(TheiaPreferenceTreeGenerator).toService(PreferenceTreeGenerator);
|
||||
|
||||
// IDE2 has a custom about dialog, so there is no need to load the Theia extensions on FE load
|
||||
bind(AboutDialog).toSelf().inSingletonScope();
|
||||
rebind(TheiaAboutDialog).toService(AboutDialog);
|
||||
|
||||
// To remove the `Run` menu item from the application menu.
|
||||
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaDebugFrontendApplicationContribution).toService(
|
||||
@@ -667,14 +843,24 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(DebugConfigurationManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaDebugConfigurationManager).toService(DebugConfigurationManager);
|
||||
|
||||
// Patch for the debug hover: https://github.com/eclipse-theia/theia/pull/9256/
|
||||
rebind(DebugEditorModelFactory)
|
||||
.toDynamicValue(
|
||||
({ container }) =>
|
||||
<DebugEditorModelFactory>(
|
||||
((editor) => DebugEditorModel.createModel(container, editor))
|
||||
)
|
||||
)
|
||||
// To avoid duplicate tabs use deepEqual instead of string equal: https://github.com/eclipse-theia/theia/issues/11309
|
||||
bind(WidgetManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaWidgetManager).toService(WidgetManager);
|
||||
|
||||
// Debounced update for the tab-bar toolbar when typing in the editor.
|
||||
bind(DockPanelRenderer).toSelf();
|
||||
rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer);
|
||||
|
||||
// Avoid running the "reset scroll" interval tasks until the preference editor opens.
|
||||
rebind(PreferencesWidget)
|
||||
.toDynamicValue(({ container }) => {
|
||||
const child = createPreferencesWidgetContainer(container);
|
||||
child.bind(PreferencesEditorWidget).toSelf().inSingletonScope();
|
||||
child
|
||||
.rebind(TheiaPreferencesEditorWidget)
|
||||
.toService(PreferencesEditorWidget);
|
||||
return child.get(PreferencesWidget);
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
// Preferences
|
||||
@@ -686,12 +872,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(SettingsWidget).toSelf().inSingletonScope();
|
||||
bind(SettingsDialog).toSelf().inSingletonScope();
|
||||
bind(SettingsDialogProps).toConstantValue({
|
||||
title: 'Preferences',
|
||||
title: nls.localize(
|
||||
'vscode/preferences.contribution/preferences',
|
||||
'Preferences'
|
||||
),
|
||||
});
|
||||
|
||||
bind(StorageWrapper).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(StorageWrapper);
|
||||
|
||||
bind(NotificationManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaNotificationManager).toService(NotificationManager);
|
||||
bind(NotificationsRenderer).toSelf().inSingletonScope();
|
||||
@@ -708,6 +894,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
id: 'arduino-sketchbook-widget',
|
||||
createWidget: () => container.get(SketchbookWidget),
|
||||
}));
|
||||
bind(SketchbookCompositeWidget).toSelf();
|
||||
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
|
||||
id: 'sketchbook-composite-widget',
|
||||
createWidget: () => ctx.container.get(SketchbookCompositeWidget),
|
||||
}));
|
||||
|
||||
bind(CloudSketchbookWidget).toSelf();
|
||||
rebind(SketchbookWidget).toService(CloudSketchbookWidget);
|
||||
@@ -716,6 +907,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
);
|
||||
bind(CreateApi).toSelf().inSingletonScope();
|
||||
bind(SketchCache).toSelf().inSingletonScope();
|
||||
bind(CreateFeatures).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(CreateFeatures);
|
||||
|
||||
bind(ShareSketchDialog).toSelf().inSingletonScope();
|
||||
bind(AuthenticationClientService).toSelf().inSingletonScope();
|
||||
@@ -732,17 +925,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(CreateFsProvider).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(CreateFsProvider);
|
||||
bind(FileServiceContribution).toService(CreateFsProvider);
|
||||
bind(CloudSketchbookContribution).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(CloudSketchbookContribution);
|
||||
bind(LocalCacheFsProvider).toSelf().inSingletonScope();
|
||||
bind(FileServiceContribution).toService(LocalCacheFsProvider);
|
||||
bind(CloudSketchbookCompositeWidget).toSelf();
|
||||
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
|
||||
bind(WidgetFactory).toDynamicValue((ctx) => ({
|
||||
id: 'cloud-sketchbook-composite-widget',
|
||||
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
|
||||
}));
|
||||
|
||||
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
|
||||
bind(UploadFirmwareDialog).toSelf().inSingletonScope();
|
||||
bind(UploadFirmwareDialogProps).toConstantValue({
|
||||
title: 'UploadFirmware',
|
||||
@@ -752,4 +942,92 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(UploadCertificateDialogProps).toConstantValue({
|
||||
title: 'UploadCertificate',
|
||||
});
|
||||
|
||||
bind(IDEUpdaterDialog).toSelf().inSingletonScope();
|
||||
bind(IDEUpdaterDialogProps).toConstantValue({
|
||||
title: 'IDEUpdater',
|
||||
});
|
||||
|
||||
bind(UserFieldsDialog).toSelf().inSingletonScope();
|
||||
bind(UserFieldsDialogProps).toConstantValue({
|
||||
title: 'UserFields',
|
||||
});
|
||||
|
||||
bind(IDEUpdaterCommands).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(IDEUpdaterCommands);
|
||||
|
||||
// Frontend binding for the IDE Updater service
|
||||
bind(IDEUpdaterClientImpl).toSelf().inSingletonScope();
|
||||
bind(IDEUpdaterClient).toService(IDEUpdaterClientImpl);
|
||||
bind(IDEUpdater)
|
||||
.toDynamicValue((context) => {
|
||||
const client = context.container.get(IDEUpdaterClientImpl);
|
||||
return ElectronIpcConnectionProvider.createProxy(
|
||||
context.container,
|
||||
IDEUpdaterPath,
|
||||
client
|
||||
);
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
bind(HostedPluginSupport).toSelf().inSingletonScope();
|
||||
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
|
||||
bind(HostedPluginEvents).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
|
||||
|
||||
// custom window titles
|
||||
bind(WindowTitleUpdater).toSelf().inSingletonScope();
|
||||
rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater);
|
||||
|
||||
// register Arduino themes
|
||||
bind(MonacoThemingService).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
|
||||
|
||||
// workaround for themes cannot be removed after registration
|
||||
// https://github.com/eclipse-theia/theia/issues/11151
|
||||
bind(CleanupObsoleteThemes).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(CleanupObsoleteThemes);
|
||||
bind(ThemesRegistrationSummary).toSelf().inSingletonScope();
|
||||
bind(MonacoThemeRegistry).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoThemeRegistry).toService(MonacoThemeRegistry);
|
||||
|
||||
// disable type-hierarchy support
|
||||
// https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be
|
||||
bind(TypeHierarchyServiceProvider).toSelf().inSingletonScope();
|
||||
rebind(TheiaTypeHierarchyServiceProvider).toService(
|
||||
TypeHierarchyServiceProvider
|
||||
);
|
||||
bind(TypeHierarchyContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution);
|
||||
|
||||
bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
|
||||
rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory);
|
||||
|
||||
bind(SidebarBottomMenuWidget).toSelf();
|
||||
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
|
||||
|
||||
bind(ArduinoComponentContextMenuRenderer).toSelf().inSingletonScope();
|
||||
|
||||
bind(DaemonPort).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(DaemonPort);
|
||||
bind(IsOnline).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(IsOnline);
|
||||
|
||||
// https://github.com/arduino/arduino-ide/issues/437
|
||||
bind(FileResourceResolver).toSelf().inSingletonScope();
|
||||
rebind(TheiaFileResourceResolver).toService(FileResourceResolver);
|
||||
|
||||
// Full control over the editor context menu to filter undesired menu items contributed by Theia.
|
||||
// https://github.com/arduino/arduino-ide/issues/1394
|
||||
// https://github.com/arduino/arduino-ide/pull/2027#pullrequestreview-1414246614
|
||||
bind(MonacoEditorMenuContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoEditorMenuContribution).toService(
|
||||
MonacoEditorMenuContribution
|
||||
);
|
||||
|
||||
// Patch terminal issues.
|
||||
bind(TerminalFrontendContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaTerminalFrontendContribution).toService(
|
||||
TerminalFrontendContribution
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,36 +1,121 @@
|
||||
import { interfaces } from 'inversify';
|
||||
import {
|
||||
createPreferenceProxy,
|
||||
PreferenceProxy,
|
||||
PreferenceService,
|
||||
PreferenceContribution,
|
||||
PreferenceProxy,
|
||||
PreferenceSchema,
|
||||
PreferenceService,
|
||||
createPreferenceProxy,
|
||||
} from '@theia/core/lib/browser/preferences';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { PreferenceSchemaProperty } from '@theia/core/lib/common/preferences/preference-schema';
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { serialMonitorWidgetLabel } from '../common/nls';
|
||||
import { CompilerWarningLiterals, CompilerWarnings } from '../common/protocol';
|
||||
|
||||
export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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 type MonitorWidgetDockPanel = Extract<
|
||||
ApplicationShell.Area,
|
||||
'bottom' | 'right'
|
||||
>;
|
||||
export const defaultMonitorWidgetDockPanel: MonitorWidgetDockPanel = 'bottom';
|
||||
export function isMonitorWidgetDockPanel(
|
||||
arg: unknown
|
||||
): arg is MonitorWidgetDockPanel {
|
||||
return arg === 'bottom' || arg === 'right';
|
||||
}
|
||||
|
||||
type StrictPreferenceSchemaProperties<T extends object> = {
|
||||
[p in keyof T]: PreferenceSchemaProperty;
|
||||
};
|
||||
type ArduinoPreferenceSchemaProperties =
|
||||
StrictPreferenceSchemaProperties<ArduinoConfiguration> & { 'arduino.window.zoomLevel': PreferenceSchemaProperty };
|
||||
|
||||
const properties: ArduinoPreferenceSchemaProperties = {
|
||||
'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.",
|
||||
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: 'True for verbose compile output. False by default',
|
||||
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:
|
||||
"Tells gcc which warning level to use. It's 'None' by default",
|
||||
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: 'True for verbose upload output. False by default.',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/upload.verbose',
|
||||
'True for verbose upload output. False by default.'
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
'arduino.upload.verify': {
|
||||
@@ -39,121 +124,212 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
},
|
||||
'arduino.window.autoScale': {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'True if the user interface automatically scales with the font size.',
|
||||
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:
|
||||
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.',
|
||||
description: '',
|
||||
default: 0,
|
||||
deprecationMessage: nls.localize(
|
||||
'arduino/preferences/window.zoomLevel/deprecationMessage',
|
||||
"Deprecated. Use 'window.zoomLevel' instead."
|
||||
),
|
||||
},
|
||||
'arduino.ide.autoUpdate': {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'True to enable automatic update checks. The IDE will check for updates automatically and periodically.',
|
||||
default: true,
|
||||
'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: 'List of certificates that can be uploaded to boards',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/board.certificates',
|
||||
'List of certificates that can be uploaded to boards'
|
||||
),
|
||||
default: '',
|
||||
},
|
||||
'arduino.sketchbook.showAllFiles': {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'True to show all sketch files inside the sketch. It is false by default.',
|
||||
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:
|
||||
'True if the sketch sync functions are enabled. Defaults to true.',
|
||||
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:
|
||||
'True if users should be warned before pulling a cloud sketch. Defaults to true.',
|
||||
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:
|
||||
'True if users should be warned before pushing a cloud sketch. Defaults to true.',
|
||||
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:
|
||||
'True if users should be warned before pushing a public sketch to the cloud. Defaults to true.',
|
||||
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.sketchSyncEnpoint': {
|
||||
'arduino.cloud.sketchSyncEndpoint': {
|
||||
type: 'string',
|
||||
description:
|
||||
'The endpoint used to push and pull sketches from a backend. By default it points to Arduino Cloud API.',
|
||||
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: 'The OAuth2 client ID.',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/auth.clientID',
|
||||
'The OAuth2 client ID.'
|
||||
),
|
||||
default: 'C34Ya6ex77jTNxyKWj01lCe1vAHIaPIo',
|
||||
},
|
||||
'arduino.auth.domain': {
|
||||
type: 'string',
|
||||
description: 'The OAuth2 domain.',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/auth.domain',
|
||||
'The OAuth2 domain.'
|
||||
),
|
||||
default: 'login.arduino.cc',
|
||||
},
|
||||
'arduino.auth.audience': {
|
||||
type: 'string',
|
||||
description: 'The 0Auth2 audience.',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/auth.audience',
|
||||
'The OAuth2 audience.'
|
||||
),
|
||||
default: 'https://api.arduino.cc',
|
||||
},
|
||||
'arduino.auth.registerUri': {
|
||||
type: 'string',
|
||||
description: 'The URI used to register a new user.',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/auth.registerUri',
|
||||
'The URI used to register a new user.'
|
||||
),
|
||||
default: 'https://auth.arduino.cc/login#/register',
|
||||
},
|
||||
'arduino.survey.notification': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/survey.notification',
|
||||
'True if users should be notified if a survey is available. True by default.'
|
||||
),
|
||||
default: true,
|
||||
},
|
||||
'arduino.cli.daemon.debug': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/cli.daemonDebug',
|
||||
"Enable debug logging of the gRPC calls to the Arduino CLI. A restart of the IDE is needed for this setting to take effect. It's false by default."
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
'arduino.checkForUpdates': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/checkForUpdate',
|
||||
"Receive notifications of available updates for the IDE, boards, and libraries. Requires an IDE restart after change. It's true by default."
|
||||
),
|
||||
default: true,
|
||||
},
|
||||
'arduino.sketch.inoBlueprint': {
|
||||
type: 'string',
|
||||
markdownDescription: nls.localize(
|
||||
'arduino/preferences/sketch/inoBlueprint',
|
||||
'Absolute filesystem path to the default `.ino` blueprint file. If specified, the content of the blueprint file will be used for every new sketch created by the IDE. The sketches will be generated with the default Arduino content if not specified. Unaccessible blueprint files are ignored. **A restart of the IDE is needed** for this setting to take effect.'
|
||||
),
|
||||
default: undefined,
|
||||
},
|
||||
'arduino.monitor.dockPanel': {
|
||||
type: 'string',
|
||||
enum: ['bottom', 'right'],
|
||||
markdownDescription: nls.localize(
|
||||
'arduino/preferences/monitor/dockPanel',
|
||||
'The area of the application shell where the _{0}_ widget will reside. It is either "bottom" or "right". It defaults to "{1}".',
|
||||
serialMonitorWidgetLabel,
|
||||
defaultMonitorWidgetDockPanel
|
||||
),
|
||||
default: defaultMonitorWidgetDockPanel,
|
||||
},
|
||||
};
|
||||
export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
type: 'object',
|
||||
properties,
|
||||
};
|
||||
|
||||
export interface ArduinoConfiguration {
|
||||
'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.window.zoomLevel': number;
|
||||
'arduino.ide.autoUpdate': boolean;
|
||||
'arduino.ide.updateChannel': UpdateChannel;
|
||||
'arduino.ide.updateBaseUrl': string;
|
||||
'arduino.board.certificates': string;
|
||||
'arduino.sketchbook.showAllFiles': boolean;
|
||||
'arduino.cloud.enabled': boolean;
|
||||
'arduino.cloud.pull.warn': boolean;
|
||||
'arduino.cloud.push.warn': boolean;
|
||||
'arduino.cloud.pushpublic.warn': boolean;
|
||||
'arduino.cloud.sketchSyncEnpoint': string;
|
||||
'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;
|
||||
'arduino.monitor.dockPanel': MonitorWidgetDockPanel;
|
||||
}
|
||||
|
||||
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);
|
||||
return createPreferenceProxy(preferences, ArduinoConfigSchema);
|
||||
});
|
||||
bind(PreferenceContribution).toConstantValue({
|
||||
schema: ArduinoConfigSchema,
|
||||
|
||||
@@ -1,73 +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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
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';
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
CommandContribution,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import {
|
||||
AuthOptions,
|
||||
AuthenticationService,
|
||||
AuthenticationServiceClient,
|
||||
AuthenticationSession,
|
||||
authServerPort,
|
||||
} 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()
|
||||
@@ -43,13 +43,15 @@ export class AuthenticationClientService
|
||||
|
||||
readonly onSessionDidChange = this.onSessionDidChangeEmitter.event;
|
||||
|
||||
onStart(): void {
|
||||
async onStart(): Promise<void> {
|
||||
this.toDispose.push(this.onSessionDidChangeEmitter);
|
||||
this.service.setClient(this);
|
||||
this.service
|
||||
.session()
|
||||
.then((session) => this.notifySessionDidChange(session));
|
||||
this.setOptions();
|
||||
|
||||
this.setOptions().then(() => this.service.initAuthSession());
|
||||
|
||||
this.arduinoPreferences.onPreferenceChanged((event) => {
|
||||
if (event.preferenceName.startsWith('arduino.auth.')) {
|
||||
this.setOptions();
|
||||
@@ -57,9 +59,9 @@ export class AuthenticationClientService
|
||||
});
|
||||
}
|
||||
|
||||
setOptions(): void {
|
||||
this.service.setOptions({
|
||||
redirectUri: `http://localhost:${serverPort}/callback`,
|
||||
setOptions(): Promise<void> {
|
||||
return this.service.setOptions({
|
||||
redirectUri: `http://localhost:${authServerPort}/callback`,
|
||||
responseType: 'code',
|
||||
clientID: this.arduinoPreferences['arduino.auth.clientID'],
|
||||
domain: this.arduinoPreferences['arduino.auth.domain'],
|
||||
@@ -81,9 +83,13 @@ export class AuthenticationClientService
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +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 = {
|
||||
export const LOGIN = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud--login',
|
||||
label: 'Sign in',
|
||||
};
|
||||
},
|
||||
'arduino/cloud/signIn'
|
||||
);
|
||||
|
||||
export const LOGOUT: Command = {
|
||||
export const LOGOUT = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud--logout',
|
||||
label: 'Sign Out',
|
||||
};
|
||||
|
||||
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
|
||||
id: 'arduino-cloud-sketchbook--open-profile-menu',
|
||||
label: 'Contextual menu',
|
||||
};
|
||||
},
|
||||
'arduino/cloud/signOut'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,101 +1,190 @@
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||
import { InstallManually } from '../../common/nls';
|
||||
import { Installable, ResponseServiceClient } from '../../common/protocol';
|
||||
import {
|
||||
BoardsService,
|
||||
BoardIdentifier,
|
||||
BoardsPackage,
|
||||
Board,
|
||||
BoardsService,
|
||||
createPlatformIdentifier,
|
||||
isBoardIdentifierChangeEvent,
|
||||
PlatformIdentifier,
|
||||
platformIdentifierEquals,
|
||||
serializePlatformIdentifier,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { Installable } from '../../common/protocol';
|
||||
import { ResponseServiceImpl } from '../response-service-impl';
|
||||
|
||||
/**
|
||||
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
|
||||
* Listens on `BoardsConfigChangeEvent`s, if a board is selected which does not
|
||||
* have the corresponding core installed, it proposes the user to install the core.
|
||||
*/
|
||||
@injectable()
|
||||
export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
private readonly messageService: MessageService;
|
||||
@inject(NotificationManager)
|
||||
private readonly notificationManager: NotificationManager;
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
private readonly boardsService: BoardsService;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(ResponseServiceImpl)
|
||||
protected readonly responseService: ResponseServiceImpl;
|
||||
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
@inject(BoardsListWidgetFrontendContribution)
|
||||
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
|
||||
private readonly boardsManagerWidgetContribution: BoardsListWidgetFrontendContribution;
|
||||
|
||||
// Workaround for https://github.com/eclipse-theia/theia/issues/9349
|
||||
protected notifications: Board[] = [];
|
||||
private readonly installNotificationInfos: Readonly<{
|
||||
boardName: string;
|
||||
platformId: string;
|
||||
notificationId: string;
|
||||
}>[] = [];
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
|
||||
onStart(): void {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(
|
||||
this.ensureCoreExists.bind(this)
|
||||
);
|
||||
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
|
||||
this.toDispose.pushAll([
|
||||
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
|
||||
if (isBoardIdentifierChangeEvent(event)) {
|
||||
this.ensureCoreExists(event.selectedBoard);
|
||||
}
|
||||
}),
|
||||
this.notificationCenter.onPlatformDidInstall((event) =>
|
||||
this.clearAllNotificationForPlatform(event.item.id)
|
||||
),
|
||||
]);
|
||||
this.boardsServiceProvider.ready.then(() => {
|
||||
const { selectedBoard } = this.boardsServiceProvider.boardsConfig;
|
||||
this.ensureCoreExists(selectedBoard);
|
||||
});
|
||||
}
|
||||
|
||||
protected ensureCoreExists(config: BoardsConfig.Config): void {
|
||||
const { selectedBoard } = config;
|
||||
if (
|
||||
selectedBoard &&
|
||||
!this.notifications.find((board) => Board.sameAs(board, selectedBoard))
|
||||
) {
|
||||
this.notifications.push(selectedBoard);
|
||||
this.boardsService.search({}).then((packages) => {
|
||||
// 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)
|
||||
);
|
||||
private async findPlatformToInstall(
|
||||
selectedBoard: BoardIdentifier
|
||||
): Promise<BoardsPackage | undefined> {
|
||||
const platformId = await this.findPlatformIdToInstall(selectedBoard);
|
||||
if (!platformId) {
|
||||
return undefined;
|
||||
}
|
||||
const id = serializePlatformIdentifier(platformId);
|
||||
const platform = await this.boardsService.getBoardPackage({ id });
|
||||
if (!platform) {
|
||||
console.warn(`Could not resolve platform for ID: ${id}`);
|
||||
return undefined;
|
||||
}
|
||||
if (platform.installedVersion) {
|
||||
return undefined;
|
||||
}
|
||||
return platform;
|
||||
}
|
||||
|
||||
// check if one of the packages for the board is already installed. if so, no hint
|
||||
private async findPlatformIdToInstall(
|
||||
selectedBoard: BoardIdentifier
|
||||
): Promise<PlatformIdentifier | undefined> {
|
||||
const selectedBoardPlatformId = createPlatformIdentifier(selectedBoard);
|
||||
// The board is installed or the FQBN is available from the `board list watch` for Arduino boards. The latter might change!
|
||||
if (selectedBoardPlatformId) {
|
||||
const installedPlatforms =
|
||||
await this.boardsService.getInstalledPlatforms();
|
||||
const installedPlatformIds = installedPlatforms
|
||||
.map((platform) => createPlatformIdentifier(platform.id))
|
||||
.filter(notEmpty);
|
||||
if (
|
||||
packagesForBoard.some(({ installedVersion }) => !!installedVersion)
|
||||
installedPlatformIds.every(
|
||||
(installedPlatformId) =>
|
||||
!platformIdentifierEquals(
|
||||
installedPlatformId,
|
||||
selectedBoardPlatformId
|
||||
)
|
||||
)
|
||||
) {
|
||||
return selectedBoardPlatformId;
|
||||
}
|
||||
} else {
|
||||
// IDE2 knows that selected board is not installed. Look for board `name` match in not yet installed platforms.
|
||||
// The order should be correct when there is a board name collision (e.g. Arduino Nano RP2040 from Arduino Mbed OS Nano Boards, [DEPRECATED] Arduino Mbed OS Nano Boards). The CLI boosts the platforms, so picking the first name match should be fine.
|
||||
const platforms = await this.boardsService.search({});
|
||||
for (const platform of platforms) {
|
||||
// Ignore installed platforms
|
||||
if (platform.installedVersion) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
platform.boards.some((board) => board.name === selectedBoard.name)
|
||||
) {
|
||||
const platformId = createPlatformIdentifier(platform.id);
|
||||
if (platformId) {
|
||||
return platformId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async ensureCoreExists(
|
||||
selectedBoard: BoardIdentifier | undefined
|
||||
): Promise<void> {
|
||||
if (!selectedBoard) {
|
||||
return;
|
||||
}
|
||||
const candidate = await this.findPlatformToInstall(selectedBoard);
|
||||
if (!candidate) {
|
||||
return;
|
||||
}
|
||||
const platformIdToInstall = candidate.id;
|
||||
const selectedBoardName = selectedBoard.name;
|
||||
if (
|
||||
this.installNotificationInfos.some(
|
||||
({ boardName, platformId }) =>
|
||||
platformIdToInstall === platformId && selectedBoardName === boardName
|
||||
)
|
||||
) {
|
||||
// Already has a notification for the board with the same platform. Nothing to do.
|
||||
return;
|
||||
}
|
||||
this.clearAllNotificationForPlatform(platformIdToInstall);
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
const candidate = candidates[0];
|
||||
if (candidate) {
|
||||
const version = candidate.availableVersions[0]
|
||||
? `[v ${candidate.availableVersions[0]}]`
|
||||
: '';
|
||||
// tslint:disable-next-line:max-line-length
|
||||
this.messageService
|
||||
.info(
|
||||
`The \`"${candidate.name} ${version}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`,
|
||||
'Install Manually',
|
||||
'Yes'
|
||||
)
|
||||
.then(async (answer) => {
|
||||
const index = this.notifications.findIndex((board) =>
|
||||
Board.sameAs(board, selectedBoard)
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
const message = 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?',
|
||||
candidate.name,
|
||||
version,
|
||||
selectedBoard.name
|
||||
);
|
||||
const notificationId = this.notificationId(message, InstallManually, yes);
|
||||
this.installNotificationInfos.push({
|
||||
boardName: selectedBoardName,
|
||||
platformId: platformIdToInstall,
|
||||
notificationId,
|
||||
});
|
||||
const answer = await this.messageService.info(
|
||||
message,
|
||||
InstallManually,
|
||||
yes
|
||||
);
|
||||
if (answer) {
|
||||
const index = this.installNotificationInfos.findIndex(
|
||||
({ boardName, platformId }) =>
|
||||
platformIdToInstall === platformId && selectedBoardName === boardName
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.notifications.splice(index, 1);
|
||||
this.installNotificationInfos.splice(index, 1);
|
||||
}
|
||||
if (answer === 'Yes') {
|
||||
if (answer === yes) {
|
||||
await Installable.installWithProgress({
|
||||
installable: this.boardsService,
|
||||
item: candidate,
|
||||
@@ -105,16 +194,36 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (answer) {
|
||||
this.boardsManagerFrontendContribution
|
||||
if (answer === InstallManually) {
|
||||
this.boardsManagerWidgetContribution
|
||||
.openView({ reveal: true })
|
||||
.then((widget) =>
|
||||
widget.refresh(candidate.name.toLocaleLowerCase())
|
||||
widget.refresh({
|
||||
query: candidate.name.toLocaleLowerCase(),
|
||||
type: 'All',
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private clearAllNotificationForPlatform(predicatePlatformId: string): void {
|
||||
// Discard all install notifications for the same platform.
|
||||
const notificationsLength = this.installNotificationInfos.length;
|
||||
for (let i = notificationsLength - 1; i >= 0; i--) {
|
||||
const { notificationId, platformId } = this.installNotificationInfos[i];
|
||||
if (platformId === predicatePlatformId) {
|
||||
this.installNotificationInfos.splice(i, 1);
|
||||
this.notificationManager.clear(notificationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private notificationId(message: string, ...actions: string[]): string {
|
||||
return this.notificationManager['getMessageId']({
|
||||
text: message,
|
||||
actions,
|
||||
type: MessageType.Info,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Event } from '@theia/core/lib/common/event';
|
||||
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import React from '@theia/core/shared/react';
|
||||
import { EditBoardsConfigActionParams } from '../../common/protocol/board-list';
|
||||
import {
|
||||
Board,
|
||||
BoardIdentifier,
|
||||
BoardWithPackage,
|
||||
DetectedPort,
|
||||
findMatchingPortIndex,
|
||||
Port,
|
||||
PortIdentifier,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import type { Defined } from '../../common/types';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { BoardsConfigDialogState } from './boards-config-dialog';
|
||||
|
||||
namespace BoardsConfigComponent {
|
||||
export interface Props {
|
||||
/**
|
||||
* This is not the real config, it's only living in the dialog. Users can change it without update and can cancel any modifications.
|
||||
*/
|
||||
readonly boardsConfig: BoardsConfigDialogState;
|
||||
readonly searchSet: BoardIdentifier[] | undefined;
|
||||
readonly notificationCenter: NotificationCenter;
|
||||
readonly appState: FrontendApplicationState;
|
||||
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
|
||||
readonly onFilteredTextDidChangeEvent: Event<
|
||||
Defined<EditBoardsConfigActionParams['query']>
|
||||
>;
|
||||
readonly onAppStateDidChange: Event<FrontendApplicationState>;
|
||||
readonly onBoardSelected: (board: BoardIdentifier) => void;
|
||||
readonly onPortSelected: (port: PortIdentifier) => void;
|
||||
readonly searchBoards: (query?: {
|
||||
query?: string;
|
||||
}) => Promise<BoardWithPackage[]>;
|
||||
readonly ports: (
|
||||
predicate?: (port: DetectedPort) => boolean
|
||||
) => readonly DetectedPort[];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
searchResults: Array<BoardWithPackage>;
|
||||
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;
|
||||
}> {
|
||||
override 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>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly onClick = () => {
|
||||
this.props.onClick(this.props.item);
|
||||
};
|
||||
}
|
||||
|
||||
export class BoardsConfigComponent extends React.Component<
|
||||
BoardsConfigComponent.Props,
|
||||
BoardsConfigComponent.State
|
||||
> {
|
||||
private readonly toDispose: DisposableCollection;
|
||||
|
||||
constructor(props: BoardsConfigComponent.Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchResults: [],
|
||||
showAllPorts: false,
|
||||
query: '',
|
||||
};
|
||||
this.toDispose = new DisposableCollection();
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.props.onAppStateDidChange(async (state) => {
|
||||
if (state === 'ready') {
|
||||
const searchResults = await this.queryBoards({});
|
||||
this.setState({ searchResults });
|
||||
}
|
||||
}),
|
||||
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) => {
|
||||
if (typeof query === 'string') {
|
||||
this.setState({ query }, () => this.updateBoards(this.state.query));
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private readonly 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 })
|
||||
);
|
||||
};
|
||||
|
||||
private readonly queryBoards = async (
|
||||
options: { query?: string } = {}
|
||||
): Promise<Array<BoardWithPackage>> => {
|
||||
const result = await this.props.searchBoards(options);
|
||||
const { searchSet } = this.props;
|
||||
if (searchSet) {
|
||||
return result.filter((board) =>
|
||||
searchSet.some(
|
||||
(restriction) =>
|
||||
restriction.fqbn === board.fqbn || restriction.name === board.fqbn
|
||||
)
|
||||
);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly toggleFilterPorts = () => {
|
||||
this.setState({ showAllPorts: !this.state.showAllPorts });
|
||||
};
|
||||
|
||||
private readonly selectPort = (selectedPort: PortIdentifier) => {
|
||||
this.props.onPortSelected(selectedPort);
|
||||
};
|
||||
|
||||
private readonly selectBoard = (selectedBoard: BoardWithPackage) => {
|
||||
this.props.onBoardSelected(selectedBoard);
|
||||
};
|
||||
|
||||
private readonly 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)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private 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>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBoards(): React.ReactNode {
|
||||
const { boardsConfig } = this.props;
|
||||
const { 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(
|
||||
boardsConfig.selectedBoard,
|
||||
searchResults
|
||||
)) {
|
||||
const key = toKey(board);
|
||||
if (!distinctBoards.has(key)) {
|
||||
distinctBoards.set(key, board);
|
||||
}
|
||||
}
|
||||
|
||||
const boardsList = Array.from(distinctBoards.values()).map((board) => (
|
||||
<Item<BoardWithPackage>
|
||||
key={toKey(board)}
|
||||
item={board}
|
||||
label={board.name}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={board.missing}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="search">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPorts(): React.ReactNode {
|
||||
const predicate = this.state.showAllPorts ? undefined : Port.isVisiblePort;
|
||||
const detectedPorts = this.props.ports(predicate);
|
||||
const matchingIndex = findMatchingPortIndex(
|
||||
this.props.boardsConfig.selectedPort,
|
||||
detectedPorts
|
||||
);
|
||||
return !detectedPorts.length ? (
|
||||
<div className="no-result">
|
||||
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="ports list">
|
||||
{detectedPorts.map((detectedPort, index) => (
|
||||
<Item<Port>
|
||||
key={`${Port.keyOf(detectedPort.port)}`}
|
||||
item={detectedPort.port}
|
||||
label={Port.toString(detectedPort.port)}
|
||||
selected={index === matchingIndex}
|
||||
onClick={this.selectPort}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ReactWidget, Message } from '@theia/core/lib/browser';
|
||||
import { BoardsService } from '../../common/protocol/boards-service';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialogWidget extends ReactWidget {
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@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 focusNode: HTMLElement | undefined;
|
||||
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
if (this.focusNode instanceof HTMLInputElement) {
|
||||
this.focusNode.select();
|
||||
}
|
||||
(this.focusNode || this.node).focus();
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { injectable, inject, postConstruct } from 'inversify';
|
||||
import { Message } from '@phosphor/messaging';
|
||||
import {
|
||||
AbstractDialog,
|
||||
DialogProps,
|
||||
Widget,
|
||||
DialogError,
|
||||
} from '@theia/core/lib/browser';
|
||||
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';
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialogProps extends DialogProps {}
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
@inject(BoardsConfigDialogWidget)
|
||||
protected readonly widget: BoardsConfigDialogWidget;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
protected config: BoardsConfig.Config = {};
|
||||
|
||||
constructor(
|
||||
@inject(BoardsConfigDialogProps)
|
||||
protected readonly props: BoardsConfigDialogProps
|
||||
) {
|
||||
super(props);
|
||||
|
||||
this.contentNode.classList.add('select-board-dialog');
|
||||
this.contentNode.appendChild(this.createDescription());
|
||||
|
||||
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('div');
|
||||
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);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected onUpdateRequest(msg: Message) {
|
||||
super.onUpdateRequest(msg);
|
||||
this.widget.update();
|
||||
}
|
||||
|
||||
protected onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.widget.activate();
|
||||
}
|
||||
|
||||
protected 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { DialogError, DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import type { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import React from '@theia/core/shared/react';
|
||||
import type { ReactNode } from '@theia/core/shared/react/index';
|
||||
import { EditBoardsConfigActionParams } from '../../common/protocol/board-list';
|
||||
import {
|
||||
BoardIdentifier,
|
||||
BoardsConfig,
|
||||
BoardWithPackage,
|
||||
DetectedPort,
|
||||
emptyBoardsConfig,
|
||||
PortIdentifier,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import type { Defined } from '../../common/types';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { ReactDialog } from '../theia/dialogs/dialogs';
|
||||
import { BoardsConfigComponent } from './boards-config-component';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialogProps extends DialogProps {}
|
||||
|
||||
export type BoardsConfigDialogState = Omit<BoardsConfig, 'selectedBoard'> & {
|
||||
selectedBoard: BoardsConfig['selectedBoard'] | BoardWithPackage;
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class BoardsConfigDialog extends ReactDialog<BoardsConfigDialogState> {
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
private readonly onFilterTextDidChangeEmitter: Emitter<
|
||||
Defined<EditBoardsConfigActionParams['query']>
|
||||
>;
|
||||
private readonly onBoardSelected = (board: BoardWithPackage): void => {
|
||||
this._boardsConfig.selectedBoard = board;
|
||||
this.update();
|
||||
};
|
||||
private readonly onPortSelected = (port: PortIdentifier): void => {
|
||||
this._boardsConfig.selectedPort = port;
|
||||
this.update();
|
||||
};
|
||||
private readonly setFocusNode = (element: HTMLElement | undefined): void => {
|
||||
this.focusNode = element;
|
||||
};
|
||||
private readonly searchBoards = (options: {
|
||||
query?: string;
|
||||
}): Promise<BoardWithPackage[]> => {
|
||||
return this.boardsServiceProvider.searchBoards(options);
|
||||
};
|
||||
private readonly ports = (
|
||||
predicate?: (port: DetectedPort) => boolean
|
||||
): readonly DetectedPort[] => {
|
||||
return this.boardsServiceProvider.boardList.ports(predicate);
|
||||
};
|
||||
private _boardsConfig: BoardsConfigDialogState;
|
||||
/**
|
||||
* When the dialog's boards result set is limited to a subset of boards when searching, this field is set.
|
||||
*/
|
||||
private _searchSet: BoardIdentifier[] | undefined;
|
||||
private focusNode: HTMLElement | undefined;
|
||||
|
||||
constructor(
|
||||
@inject(BoardsConfigDialogProps)
|
||||
protected override readonly props: BoardsConfigDialogProps
|
||||
) {
|
||||
super({ ...props, maxWidth: 500 });
|
||||
this.node.id = 'select-board-dialog-container';
|
||||
this.contentNode.classList.add('select-board-dialog');
|
||||
this.appendCloseButton(
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel')
|
||||
);
|
||||
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
|
||||
this._boardsConfig = emptyBoardsConfig();
|
||||
this.onFilterTextDidChangeEmitter = new Emitter();
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.boardsServiceProvider.onBoardListDidChange(() => {
|
||||
this._boardsConfig = deepClone(this.boardsServiceProvider.boardsConfig);
|
||||
this.update();
|
||||
});
|
||||
this._boardsConfig = deepClone(this.boardsServiceProvider.boardsConfig);
|
||||
}
|
||||
|
||||
override async open(
|
||||
params?: EditBoardsConfigActionParams
|
||||
): Promise<BoardsConfig | undefined> {
|
||||
this._searchSet = undefined;
|
||||
this._boardsConfig.selectedBoard =
|
||||
this.boardsServiceProvider.boardsConfig.selectedBoard;
|
||||
this._boardsConfig.selectedPort =
|
||||
this.boardsServiceProvider.boardsConfig.selectedPort;
|
||||
if (params) {
|
||||
if (typeof params.query === 'string') {
|
||||
this.onFilterTextDidChangeEmitter.fire(params.query);
|
||||
}
|
||||
if (params.portToSelect) {
|
||||
this._boardsConfig.selectedPort = params.portToSelect;
|
||||
}
|
||||
if (params.boardToSelect) {
|
||||
this._boardsConfig.selectedBoard = params.boardToSelect;
|
||||
}
|
||||
if (params.searchSet) {
|
||||
this._searchSet = params.searchSet.slice();
|
||||
}
|
||||
}
|
||||
return super.open();
|
||||
}
|
||||
|
||||
protected override onAfterAttach(msg: Message): void {
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected override render(): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<div className="head">
|
||||
<div className="text">
|
||||
<div>
|
||||
{nls.localize(
|
||||
'arduino/board/configDialog1',
|
||||
'Select both a Board and a Port if you want to upload a sketch.'
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{nls.localize(
|
||||
'arduino/board/configDialog2',
|
||||
'If you only select a Board you will be able to compile, but not to upload your sketch.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="select-board-dialog" className="p-Widget ps">
|
||||
<div className="selectBoardContainer">
|
||||
<BoardsConfigComponent
|
||||
boardsConfig={this._boardsConfig}
|
||||
searchSet={this._searchSet}
|
||||
onBoardSelected={this.onBoardSelected}
|
||||
onPortSelected={this.onPortSelected}
|
||||
notificationCenter={this.notificationCenter}
|
||||
onFocusNodeSet={this.setFocusNode}
|
||||
onFilteredTextDidChangeEvent={
|
||||
this.onFilterTextDidChangeEmitter.event
|
||||
}
|
||||
appState={this.appStateService.state}
|
||||
onAppStateDidChange={this.notificationCenter.onAppStateDidChange}
|
||||
searchBoards={this.searchBoards}
|
||||
ports={this.ports}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
if (this.focusNode instanceof HTMLInputElement) {
|
||||
this.focusNode.select();
|
||||
}
|
||||
(this.focusNode || this.node).focus();
|
||||
}
|
||||
|
||||
protected override handleEnter(event: KeyboardEvent): boolean | void {
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override isValid(value: BoardsConfig): 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(): BoardsConfigDialogState {
|
||||
return this._boardsConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
import * as React from '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 { NotificationCenter } from '../notification-center';
|
||||
import { BoardsServiceProvider } from './boards-service-provider';
|
||||
|
||||
export namespace BoardsConfig {
|
||||
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 abstract class Item<T> extends React.Component<{
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
protected onClick = () => {
|
||||
this.props.onClick(this.props.item);
|
||||
};
|
||||
}
|
||||
|
||||
export class BoardsConfig extends React.Component<
|
||||
BoardsConfig.Props,
|
||||
BoardsConfig.State
|
||||
> {
|
||||
protected toDispose = new DisposableCollection();
|
||||
|
||||
constructor(props: BoardsConfig.Props) {
|
||||
super(props);
|
||||
|
||||
const { boardsConfig } = props.boardsServiceProvider;
|
||||
this.state = {
|
||||
searchResults: [],
|
||||
knownPorts: [],
|
||||
showAllPorts: false,
|
||||
query: '',
|
||||
...boardsConfig,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 function setConfig(
|
||||
config: Config | undefined,
|
||||
urlToAttachTo: URL
|
||||
): URL {
|
||||
const copy = new URL(urlToAttachTo.toString());
|
||||
if (!config) {
|
||||
copy.searchParams.delete('boards-config');
|
||||
return copy;
|
||||
}
|
||||
|
||||
const selectedBoard = config.selectedBoard
|
||||
? {
|
||||
name: config.selectedBoard.name,
|
||||
fqbn: config.selectedBoard.fqbn,
|
||||
}
|
||||
: undefined;
|
||||
const selectedPort = config.selectedPort
|
||||
? {
|
||||
protocol: config.selectedPort.protocol,
|
||||
address: config.selectedPort.address,
|
||||
}
|
||||
: undefined;
|
||||
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
|
||||
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function getConfig(url: URL): Config | undefined {
|
||||
const encoded = url.searchParams.get('boards-config');
|
||||
if (!encoded) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const raw = decodeURIComponent(encoded);
|
||||
const candidate = JSON.parse(raw);
|
||||
if (typeof candidate === 'object') {
|
||||
return candidate;
|
||||
}
|
||||
console.warn(
|
||||
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
|
||||
);
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
console.log(`Could not get board config from URL: ${url}.`, e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,86 @@
|
||||
import { injectable, inject, named } from 'inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
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 { notEmpty } from '../../common/utils';
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
BoardDetails,
|
||||
BoardsService,
|
||||
ConfigOption,
|
||||
Installable,
|
||||
BoardDetails,
|
||||
Programmer,
|
||||
} from '../../common/protocol';
|
||||
import { notEmpty } from '../../common/utils';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
@inject(ILogger)
|
||||
@named('store')
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
private readonly logger: ILogger;
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
private readonly boardsService: BoardsService;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
@inject(LocalStorageService)
|
||||
protected readonly storageService: LocalStorageService;
|
||||
private readonly storageService: LocalStorageService;
|
||||
|
||||
protected readonly onChangedEmitter = new Emitter<void>();
|
||||
private readonly onChangedEmitter = new Emitter<string[]>();
|
||||
private readonly toDispose = new DisposableCollection(this.onChangedEmitter);
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onPlatformInstalled(async ({ item }) => {
|
||||
const { installedVersion: version } = item;
|
||||
if (!version) {
|
||||
return;
|
||||
}
|
||||
let shouldFireChanged = false;
|
||||
this.toDispose.push(
|
||||
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, version);
|
||||
let data = await this.storageService.getData<
|
||||
ConfigOption[] | undefined
|
||||
>(key);
|
||||
const key = this.getStorageKey(fqbn);
|
||||
let data = await this.storageService.getData<ConfigOption[]>(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;
|
||||
dataDidChangePerFqbn.push(fqbn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldFireChanged) {
|
||||
this.fireChanged();
|
||||
if (dataDidChangePerFqbn.length) {
|
||||
this.fireChanged(...dataDidChangePerFqbn);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get onChanged(): Event<void> {
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get onChanged(): Event<string[]> {
|
||||
return this.onChangedEmitter.event;
|
||||
}
|
||||
|
||||
async appendConfigToFqbn(
|
||||
fqbn: string | undefined,
|
||||
boardsPackageVersion: MaybePromise<
|
||||
Installable.Version | undefined
|
||||
> = this.getBoardsPackageVersion(fqbn)
|
||||
fqbn: string | undefined
|
||||
): Promise<string | undefined> {
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { configOptions } = await this.getData(fqbn, boardsPackageVersion);
|
||||
const { configOptions } = await this.getData(fqbn);
|
||||
return ConfigOption.decorate(fqbn, configOptions);
|
||||
}
|
||||
|
||||
async getData(
|
||||
fqbn: string | undefined,
|
||||
boardsPackageVersion: MaybePromise<
|
||||
Installable.Version | undefined
|
||||
> = this.getBoardsPackageVersion(fqbn)
|
||||
): Promise<BoardsDataStore.Data> {
|
||||
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
|
||||
if (!fqbn) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return BoardsDataStore.Data.EMPTY;
|
||||
}
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
const key = this.getStorageKey(fqbn);
|
||||
let data = await this.storageService.getData<
|
||||
BoardsDataStore.Data | undefined
|
||||
>(key, undefined);
|
||||
@@ -119,46 +101,37 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
return data;
|
||||
}
|
||||
|
||||
async selectProgrammer(
|
||||
{
|
||||
async selectProgrammer({
|
||||
fqbn,
|
||||
selectedProgrammer,
|
||||
}: { fqbn: string; selectedProgrammer: Programmer },
|
||||
boardsPackageVersion: MaybePromise<
|
||||
Installable.Version | undefined
|
||||
> = this.getBoardsPackageVersion(fqbn)
|
||||
): Promise<boolean> {
|
||||
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
|
||||
}: {
|
||||
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;
|
||||
}
|
||||
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.setData({
|
||||
fqbn,
|
||||
data: { ...data, selectedProgrammer },
|
||||
version,
|
||||
});
|
||||
this.fireChanged();
|
||||
this.fireChanged(fqbn);
|
||||
return true;
|
||||
}
|
||||
|
||||
async selectConfigOption(
|
||||
{
|
||||
async selectConfigOption({
|
||||
fqbn,
|
||||
option,
|
||||
selectedValue,
|
||||
}: { fqbn: string; option: string; selectedValue: string },
|
||||
boardsPackageVersion: MaybePromise<
|
||||
Installable.Version | undefined
|
||||
> = this.getBoardsPackageVersion(fqbn)
|
||||
): Promise<boolean> {
|
||||
const data = deepClone(await this.getData(fqbn, boardsPackageVersion));
|
||||
}: {
|
||||
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) {
|
||||
@@ -176,31 +149,24 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
const version = await boardsPackageVersion;
|
||||
if (!version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.setData({ fqbn, data, version });
|
||||
this.fireChanged();
|
||||
await this.setData({ fqbn, data });
|
||||
this.fireChanged(fqbn);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async setData({
|
||||
fqbn,
|
||||
data,
|
||||
version,
|
||||
}: {
|
||||
fqbn: string;
|
||||
data: BoardsDataStore.Data;
|
||||
version: Installable.Version;
|
||||
}): Promise<void> {
|
||||
const key = this.getStorageKey(fqbn, version);
|
||||
const key = this.getStorageKey(fqbn);
|
||||
return this.storageService.setData(key, data);
|
||||
}
|
||||
|
||||
protected getStorageKey(fqbn: string, version: Installable.Version): string {
|
||||
return `.arduinoIDE-configOptions-${version}-${fqbn}`;
|
||||
protected getStorageKey(fqbn: string): string {
|
||||
return `.arduinoIDE-configOptions-${fqbn}`;
|
||||
}
|
||||
|
||||
protected async getBoardDetailsSafe(
|
||||
@@ -228,23 +194,8 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
}
|
||||
}
|
||||
|
||||
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 fireChanged(...fqbn: string[]): void {
|
||||
this.onChangedEmitter.fire(fqbn);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +1,55 @@
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
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 = 'Boards Manager';
|
||||
static WIDGET_LABEL = nls.localize('arduino/boardsManager', 'Boards Manager');
|
||||
|
||||
constructor(
|
||||
@inject(BoardsService) protected service: BoardsService,
|
||||
@inject(ListItemRenderer)
|
||||
protected itemRenderer: ListItemRenderer<BoardsPackage>
|
||||
@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-microchip',
|
||||
iconClass: 'fa fa-arduino-boards',
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemDeprecated: (item: BoardsPackage) => item.deprecated,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All' },
|
||||
});
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onPlatformInstalled(() =>
|
||||
this.notificationCenter.onPlatformDidInstall(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
this.notificationCenter.onPlatformUninstalled(() =>
|
||||
this.notificationCenter.onPlatformDidUninstall(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
protected async install({
|
||||
protected override async install({
|
||||
item,
|
||||
progressId,
|
||||
version,
|
||||
@@ -52,12 +60,17 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
}): Promise<void> {
|
||||
await super.install({ item, progressId, version });
|
||||
this.messageService.info(
|
||||
`Successfully installed platform ${item.name}:${version}`,
|
||||
nls.localize(
|
||||
'arduino/board/succesfullyInstalledPlatform',
|
||||
'Successfully installed platform {0}:{1}',
|
||||
item.name,
|
||||
version
|
||||
),
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
}
|
||||
|
||||
protected async uninstall({
|
||||
protected override async uninstall({
|
||||
item,
|
||||
progressId,
|
||||
}: {
|
||||
@@ -66,7 +79,12 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
}): Promise<void> {
|
||||
await super.uninstall({ item, progressId });
|
||||
this.messageService.info(
|
||||
`Successfully uninstalled platform ${item.name}:${item.installedVersion}`,
|
||||
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,13 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar';
|
||||
import { codicon } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Port } from '../../common/protocol';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { ArduinoCommands } from '../arduino-commands';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import React from '@theia/core/shared/react';
|
||||
import ReactDOM from '@theia/core/shared/react-dom';
|
||||
import classNames from 'classnames';
|
||||
import { boardIdentifierLabel, Port } from '../../common/protocol';
|
||||
import { BoardListItemUI } from '../../common/protocol/board-list';
|
||||
import { assertUnreachable } from '../../common/utils';
|
||||
import type {
|
||||
BoardListUI,
|
||||
BoardsServiceProvider,
|
||||
AvailableBoard,
|
||||
} from './boards-service-provider';
|
||||
|
||||
export interface BoardsDropDownListCoords {
|
||||
@@ -20,17 +27,19 @@ export interface BoardsDropDownListCoords {
|
||||
export namespace BoardsDropDown {
|
||||
export interface Props {
|
||||
readonly coords: BoardsDropDownListCoords | 'hidden';
|
||||
readonly items: Array<AvailableBoard & { onClick: () => void; port: Port }>;
|
||||
readonly boardList: BoardListUI;
|
||||
readonly openBoardsConfig: () => void;
|
||||
readonly hide: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
protected dropdownElement: HTMLElement;
|
||||
export class BoardListDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
private dropdownElement: HTMLElement;
|
||||
private listRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: BoardsDropDown.Props) {
|
||||
super(props);
|
||||
|
||||
this.listRef = React.createRef();
|
||||
let list = document.getElementById('boards-dropdown-container');
|
||||
if (!list) {
|
||||
list = document.createElement('div');
|
||||
@@ -40,15 +49,28 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
override render(): React.ReactNode {
|
||||
return ReactDOM.createPortal(
|
||||
this.renderBoardListItems(),
|
||||
this.dropdownElement
|
||||
);
|
||||
}
|
||||
|
||||
private renderBoardListItems(): React.ReactNode {
|
||||
const { coords, boardList } = 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"
|
||||
@@ -56,39 +78,148 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
position: 'absolute',
|
||||
...coords,
|
||||
}}
|
||||
ref={this.listRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
{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)}
|
||||
<div className="arduino-boards-dropdown-list--items-container">
|
||||
{boardList.items.map((item, index) =>
|
||||
this.renderBoardListItem({
|
||||
item,
|
||||
selected: index === boardList.selectedIndex,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
key={footerLabel}
|
||||
tabIndex={0}
|
||||
className="arduino-boards-dropdown-item arduino-board-dropdown-footer"
|
||||
onClick={() => this.props.openBoardsConfig()}
|
||||
>
|
||||
<div>{footerLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected renderItem({
|
||||
label,
|
||||
private readonly onDefaultAction = (item: BoardListItemUI): unknown => {
|
||||
const { boardList, hide } = this.props;
|
||||
const { type, params } = item.defaultAction;
|
||||
hide();
|
||||
switch (type) {
|
||||
case 'select-boards-config': {
|
||||
return boardList.select(params);
|
||||
}
|
||||
case 'edit-boards-config': {
|
||||
return boardList.edit(params);
|
||||
}
|
||||
default:
|
||||
return assertUnreachable(type);
|
||||
}
|
||||
};
|
||||
|
||||
private renderBoardListItem({
|
||||
item,
|
||||
selected,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
onClick: () => void;
|
||||
item: BoardListItemUI;
|
||||
selected: boolean;
|
||||
}): React.ReactNode {
|
||||
const { boardLabel, portLabel, portProtocol, tooltip } = item.labels;
|
||||
const port = item.port;
|
||||
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.onDefaultAction(item);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={label}
|
||||
className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`}
|
||||
onClick={onClick}
|
||||
key={`board-item--${Port.keyOf(port)}`}
|
||||
className={classNames('arduino-boards-dropdown-item', {
|
||||
'arduino-boards-dropdown-item--selected': selected,
|
||||
})}
|
||||
onClick={() => this.onDefaultAction(item)}
|
||||
onKeyUp={onKeyUp}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div>{label}</div>
|
||||
{selected ? <span className="fa fa-check" /> : ''}
|
||||
<div
|
||||
className={classNames(
|
||||
'arduino-boards-dropdown-item--protocol',
|
||||
'fa',
|
||||
iconNameFromProtocol(portProtocol)
|
||||
)}
|
||||
/>
|
||||
<div className="arduino-boards-dropdown-item--label" title={tooltip}>
|
||||
<div className="arduino-boards-dropdown-item--board-header">
|
||||
<div className="arduino-boards-dropdown-item--board-label noWrapInfo noselect">
|
||||
{boardLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="arduino-boards-dropdown-item--port-label noWrapInfo noselect">
|
||||
{portLabel}
|
||||
</div>
|
||||
</div>
|
||||
{this.renderActions(item)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderActions(item: BoardListItemUI): React.ReactNode {
|
||||
const { boardList, hide } = this.props;
|
||||
const { revert, edit } = item.otherActions;
|
||||
if (!edit && !revert) {
|
||||
return undefined;
|
||||
}
|
||||
const handleOnClick = (
|
||||
event: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||
callback: () => void
|
||||
) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
hide();
|
||||
callback();
|
||||
};
|
||||
return (
|
||||
<div className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR}>
|
||||
{edit && (
|
||||
<div
|
||||
className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} enabled`}
|
||||
>
|
||||
{
|
||||
<div
|
||||
id="edit"
|
||||
className={codicon('pencil', true)}
|
||||
title={nls.localize(
|
||||
'arduino/board/editBoardsConfig',
|
||||
'Edit Board and Port...'
|
||||
)}
|
||||
onClick={(event) =>
|
||||
handleOnClick(event, () => boardList.edit(edit.params))
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{revert && (
|
||||
<div
|
||||
className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} enabled`}
|
||||
>
|
||||
{
|
||||
<div
|
||||
id="revert"
|
||||
className={codicon('discard', true)}
|
||||
title={nls.localize(
|
||||
'arduino/board/revertBoardsConfig',
|
||||
"Use '{0}' discovered on '{1}'",
|
||||
boardIdentifierLabel(revert.params.selectedBoard),
|
||||
item.labels.portLabel
|
||||
)}
|
||||
onClick={(event) =>
|
||||
handleOnClick(event, () => boardList.select(revert.params))
|
||||
}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -100,34 +231,35 @@ export class BoardsToolBarItem extends React.Component<
|
||||
> {
|
||||
static TOOLBAR_ID: 'boards-toolbar';
|
||||
|
||||
protected readonly toDispose: DisposableCollection =
|
||||
new DisposableCollection();
|
||||
private readonly toDispose: DisposableCollection;
|
||||
|
||||
constructor(props: BoardsToolBarItem.Props) {
|
||||
super(props);
|
||||
|
||||
const { availableBoards } = props.boardsServiceClient;
|
||||
const { boardList } = props.boardsServiceProvider;
|
||||
this.state = {
|
||||
availableBoards,
|
||||
boardList,
|
||||
coords: 'hidden',
|
||||
};
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
this.setState({ coords: 'hidden' });
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.boardsServiceClient.onAvailableBoardsChanged((availableBoards) =>
|
||||
this.setState({ availableBoards })
|
||||
const listener = () => this.setState({ coords: 'hidden' });
|
||||
document.addEventListener('click', listener);
|
||||
this.toDispose = new DisposableCollection(
|
||||
Disposable.create(() => document.removeEventListener('click', listener))
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
override componentDidMount(): void {
|
||||
this.toDispose.push(
|
||||
this.props.boardsServiceProvider.onBoardListDidChange((boardList) =>
|
||||
this.setState({ boardList })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
|
||||
private readonly show = (event: React.MouseEvent<HTMLElement>): void => {
|
||||
const { currentTarget: element } = event;
|
||||
if (element instanceof HTMLElement) {
|
||||
if (this.state.coords === 'hidden') {
|
||||
@@ -148,75 +280,73 @@ export class BoardsToolBarItem extends React.Component<
|
||||
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 '';
|
||||
})();
|
||||
private readonly hide = () => {
|
||||
this.setState({ coords: 'hidden' });
|
||||
};
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { coords, boardList } = this.state;
|
||||
const { boardLabel, selected, portProtocol, tooltip } = boardList.labels;
|
||||
const protocolIcon = portProtocol
|
||||
? iconNameFromProtocol(portProtocol)
|
||||
: null;
|
||||
const protocolIconClassNames = classNames(
|
||||
'arduino-boards-toolbar-item--protocol',
|
||||
'fa',
|
||||
protocolIcon
|
||||
);
|
||||
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
|
||||
className="arduino-boards-toolbar-item-container"
|
||||
title={tooltip}
|
||||
onClick={this.show}
|
||||
>
|
||||
{protocolIcon && <div className={protocolIconClassNames} />}
|
||||
<div
|
||||
className={classNames(
|
||||
'arduino-boards-toolbar-item--label',
|
||||
'noWrapInfo',
|
||||
'noselect',
|
||||
{ 'arduino-boards-toolbar-item--label-connected': selected }
|
||||
)}
|
||||
>
|
||||
{boardLabel}
|
||||
</div>
|
||||
<span className="fa fa-caret-down caret" />
|
||||
<div className="fa fa-caret-down caret" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BoardsDropDown
|
||||
<BoardListDropDown
|
||||
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>
|
||||
boardList={boardList}
|
||||
openBoardsConfig={() => boardList.edit({ query: '' })}
|
||||
hide={this.hide}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
protected openDialog = () => {
|
||||
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
|
||||
this.setState({ coords: 'hidden' });
|
||||
};
|
||||
}
|
||||
export namespace BoardsToolBarItem {
|
||||
export interface Props {
|
||||
readonly boardsServiceClient: BoardsServiceProvider;
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
readonly commands: CommandRegistry;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
availableBoards: AvailableBoard[];
|
||||
boardList: BoardListUI;
|
||||
coords: BoardsDropDownListCoords | 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
function iconNameFromProtocol(protocol: string): string {
|
||||
switch (protocol) {
|
||||
case 'serial':
|
||||
return 'fa-arduino-technology-usb';
|
||||
case 'network':
|
||||
return 'fa-arduino-technology-connection';
|
||||
// it is fine to assign dedicated icons to the protocols used by the official boards,
|
||||
// but other than that it is best to avoid implementing any special handling
|
||||
// for specific protocols in the IDE codebase.
|
||||
default:
|
||||
return 'fa-arduino-technology-3dimensionscube';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
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,
|
||||
@@ -18,7 +25,16 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 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,61 +1,65 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import * as moment from 'moment';
|
||||
import { remote } from 'electron';
|
||||
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 { nls } from '@theia/core/lib/common/nls';
|
||||
import { isOSX, isWindows } from '@theia/core/lib/common/os';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import moment from 'moment';
|
||||
import { AppService } from '../app-service';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ConfigService } from '../../common/protocol';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
Contribution,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class About extends Contribution {
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
private readonly clipboardService: ClipboardService;
|
||||
@inject(AppService)
|
||||
private readonly appService: AppService;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(About.Commands.ABOUT_APP, {
|
||||
execute: () => this.showAbout(),
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__ABOUT_GROUP, {
|
||||
commandId: About.Commands.ABOUT_APP.id,
|
||||
label: `About ${this.applicationName}`,
|
||||
label: nls.localize(
|
||||
'arduino/about/label',
|
||||
'About {0}',
|
||||
this.applicationName
|
||||
),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
|
||||
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}]
|
||||
private async showAbout(): Promise<void> {
|
||||
const appInfo = await this.appService.info();
|
||||
const { appVersion, cliVersion, buildDate } = appInfo;
|
||||
|
||||
${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
|
||||
`;
|
||||
const ok = 'OK';
|
||||
const copy = 'Copy';
|
||||
const detail = (showAll: boolean) =>
|
||||
nls.localize(
|
||||
'arduino/about/detail',
|
||||
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}',
|
||||
appVersion,
|
||||
buildDate ? buildDate : nls.localize('', 'dev build'),
|
||||
buildDate && showAll ? ` (${this.ago(buildDate)})` : '',
|
||||
cliVersion,
|
||||
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(),
|
||||
{
|
||||
const { response } = await this.dialogService.showMessageBox({
|
||||
message: `${this.applicationName}`,
|
||||
title: `${this.applicationName}`,
|
||||
type: 'info',
|
||||
@@ -64,47 +68,102 @@ ${showAll ? `Copyright © ${new Date().getFullYear()} Arduino SA` : ''}
|
||||
noLink: true,
|
||||
defaultId: buttons.indexOf(ok),
|
||||
cancelId: buttons.indexOf(ok),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (buttons[response] === copy) {
|
||||
await this.clipboardService.writeText(detail(false).trim());
|
||||
}
|
||||
}
|
||||
|
||||
protected get applicationName(): string {
|
||||
private get applicationName(): string {
|
||||
return FrontendApplicationConfigProvider.get().applicationName;
|
||||
}
|
||||
|
||||
protected get buildDate(): string | undefined {
|
||||
return FrontendApplicationConfigProvider.get().buildDate;
|
||||
}
|
||||
|
||||
protected ago(isoTime: string): string {
|
||||
private 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`;
|
||||
return result === 1
|
||||
? nls.localize(
|
||||
'vscode/date/date.fromNow.minutes.singular.ago',
|
||||
'{0} minute ago',
|
||||
result.toString()
|
||||
)
|
||||
: nls.localize(
|
||||
'vscode/date/date.fromNow.minutes.plural.ago',
|
||||
'{0} minutes ago',
|
||||
result.toString()
|
||||
);
|
||||
}
|
||||
result = now.diff(other, 'hour');
|
||||
if (result < 25) {
|
||||
return result === 1 ? `${result} hour ago` : `${result} hours ago`;
|
||||
return result === 1
|
||||
? nls.localize(
|
||||
'vscode/date/date.fromNow.hours.singular.ago',
|
||||
'{0} hour ago',
|
||||
result.toString()
|
||||
)
|
||||
: nls.localize(
|
||||
'vscode/date/date.fromNow.hours.plural.ago',
|
||||
'{0} hours ago',
|
||||
result.toString()
|
||||
);
|
||||
}
|
||||
result = now.diff(other, 'day');
|
||||
if (result < 8) {
|
||||
return result === 1 ? `${result} day ago` : `${result} days ago`;
|
||||
return result === 1
|
||||
? nls.localize(
|
||||
'vscode/date/date.fromNow.days.singular.ago',
|
||||
'{0} day ago',
|
||||
result.toString()
|
||||
)
|
||||
: nls.localize(
|
||||
'vscode/date/date.fromNow.days.plural.ago',
|
||||
'{0} days ago',
|
||||
result.toString()
|
||||
);
|
||||
}
|
||||
result = now.diff(other, 'week');
|
||||
if (result < 5) {
|
||||
return result === 1 ? `${result} week ago` : `${result} weeks ago`;
|
||||
return result === 1
|
||||
? nls.localize(
|
||||
'vscode/date/date.fromNow.weeks.singular.ago',
|
||||
'{0} week ago',
|
||||
result.toString()
|
||||
)
|
||||
: nls.localize(
|
||||
'vscode/date/date.fromNow.weeks.plural.ago',
|
||||
'{0} weeks ago',
|
||||
result.toString()
|
||||
);
|
||||
}
|
||||
result = now.diff(other, 'month');
|
||||
if (result < 13) {
|
||||
return result === 1 ? `${result} month ago` : `${result} months ago`;
|
||||
return result === 1
|
||||
? nls.localize(
|
||||
'vscode/date/date.fromNow.months.singular.ago',
|
||||
'{0} month ago',
|
||||
result.toString()
|
||||
)
|
||||
: nls.localize(
|
||||
'vscode/date/date.fromNow.months.plural.ago',
|
||||
'{0} months ago',
|
||||
result.toString()
|
||||
);
|
||||
}
|
||||
result = now.diff(other, 'year');
|
||||
return result === 1 ? `${result} year ago` : `${result} years ago`;
|
||||
return result === 1
|
||||
? nls.localize(
|
||||
'vscode/date/date.fromNow.years.singular.ago',
|
||||
'{0} year ago',
|
||||
result.toString()
|
||||
)
|
||||
: nls.localize(
|
||||
'vscode/date/date.fromNow.years.plural.ago',
|
||||
'{0} years ago',
|
||||
result.toString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
155
arduino-ide-extension/src/browser/contributions/account.ts
Normal file
155
arduino-ide-extension/src/browser/contributions/account.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
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 { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
|
||||
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;
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
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 });
|
||||
const loggedIn = () => Boolean(this.createFeatures.session);
|
||||
const loggedInWithInternetConnection = () =>
|
||||
loggedIn() && this.connectionStatus.offlineStatus !== 'internet';
|
||||
registry.registerCommand(Account.Commands.LEARN_MORE, {
|
||||
execute: () => openExternal(LEARN_MORE_URL),
|
||||
isEnabled: () => !loggedIn(),
|
||||
isVisible: () => !loggedIn(),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_PROFILE, {
|
||||
execute: () => openExternal('https://id.arduino.cc/'),
|
||||
isEnabled: () => loggedInWithInternetConnection(),
|
||||
isVisible: () => loggedIn(),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, {
|
||||
execute: () => openExternal('https://create.arduino.cc/editor'),
|
||||
isEnabled: () => loggedInWithInternetConnection(),
|
||||
isVisible: () => loggedIn(),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, {
|
||||
execute: () => openExternal('https://create.arduino.cc/iot/'),
|
||||
isEnabled: () => loggedInWithInternetConnection(),
|
||||
isVisible: () => loggedIn(),
|
||||
});
|
||||
}
|
||||
|
||||
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,58 +1,66 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { FileDialogService } from '@theia/filesystem/lib/browser';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import {
|
||||
SketchContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
SketchContribution,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { FileDialogService } from '@theia/filesystem/lib/browser';
|
||||
|
||||
@injectable()
|
||||
export class AddFile extends SketchContribution {
|
||||
@inject(FileDialogService)
|
||||
protected readonly fileDialogService: FileDialogService;
|
||||
private readonly fileDialogService: FileDialogService; // TODO: use dialogService
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddFile.Commands.ADD_FILE, {
|
||||
execute: () => this.addFile(),
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
||||
commandId: AddFile.Commands.ADD_FILE.id,
|
||||
label: 'Add File...',
|
||||
label: nls.localize('arduino/contributions/addFile', 'Add File') + '...',
|
||||
order: '2',
|
||||
});
|
||||
}
|
||||
|
||||
protected async addFile(): Promise<void> {
|
||||
private async addFile(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const toAddUri = await this.fileDialogService.showOpenDialog({
|
||||
title: 'Add File',
|
||||
title: nls.localize('arduino/contributions/addFile', 'Add File'),
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
modal: true,
|
||||
});
|
||||
if (!toAddUri) {
|
||||
return;
|
||||
}
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const filename = toAddUri.path.base;
|
||||
const targetUri = sketchUri.resolve('data').resolve(filename);
|
||||
const { uri: targetUri, filename } = this.resolveTarget(sketch, toAddUri);
|
||||
const exists = await this.fileService.exists(targetUri);
|
||||
if (exists) {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
const { response } = await this.dialogService.showMessageBox({
|
||||
type: 'question',
|
||||
title: 'Replace',
|
||||
buttons: ['Cancel', 'OK'],
|
||||
message: `Replace the existing version of ${filename}?`,
|
||||
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
|
||||
@@ -60,9 +68,31 @@ export class AddFile extends SketchContribution {
|
||||
}
|
||||
}
|
||||
await this.fileService.copy(toAddUri, targetUri, { overwrite: true });
|
||||
this.messageService.info('One file added to the sketch.', {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,61 +1,56 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ResponseServiceImpl } from '../response-service-impl';
|
||||
import { Installable, LibraryService } from '../../common/protocol';
|
||||
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(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(ResponseServiceImpl)
|
||||
protected readonly responseService: ResponseServiceImpl;
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
|
||||
execute: () => this.addZipLibrary(),
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override 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...',
|
||||
label: nls.localize('arduino/library/addZip', 'Add .ZIP Library...'),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
async addZipLibrary(): Promise<void> {
|
||||
private async addZipLibrary(): Promise<void> {
|
||||
const homeUri = await this.envVariableServer.getHomeDirUri();
|
||||
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
|
||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
|
||||
title: "Select a zip file containing the library you'd like to add",
|
||||
const { canceled, filePaths } = await this.dialogService.showOpenDialog({
|
||||
title: nls.localize(
|
||||
'arduino/selectZip',
|
||||
"Select a zip file containing the library you'd like to add"
|
||||
),
|
||||
defaultPath,
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: 'Library',
|
||||
name: nls.localize('arduino/library/zipLibrary', 'Library'),
|
||||
extensions: ['zip'],
|
||||
},
|
||||
],
|
||||
@@ -68,9 +63,12 @@ export class AddZipLibrary extends SketchContribution {
|
||||
if (error instanceof AlreadyInstalledError) {
|
||||
const result = await new ConfirmDialog({
|
||||
msg: error.message,
|
||||
title: 'Do you want to overwrite the existing library?',
|
||||
ok: 'Yes',
|
||||
cancel: 'No',
|
||||
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);
|
||||
@@ -82,16 +80,20 @@ export class AddZipLibrary extends SketchContribution {
|
||||
|
||||
private async doInstall(zipUri: string, overwrite?: boolean): Promise<void> {
|
||||
try {
|
||||
await Installable.doWithProgress({
|
||||
await ExecuteWithProgress.doWithProgress({
|
||||
messageService: this.messageService,
|
||||
progressText: `Processing ${new URI(zipUri).path.base}`,
|
||||
progressText:
|
||||
nls.localize('arduino/common/processing', 'Processing') +
|
||||
` ${new URI(zipUri).path.base}`,
|
||||
responseService: this.responseService,
|
||||
run: () => this.libraryService.installZip({ zipUri, overwrite }),
|
||||
});
|
||||
this.messageService.info(
|
||||
`Successfully installed library from ${
|
||||
nls.localize(
|
||||
'arduino/library/successfullyInstalledZipLibrary',
|
||||
'Successfully installed library from {0} archive',
|
||||
new URI(zipUri).path.base
|
||||
} archive`,
|
||||
),
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -101,12 +103,19 @@ export class AddZipLibrary extends SketchContribution {
|
||||
const name = match[1].trim();
|
||||
if (name) {
|
||||
throw new AlreadyInstalledError(
|
||||
`A library folder named ${name} already exists. Do you want to overwrite it?`,
|
||||
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?'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import dateFormat from 'dateformat';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
SketchContribution,
|
||||
@@ -9,40 +7,42 @@ import {
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class ArchiveSketch extends SketchContribution {
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(ArchiveSketch.Commands.ARCHIVE_SKETCH, {
|
||||
execute: () => this.archiveSketch(),
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
commandId: ArchiveSketch.Commands.ARCHIVE_SKETCH.id,
|
||||
label: 'Archive Sketch',
|
||||
label: nls.localize('arduino/sketch/archiveSketch', 'Archive Sketch'),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
protected async archiveSketch(): Promise<void> {
|
||||
const [sketch, config] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
if (!sketch) {
|
||||
private async archiveSketch(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(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...',
|
||||
const defaultContainerUri = await this.defaultUri();
|
||||
const defaultUri = defaultContainerUri.resolve(archiveBasename);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await this.dialogService.showSaveDialog({
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveSketchAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
});
|
||||
if (!filePath || canceled) {
|
||||
@@ -52,10 +52,17 @@ export class ArchiveSketch extends SketchContribution {
|
||||
if (!destinationUri) {
|
||||
return;
|
||||
}
|
||||
await this.sketchService.archive(sketch, destinationUri.toString());
|
||||
this.messageService.info(`Created archive '${archiveBasename}'.`, {
|
||||
await this.sketchesService.archive(sketch, destinationUri.toString());
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/sketch/createdArchive',
|
||||
"Created archive '{0}'.",
|
||||
archiveBasename
|
||||
),
|
||||
{
|
||||
timeout: 2000,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +1,111 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import {
|
||||
DisposableCollection,
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { firstToUpperCase } from '../../common/utils';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry';
|
||||
import type { MenuPath } from '@theia/core/lib/common/menu/menu-types';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import {
|
||||
BoardsService,
|
||||
BoardWithPackage,
|
||||
createPlatformIdentifier,
|
||||
getBoardInfo,
|
||||
InstalledBoardWithPackage,
|
||||
platformIdentifierEquals,
|
||||
Port,
|
||||
serializePlatformIdentifier,
|
||||
} from '../../common/protocol';
|
||||
import type { BoardList } from '../../common/protocol/board-list';
|
||||
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 { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Command, CommandRegistry, SketchContribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class BoardSelection extends SketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
private readonly mainMenuManager: MainMenuManager;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuModelRegistry: MenuModelRegistry;
|
||||
|
||||
private readonly menuModelRegistry: MenuModelRegistry;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
@inject(BoardsService)
|
||||
protected readonly boardsService: BoardsService;
|
||||
|
||||
private readonly boardsService: BoardsService;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
|
||||
private readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
|
||||
// do not query installed platforms on every change
|
||||
private _installedBoards: Deferred<InstalledBoardWithPackage[]> | undefined;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
|
||||
execute: async () => {
|
||||
const { selectedBoard, selectedPort } =
|
||||
this.boardsServiceProvider.boardsConfig;
|
||||
if (!selectedBoard) {
|
||||
this.messageService.info(
|
||||
'Please select a board to obtain board info.'
|
||||
const boardInfo = await getBoardInfo(
|
||||
this.boardsServiceProvider.boardList
|
||||
);
|
||||
if (typeof boardInfo === 'string') {
|
||||
this.messageService.info(boardInfo);
|
||||
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}
|
||||
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',
|
||||
PID: ${PID}
|
||||
SN: ${SN}
|
||||
`.trim();
|
||||
await this.dialogService.showMessageBox({
|
||||
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
type: 'info',
|
||||
detail,
|
||||
buttons: ['OK'],
|
||||
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onPlatformDidInstall(() => this.updateMenus(true));
|
||||
this.notificationCenter.onPlatformDidUninstall(() =>
|
||||
this.updateMenus(true)
|
||||
);
|
||||
this.boardsServiceProvider.onBoardListDidChange(() => this.updateMenus());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<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)
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
private async updateMenus(discardCache = false): Promise<void> {
|
||||
if (discardCache) {
|
||||
this._installedBoards?.reject();
|
||||
this._installedBoards = undefined;
|
||||
}
|
||||
if (!this._installedBoards) {
|
||||
this._installedBoards = new Deferred();
|
||||
this.installedBoards().then((installedBoards) =>
|
||||
this._installedBoards?.resolve(installedBoards)
|
||||
);
|
||||
}
|
||||
const installedBoards = await this._installedBoards.promise;
|
||||
this.rebuildMenus(installedBoards, this.boardsServiceProvider.boardList);
|
||||
}
|
||||
|
||||
protected rebuildMenus(
|
||||
private rebuildMenus(
|
||||
installedBoards: InstalledBoardWithPackage[],
|
||||
availablePorts: AvailablePorts,
|
||||
config: BoardsConfig.Config
|
||||
boardList: BoardList
|
||||
): void {
|
||||
this.toDisposeBeforeMenuRebuild.dispose();
|
||||
|
||||
@@ -122,12 +114,17 @@ PID: ${PID}`;
|
||||
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||
'1_boards',
|
||||
];
|
||||
const boardsSubmenuLabel = config.selectedBoard?.name;
|
||||
const { selectedBoard, selectedPort } = boardList.boardsConfig;
|
||||
const boardsSubmenuLabel = 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}"` : ''}`,
|
||||
nls.localize(
|
||||
'arduino/board/board',
|
||||
'Board{0}',
|
||||
!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''
|
||||
),
|
||||
{ order: '100' }
|
||||
);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -137,14 +134,15 @@ PID: ${PID}`;
|
||||
);
|
||||
|
||||
// Ports submenu
|
||||
const portsSubmenuPath = [
|
||||
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||
'2_ports',
|
||||
];
|
||||
const portsSubmenuLabel = config.selectedPort?.address;
|
||||
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
|
||||
const portsSubmenuLabel = selectedPort?.address;
|
||||
this.menuModelRegistry.registerSubmenu(
|
||||
portsSubmenuPath,
|
||||
`Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`,
|
||||
nls.localize(
|
||||
'arduino/board/port',
|
||||
'Port{0}',
|
||||
portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''
|
||||
),
|
||||
{ order: '101' }
|
||||
);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -155,7 +153,7 @@ PID: ${PID}`;
|
||||
|
||||
const getBoardInfo = {
|
||||
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
|
||||
label: 'Get Board Info',
|
||||
label: nls.localize('arduino/board/getBoardInfo', 'Get Board Info'),
|
||||
order: '103',
|
||||
};
|
||||
this.menuModelRegistry.registerMenuAction(
|
||||
@@ -173,67 +171,135 @@ PID: ${PID}`;
|
||||
|
||||
this.menuModelRegistry.registerMenuAction(boardsManagerGroup, {
|
||||
commandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
|
||||
label: 'Boards Manager...',
|
||||
label: `${BoardsListWidget.WIDGET_LABEL}...`,
|
||||
});
|
||||
|
||||
// Installed boards
|
||||
for (const board of installedBoards) {
|
||||
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
|
||||
const selectedBoardPlatformId = selectedBoard
|
||||
? createPlatformIdentifier(selectedBoard)
|
||||
: undefined;
|
||||
|
||||
const packageLabel =
|
||||
packageName + `${manuallyInstalled ? ' (in Sketchbook)' : ''}`;
|
||||
// Keys are the vendor IDs
|
||||
type BoardsPerVendor = Record<string, BoardWithPackage[]>;
|
||||
// Group boards by their platform names. The keys are the platform names as menu labels.
|
||||
// If there is a platform name (menu label) collision, refine the menu label with the vendor ID.
|
||||
const groupedBoards = new Map<string, BoardsPerVendor>();
|
||||
for (const board of installedBoards) {
|
||||
const { packageId, packageName } = board;
|
||||
const { vendorId } = packageId;
|
||||
let boardsPerPackageName = groupedBoards.get(packageName);
|
||||
if (!boardsPerPackageName) {
|
||||
boardsPerPackageName = {} as BoardsPerVendor;
|
||||
groupedBoards.set(packageName, boardsPerPackageName);
|
||||
}
|
||||
let boardPerVendor: BoardWithPackage[] | undefined =
|
||||
boardsPerPackageName[vendorId];
|
||||
if (!boardPerVendor) {
|
||||
boardPerVendor = [];
|
||||
boardsPerPackageName[vendorId] = boardPerVendor;
|
||||
}
|
||||
boardPerVendor.push(board);
|
||||
}
|
||||
|
||||
// Installed boards
|
||||
Array.from(groupedBoards.entries()).forEach(
|
||||
([packageName, boardsPerPackage]) => {
|
||||
const useVendorSuffix = Object.keys(boardsPerPackage).length > 1;
|
||||
Object.entries(boardsPerPackage).forEach(([vendorId, boards]) => {
|
||||
let platformMenuPath: MenuPath | undefined = undefined;
|
||||
boards.forEach((board, index) => {
|
||||
const { packageId, fqbn, name, manuallyInstalled } = board;
|
||||
// create the platform submenu once.
|
||||
// creating and registering the same submenu twice in Theia is a noop, though.
|
||||
if (!platformMenuPath) {
|
||||
let packageLabel =
|
||||
packageName +
|
||||
`${
|
||||
manuallyInstalled
|
||||
? nls.localize(
|
||||
'arduino/board/inSketchbook',
|
||||
' (in Sketchbook)'
|
||||
)
|
||||
: ''
|
||||
}`;
|
||||
if (
|
||||
selectedBoardPlatformId &&
|
||||
platformIdentifierEquals(packageId, selectedBoardPlatformId)
|
||||
) {
|
||||
packageLabel = `● ${packageLabel}`;
|
||||
}
|
||||
if (useVendorSuffix) {
|
||||
packageLabel += ` (${vendorId})`;
|
||||
}
|
||||
// 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);
|
||||
platformMenuPath = [
|
||||
...boardsPackagesGroup,
|
||||
serializePlatformIdentifier(packageId),
|
||||
];
|
||||
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,
|
||||
execute: () =>
|
||||
this.boardsServiceProvider.updateConfig({
|
||||
name: name,
|
||||
fqbn: fqbn,
|
||||
}),
|
||||
isToggled: () => fqbn === selectedBoard?.fqbn,
|
||||
};
|
||||
|
||||
// Board menu
|
||||
const menuAction = { commandId: id, label: name };
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label: name,
|
||||
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
|
||||
Disposable.create(() =>
|
||||
this.commandRegistry.unregisterCommand(command)
|
||||
)
|
||||
);
|
||||
this.menuModelRegistry.registerMenuAction(
|
||||
platformMenuPath,
|
||||
menuAction
|
||||
);
|
||||
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
|
||||
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Installed ports
|
||||
const registerPorts = (ports: AvailablePorts) => {
|
||||
const addresses = Object.keys(ports);
|
||||
if (!addresses.length) {
|
||||
// Detected ports
|
||||
const registerPorts = (
|
||||
protocol: string,
|
||||
ports: ReturnType<BoardList['ports']>,
|
||||
protocolOrder: number
|
||||
) => {
|
||||
if (!ports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register placeholder for protocol
|
||||
const [port] = ports[addresses[0]];
|
||||
const protocol = port.protocol;
|
||||
const menuPath = [...portsSubmenuPath, protocol];
|
||||
const menuPath = [
|
||||
...portsSubmenuPath,
|
||||
`${protocolOrder.toString()}_${protocol}`,
|
||||
];
|
||||
const placeholder = new PlaceholderMenuNode(
|
||||
menuPath,
|
||||
`${firstToUpperCase(port.protocol)} ports`
|
||||
nls.localize(
|
||||
'arduino/board/typeOfPorts',
|
||||
'{0} ports',
|
||||
Port.Protocols.protocolLabel(protocol)
|
||||
),
|
||||
{ order: protocolOrder.toString().padStart(4) }
|
||||
);
|
||||
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -242,45 +308,31 @@ PID: ${PID}`;
|
||||
)
|
||||
);
|
||||
|
||||
for (const address of addresses) {
|
||||
if (!!ports[address]) {
|
||||
const [port, boards] = ports[address];
|
||||
if (!boards.length) {
|
||||
boards.push({
|
||||
name: '',
|
||||
});
|
||||
for (let i = 0; i < ports.length; i++) {
|
||||
const { port, boards } = ports[i];
|
||||
const portKey = Port.keyOf(port);
|
||||
let label = `${port.addressLabel}`;
|
||||
if (boards?.length) {
|
||||
const boardsList = boards.map((board) => board.name).join(', ');
|
||||
label = `${label} (${boardsList})`;
|
||||
}
|
||||
for (const { name, fqbn } of boards) {
|
||||
const id = `arduino-select-port--${address}${
|
||||
fqbn ? `--${fqbn}` : ''
|
||||
}`;
|
||||
const id = `arduino-select-port--${portKey}`;
|
||||
const command = { id };
|
||||
const handler = {
|
||||
execute: () => {
|
||||
if (
|
||||
!Port.equals(
|
||||
port,
|
||||
this.boardsServiceProvider.boardsConfig.selectedPort
|
||||
)
|
||||
) {
|
||||
this.boardsServiceProvider.boardsConfig = {
|
||||
selectedBoard:
|
||||
this.boardsServiceProvider.boardsConfig.selectedBoard,
|
||||
selectedPort: port,
|
||||
};
|
||||
}
|
||||
this.boardsServiceProvider.updateConfig({
|
||||
protocol: port.protocol,
|
||||
address: port.address,
|
||||
});
|
||||
},
|
||||
isToggled: () => {
|
||||
return i === ports.matchingIndex;
|
||||
},
|
||||
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`
|
||||
order: String(protocolOrder + i + 1).padStart(4),
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -290,21 +342,19 @@ PID: ${PID}`;
|
||||
);
|
||||
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { serial, network, unknown } =
|
||||
AvailablePorts.groupByProtocol(availablePorts);
|
||||
registerPorts(serial);
|
||||
registerPorts(network);
|
||||
registerPorts(unknown);
|
||||
|
||||
const groupedPorts = boardList.portsGroupedByProtocol();
|
||||
let protocolOrder = 100;
|
||||
Object.entries(groupedPorts).forEach(([protocol, ports]) => {
|
||||
registerPorts(protocol, ports, protocolOrder);
|
||||
protocolOrder += 100;
|
||||
});
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
|
||||
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
|
||||
const allBoards = await this.boardsService.searchBoards({});
|
||||
const allBoards = await this.boardsService.getInstalledBoards();
|
||||
return allBoards.filter(InstalledBoardWithPackage.is);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,66 @@
|
||||
import * as PQueue from 'p-queue';
|
||||
import { inject, injectable } from '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 { 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 { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import PQueue from 'p-queue';
|
||||
import {
|
||||
BoardIdentifier,
|
||||
ConfigOption,
|
||||
isBoardIdentifierChangeEvent,
|
||||
Programmer,
|
||||
} from '../../common/protocol';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus';
|
||||
import {
|
||||
CommandRegistry,
|
||||
Contribution,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
export class BoardsDataMenuUpdater extends Contribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
private readonly boardsDataStore: BoardsDataStore;
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
protected readonly toDisposeOnBoardChange = new DisposableCollection();
|
||||
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
private readonly toDisposeOnBoardChange = new DisposableCollection();
|
||||
|
||||
async onStart(): Promise<void> {
|
||||
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
|
||||
override onStart(): void {
|
||||
this.boardsDataStore.onChanged(() =>
|
||||
this.updateMenuActions(
|
||||
this.boardsServiceClient.boardsConfig.selectedBoard
|
||||
this.boardsServiceProvider.boardsConfig.selectedBoard
|
||||
)
|
||||
);
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||
this.updateMenuActions(selectedBoard)
|
||||
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
|
||||
if (isBoardIdentifierChangeEvent(event)) {
|
||||
this.updateMenuActions(event.selectedBoard);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.boardsServiceProvider.ready.then(() =>
|
||||
this.updateMenuActions(
|
||||
this.boardsServiceProvider.boardsConfig.selectedBoard
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected async updateMenuActions(
|
||||
selectedBoard: Board | undefined
|
||||
private async updateMenuActions(
|
||||
selectedBoard: BoardIdentifier | undefined
|
||||
): Promise<void> {
|
||||
return this.queue.add(async () => {
|
||||
this.toDisposeOnBoardChange.dispose();
|
||||
this.mainMenuManager.update();
|
||||
this.menuManager.update();
|
||||
if (selectedBoard) {
|
||||
const { fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
@@ -69,16 +79,16 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
string,
|
||||
Disposable & { label: string }
|
||||
>();
|
||||
let selectedValue = '';
|
||||
for (const value of values) {
|
||||
const id = `${fqbn}-${option}--${value.value}`;
|
||||
const command = { id };
|
||||
const selectedValue = value.value;
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.boardsDataStore.selectConfigOption({
|
||||
fqbn,
|
||||
option,
|
||||
selectedValue,
|
||||
selectedValue: value.value,
|
||||
}),
|
||||
isToggled: () => value.selected,
|
||||
};
|
||||
@@ -89,8 +99,14 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
{ label: value.label }
|
||||
)
|
||||
);
|
||||
if (value.selected) {
|
||||
selectedValue = value.label;
|
||||
}
|
||||
this.menuRegistry.registerSubmenu(menuPath, label);
|
||||
}
|
||||
this.menuRegistry.registerSubmenu(
|
||||
menuPath,
|
||||
`${label}${selectedValue ? `: "${selectedValue}"` : ''}`
|
||||
);
|
||||
this.toDisposeOnBoardChange.pushAll([
|
||||
...commands.values(),
|
||||
Disposable.create(() =>
|
||||
@@ -100,7 +116,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
const { label } = commands.get(commandId)!;
|
||||
this.menuRegistry.registerMenuAction(menuPath, {
|
||||
commandId,
|
||||
order: `${i}`,
|
||||
order: String(i).padStart(4),
|
||||
label,
|
||||
});
|
||||
return Disposable.create(() =>
|
||||
@@ -115,9 +131,13 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
...ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP,
|
||||
'z02_programmers',
|
||||
];
|
||||
const programmerNls = nls.localize(
|
||||
'arduino/board/programmer',
|
||||
'Programmer'
|
||||
);
|
||||
const label = selectedProgrammer
|
||||
? `Programmer: "${selectedProgrammer.name}"`
|
||||
: 'Programmer';
|
||||
? `${programmerNls}: "${selectedProgrammer.name}"`
|
||||
: programmerNls;
|
||||
this.menuRegistry.registerSubmenu(programmersMenuPath, label);
|
||||
this.toDisposeOnBoardChange.push(
|
||||
Disposable.create(() =>
|
||||
@@ -151,7 +171,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
]);
|
||||
}
|
||||
}
|
||||
this.mainMenuManager.update();
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,56 +1,65 @@
|
||||
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,
|
||||
CoreServiceContribution,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class BurnBootloader extends SketchContribution {
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(MonitorConnection)
|
||||
protected readonly monitorConnection: MonitorConnection;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
protected readonly boardsDataStore: BoardsDataStore;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClientImpl: BoardsServiceProvider;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
export class BurnBootloader extends CoreServiceContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(BurnBootloader.Commands.BURN_BOOTLOADER, {
|
||||
execute: () => this.burnBootloader(),
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SETTINGS_GROUP, {
|
||||
commandId: BurnBootloader.Commands.BURN_BOOTLOADER.id,
|
||||
label: 'Burn Bootloader',
|
||||
label: nls.localize(
|
||||
'arduino/bootloader/burnBootloader',
|
||||
'Burn Bootloader'
|
||||
),
|
||||
order: 'z99',
|
||||
});
|
||||
}
|
||||
|
||||
async burnBootloader(): Promise<void> {
|
||||
const monitorConfig = this.monitorConnection.monitorConfig;
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.disconnect();
|
||||
}
|
||||
private async burnBootloader(): Promise<void> {
|
||||
this.clearVisibleNotification();
|
||||
const options = await this.options();
|
||||
try {
|
||||
const { boardsConfig } = this.boardsServiceClientImpl;
|
||||
const port = boardsConfig.selectedPort?.address;
|
||||
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(
|
||||
@@ -60,24 +69,13 @@ export class BurnBootloader extends SketchContribution {
|
||||
this.preferences.get('arduino.upload.verify'),
|
||||
this.preferences.get('arduino.upload.verbose'),
|
||||
]);
|
||||
this.outputChannelManager.getChannel('Arduino').clear();
|
||||
await this.coreService.burnBootloader({
|
||||
return {
|
||||
fqbn,
|
||||
programmer,
|
||||
port,
|
||||
verify,
|
||||
verbose,
|
||||
});
|
||||
this.messageService.info('Done burning bootloader.', {
|
||||
timeout: 3000,
|
||||
});
|
||||
} catch (e) {
|
||||
this.messageService.error(e.toString());
|
||||
} finally {
|
||||
if (monitorConfig) {
|
||||
await this.monitorConnection.connect(monitorConfig);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,38 +1,45 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { toArray } from '@phosphor/algorithm';
|
||||
import { remote } from 'electron';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import type {
|
||||
FrontendApplication,
|
||||
OnWillStopAction,
|
||||
} from '@theia/core/lib/browser/frontend-application';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import type { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||
import {
|
||||
SketchContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
SketchContribution,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
@inject(WindowServiceExt)
|
||||
private readonly windowServiceExt: WindowServiceExt;
|
||||
|
||||
protected shell: ApplicationShell;
|
||||
private shell: ApplicationShell | undefined;
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
override onStart(app: FrontendApplication): MaybePromise<void> {
|
||||
this.shell = app.shell;
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Close.Commands.CLOSE, {
|
||||
execute: async () => {
|
||||
execute: () => {
|
||||
// Close current editor if closeable.
|
||||
const { currentEditor } = this.editorManager;
|
||||
if (currentEditor && currentEditor.title.closable) {
|
||||
@@ -40,6 +47,7 @@ export class Close extends SketchContribution {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shell) {
|
||||
// Close current widget from the main area if possible.
|
||||
const { currentWidget } = this.shell;
|
||||
if (currentWidget) {
|
||||
@@ -50,61 +58,141 @@ export class Close extends SketchContribution {
|
||||
return currentWidgetInMain.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Close the sketch (window).
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
if (isTemp && (await this.wasTouched(uri))) {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
type: 'question',
|
||||
buttons: ["Don't Save", 'Cancel', 'Save'],
|
||||
message:
|
||||
'Do you want to save changes to this sketch before closing?',
|
||||
detail: "If you don't save, your changes will be lost.",
|
||||
});
|
||||
if (response === 1) {
|
||||
// Cancel
|
||||
return;
|
||||
}
|
||||
if (response === 2) {
|
||||
// Save
|
||||
const saved = await this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
{ openAfterMove: false, execOnlyIfTemp: true }
|
||||
);
|
||||
if (!saved) {
|
||||
// If it was not saved, do bail the close.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
window.close();
|
||||
return this.windowServiceExt.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: Close.Commands.CLOSE.id,
|
||||
label: 'Close',
|
||||
order: '5',
|
||||
label: nls.localize('vscode/editor.contribution/close', 'Close'),
|
||||
order: '6',
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
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 this.dialogService.showMessageBox({
|
||||
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.
|
||||
*/
|
||||
@@ -114,13 +202,23 @@ export class Close extends SketchContribution {
|
||||
const { editor } = editorWidget;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
const versionId = editor.getControl().getModel()?.getVersionId();
|
||||
if (Number.isInteger(versionId) && versionId! > 1) {
|
||||
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 {
|
||||
|
||||
@@ -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 }, true);
|
||||
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,15 +1,23 @@
|
||||
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 {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { Saveable } from '@theia/core/lib/browser/saveable';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
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,
|
||||
@@ -32,16 +40,35 @@ import {
|
||||
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 { SettingsService } from '../dialogs/settings/settings';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../sketches-service-client-impl';
|
||||
import {
|
||||
SketchesService,
|
||||
ConfigService,
|
||||
FileSystemExt,
|
||||
Sketch,
|
||||
CoreService,
|
||||
CoreError,
|
||||
ResponseServiceClient,
|
||||
} from '../../common/protocol';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { nls } from '@theia/core';
|
||||
import { OutputChannelManager } from '../theia/output/output-channel';
|
||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { NotificationManager } from '@theia/messages/lib/browser/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';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { DialogService } from '../dialog-service';
|
||||
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
|
||||
|
||||
export {
|
||||
Command,
|
||||
@@ -75,24 +102,46 @@ export abstract class Contribution
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(EditorMode)
|
||||
protected readonly editorMode: EditorMode;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(SettingsService)
|
||||
protected readonly settingsService: SettingsService;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly preferences: ArduinoPreferences;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
protected readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
|
||||
@inject(DialogService)
|
||||
protected readonly dialogService: DialogService;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.appStateService.reachedState('ready').then(() => this.onReady());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
|
||||
onStart(app: FrontendApplication): MaybePromise<void> {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function, unused-imports/no-unused-vars
|
||||
registerCommands(registry: CommandRegistry): 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()
|
||||
@@ -103,11 +152,11 @@ export abstract class SketchContribution extends Contribution {
|
||||
@inject(FileSystemExt)
|
||||
protected readonly fileSystemExt: FileSystemExt;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
@inject(ConfigServiceClient)
|
||||
protected readonly configService: ConfigServiceClient;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
protected readonly sketchesService: SketchesService;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
@@ -115,19 +164,22 @@ export abstract class SketchContribution extends Contribution {
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly preferences: ArduinoPreferences;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
protected readonly connectionStatusService: ApplicationConnectionStatusContribution;
|
||||
|
||||
protected async sourceOverride(): Promise<Record<string, string>> {
|
||||
const override: Record<string, string> = {};
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (sketch) {
|
||||
if (CurrentSketch.isValid(sketch)) {
|
||||
for (const editor of this.editorManager.all) {
|
||||
const uri = editor.editor.uri;
|
||||
if (Saveable.isDirty(editor) && Sketch.isInSketch(uri, sketch)) {
|
||||
@@ -137,10 +189,155 @@ export abstract class SketchContribution extends Contribution {
|
||||
}
|
||||
return override;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults to `directories.user` if defined and not CLI config errors were detected.
|
||||
* Otherwise, the URI of the user home directory.
|
||||
*/
|
||||
protected async defaultUri(): Promise<URI> {
|
||||
const errors = this.configService.tryGetMessages();
|
||||
let defaultUri = this.configService.tryGetSketchDirUri();
|
||||
if (!defaultUri || errors?.length) {
|
||||
// Fall back to user home when the `directories.user` is not available or there are known CLI config errors
|
||||
defaultUri = new URI(await this.envVariableServer.getHomeDirUri());
|
||||
}
|
||||
return defaultUri;
|
||||
}
|
||||
|
||||
protected async defaultPath(): Promise<string> {
|
||||
const defaultUri = await this.defaultUri();
|
||||
return this.fileService.fsPath(defaultUri);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
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;
|
||||
|
||||
@inject(ApplicationShell)
|
||||
private readonly shell: ApplicationShell;
|
||||
|
||||
/**
|
||||
* 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 toDisposeOnComplete = new DisposableCollection(
|
||||
this.maybeActivateMonitorWidget()
|
||||
);
|
||||
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,
|
||||
});
|
||||
toDisposeOnComplete.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: cleanup!
|
||||
// this dependency does not belong here
|
||||
// support core command contribution handlers, the monitor-widget should implement it and register itself as a handler
|
||||
// the monitor widget should reveal itself after a successful core command execution
|
||||
private maybeActivateMonitorWidget(): Disposable {
|
||||
const currentWidget = this.shell.bottomPanel.currentTitle?.owner;
|
||||
if (currentWidget?.id === 'serial-monitor') {
|
||||
return Disposable.create(() =>
|
||||
this.shell.bottomPanel.activateWidget(currentWidget)
|
||||
);
|
||||
}
|
||||
return Disposable.NULL;
|
||||
}
|
||||
|
||||
private notificationId(message: string, ...actions: string[]): string {
|
||||
return this.notificationManager['getMessageId']({
|
||||
text: message,
|
||||
actions,
|
||||
type: MessageType.Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Contribution {
|
||||
export function configure<T>(
|
||||
export function configure(
|
||||
bind: interfaces.Bind,
|
||||
serviceIdentifier: typeof Contribution
|
||||
): void {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell';
|
||||
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Create } from '../create/typings';
|
||||
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { SketchbookTree } from '../widgets/sketchbook/sketchbook-tree';
|
||||
import { SketchbookTreeModel } from '../widgets/sketchbook/sketchbook-tree-model';
|
||||
import { CloudSketchContribution, pushingSketch } from './cloud-contribution';
|
||||
import {
|
||||
CreateNewCloudSketchCallback,
|
||||
NewCloudSketch,
|
||||
NewCloudSketchParams,
|
||||
} from './new-cloud-sketch';
|
||||
import { saveOntoCopiedSketch } from './save-as-sketch';
|
||||
|
||||
interface CreateCloudCopyParams {
|
||||
readonly model: SketchbookTreeModel;
|
||||
readonly node: SketchbookTree.SketchDirNode;
|
||||
}
|
||||
function isCreateCloudCopyParams(arg: unknown): arg is CreateCloudCopyParams {
|
||||
return (
|
||||
typeof arg === 'object' &&
|
||||
(<CreateCloudCopyParams>arg).model !== undefined &&
|
||||
(<CreateCloudCopyParams>arg).model instanceof SketchbookTreeModel &&
|
||||
(<CreateCloudCopyParams>arg).node !== undefined &&
|
||||
SketchbookTree.SketchDirNode.is((<CreateCloudCopyParams>arg).node)
|
||||
);
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class CreateCloudCopy extends CloudSketchContribution {
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
private shell: ApplicationShell;
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.shell = app.shell;
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(CreateCloudCopy.Commands.CREATE_CLOUD_COPY, {
|
||||
execute: (args: CreateCloudCopyParams) => this.createCloudCopy(args),
|
||||
isEnabled: (args: unknown) =>
|
||||
Boolean(this.createFeatures.session) && isCreateCloudCopyParams(args),
|
||||
isVisible: (args: unknown) =>
|
||||
Boolean(this.createFeatures.enabled) &&
|
||||
Boolean(this.createFeatures.session) &&
|
||||
this.connectionStatus.offlineStatus !== 'internet' &&
|
||||
isCreateCloudCopyParams(args),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* - creates new cloud sketch with the name of the params sketch,
|
||||
* - pulls the cloud sketch,
|
||||
* - copies files from params sketch to pulled cloud sketch in the cache folder,
|
||||
* - pushes the cloud sketch, and
|
||||
* - opens in new window.
|
||||
*/
|
||||
private async createCloudCopy(params: CreateCloudCopyParams): Promise<void> {
|
||||
const sketch = await this.sketchesService.loadSketch(
|
||||
params.node.fileStat.resource.toString()
|
||||
);
|
||||
const callback: CreateNewCloudSketchCallback = async (
|
||||
newSketch: Create.Sketch,
|
||||
newNode: CloudSketchbookTree.CloudSketchDirNode,
|
||||
progress: Progress
|
||||
) => {
|
||||
const treeModel = await this.treeModel();
|
||||
if (!treeModel) {
|
||||
throw new Error('Could not retrieve the cloud sketchbook tree model.');
|
||||
}
|
||||
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/createCloudCopy/copyingSketchFilesMessage',
|
||||
'Copying local sketch files...'
|
||||
),
|
||||
});
|
||||
const localCacheFolderUri = newNode.uri.toString();
|
||||
await this.sketchesService.copy(sketch, {
|
||||
destinationUri: localCacheFolderUri,
|
||||
onlySketchFiles: true,
|
||||
});
|
||||
await saveOntoCopiedSketch(
|
||||
sketch,
|
||||
localCacheFolderUri,
|
||||
this.shell,
|
||||
this.editorManager
|
||||
);
|
||||
|
||||
progress.report({ message: pushingSketch(newSketch.name) });
|
||||
await treeModel.sketchbookTree().push(newNode, true, true);
|
||||
};
|
||||
return this.commandService.executeCommand(
|
||||
NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
<NewCloudSketchParams>{
|
||||
initialValue: params.node.fileStat.name,
|
||||
callback,
|
||||
skipShowErrorMessageOnOpen: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CreateCloudCopy {
|
||||
export namespace Commands {
|
||||
export const CREATE_CLOUD_COPY: Command = {
|
||||
id: 'arduino-create-cloud-copy',
|
||||
iconClass: 'fa fa-arduino-cloud-upload',
|
||||
};
|
||||
}
|
||||
}
|
||||
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,9 +1,16 @@
|
||||
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,
|
||||
BoardIdentifier,
|
||||
BoardsService,
|
||||
ExecutableService,
|
||||
isBoardIdentifierChangeEvent,
|
||||
Sketch,
|
||||
} from '../../common/protocol';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import {
|
||||
URI,
|
||||
@@ -12,110 +19,162 @@ import {
|
||||
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)
|
||||
protected hostedPluginSupport: HostedPluginSupport;
|
||||
private readonly hostedPluginSupport: HostedPluginSupport;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(ExecutableService)
|
||||
protected readonly executableService: ExecutableService;
|
||||
private readonly executableService: ExecutableService;
|
||||
|
||||
@inject(BoardsService)
|
||||
protected readonly boardService: BoardsService;
|
||||
private readonly boardService: BoardsService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
/**
|
||||
* 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 =
|
||||
private _disabledMessages?: string = nls.localize(
|
||||
'arduino/common/noBoardSelected',
|
||||
'No board selected'
|
||||
); // Initial pessimism.
|
||||
private disabledMessageDidChangeEmitter = new Emitter<string | undefined>();
|
||||
private onDisabledMessageDidChange =
|
||||
this.disabledMessageDidChangeEmitter.event;
|
||||
|
||||
protected get disabledMessage(): string | undefined {
|
||||
private get disabledMessage(): string | undefined {
|
||||
return this._disabledMessages;
|
||||
}
|
||||
protected set disabledMessage(message: string | undefined) {
|
||||
private set disabledMessage(message: string | undefined) {
|
||||
this._disabledMessages = message;
|
||||
this.disabledMessageDidChangeEmitter.fire(this._disabledMessages);
|
||||
}
|
||||
|
||||
protected readonly debugToolbarItem = {
|
||||
private readonly debugToolbarItem = {
|
||||
id: Debug.Commands.START_DEBUGGING.id,
|
||||
command: Debug.Commands.START_DEBUGGING.id,
|
||||
tooltip: `${
|
||||
this.disabledMessage
|
||||
? `Debug - ${this.disabledMessage}`
|
||||
: 'Start Debugging'
|
||||
? nls.localize(
|
||||
'arduino/debug/debugWithMessage',
|
||||
'Debug - {0}',
|
||||
this.disabledMessage
|
||||
)
|
||||
: Debug.Commands.START_DEBUGGING.label
|
||||
}`,
|
||||
priority: 3,
|
||||
onDidChange: this.onDisabledMessageDidChange as Event<void>,
|
||||
};
|
||||
|
||||
onStart(): void {
|
||||
override onStart(): void {
|
||||
this.onDisabledMessageDidChange(
|
||||
() =>
|
||||
(this.debugToolbarItem.tooltip = `${
|
||||
this.disabledMessage
|
||||
? `Debug - ${this.disabledMessage}`
|
||||
: 'Start Debugging'
|
||||
? nls.localize(
|
||||
'arduino/debug/debugWithMessage',
|
||||
'Debug - {0}',
|
||||
this.disabledMessage
|
||||
)
|
||||
: Debug.Commands.START_DEBUGGING.label
|
||||
}`)
|
||||
);
|
||||
const refreshState = async (
|
||||
board: Board | undefined = this.boardsServiceProvider.boardsConfig
|
||||
.selectedBoard
|
||||
) => {
|
||||
if (!board) {
|
||||
this.disabledMessage = 'No board selected';
|
||||
return;
|
||||
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
|
||||
if (isBoardIdentifierChangeEvent(event)) {
|
||||
this.refreshState(event.selectedBoard);
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||
refreshState(selectedBoard)
|
||||
);
|
||||
this.notificationCenter.onPlatformInstalled(() => refreshState());
|
||||
this.notificationCenter.onPlatformUninstalled(() => refreshState());
|
||||
refreshState();
|
||||
});
|
||||
this.notificationCenter.onPlatformDidInstall(() => this.refreshState());
|
||||
this.notificationCenter.onPlatformDidUninstall(() => this.refreshState());
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem(this.debugToolbarItem);
|
||||
}
|
||||
|
||||
protected async startDebug(
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private async startDebug(
|
||||
board: BoardIdentifier | undefined = this.boardsServiceProvider.boardsConfig
|
||||
.selectedBoard
|
||||
): Promise<void> {
|
||||
if (!board) {
|
||||
return;
|
||||
@@ -129,10 +188,10 @@ export class Debug extends SketchContribution {
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.executableService.list(),
|
||||
]);
|
||||
if (!sketch) {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
|
||||
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
|
||||
sketch
|
||||
);
|
||||
const [cliPath, sketchPath, configPath] = await Promise.all([
|
||||
@@ -149,16 +208,79 @@ export class Debug extends SketchContribution {
|
||||
sketchPath,
|
||||
configPath,
|
||||
};
|
||||
return this.commandService.executeCommand('arduino.debug.start', config);
|
||||
try {
|
||||
await this.commandService.executeCommand('arduino.debug.start', config);
|
||||
} catch (err) {
|
||||
if (await this.isSketchNotVerifiedError(err, sketch)) {
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
const answer = await this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/debug/sketchIsNotCompiled',
|
||||
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
|
||||
sketch.name
|
||||
),
|
||||
yes
|
||||
);
|
||||
if (answer === yes) {
|
||||
this.commandService.executeCommand('arduino-verify-sketch');
|
||||
}
|
||||
} else {
|
||||
this.messageService.error(
|
||||
err instanceof Error ? err.message : String(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get compileForDebug(): boolean {
|
||||
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;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Debug {
|
||||
export namespace Commands {
|
||||
export const START_DEBUGGING: Command = {
|
||||
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 { 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 { Sketch } from '../contributions/contribution';
|
||||
import { isNotFound } from '../create/typings';
|
||||
import { Command, CommandRegistry } from './contribution';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
import { AppService } from '../app-service';
|
||||
|
||||
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;
|
||||
@inject(AppService)
|
||||
private readonly appService: AppService;
|
||||
|
||||
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 this.dialogService.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 {
|
||||
this.appService.scheduleDeletion(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,7 +1,6 @@
|
||||
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,
|
||||
@@ -11,21 +10,21 @@ import {
|
||||
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)
|
||||
protected readonly codeEditorService: MonacoEditorService;
|
||||
private readonly codeEditorService: MonacoEditorService;
|
||||
|
||||
@inject(ClipboardService)
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
private readonly clipboardService: ClipboardService;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferences: PreferenceService;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(EditContributions.Commands.GO_TO_LINE, {
|
||||
execute: () => this.run('editor.action.gotoLine'),
|
||||
});
|
||||
@@ -42,38 +41,14 @@ export class EditContributions extends Contribution {
|
||||
execute: () => this.run('actions.find'),
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.FIND_NEXT, {
|
||||
execute: () => this.run('actions.findWithSelection'),
|
||||
execute: () => this.run('editor.action.nextMatchFindAction'),
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.FIND_PREVIOUS, {
|
||||
execute: () => this.run('editor.action.nextMatchFindAction'),
|
||||
execute: () => this.run('editor.action.previousMatchFindAction'),
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
|
||||
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
|
||||
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') }
|
||||
@@ -82,25 +57,17 @@ export class EditContributions extends Contribution {
|
||||
execute: async () => {
|
||||
const value = await this.currentValue();
|
||||
if (value !== undefined) {
|
||||
this.clipboardService.writeText(`[code]
|
||||
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 {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.CUT.id,
|
||||
order: '0',
|
||||
@@ -111,95 +78,97 @@ ${value}
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.COPY_FOR_FORUM.id,
|
||||
label: 'Copy for Forum',
|
||||
label: nls.localize(
|
||||
'arduino/editor/copyForForum',
|
||||
'Copy for Forum (Markdown)'
|
||||
),
|
||||
order: '2',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.COPY_FOR_GITHUB.id,
|
||||
label: 'Copy for GitHub',
|
||||
commandId: CommonCommands.PASTE.id,
|
||||
order: '3',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__TEXT_CONTROL_GROUP, {
|
||||
commandId: CommonCommands.PASTE.id,
|
||||
commandId: CommonCommands.SELECT_ALL.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',
|
||||
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: 'Comment/Uncomment',
|
||||
label: nls.localize(
|
||||
'arduino/editor/commentUncomment',
|
||||
'Comment/Uncomment'
|
||||
),
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INDENT_LINES.id,
|
||||
label: 'Increase Indent',
|
||||
label: nls.localize('arduino/editor/increaseIndent', 'Increase Indent'),
|
||||
order: '1',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.OUTDENT_LINES.id,
|
||||
label: 'Decrease Indent',
|
||||
label: nls.localize('arduino/editor/decreaseIndent', '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__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: 'Find',
|
||||
label: nls.localize('vscode/findController/startFindAction', 'Find'),
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND_NEXT.id,
|
||||
label: 'Find Next',
|
||||
label: nls.localize(
|
||||
'vscode/findController/findNextMatchAction',
|
||||
'Find Next'
|
||||
),
|
||||
order: '1',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND_PREVIOUS.id,
|
||||
label: 'Find Previous',
|
||||
label: nls.localize(
|
||||
'vscode/findController/findPreviousMatchAction',
|
||||
'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`.
|
||||
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: 'Auto Format', // XXX: The Java IDE uses `Use Selection For Find`.
|
||||
label: nls.localize('arduino/editor/autoFormat', 'Auto Format'), // XXX: The Java IDE uses `Use Selection For Find`.
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
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.COPY_FOR_GITHUB.id,
|
||||
keybinding: 'CtrlCmd+Alt+C',
|
||||
when: 'editorFocus',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.GO_TO_LINE.id,
|
||||
keybinding: 'CtrlCmd+L',
|
||||
@@ -212,15 +181,6 @@ ${value}
|
||||
when: 'editorFocus',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+=',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+-',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND.id,
|
||||
keybinding: 'CtrlCmd+F',
|
||||
@@ -245,10 +205,13 @@ ${value}
|
||||
});
|
||||
}
|
||||
|
||||
protected async current(): Promise<monaco.editor.ICodeEditor | undefined> {
|
||||
protected async current(): Promise<
|
||||
ICodeEditor | StandaloneCodeEditor | undefined
|
||||
> {
|
||||
return (
|
||||
this.codeEditorService.getFocusedCodeEditor() ||
|
||||
this.codeEditorService.getActiveCodeEditor()
|
||||
this.codeEditorService.getActiveCodeEditor() ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -280,9 +243,6 @@ export namespace EditContributions {
|
||||
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',
|
||||
};
|
||||
@@ -307,12 +267,6 @@ export namespace EditContributions {
|
||||
export const USE_FOR_FIND: Command = {
|
||||
id: 'arduino-for-find',
|
||||
};
|
||||
export const INCREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-increase-font-size',
|
||||
};
|
||||
export const DECREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-decrease-font-size',
|
||||
};
|
||||
export const AUTO_FORMAT: Command = {
|
||||
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
|
||||
};
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import * as PQueue from 'p-queue';
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||
import {
|
||||
MenuPath,
|
||||
CompositeMenuNode,
|
||||
SubMenuOptions,
|
||||
} from '@theia/core/lib/common/menu';
|
||||
import PQueue from 'p-queue';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandHandler, CommandService } from '@theia/core/lib/common/command';
|
||||
import { MenuPath, 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 {
|
||||
ArduinoMenus,
|
||||
examplesLabel,
|
||||
PlaceholderMenuNode,
|
||||
} from '../menu/arduino-menus';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import {
|
||||
@@ -21,63 +20,144 @@ import {
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board, Sketch, SketchContainer } from '../../common/protocol';
|
||||
import {
|
||||
Board,
|
||||
SketchRef,
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
CoreService,
|
||||
SketchesService,
|
||||
Sketch,
|
||||
isBoardIdentifierChangeEvent,
|
||||
BoardIdentifier,
|
||||
} from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { unregisterSubmenu } from '../menu/arduino-menus';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
|
||||
/**
|
||||
* Creates a cloned copy of the example sketch and opens it in a new window.
|
||||
*/
|
||||
export async function openClonedExample(
|
||||
uri: string,
|
||||
services: {
|
||||
sketchesService: SketchesService;
|
||||
commandService: CommandService;
|
||||
},
|
||||
onError: {
|
||||
onDidFailClone?: (
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
uri: string;
|
||||
}
|
||||
>,
|
||||
uri: string
|
||||
) => MaybePromise<unknown>;
|
||||
onDidFailOpen?: (
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
uri: string;
|
||||
}
|
||||
>,
|
||||
sketch: Sketch
|
||||
) => MaybePromise<unknown>;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { sketchesService, commandService } = services;
|
||||
const { onDidFailClone, onDidFailOpen } = onError;
|
||||
try {
|
||||
const sketch = await sketchesService.cloneExample(uri);
|
||||
try {
|
||||
await commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (openError) {
|
||||
if (SketchesError.NotFound.is(openError)) {
|
||||
if (onDidFailOpen) {
|
||||
await onDidFailOpen(openError, sketch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw openError;
|
||||
}
|
||||
} catch (cloneError) {
|
||||
if (SketchesError.NotFound.is(cloneError)) {
|
||||
if (onDidFailClone) {
|
||||
await onDidFailClone(cloneError, uri);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw cloneError;
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class Examples extends SketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
@postConstruct()
|
||||
init(): void {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||
this.handleBoardChanged(selectedBoard)
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
|
||||
if (isBoardIdentifierChangeEvent(event)) {
|
||||
this.handleBoardChanged(event.selectedBoard);
|
||||
}
|
||||
});
|
||||
this.notificationCenter.onDidReinitialize(() =>
|
||||
this.update({
|
||||
board: this.boardsServiceProvider.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
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
protected abstract update(options?: {
|
||||
board?: BoardIdentifier | undefined;
|
||||
forceRefresh?: boolean;
|
||||
}): void;
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
// 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', {
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
|
||||
examplesLabel,
|
||||
{
|
||||
order: '4',
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registerRecursively(
|
||||
sketchContainerOrPlaceholder:
|
||||
| SketchContainer
|
||||
| (Sketch | SketchContainer)[]
|
||||
| (SketchRef | SketchContainer)[]
|
||||
| string,
|
||||
menuPath: MenuPath,
|
||||
pushToDispose: DisposableCollection = new DisposableCollection(),
|
||||
@@ -95,7 +175,7 @@ export abstract class Examples extends SketchContribution {
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const sketches: Sketch[] = [];
|
||||
const sketches: SketchRef[] = [];
|
||||
const children: SketchContainer[] = [];
|
||||
let submenuPath = menuPath;
|
||||
|
||||
@@ -103,6 +183,11 @@ export abstract class Examples extends SketchContribution {
|
||||
const { label } = sketchContainerOrPlaceholder;
|
||||
submenuPath = [...menuPath, label];
|
||||
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
|
||||
this.toDispose.push(
|
||||
Disposable.create(() =>
|
||||
unregisterSubmenu(submenuPath, this.menuRegistry)
|
||||
)
|
||||
);
|
||||
sketches.push(...sketchContainerOrPlaceholder.sketches);
|
||||
children.push(...sketchContainerOrPlaceholder.children);
|
||||
} else {
|
||||
@@ -142,12 +227,29 @@ export abstract class Examples extends SketchContribution {
|
||||
}
|
||||
|
||||
protected createHandler(uri: string): CommandHandler {
|
||||
const forceUpdate = () =>
|
||||
this.update({
|
||||
board: this.boardsServiceProvider.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
return {
|
||||
execute: async () => {
|
||||
const sketch = await this.sketchService.cloneExample(uri);
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
await openClonedExample(
|
||||
uri,
|
||||
{
|
||||
sketchesService: this.sketchesService,
|
||||
commandService: this.commandRegistry,
|
||||
},
|
||||
{
|
||||
onDidFailClone: () => {
|
||||
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
||||
forceUpdate();
|
||||
},
|
||||
onDidFailOpen: (err) => {
|
||||
this.messageService.error(err.message);
|
||||
forceUpdate();
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -156,21 +258,29 @@ export abstract class Examples extends SketchContribution {
|
||||
|
||||
@injectable()
|
||||
export class BuiltInExamples extends Examples {
|
||||
onStart(): void {
|
||||
this.register(); // no `await`
|
||||
override async onReady(): Promise<void> {
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
protected async register(): Promise<void> {
|
||||
protected override async update(): Promise<void> {
|
||||
let sketchContainers: SketchContainer[] | undefined;
|
||||
try {
|
||||
sketchContainers = await this.examplesService.builtIns();
|
||||
} catch (e) {
|
||||
console.error('Could not initialize built-in examples.', e);
|
||||
this.messageService.error('Could not initialize built-in examples.');
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/examples/couldNotInitializeExamples',
|
||||
'Could not initialize built-in examples.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.toDispose.dispose();
|
||||
for (const container of ['Built-in examples', ...sketchContainers]) {
|
||||
for (const container of [
|
||||
nls.localize('arduino/examples/builtInExamples', 'Built-in examples'),
|
||||
...sketchContainers,
|
||||
]) {
|
||||
this.registerRecursively(
|
||||
container,
|
||||
ArduinoMenus.EXAMPLES__BUILT_IN_GROUP,
|
||||
@@ -183,27 +293,32 @@ export class BuiltInExamples extends Examples {
|
||||
|
||||
@injectable()
|
||||
export class LibraryExamples extends Examples {
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
onStart(): void {
|
||||
this.register(); // no `await`
|
||||
this.notificationCenter.onLibraryInstalled(() => this.register());
|
||||
this.notificationCenter.onLibraryUninstalled(() => this.register());
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.update());
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.update());
|
||||
}
|
||||
|
||||
protected handleBoardChanged(board: Board | undefined): void {
|
||||
this.register(board);
|
||||
override async onReady(): Promise<void> {
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
protected async register(
|
||||
board: Board | undefined = this.boardsServiceClient.boardsConfig
|
||||
.selectedBoard
|
||||
protected override handleBoardChanged(board: Board | undefined): void {
|
||||
this.update({ board });
|
||||
}
|
||||
|
||||
protected override async update(
|
||||
options: { board?: Board; forceRefresh?: boolean } = {
|
||||
board: this.boardsServiceProvider.boardsConfig.selectedBoard,
|
||||
}
|
||||
): Promise<void> {
|
||||
const { board, forceRefresh } = options;
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
if (forceRefresh) {
|
||||
await this.coreService.refresh();
|
||||
}
|
||||
const fqbn = board?.fqbn;
|
||||
const name = board?.name;
|
||||
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
|
||||
@@ -211,13 +326,22 @@ export class LibraryExamples extends Examples {
|
||||
fqbn,
|
||||
});
|
||||
if (user.length) {
|
||||
(user as any).unshift('Examples from Custom Libraries');
|
||||
(user as any).unshift(
|
||||
nls.localize(
|
||||
'arduino/examples/customLibrary',
|
||||
'Examples from Custom Libraries'
|
||||
)
|
||||
);
|
||||
}
|
||||
if (name && fqbn && current.length) {
|
||||
(current as any).unshift(`Examples for ${name}`);
|
||||
(current as any).unshift(
|
||||
nls.localize('arduino/examples/for', 'Examples for {0}', name)
|
||||
);
|
||||
}
|
||||
if (any.length) {
|
||||
(any as any).unshift('Examples for any board');
|
||||
(any as any).unshift(
|
||||
nls.localize('arduino/examples/forAny', 'Examples for any board')
|
||||
);
|
||||
}
|
||||
for (const container of user) {
|
||||
this.registerRecursively(
|
||||
|
||||
@@ -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,10 +1,10 @@
|
||||
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 { QuickInputService } from '@theia/core/lib/browser/quick-input/quick-input-service';
|
||||
import {
|
||||
Contribution,
|
||||
Command,
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
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 {
|
||||
@@ -24,7 +28,7 @@ export class Help extends Contribution {
|
||||
@inject(QuickInputService)
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
const open = (url: string) =>
|
||||
this.windowService.openNewWindow(url, { external: true });
|
||||
const createOpenHandler = (url: string) =>
|
||||
@@ -37,7 +41,9 @@ export class Help extends Contribution {
|
||||
);
|
||||
registry.registerCommand(
|
||||
Help.Commands.ENVIRONMENT,
|
||||
createOpenHandler('https://www.arduino.cc/en/Guide/Environment')
|
||||
createOpenHandler(
|
||||
'https://docs.arduino.cc/software/ide-v2/tutorials/getting-started-ide-v2'
|
||||
)
|
||||
);
|
||||
registry.registerCommand(
|
||||
Help.Commands.TROUBLESHOOTING,
|
||||
@@ -60,9 +66,9 @@ export class Help extends Contribution {
|
||||
}
|
||||
}
|
||||
if (!searchFor) {
|
||||
searchFor = await this.quickInputService.open({
|
||||
prompt: 'Search on Arduino.cc',
|
||||
placeHolder: 'Type a keyword',
|
||||
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) {
|
||||
@@ -82,9 +88,17 @@ export class Help extends Contribution {
|
||||
Help.Commands.VISIT_ARDUINO,
|
||||
createOpenHandler('https://www.arduino.cc/')
|
||||
);
|
||||
registry.registerCommand(
|
||||
Help.Commands.PRIVACY_POLICY,
|
||||
createOpenHandler('https://www.arduino.cc/en/privacy-policy')
|
||||
);
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.unregisterMenuAction({
|
||||
commandId: ElectronCommands.TOGGLE_DEVELOPER_TOOLS.id,
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.HELP__MAIN_GROUP, {
|
||||
commandId: Help.Commands.GETTING_STARTED.id,
|
||||
order: '0',
|
||||
@@ -114,9 +128,17 @@ export class Help extends Contribution {
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: Help.Commands.FIND_IN_REFERENCE.id,
|
||||
keybinding: 'CtrlCmd+Shift+F',
|
||||
@@ -128,37 +150,42 @@ export namespace Help {
|
||||
export namespace Commands {
|
||||
export const GETTING_STARTED: Command = {
|
||||
id: 'arduino-getting-started',
|
||||
label: 'Getting Started',
|
||||
label: nls.localize('arduino/help/gettingStarted', 'Getting Started'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const ENVIRONMENT: Command = {
|
||||
id: 'arduino-environment',
|
||||
label: 'Environment',
|
||||
label: nls.localize('arduino/help/environment', 'Environment'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const TROUBLESHOOTING: Command = {
|
||||
id: 'arduino-troubleshooting',
|
||||
label: 'Troubleshooting',
|
||||
label: nls.localize('arduino/help/troubleshooting', 'Troubleshooting'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const REFERENCE: Command = {
|
||||
id: 'arduino-reference',
|
||||
label: 'Reference',
|
||||
label: nls.localize('arduino/help/reference', 'Reference'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const FIND_IN_REFERENCE: Command = {
|
||||
id: 'arduino-find-in-reference',
|
||||
label: 'Find in Reference',
|
||||
label: nls.localize('arduino/help/findInReference', 'Find in Reference'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const FAQ: Command = {
|
||||
id: 'arduino-faq',
|
||||
label: 'Frequently Asked Questions',
|
||||
label: nls.localize('arduino/help/faq', 'Frequently Asked Questions'),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const VISIT_ARDUINO: Command = {
|
||||
id: 'arduino-visit-arduino',
|
||||
label: 'Visit Arduino.cc',
|
||||
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,5 +1,5 @@
|
||||
import * as PQueue from 'p-queue';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import PQueue from 'p-queue';
|
||||
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';
|
||||
@@ -15,6 +15,9 @@ 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 {
|
||||
@@ -28,13 +31,13 @@ export class IncludeLibrary extends SketchContribution {
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
protected override readonly editorManager: EditorManager;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
protected readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
@@ -42,34 +45,45 @@ export class IncludeLibrary extends SketchContribution {
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
onStart(): void {
|
||||
this.updateMenuActions();
|
||||
this.boardsServiceClient.onBoardsConfigChanged(() =>
|
||||
override onStart(): void {
|
||||
this.boardsServiceProvider.onBoardsConfigDidChange(() =>
|
||||
this.updateMenuActions()
|
||||
);
|
||||
this.notificationCenter.onLibraryInstalled(() => this.updateMenuActions());
|
||||
this.notificationCenter.onLibraryUninstalled(() =>
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.updateMenuActions());
|
||||
this.notificationCenter.onLibraryDidUninstall(() =>
|
||||
this.updateMenuActions()
|
||||
);
|
||||
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override async onReady(): Promise<void> {
|
||||
this.updateMenuActions();
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
// `Include Library` submenu
|
||||
const includeLibMenuPath = [
|
||||
...ArduinoMenus.SKETCH__UTILS_GROUP,
|
||||
'0_include',
|
||||
];
|
||||
registry.registerSubmenu(includeLibMenuPath, 'Include Library', {
|
||||
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: 'Manage Libraries...',
|
||||
label: nls.localize(
|
||||
'arduino/library/manageLibraries',
|
||||
'Manage Libraries...'
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY, {
|
||||
execute: async (arg) => {
|
||||
if (LibraryPackage.is(arg)) {
|
||||
@@ -84,7 +98,7 @@ export class IncludeLibrary extends SketchContribution {
|
||||
this.toDispose.dispose();
|
||||
this.mainMenuManager.update();
|
||||
const libraries: LibraryPackage[] = [];
|
||||
const fqbn = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn;
|
||||
const fqbn = this.boardsServiceProvider.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 })));
|
||||
@@ -101,10 +115,17 @@ export class IncludeLibrary extends SketchContribution {
|
||||
const userMenuPath = [...includeLibMenuPath, '3_contributed'];
|
||||
const { user, rest } = LibraryPackage.groupByLocation(libraries);
|
||||
if (rest.length) {
|
||||
(rest as any).unshift('Arduino libraries');
|
||||
(rest as any).unshift(
|
||||
nls.localize('arduino/library/arduinoLibraries', 'Arduino libraries')
|
||||
);
|
||||
}
|
||||
if (user.length) {
|
||||
(user as any).unshift('Contributed libraries');
|
||||
(user as any).unshift(
|
||||
nls.localize(
|
||||
'arduino/library/contributedLibraries',
|
||||
'Contributed libraries'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (const library of user) {
|
||||
@@ -153,7 +174,7 @@ export class IncludeLibrary extends SketchContribution {
|
||||
|
||||
protected async includeLibrary(library: LibraryPackage): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
// If the current editor is one of the additional files from the sketch, we use that.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
225
arduino-ide-extension/src/browser/contributions/ino-language.ts
Normal file
225
arduino-ide-extension/src/browser/contributions/ino-language.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import {
|
||||
ArduinoDaemon,
|
||||
assertSanitizedFqbn,
|
||||
BoardIdentifier,
|
||||
BoardsService,
|
||||
ExecutableService,
|
||||
isBoardIdentifierChangeEvent,
|
||||
sanitizeFqbn,
|
||||
} from '../../common/protocol';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
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: BoardIdentifier | undefined,
|
||||
forceStart = false
|
||||
) => {
|
||||
if (selectedBoard) {
|
||||
const { name, fqbn } = selectedBoard;
|
||||
if (fqbn) {
|
||||
this.startLanguageServer(fqbn, name, forceStart);
|
||||
}
|
||||
}
|
||||
};
|
||||
const forceRestart = () => {
|
||||
start(this.boardsServiceProvider.boardsConfig.selectedBoard, true);
|
||||
};
|
||||
this.toDispose.pushAll([
|
||||
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
|
||||
if (isBoardIdentifierChangeEvent(event)) {
|
||||
start(event.selectedBoard);
|
||||
}
|
||||
}),
|
||||
this.hostedPluginEvents.onPluginsDidStart(() =>
|
||||
start(this.boardsServiceProvider.boardsConfig.selectedBoard)
|
||||
),
|
||||
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.selectedBoard);
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
this.boardsServiceProvider.ready.then(() =>
|
||||
start(this.boardsServiceProvider.boardsConfig.selectedBoard)
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
const toDisposeOnRelease = new DisposableCollection();
|
||||
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) => {
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error(`Timeout after ${20_000} ms.`)),
|
||||
20_000
|
||||
);
|
||||
toDisposeOnRelease.push(Disposable.create(() => clearTimeout(timer)));
|
||||
}),
|
||||
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 {
|
||||
toDisposeOnRelease.dispose();
|
||||
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 from '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,204 @@
|
||||
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 { Create, 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 {
|
||||
CloudSketchContribution,
|
||||
pullingSketch,
|
||||
sketchAlreadyExists,
|
||||
synchronizingSketchbook,
|
||||
} from './cloud-contribution';
|
||||
import { Command, CommandRegistry, Sketch } from './contribution';
|
||||
|
||||
export interface CreateNewCloudSketchCallback {
|
||||
(
|
||||
newSketch: Create.Sketch,
|
||||
newNode: CloudSketchbookTree.CloudSketchDirNode,
|
||||
progress: Progress
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface NewCloudSketchParams {
|
||||
/**
|
||||
* Value to populate the dialog `<input>` when it opens.
|
||||
*/
|
||||
readonly initialValue?: string | undefined;
|
||||
/**
|
||||
* Additional callback to call when the new cloud sketch has been created.
|
||||
*/
|
||||
readonly callback?: CreateNewCloudSketchCallback;
|
||||
/**
|
||||
* If `true`, the validation error message will not be visible in the input dialog, but the `OK` button will be disabled. Defaults to `true`.
|
||||
*/
|
||||
readonly skipShowErrorMessageOnOpen?: boolean;
|
||||
}
|
||||
|
||||
@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: (params: NewCloudSketchParams) =>
|
||||
this.createNewSketch(
|
||||
params?.skipShowErrorMessageOnOpen === false ? false : true,
|
||||
params?.initialValue,
|
||||
params?.callback
|
||||
),
|
||||
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,
|
||||
callback?: CreateNewCloudSketchCallback
|
||||
): Promise<void> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (treeModel) {
|
||||
const rootNode = treeModel.root;
|
||||
return this.openWizard(
|
||||
rootNode,
|
||||
treeModel,
|
||||
skipShowErrorMessageOnOpen,
|
||||
initialValue,
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async openWizard(
|
||||
rootNode: CompositeTreeNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined,
|
||||
callback?: CreateNewCloudSketchCallback
|
||||
): Promise<void> {
|
||||
const existingNames = rootNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
const taskFactory = new TaskFactoryImpl((value) =>
|
||||
this.createNewSketchWithProgress(treeModel, value, callback)
|
||||
);
|
||||
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,
|
||||
callback
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private createNewSketchWithProgress(
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
value: string,
|
||||
callback?: CreateNewCloudSketchCallback
|
||||
): (
|
||||
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);
|
||||
if (callback && node) {
|
||||
await callback(sketch, node, progress);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
private openInNewWindow(
|
||||
node: CloudSketchbookTree.CloudSketchDirNode
|
||||
): Promise<void> {
|
||||
return this.commandService.executeCommand(
|
||||
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||
{ node, treeWidgetId: 'cloud-sketchbook-composite-widget' }
|
||||
);
|
||||
}
|
||||
}
|
||||
export namespace NewCloudSketch {
|
||||
export namespace Commands {
|
||||
export const NEW_CLOUD_SKETCH: Command = {
|
||||
id: 'arduino-new-cloud-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
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,
|
||||
@@ -8,49 +8,34 @@ import {
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
} from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class NewSketch extends SketchContribution {
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override 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),
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
||||
label: 'New',
|
||||
label: nls.localize('arduino/sketch/new', 'New Sketch'),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
override 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();
|
||||
const sketch = await this.sketchesService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
@@ -63,8 +48,5 @@ export namespace NewSketch {
|
||||
export const NEW_SKETCH: Command = {
|
||||
id: 'arduino-new-sketch',
|
||||
};
|
||||
export const NEW_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-new-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import type { EditBoardsConfigActionParams } from '../../common/protocol/board-list';
|
||||
import { BoardsConfigDialog } from '../boards/boards-config-dialog';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class OpenBoardsConfig extends Contribution {
|
||||
@inject(BoardsConfigDialog)
|
||||
private readonly boardsConfigDialog: BoardsConfigDialog;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenBoardsConfig.Commands.OPEN_DIALOG, {
|
||||
execute: async (params?: EditBoardsConfigActionParams) =>
|
||||
this.boardsConfigDialog.open(params),
|
||||
});
|
||||
}
|
||||
}
|
||||
export namespace OpenBoardsConfig {
|
||||
export namespace Commands {
|
||||
export const OPEN_DIALOG: Command = {
|
||||
id: 'arduino-open-boards-dialog',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol';
|
||||
import {
|
||||
Disposable,
|
||||
@@ -14,6 +14,8 @@ 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 {
|
||||
@@ -32,42 +34,58 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
||||
protected toDispose = new DisposableCollection();
|
||||
|
||||
onStart(): void {
|
||||
const refreshMenu = (sketches: Sketch[]) => {
|
||||
this.register(sketches);
|
||||
this.mainMenuManager.update();
|
||||
};
|
||||
this.notificationCenter.onRecentSketchesChanged(({ sketches }) =>
|
||||
refreshMenu(sketches)
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||
this.refreshMenu(sketches)
|
||||
);
|
||||
this.sketchService.recentlyOpenedSketches().then(refreshMenu);
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override async onReady(): Promise<void> {
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update(forceUpdate?: boolean): void {
|
||||
this.sketchesService
|
||||
.recentlyOpenedSketches(forceUpdate)
|
||||
.then((sketches) => this.refreshMenu(sketches));
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.FILE__OPEN_RECENT_SUBMENU,
|
||||
'Open Recent',
|
||||
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 toDispose = this.toDisposeBeforeRegister.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
const command = { id: `arduino-open-recent--${uri}` };
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.commandRegistry.executeCommand(
|
||||
execute: async () => {
|
||||
try {
|
||||
await this.commandRegistry.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.update(true);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.menuRegistry.registerMenuAction(
|
||||
@@ -78,8 +96,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
order: String(order),
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeRegister.set(
|
||||
sketch.uri,
|
||||
this.toDispose.pushAll([
|
||||
new DisposableCollection(
|
||||
Disposable.create(() =>
|
||||
this.commandRegistry.unregisterCommand(command)
|
||||
@@ -87,8 +104,8 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
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,5 +1,4 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
@@ -9,24 +8,25 @@ import {
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class OpenSketchExternal extends SketchContribution {
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketchExternal.Commands.OPEN_EXTERNAL, {
|
||||
execute: () => this.openExternal(),
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__UTILS_GROUP, {
|
||||
commandId: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||
label: 'Show Sketch Folder',
|
||||
label: nls.localize('arduino/sketch/showFolder', 'Show Sketch Folder'),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: OpenSketchExternal.Commands.OPEN_EXTERNAL.id,
|
||||
keybinding: 'CtrlCmd+Alt+K',
|
||||
@@ -36,11 +36,11 @@ export class OpenSketchExternal extends SketchContribution {
|
||||
protected async openExternal(): Promise<void> {
|
||||
const uri = await this.sketchServiceClient.currentSketchFile();
|
||||
if (uri) {
|
||||
const exists = this.fileService.exists(new URI(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);
|
||||
window.electronTheiaCore.showItemInFolder(fsPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
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',
|
||||
preview: false,
|
||||
counter: 0,
|
||||
});
|
||||
}
|
||||
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,
|
||||
dialogService: this.dialogService,
|
||||
});
|
||||
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,161 +1,94 @@
|
||||
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 { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
SketchesError,
|
||||
SketchesService,
|
||||
SketchRef,
|
||||
} from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
SketchContribution,
|
||||
Sketch,
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
SketchContribution,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import { BuiltInExamples } from './examples';
|
||||
import { Sketchbook } from './sketchbook';
|
||||
import { SketchContainer } from '../../common/protocol';
|
||||
import { DialogService } from '../dialog-service';
|
||||
|
||||
export type SketchLocation = string | URI | SketchRef;
|
||||
export namespace SketchLocation {
|
||||
export function toUri(location: SketchLocation): URI {
|
||||
if (typeof location === 'string') {
|
||||
return new URI(location);
|
||||
} else if (SketchRef.is(location)) {
|
||||
return toUri(location.uri);
|
||||
} else {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
export function is(arg: unknown): arg is SketchLocation {
|
||||
return typeof arg === 'string' || arg instanceof URI || SketchRef.is(arg);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class OpenSketch extends SketchContribution {
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(BuiltInExamples)
|
||||
protected readonly builtInExamples: BuiltInExamples;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(Sketchbook)
|
||||
protected readonly sketchbook: Sketchbook;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
||||
execute: (arg) =>
|
||||
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
|
||||
});
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (_: Widget, target: EventTarget) => {
|
||||
const container = await this.sketchService.getSketches({
|
||||
exclude: ['**/hardware/**'],
|
||||
});
|
||||
if (SketchContainer.isEmpty(container)) {
|
||||
this.openSketch();
|
||||
} else {
|
||||
this.toDispose.dispose();
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
|
||||
{
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: 'Open...',
|
||||
}
|
||||
);
|
||||
this.toDispose.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
OpenSketch.Commands.OPEN_SKETCH
|
||||
)
|
||||
)
|
||||
);
|
||||
this.sketchbook.registerRecursively(
|
||||
[...container.children, ...container.sketches],
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
try {
|
||||
const containers = await this.examplesService.builtIns();
|
||||
for (const container of containers) {
|
||||
this.builtInExamples.registerRecursively(
|
||||
container,
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error when collecting built-in examples.', e);
|
||||
}
|
||||
const options = {
|
||||
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
y:
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
execute: async (arg) => {
|
||||
const toOpen = !SketchLocation.is(arg)
|
||||
? await this.selectSketch()
|
||||
: arg;
|
||||
if (toOpen) {
|
||||
return this.openSketch(toOpen);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: 'Open...',
|
||||
order: '1',
|
||||
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
|
||||
order: '2',
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
override 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,
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
async openSketch(
|
||||
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
|
||||
): Promise<void> {
|
||||
const sketch = await toOpen;
|
||||
if (sketch) {
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
}
|
||||
}
|
||||
|
||||
protected async selectSketch(): Promise<Sketch | undefined> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri)
|
||||
);
|
||||
const { filePaths } = await remote.dialog.showOpenDialog({
|
||||
private async selectSketch(): Promise<Sketch | undefined> {
|
||||
const defaultPath = await this.defaultPath();
|
||||
const { filePaths } = await this.dialogService.showOpenDialog({
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: 'Sketch',
|
||||
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
|
||||
extensions: ['ino', 'pde'],
|
||||
},
|
||||
],
|
||||
@@ -170,38 +103,17 @@ export class OpenSketch extends SketchContribution {
|
||||
}
|
||||
const sketchFilePath = filePaths[0];
|
||||
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
|
||||
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
|
||||
const sketch = await this.sketchesService.getSketchFolder(sketchFileUri);
|
||||
if (sketch) {
|
||||
return sketch;
|
||||
}
|
||||
if (Sketch.isSketchFile(sketchFileUri)) {
|
||||
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?`,
|
||||
return promptMoveSketch(sketchFileUri, {
|
||||
fileService: this.fileService,
|
||||
sketchesService: this.sketchesService,
|
||||
labelProvider: this.labelProvider,
|
||||
dialogService: this.dialogService,
|
||||
});
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,8 +123,59 @@ export namespace OpenSketch {
|
||||
export const OPEN_SKETCH: Command = {
|
||||
id: 'arduino-open-sketch',
|
||||
};
|
||||
export const OPEN_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function promptMoveSketch(
|
||||
sketchFileUri: string | URI,
|
||||
options: {
|
||||
fileService: FileService;
|
||||
sketchesService: SketchesService;
|
||||
labelProvider: LabelProvider;
|
||||
dialogService: DialogService;
|
||||
}
|
||||
): Promise<Sketch | undefined> {
|
||||
const { fileService, sketchesService, labelProvider, dialogService } =
|
||||
options;
|
||||
const uri =
|
||||
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
|
||||
const name = uri.path.name;
|
||||
const nameWithExt = labelProvider.getName(uri);
|
||||
const { response } = await dialogService.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 dialogService.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,5 +1,4 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import {
|
||||
Contribution,
|
||||
@@ -9,29 +8,34 @@ import {
|
||||
CommandRegistry,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { AppService } from '../app-service';
|
||||
|
||||
@injectable()
|
||||
export class QuitApp extends Contribution {
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
@inject(AppService)
|
||||
private readonly appService: AppService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
if (!isOSX) {
|
||||
registry.registerCommand(QuitApp.Commands.QUIT_APP, {
|
||||
execute: () => remote.app.quit(),
|
||||
execute: () => this.appService.quit(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
// On macOS we will get the `Quit ${YOUR_APP_NAME}` menu item natively, no need to duplicate it.
|
||||
if (!isOSX) {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__QUIT_GROUP, {
|
||||
commandId: QuitApp.Commands.QUIT_APP.id,
|
||||
label: 'Quit',
|
||||
label: nls.localize('vscode/bulkEditService/quit', 'Quit'),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
if (!isOSX) {
|
||||
registry.registerKeybinding({
|
||||
command: QuitApp.Commands.QUIT_APP.id,
|
||||
|
||||
@@ -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, true);
|
||||
|
||||
// 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,33 +1,53 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { remote } from 'electron';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
|
||||
import { Saveable } from '@theia/core/lib/browser/saveable';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { StartupTasks } from '../../electron-common/startup-task';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
import {
|
||||
SketchContribution,
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { DeleteSketch } from './delete-sketch';
|
||||
import {
|
||||
RenameCloudSketch,
|
||||
RenameCloudSketchParams,
|
||||
} from './rename-cloud-sketch';
|
||||
import { assertConnectedToBackend } from './save-sketch';
|
||||
|
||||
@injectable()
|
||||
export class SaveAsSketch extends SketchContribution {
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
export class SaveAsSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
|
||||
execute: (args) => this.saveAs(args),
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
label: 'Save As...',
|
||||
label: nls.localizeByDefault('Save As...'),
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+Shift+S',
|
||||
@@ -37,71 +57,195 @@ export class SaveAsSketch extends SketchContribution {
|
||||
/**
|
||||
* Resolves `true` if the sketch was successfully saved as something.
|
||||
*/
|
||||
async saveAs(
|
||||
private async saveAs(
|
||||
{
|
||||
execOnlyIfTemp,
|
||||
openAfterMove,
|
||||
wipeOriginal,
|
||||
markAsRecentlyOpened,
|
||||
}: 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,
|
||||
assertConnectedToBackend({
|
||||
connectionStatusService: this.connectionStatusService,
|
||||
messageService: this.messageService,
|
||||
});
|
||||
if (!filePath || canceled) {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
|
||||
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 workspaceUri = await this.sketchService.copy(sketch, {
|
||||
|
||||
const copiedSketch = await this.sketchesService.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 */
|
||||
const newWorkspaceUri = copiedSketch.uri;
|
||||
|
||||
await saveOntoCopiedSketch(
|
||||
sketch,
|
||||
newWorkspaceUri,
|
||||
this.shell,
|
||||
this.editorManager
|
||||
);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
|
||||
}
|
||||
}
|
||||
this.workspaceService.open(new URI(workspaceUri), {
|
||||
const options: WorkspaceInput & StartupTasks = {
|
||||
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 }],
|
||||
});
|
||||
}
|
||||
return !!workspaceUri;
|
||||
this.workspaceService.open(new URI(newWorkspaceUri), options);
|
||||
}
|
||||
return !!newWorkspaceUri;
|
||||
}
|
||||
|
||||
private async createCloudCopy(
|
||||
params: RenameCloudSketchParams
|
||||
): Promise<string | undefined> {
|
||||
return this.commandService.executeCommand<string>(
|
||||
RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH.id,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
private async createLocalCopy(
|
||||
sketch: Sketch,
|
||||
execOnlyIfTemp?: boolean
|
||||
): Promise<string | undefined> {
|
||||
const isTemp = await this.sketchesService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const sketchbookDirUri = 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 this.dialogService.showSaveDialog({
|
||||
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 this.dialogService.showMessageBox({
|
||||
message,
|
||||
buttons: [Dialog.CANCEL, Dialog.YES],
|
||||
});
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
sketchFolderDestinationUri = destinationUri;
|
||||
}
|
||||
}
|
||||
return sketchFolderDestinationUri;
|
||||
}
|
||||
}
|
||||
|
||||
interface InvalidSketchFolderDialogContent {
|
||||
readonly message: string;
|
||||
readonly details: string;
|
||||
readonly question: string;
|
||||
}
|
||||
|
||||
export namespace SaveAsSketch {
|
||||
@@ -117,12 +261,59 @@ export namespace SaveAsSketch {
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveOntoCopiedSketch(
|
||||
sketch: Sketch,
|
||||
newSketchFolderUri: string,
|
||||
shell: ApplicationShell,
|
||||
editorManager: EditorManager
|
||||
): Promise<void> {
|
||||
const widgets = shell.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 editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,55 +1,63 @@
|
||||
import { injectable } from 'inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
|
||||
import {
|
||||
SketchContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
MenuModelRegistry,
|
||||
SketchContribution,
|
||||
} from './contribution';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
|
||||
@injectable()
|
||||
export class SaveSketch extends SketchContribution {
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override 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 {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
label: 'Save',
|
||||
order: '6',
|
||||
label: nls.localize('vscode/fileCommands/save', 'Save'),
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
override 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> {
|
||||
assertConnectedToBackend({
|
||||
connectionStatusService: this.connectionStatusService,
|
||||
messageService: this.messageService,
|
||||
});
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async saveSketch(): Promise<void> {
|
||||
return this.commandService.executeCommand(CommonCommands.SAVE_ALL.id);
|
||||
}
|
||||
}
|
||||
@@ -59,8 +67,20 @@ export namespace SaveSketch {
|
||||
export const SAVE_SKETCH: Command = {
|
||||
id: 'arduino-save-sketch',
|
||||
};
|
||||
export const SAVE_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-save-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/arduino/arduino-ide/issues/2081
|
||||
export function assertConnectedToBackend(param: {
|
||||
connectionStatusService: ApplicationConnectionStatusContribution;
|
||||
messageService: MessageService;
|
||||
}): void {
|
||||
if (param.connectionStatusService.offlineStatus === 'backend') {
|
||||
const message = nls.localize(
|
||||
'theia/core/couldNotSave',
|
||||
'Could not save the sketch. Please copy your unsaved work into your favorite text editor, and restart the IDE.'
|
||||
);
|
||||
param.messageService.error(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
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 type {
|
||||
BoardList,
|
||||
BoardListItem,
|
||||
} from '../../common/protocol/board-list';
|
||||
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.onBoardListDidChange(() =>
|
||||
this.update(this.boardsServiceProvider.boardList)
|
||||
);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.update(this.boardsServiceProvider.boardList);
|
||||
}
|
||||
|
||||
private update(boardList: BoardList): void {
|
||||
const { selectedBoard, selectedPort } = boardList.boardsConfig;
|
||||
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) {
|
||||
const notConnectedLabel = nls.localize(
|
||||
'arduino/common/notConnected',
|
||||
'[not connected]'
|
||||
);
|
||||
let portLabel = notConnectedLabel;
|
||||
if (selectedPort) {
|
||||
portLabel = nls.localize(
|
||||
'arduino/common/selectedOn',
|
||||
'on {0}',
|
||||
selectedPort.address
|
||||
);
|
||||
const selectedItem: BoardListItem | undefined =
|
||||
boardList.items[boardList.selectedIndex];
|
||||
if (!selectedItem) {
|
||||
portLabel += ` ${notConnectedLabel}`; // append ` [not connected]` when the port is selected but it's not detected by the CLI
|
||||
}
|
||||
}
|
||||
this.statusBar.setElement('arduino-selected-port', {
|
||||
alignment: StatusBarAlignment.RIGHT,
|
||||
text: portLabel,
|
||||
className: 'arduino-selected-port',
|
||||
});
|
||||
} else {
|
||||
this.statusBar.removeElement('arduino-selected-port');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +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__PREFERENCES_GROUP, {
|
||||
commandId: Settings.Commands.OPEN.id,
|
||||
label: 'Preferences...',
|
||||
order: '0',
|
||||
});
|
||||
registry.registerSubmenu(ArduinoMenus.FILE__ADVANCED_SUBMENU, 'Advanced');
|
||||
}
|
||||
|
||||
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,51 +1,39 @@
|
||||
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 { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import {
|
||||
URI,
|
||||
SketchContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
MenuModelRegistry,
|
||||
open,
|
||||
SketchContribution,
|
||||
TabBarToolbarRegistry,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { SketchesServiceClientImpl } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
|
||||
|
||||
@injectable()
|
||||
export class SketchControl extends SketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(LocalCacheFsProvider)
|
||||
protected readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
private readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
protected readonly toDisposeBeforeCreateNewContextMenu =
|
||||
new DisposableCollection();
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(
|
||||
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR,
|
||||
{
|
||||
@@ -53,47 +41,28 @@ export class SketchControl extends SketchContribution {
|
||||
this.shell.getWidgets('main').indexOf(widget) !== -1,
|
||||
execute: async () => {
|
||||
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!sketch) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parentElement: HTMLElement | undefined = undefined;
|
||||
const target = document.getElementById(
|
||||
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
||||
);
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
if (target instanceof HTMLElement) {
|
||||
parentElement = target.parentElement ?? undefined;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainFileUri, rootFolderFileUris } =
|
||||
await this.sketchService.loadSketch(sketch.uri);
|
||||
const uris = [mainFileUri, ...rootFolderFileUris];
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSketch =
|
||||
await this.sketchesServiceClient.currentSketch();
|
||||
const parentsketchUri = this.editorManager.currentEditor
|
||||
?.getResourceUri()
|
||||
?.toString();
|
||||
const parentsketch = await this.sketchService.getSketchFolder(
|
||||
parentsketchUri || ''
|
||||
);
|
||||
|
||||
// if the current file is in the current opened sketch, show extra menus
|
||||
if (
|
||||
currentSketch &&
|
||||
parentsketch &&
|
||||
parentsketch.uri === currentSketch.uri &&
|
||||
(await this.allowRename(parentsketch.uri))
|
||||
) {
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_RENAME.id,
|
||||
label: 'Rename',
|
||||
label: nls.localize('vscode/fileActions/rename', 'Rename'),
|
||||
order: '1',
|
||||
}
|
||||
);
|
||||
@@ -104,33 +73,12 @@ export class SketchControl extends SketchContribution {
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const renamePlaceholder = new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
'Rename'
|
||||
);
|
||||
this.menuRegistry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
renamePlaceholder
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuNode(renamePlaceholder.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
currentSketch &&
|
||||
parentsketch &&
|
||||
parentsketch.uri === currentSketch.uri &&
|
||||
(await this.allowDelete(parentsketch.uri))
|
||||
) {
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
|
||||
label: 'Delete',
|
||||
commandId: WorkspaceCommands.FILE_DELETE.id,
|
||||
label: nls.localize('vscode/fileActions/delete', 'Delete'),
|
||||
order: '2',
|
||||
}
|
||||
);
|
||||
@@ -141,22 +89,9 @@ export class SketchControl extends SketchContribution {
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const deletePlaceholder = new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
'Delete'
|
||||
);
|
||||
this.menuRegistry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
deletePlaceholder
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuNode(deletePlaceholder.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||
const uris = [mainFileUri, ...rootFolderFileUris];
|
||||
for (let i = 0; i < uris.length; i++) {
|
||||
const uri = new URI(uris[i]);
|
||||
|
||||
@@ -175,7 +110,7 @@ export class SketchControl extends SketchContribution {
|
||||
{
|
||||
commandId: command.id,
|
||||
label: this.labelProvider.getName(uri),
|
||||
order: `${i}`,
|
||||
order: String(i).padStart(4),
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
@@ -192,6 +127,7 @@ export class SketchControl extends SketchContribution {
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
showDisabled: true,
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
},
|
||||
@@ -199,12 +135,12 @@ export class SketchControl extends SketchContribution {
|
||||
);
|
||||
}
|
||||
|
||||
registerMenus(registry: MenuModelRegistry): void {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.NEW_FILE.id,
|
||||
label: 'New Tab',
|
||||
label: nls.localize('vscode/menubar/mNewTab', 'New Tab'),
|
||||
order: '0',
|
||||
}
|
||||
);
|
||||
@@ -213,7 +149,7 @@ export class SketchControl extends SketchContribution {
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
|
||||
{
|
||||
commandId: CommonCommands.PREVIOUS_TAB.id,
|
||||
label: 'Previous Tab',
|
||||
label: nls.localize('vscode/menubar/mShowPreviousTab', 'Previous Tab'),
|
||||
order: '0',
|
||||
}
|
||||
);
|
||||
@@ -221,20 +157,20 @@ export class SketchControl extends SketchContribution {
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__NAVIGATION_GROUP,
|
||||
{
|
||||
commandId: CommonCommands.NEXT_TAB.id,
|
||||
label: 'Next Tab',
|
||||
label: nls.localize('vscode/menubar/mShowNextTab', 'Next Tab'),
|
||||
order: '0',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
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', // TODO: check why electron does not show the keybindings in the UI.
|
||||
keybinding: 'CtrlCmd+Alt+Left',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: CommonCommands.NEXT_TAB.id,
|
||||
@@ -242,36 +178,19 @@ export class SketchControl extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
||||
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
||||
});
|
||||
}
|
||||
|
||||
protected async isCloudSketch(uri: string) {
|
||||
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
|
||||
|
||||
if (cloudCacheLocation) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async allowRename(uri: string) {
|
||||
return !this.isCloudSketch(uri);
|
||||
}
|
||||
|
||||
protected async allowDelete(uri: string) {
|
||||
return !this.isCloudSketch(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SketchControl {
|
||||
export namespace Commands {
|
||||
export const OPEN_SKETCH_CONTROL__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch-control--toolbar',
|
||||
iconClass: 'fa fa-caret-down',
|
||||
iconClass: 'fa fa-arduino-sketch-tabs-menu',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { SaveableWidget } from '@theia/core/lib/browser/saveable';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/common/files';
|
||||
import { CurrentSketch } from '../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();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user