Compare commits

...

183 Commits

Author SHA1 Message Date
Akos Kitta
05850b5f27 Ensure default data and sketch dir existence.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 15:13:53 +02:00
Akos Kitta
21dedd4b09 DROPME: disabled the uglification in the bundle.js
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 14:10:06 +02:00
Akos Kitta
90d7d88162 Updated the application name, version to 0.0.2.
Increased the heap size for the packager.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
3efb5a4e08 Switched from execFile to spawn.
So that we can add guards against whitespaces in the path.
Also cleaned up the code a bit.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
4353bfb5b9 Updated port discovery to support unknown boards
From now on, we do not retrieve the ports from the attached boards.
A board can be unknown but the port is still relevant.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
8971dc4c5f Implemented naive reconnecting.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
2be54944bf Fixed the checkmark position in the dropdown.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
2f84b5c6b7 Show the CLI version from the about dialog.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
cebe15ef69 Removed duplicate log level from the log.
Print CLI version at start-up.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
1dda5dd95b Exposed version from the CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
de1caf1451 PROEDITOR-46: Added a core auto-installer.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:33 +02:00
Akos Kitta
fb6785c5d3 PROEDITOR-53: Changed the way we set the workspace
Got rid of the `sketch` search parameter from the URL.

Rules:
 - Get the desired workspace location from the
  - `Path` defined as the `window.location.hash` of the URL,
  - most recent workspaces,
  - most recent sketches from the default sketch folder.
 - Validate the location.
 - If no valid location was found, create a new sketch in the default sketch folder.

Note: when validating the location of the workspace root, the root must always exist. However, when in pro-mode, the desired workspace root must
not be a sketch directory with the `.ino` file, but can be any existing location.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-25 11:48:24 +02:00
Akos Kitta
de1f341d19 Merge pull request #74 from bcmi-labs/remove-tab-context-menu-no-pro-mode
Removed the tab context menu when not in pro-mode.
2019-10-23 10:30:30 +02:00
Akos Kitta
817a28291b Removed the tab context menu when not in pro-mode.
When we are not in `pro-mode`, it does not make much sense to show the
context menu. It does not work anyways.

See: https://arduino.slack.com/archives/CJJHJCJSJ/p1569502896082800?thread_ts=1569489282.073200&cid=CJJHJCJSJ

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-23 09:57:56 +02:00
Akos Kitta
06ef598806 Merge pull request #72 from bcmi-labs/azure-service-connection-update
Switched to `typefox-service-account1`.
2019-10-17 10:00:27 +02:00
Akos Kitta
cc6a0ae212 Fixed various electron packager issues
- We have to reuse the `yarn.lock` file for the final app, not just
for the extensions.
 - Simplified the clean-up phase.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-17 08:45:26 +02:00
Akos Kitta
65a58ce2be Switched to typefox-service-account1.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-10-16 13:56:40 +02:00
Akos Kitta
6331b7ddfd Merge pull request #65 from bcmi-labs/increase_maxBuffer
Increase maxBuffer for spawned daemon
2019-10-07 16:57:25 +02:00
Miro Spönemann
f3667f0270 Merge pull request #67 from bcmi-labs/restartLSOnBoardChange
Restart the language server when the board is changed
2019-10-04 11:54:13 +02:00
Miro Spönemann
aa4f216544 Restart the language server when the board is changed 2019-10-02 13:16:10 +02:00
Akos Kitta
065f9f042b Merge pull request #66 from bcmi-labs/various-bug-fixes
Various bug fixes
2019-09-26 11:59:23 +02:00
Akos Kitta
765fcdfba7 Fixed the styles for the boards list dropdown.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-26 10:15:57 +02:00
Akos Kitta
476e658fea Cleaned up the menu contributions.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-26 10:15:30 +02:00
Akos Kitta
a96ed31a45 Fixed the daemon log.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-25 16:34:42 +02:00
Miro Spönemann
ed4f23a32a Merge pull request #64 from bcmi-labs/msp_sendBoardConfig
Added JSON-RPC message to send the board configuration to the language server
2019-09-25 09:12:59 +02:00
Akos Kitta
4949df7395 Fixed missing New Sketch menu in electron.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-24 16:01:19 +02:00
Akos Kitta
55923be7fd PROEDITOR-48: Open last sketch at start-up
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-24 14:23:16 +02:00
Martino Facchin
2ef0d1d0db Increase maxBuffer for spawned daemon
When installed cores are A LOT, core listing needs a lot of space :)
2019-09-23 17:06:10 +02:00
Akos Kitta
7244694bd3 PROEDITOR-47: FIxed the visibility of close button
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-20 16:09:58 +02:00
Akos Kitta
d9e71c7e0d PROEDITOR-51: Fixed the styling for toggeled icons
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-20 15:39:15 +02:00
Akos Kitta
daedae1ba7 PROEDITOR-50: Customized the editor for built-ins
- From now on, sources from the `dataDir` opened in a read-only editor.
 - Disabled the tab-bar decoration for built-ins.
 - No problem markers for the built-ins.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-20 15:10:38 +02:00
Akos Kitta
ac4e877a10 Open More info in the default browser.
Instead of the Electron application.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-18 15:37:58 +02:00
Miro Spönemann
09243ff74d Added JSON-RPC message to send the board configuration to the language server 2019-09-18 09:46:07 +02:00
Miro Spönemann
5496edbb42 Merge pull request #62 from bcmi-labs/setup_cpp
Configure cpp extension to use embedded clangd
2019-09-16 13:09:23 +02:00
Miro Spönemann
62eff29172 Configure cpp extension to use embedded clangd 2019-09-16 08:42:30 +02:00
jbicker
2220e66f4b Adapted Dark, Light and Arduino themes
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-11 11:55:29 +02:00
jbicker
c98ec29810 Merge branch 'add-cpp-extension' 2019-09-09 17:33:46 +02:00
jbicker
768958dfd5 Updated Yarn.lock
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-09 17:25:07 +02:00
Jan Bicker
8e747e19a6 CPP extension added. Theia updated.
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2019-09-09 17:10:18 +02:00
Miro Spönemann
b1c69aef9f Merge pull request #59 from bcmi-labs/ino-language
Added language support for ino files
2019-09-09 10:28:40 +02:00
Miro Spönemann
ec6b5ed3f3 Added command line argument for specifying cli path 2019-09-06 14:15:59 +02:00
Miro Spönemann
60bf58ac0f Added download script for arduino-language-server 2019-09-06 14:15:59 +02:00
Miro Spönemann
c2675efea4 Fixed z-index of main toolbar 2019-09-06 14:15:59 +02:00
Miro Spönemann
7d04c7efb8 Added language contribution for ino files 2019-09-06 14:14:35 +02:00
Jan Bicker
fb542e2e40 Merge pull request #56 from bcmi-labs/advanced-mode-in-top-panel
Advanced mode in top panel
2019-09-06 13:08:18 +02:00
Jan Bicker
3e0842e93a Merge pull request #54 from bcmi-labs/monitor
Serial Monitor
2019-09-06 13:06:56 +02:00
Jan Bicker
90add23dae Merge pull request #61 from bcmi-labs/file-menu-fix
Fixed file menu
2019-09-06 13:05:18 +02:00
jbicker
6ff5405337 show advanced mode button in top panel
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-04 09:07:57 +02:00
jbicker
c564572718 Fixed file menu, New Sketch and New File items were not shown in main menu
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:44:50 +02:00
jbicker
79731304c1 Reestablish monitor connection after reloading the window, reconnect after selecting another board
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
2046c0bdee Continue monitoring with last connection if respective board is reconnected
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
3eebd580d8 Fixed wrong toppanel layout in electron
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
d8454456a9 Set focus on send field initially
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
dac9c6437e Preserve baud rate and line ending; Use google protobuf Struct type now for setting configs
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
7f33b62e0b Preserve Autoscroll and Timestamp states
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
459e55a69a Improved serial monitor dropdowns
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
f76f4543e9 Minor code improvements
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
9b255ac072 Timestamp formatted
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
592086466c Clear send field after sending the value
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
c81ee1ede2 Arduino Toolbar Tooltips don't get clipped anymore.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
6d2816a7f3 Added icon, changed appearance of active toolbar item
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
dd69092afd Added additional toolbar to the right of the toppanel.
Added Toolbar button for toggling serial monitor and tooltips for serial monitor toolbar items.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:42:00 +02:00
jbicker
76d0f5a464 Implemented the Widget
Re-introduced bottom panel tabs

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-09-03 16:40:19 +02:00
Jan Bicker
206b65f138 Merge pull request #58 from bcmi-labs/use-sketch-as-ws
Use sketch folder as workspace
2019-09-03 16:32:08 +02:00
Akos Kitta
6d590cd111 Fixed the packager for the new folder structure.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-02 14:57:28 +02:00
Akos Kitta
e8e3c3dc4c Use the latest CLI, as stated in the doc.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-09-02 13:45:36 +02:00
Akos Kitta
e6e042c856 Moved System declaration before its use-site.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-30 10:41:05 +02:00
Akos Kitta
9298a8cc55 Log clean-up.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-30 10:26:13 +02:00
Akos Kitta
98764b56aa Switched to the JSON log format for the daemon.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-30 10:11:43 +02:00
Akos Kitta
9f7aec4091 Adapted to latest CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 09:52:56 +02:00
Akos Kitta
e636e06a7e Download today's CLI.
The issues was an interim CI problem.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 09:21:51 +02:00
Akos Kitta
c6311ecb1d Adapted the CLI download script.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 09:17:44 +02:00
Akos Kitta
cd94608aee Renamed the applications.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-29 08:05:47 +02:00
Jan Bicker
b82d5e4f0b Use sketch directory as url param when a new created sketch gets opened
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2019-08-28 08:13:11 +00:00
Akos Kitta
9ae721292d Fixed the FS path issue on Windows.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-27 18:27:12 +02:00
jbicker
41c603937c Setting download dir to {dataDir}/staging; Create data and sketch folders if they don't exist.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-08-26 16:53:59 +02:00
jbicker
d5589c435f Get the default sketchbook path from backend
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-08-26 15:59:18 +02:00
jbicker
d809daa20a Use sketch folder as workspace
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-08-26 13:20:01 +02:00
Akos Kitta
f9641a3d76 Initial support of the default paths from the CLI.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-26 12:42:27 +02:00
Sven Efftinge
59553bf81f Merge pull request #55 from bcmi-labs/se/better-output
Better output format
2019-08-16 14:00:44 +02:00
Sven Efftinge
037efbaba2 Better output format 2019-08-15 11:50:50 +00:00
Luca Cipriani
a936e4c505 Merge pull request #49 from bcmi-labs/boards-manager
generalized the boards and the libraries views.
2019-08-14 17:34:55 +02:00
Akos Kitta
7c2a295631 Fixed bug when checking if a core is installed
`startsWith` was incorrect: `arduino:samd` Vs. `arduino:samd_beta`

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-12 11:40:39 +02:00
Akos Kitta
c5796677f8 Ignore whitespaces when searhing for libs, cores.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-12 11:39:02 +02:00
Akos Kitta
b6306c330f Update/download the library_index.json at start
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:42:24 +02:00
Akos Kitta
692c3f6e3f Implemented serial-monitoring for the backend.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:42:24 +02:00
Akos Kitta
8d79bb3ffb restored the the search limit for the lib manager.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
ec50ea673c do not await for attached boards.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
502e9042ad fine tuned selectedPort update
based on the detached boards.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
66f429c478 workaround for non-unique names.
Fine tuned the port unnselection when attached boards change.

This should make sure we do not have to `await` for the attached boards
from the backend.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
0dc45daf01 aligned list view styles.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
3fcf5a6ac9 generalized the boards and the libraries views.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 19:40:25 +02:00
Akos Kitta
b24d440e22 Merge pull request #53 from bcmi-labs/PROEDITOR-27
PROEDITOR-27: Try to preserve the window.
2019-08-05 13:47:53 +02:00
Akos Kitta
af9b9fbeab Yet another attempt to fix timeout issues on Azure.
```
info There appears to be trouble with your network connection. Retrying...
error An unexpected error occurred: "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz: ESOCKETTIMEDOUT".
info If you think this is a bug, please open a bug report with the information provided in "D:\\a\\1\\s\\yarn-error.log".
```

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 13:17:59 +02:00
Stefania
37db6c4b43 Merge pull request #51 from bcmi-labs/search-in-workspace-and-git
Added "Search In Workspace" and "SCM/Git" Extensions.
2019-08-05 11:12:46 +02:00
Jan Bicker
9d5ad9b003 Added "Search In Workspace" and "SCM/Git" Extensions.
Signed-off-by: Jan Bicker <jan.bicker@typefox.io>
2019-08-05 09:58:52 +02:00
Akos Kitta
ded838b4e8 Fixed the preloadTemplate for electron.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 09:57:07 +02:00
Akos Kitta
82df8a6add PROEDITOR-27: Try to preserve the window.
When opening sketches. Rules:
 - If `sketch` is missing from the URL, we reuse the same window.
 - NOOP, if we try to open the currently opened sketch.
 - Otherwise, use the existing logic: open sketch in a new window.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-08-05 09:57:05 +02:00
Luca Cipriani
2914379586 Merge pull request #46 from bcmi-labs/poll-boards
Implemented the board discovery with polling.
2019-08-02 17:08:08 +02:00
Akos Kitta
6b25659fa6 Merge pull request #48 from bcmi-labs/monitoring-api
Updated the TS/JS API with the monitoring.
2019-07-30 13:04:46 +02:00
Akos Kitta
27dc6f9816 Updated the TS/JS API with the monitoring.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-30 10:57:23 +02:00
Akos Kitta
b78ddbeb64 Implemented the board discovery with polling.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-30 10:32:10 +02:00
jbicker
db78c8ac2d Minor css adjustments
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-26 12:00:26 +02:00
Luca Cipriani
95c5536060 Merge pull request #45 from bcmi-labs/new-file-menu-item
Added 'New File' Item in file menu
2019-07-26 11:37:48 +02:00
Akos Kitta
0aa34b1169 Added the Prerequisites section to the README.
Closes #44.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-26 11:25:40 +02:00
jbicker
b7d951b809 Added 'New File' Item in file menu
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-26 11:00:18 +02:00
Luca Cipriani
e11d9e0c78 Merge pull request #43 from bcmi-labs/toggle-pro-mode
One can toggle between pro and simple mode
2019-07-25 18:29:17 +02:00
jbicker
436e660d47 Avoid duplicate react keys in board select dialog
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-25 18:04:59 +02:00
jbicker
23a967bd4c One can toggle between pro and simple mode
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-25 17:38:16 +02:00
Jan Bicker
17fab651e5 Merge pull request #41 from bcmi-labs/made-board-selector-async
Made boards selector async
2019-07-25 08:04:02 -07:00
Luca Cipriani
df7225c32b Merge pull request #29 from bcmi-labs/PROEDITOR-20
PROEDITOR-20: Restored the dirt flag in editor tab
2019-07-25 16:52:50 +02:00
Akos Kitta
69f63668b2 Merge pull request #42 from bcmi-labs/terminate-daemon
Fixed the daemon process termination.
2019-07-25 16:44:04 +02:00
Akos Kitta
d29ed24e49 Fixed the daemon process termination.
From now on, we spawn a detached process that
will periodically check whether the parent Theia
the process is alive, if no, terminates the daemon.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-25 16:11:40 +02:00
jbicker
a5294417c3 Made boards selector async
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-25 11:20:56 +02:00
Akos Kitta
83e966d208 Merge pull request #40 from bcmi-labs/arduino-cli
Switched to the official `arduino-cli`.
2019-07-25 09:57:13 +02:00
Luca Cipriani
5cc75118cd Merge pull request #30 from bcmi-labs/PROEDITOR-19
PROEDITOR-19: Open new sketches in new windows
2019-07-25 09:21:15 +02:00
Akos Kitta
19f058cb7f Run the Board.List requests sequentially.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-25 08:50:05 +02:00
Akos Kitta
7494beca33 Switched to the official arduino-cli.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-25 07:58:00 +02:00
Sven Efftinge
835107b5a6 [styling] use arduino theme background color from start 2019-07-24 15:00:01 +00:00
jbicker
4ced8237f7 Merge branch 'sketch-loader-bug-fix' 2019-07-24 16:50:52 +02:00
Jan Bicker
40eb74aeff Merge pull request #39 from bcmi-labs/sketch-loader-bug-fix-win
[win] Open sketch.
2019-07-24 07:48:50 -07:00
Jan Bicker
457b283ca5 Use theia main menu factory for electron again. 2019-07-24 07:47:02 -07:00
Akos Kitta
c0e279f3e8 [win] Open sketch.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-23 21:35:06 +02:00
jbicker
75f7d3ca7c Fixed sketch service 2019-07-23 16:42:25 +02:00
Jan Bicker
cd8c138e1e Merge pull request #38 from bcmi-labs/status-bar-contribution
Added status bar contribution for line number and selected board.
2019-07-23 16:19:38 +02:00
jbicker
c0dea5c6b7 Added status bar contribution for line number and selected board.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-23 15:39:38 +02:00
Akos Kitta
c45e85eb89 PROEDITOR-19: Open new sketches in new windows
- From now on, each new sketch has its own window.
 - Fixed the `Download (of)? X completed` logging during index update.
 - Validate the file before opening the sketch in new window.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-23 08:25:49 +02:00
Akos Kitta
87cf5c6fd7 Merge pull request #34 from bcmi-labs/PROEDITOR-7
PROEDITOR-7: Cloned the Library Manager layout.
2019-07-22 16:46:18 +02:00
jbicker
0bfb4ba6ab Added noselect class to boards toolbar item
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-22 16:08:29 +02:00
Akos Kitta
8d838fcce4 PROEDITOR-7: Cloned the Library Manager layout.
To match with the official Arduino editor's UI.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-22 15:52:08 +02:00
Akos Kitta
d6637c44e5 PROEDITOR-20: Restored the dirt flag in editor tab
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-07-22 14:19:49 +02:00
Luca Cipriani
cf44fe2ad0 Merge pull request #36 from bcmi-labs/board-select-dialog-ui-improvements
Improved layout of "Board Select" and "File Navigator" dialogs
2019-07-22 14:10:34 +02:00
jbicker
a6e1c4a93e Improved layout of "Board Select" and "File Navigator" dialogs
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-22 14:09:55 +02:00
Luca Cipriani
269f08b74d Merge pull request #37 from bcmi-labs/board-selector-toolbar-item
Board selector toolbar item
2019-07-22 14:03:13 +02:00
jbicker
4d2bd87f74 Implemented custom dropdown for board selection
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-22 11:06:54 +02:00
jbicker
c2fbccc9e8 App doesn't show "open..." anymore if there are no sketches in default sketch folder.
Opens file navigator directly instead.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-18 13:49:28 +02:00
jbicker
6e0a0a19c5 Fixed Open Button
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-18 11:35:54 +02:00
jbicker
c88d8dd17b Small ui fix. Adapted css for browser menu active items.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-18 10:08:49 +02:00
Jan Bicker
7dfb28975e Merge pull request #35 from bcmi-labs/ui-improvements
Ui improvements
2019-07-18 09:59:10 +02:00
jbicker
2d4c710b6a Syntax coloring
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-17 18:40:48 +02:00
jbicker
dfc2ef967a Colors adapted
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 15:56:45 +02:00
jbicker
e2d3c7c1bd Made buttons bigger
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 15:56:45 +02:00
Luca Cipriani
5a97a71d9b Merge pull request #32 from bcmi-labs/bring-back-menu
PROEDITOR-22: Brought back the menu.
2019-07-15 15:51:39 +02:00
jbicker
5e728523ef Changed layout of top panel in browser.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 13:12:33 +02:00
jbicker
2a0273c771 Brought back the menu.
With a restricted set of items.

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-15 12:53:17 +02:00
Luca Cipriani
35086ca1a1 Merge pull request #31 from bcmi-labs/open-boards-dialog
PROEDITOR-9: Open boards dialog
2019-07-15 12:44:16 +02:00
jbicker
23446284b7 Toggle the selected board in dropdown
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-10 17:48:20 +02:00
jbicker
89fb2fddbd Cache attached boards
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-10 11:50:44 +02:00
jbicker
a039597d40 Select Board Dialog Style and Layout
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-09 18:31:08 +02:00
jbicker
4429094139 Styling of select board and port dialog
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-08 13:55:31 +02:00
jbicker
769689ff6d Added dialog implementation
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-07-08 11:58:59 +02:00
jbicker
4c66dec36e Added board select toolbar item
fill context menu with connected boards

Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-26 14:08:45 +02:00
jbicker
ff336dccc5 Enhanced boards toolbar item.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-26 09:03:26 +02:00
Jan Bicker
75ef8ea987 Merge pull request #27 from bcmi-labs/electron-4x-menu
Several UI enhancements
2019-06-26 09:02:14 +02:00
jbicker
1b90b7af29 Updated theia
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-26 08:43:38 +02:00
jbicker
c7c5ab57c2 Bugfix and yarn.lock update
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-25 17:37:58 +02:00
Akos Kitta
b7e17b5828 Updated to next (26c3273a) Theia.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-06-25 08:47:58 +02:00
jbicker
6195da3858 Added boards toolbar item.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-24 18:00:17 +02:00
jbicker
c54ae96a3c Overwritten EditorWidgetFactory, made editor widget not closable.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-24 15:09:04 +02:00
jbicker
54d90d5413 Added save button and turned auto save on by default.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-21 13:11:16 +02:00
jbicker
f4f7a9998e Small fixes
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-20 17:28:53 +02:00
jbicker
4096afde96 Open the sketch in a new window.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-20 09:35:56 +02:00
jbicker
92afa48c05 Implemented possibility to open sketches via file navigator.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-19 11:30:26 +02:00
jbicker
9d3cbf2ea0 Use service to load sketches
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-19 09:48:28 +02:00
jbicker
0c937212e2 Added Open Toolbutton
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-17 18:26:24 +02:00
Jan Bicker
7760915014 Merge pull request #23 from bcmi-labs/ui-changes
Ui changes
2019-06-17 09:36:26 +02:00
jbicker
f9746b350d Further customization of the statusbar
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 18:32:32 +02:00
jbicker
2066f20d78 Added the tooltip to the right of the toolbar
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 17:37:46 +02:00
jbicker
f5560626e5 Removed/hidden elements from bottom widget bar (Log output)
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 16:08:19 +02:00
jbicker
1f1861e1d8 Adapted design of toolbar.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-14 15:39:03 +02:00
jbicker
6a956afbcd Added toolbar to top panel.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 17:02:07 +02:00
jbicker
4c3becd3e8 Sidebar with Filenavigator, Librarylist and Boardlist does not get rendered initially anymore.
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 10:10:29 +02:00
jbicker
088951ae01 More color adapted
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 08:30:59 +02:00
jbicker
b7cf9e6755 Removed menu, changed colors
Signed-off-by: jbicker <jan.bicker@typefox.io>
2019-06-13 07:55:58 +02:00
Akos Kitta
1d6ba97d68 Merge pull request #22 from bcmi-labs/build-fix2
Yet another attempt to fix the build.
2019-05-10 11:08:58 +02:00
Akos Kitta
94b9fbe29a Yet another attempt to fix the build.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-05-10 10:23:40 +02:00
Christian Weichel
d6fb7e2466 Update known-issues.txt 2019-05-09 20:23:05 +02:00
Christian Weichel
7da8851fad Added refresh attached boards command 2019-05-09 20:22:17 +02:00
Sven Efftinge
97135bd08e More styling impr. and less UI contributions 2019-05-09 19:32:01 +02:00
Christian Weichel
20db72fec7 Update known-issues.txt 2019-05-09 11:40:24 +02:00
Akos Kitta
d670e66f0e Merge pull request #18 from bcmi-labs/doc
Updated with the application download link.
2019-05-09 11:04:21 +02:00
Akos Kitta
93daed6a69 Updated with the application download link.
Signed-off-by: Akos Kitta <kittaakos@typefox.io>
2019-05-09 11:02:56 +02:00
Christian Weichel
f592ac69f0 Updated known issues 2019-05-09 10:49:38 +02:00
161 changed files with 24235 additions and 13424 deletions

4
.gitignore vendored
View File

@@ -3,9 +3,11 @@ node_modules/
.node_modules/
lib/
build/
downloads/
!electron/build/
src-gen/
arduino-ide-*/webpack.config.js
browser-app/webpack.config.js
electron-app/webpack.config.js
.DS_Store
/workspace/static
# switching from `electron` to `browser` in dev mode.

View File

@@ -8,7 +8,7 @@ ports:
tasks:
- init: >
yarn &&
yarn --cwd ./arduino-ide-browser start
yarn --cwd ./browser-app start
github:
prebuilds:

13
.vscode/launch.json vendored
View File

@@ -4,11 +4,18 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Electron Packager",
"program": "${workspaceRoot}/electron/packager/index.js",
"cwd": "${workspaceFolder}/electron/packager"
},
{
"type": "node",
"request": "launch",
"name": "Launch Backend",
"program": "${workspaceRoot}/arduino-ide-browser/src-gen/backend/main.js",
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
@@ -20,8 +27,8 @@
},
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/arduino-ide-browser/src-gen/backend/*.js",
"${workspaceRoot}/arduino-ide-browser/lib/**/*.js",
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js"
],
"smartStep": true,

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

16
.vscode/tasks.json vendored
View File

@@ -4,9 +4,9 @@
"version": "2.0.0",
"tasks": [
{
"label": "Arduino-PoC - Start Browser Example",
"label": "Arduino Editor - Start Browser Example",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-browser start",
"command": "yarn --cwd ./browser-app start",
"group": "build",
"presentation": {
"reveal": "always",
@@ -15,7 +15,7 @@
}
},
{
"label": "Arduino-PoC - Watch Theia Extension",
"label": "Arduino Editor - Watch Theia Extension",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-extension watch",
"group": "build",
@@ -26,9 +26,9 @@
}
},
{
"label": "Arduino-PoC - Watch Browser Example",
"label": "Arduino Editor - Watch Browser Example",
"type": "shell",
"command": "yarn --cwd ./arduino-ide-browser watch",
"command": "yarn --cwd ./browser-app watch",
"group": "build",
"presentation": {
"reveal": "always",
@@ -37,11 +37,11 @@
}
},
{
"label": "Arduino-PoC - Watch All",
"label": "Arduino Editor - Watch All",
"type": "shell",
"dependsOn": [
"Arduino-PoC - Watch Theia Extension",
"Arduino-PoC - Watch Browser Example"
"Arduino Editor - Watch Theia Extension",
"Arduino Editor - Watch Browser Example"
]
}
]

View File

@@ -9,12 +9,15 @@ It's built on top of a [fork of the arduino-cli](https://github.com/typefox/ardu
## How to try (offline)
### Prerequisites
You should be able to build Theia locally. The requirements are defined [here](https://github.com/theia-ide/theia/blob/master/doc/Developing.md#prerequisites).
```
git clone https://github.com/bcmi-labs/arduino-editor
cd arduino-editor
yarn
yarn rebuild:electron
yarn --cwd arduino-ide-electron start
yarn --cwd electron-app start
```
If you want to switch back to the browser-based example, execute the following in the repository root
@@ -23,13 +26,22 @@ yarn rebuild:browser
```
Then you can start the browser example again:
```
yarn --cwd arduino-ide-browser start
yarn --cwd browser-app start
```
## Arduino-PoC Electron Application
- The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino).
- The configured pipelines are available [here](https://dev.azure.com/typefox/Arduino/_build?definitionId=4).
- Currently, we build for Windows, macOS, and Linux. If you want to download the Arduino-PoC Electron application, follow the steps from below.
## Arduino Pro IDE Electron Application
The project is built on [Azure DevOps](https://dev.azure.com/typefox/Arduino).
Currently, we provide the Arduino Pro IDE for the following platforms:
- Windows,
- macOS, and
- Linux.
You can download the latest release applications fom [here](https://github.com/bcmi-labs/arduino-editor/releases/latest).
If you want to get a nightly build, go to the [Azure DevOps page](https://dev.azure.com/typefox/Arduino/_build?definitionId=4),
and follow the steps from below.
![](static/download_01.gif)
![](static/download_02.gif)
- Click [here](./electron/README.md) more details on the CI/CD, the GitHub release, and the build process in general.
Click [here](./electron/README.md) for more details on the CI/CD, the GitHub release, and the build process in general.

View File

@@ -1,38 +0,0 @@
{
"private": true,
"name": "arduino-ide-browser",
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@theia/core": "next",
"@theia/editor": "next",
"@theia/file-search": "next",
"@theia/filesystem": "next",
"@theia/languages": "next",
"@theia/messages": "next",
"@theia/monaco": "next",
"@theia/navigator": "next",
"@theia/preferences": "next",
"@theia/process": "next",
"@theia/terminal": "next",
"@theia/workspace": "next",
"@theia/textmate-grammars": "next",
"arduino-ide-extension": "0.0.1"
},
"devDependencies": {
"@theia/cli": "next"
},
"scripts": {
"prepare": "theia build --mode development",
"start": "theia start --root-dir=../workspace",
"watch": "theia build --watch --mode development"
},
"theia": {
"frontend": {
"config": {
"applicationName": "Arduino-PoC",
"defaultTheme": "arduino-theme"
}
}
}
}

View File

@@ -8,6 +8,113 @@
"name": "Ino",
"scopeName": "source.ino",
"patterns": [
{
"include": "#special_block"
},
{
"include": "#strings"
},
{
"match": "\\b(friend|explicit|virtual|override|final|noexcept)\\b",
"name": "storage.modifier.cpp"
},
{
"match": "\\b(private:|protected:|public:)",
"name": "storage.modifier.cpp"
},
{
"match": "\\b(catch|operator|try|throw|using)\\b",
"name": "keyword.control.cpp"
},
{
"match": "\\bdelete\\b(\\s*\\[\\])?|\\bnew\\b(?!])",
"name": "keyword.control.cpp"
},
{
"match": "\\b(f|m)[A-Z]\\w*\\b",
"name": "variable.other.readwrite.member.cpp"
},
{
"match": "\\bthis\\b",
"name": "variable.language.this.cpp"
},
{
"match": "\\bnullptr\\b",
"name": "constant.language.cpp"
},
{
"match": "\\btemplate\\b\\s*",
"name": "storage.type.template.cpp"
},
{
"match": "\\b(const_cast|dynamic_cast|reinterpret_cast|static_cast)\\b\\s*",
"name": "keyword.operator.cast.cpp"
},
{
"match": "::",
"name": "punctuation.separator.namespace.access.cpp"
},
{
"match": "\\b(and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|typeid|xor|xor_eq|alignof|alignas)\\b",
"name": "keyword.operator.cpp"
},
{
"match": "\\b(class|decltype|wchar_t|char16_t|char32_t)\\b",
"name": "storage.type.cpp"
},
{
"match": "\\b(constexpr|export|mutable|typename|thread_local)\\b",
"name": "storage.modifier.cpp"
},
{
"begin": "(?x)\n(?:\n ^ | # beginning of line\n (?:(?<!else|new|=)) # or word + space before name\n)\n((?:[A-Za-z_][A-Za-z0-9_]*::)*+~[A-Za-z_][A-Za-z0-9_]*) # actual name\n\\s*(\\() # opening bracket",
"beginCaptures": {
"1": {
"name": "entity.name.function.cpp"
},
"2": {
"name": "punctuation.definition.parameters.begin.c"
}
},
"end": "\\)",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.c"
}
},
"name": "meta.function.destructor.cpp",
"patterns": [
{
"include": "$base"
}
]
},
{
"begin": "(?x)\n(?:\n ^ | # beginning of line\n (?:(?<!else|new|=)) # or word + space before name\n)\n((?:[A-Za-z_][A-Za-z0-9_]*::)*+~[A-Za-z_][A-Za-z0-9_]*) # actual name\n\\s*(\\() # opening bracket",
"beginCaptures": {
"1": {
"name": "entity.name.function.cpp"
},
"2": {
"name": "punctuation.definition.parameters.begin.c"
}
},
"end": "\\)",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.c"
}
},
"name": "meta.function.destructor.prototype.cpp",
"patterns": [
{
"include": "$base"
}
]
},
{
"include": "source.c"
},
{
"include": "#preprocessor-rule-enabled"
},
@@ -321,6 +428,19 @@
"match": "((\\.)|(->))\\s*(([a-zA-Z_][a-zA-Z_0-9]*)\\b(?!\\s*\\())?"
},
"block": {
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.section.block.begin.bracket.curly.c"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.section.block.end.bracket.curly.c"
}
},
"name": "meta.block.cpp",
"patterns": [
{
"begin": "{",
@@ -341,6 +461,9 @@
"include": "#block_innards"
}
]
},
{
"include": "$base"
}
]
},
@@ -706,6 +829,69 @@
"include": "#line_continuation_character"
}
]
},
{
"begin": "(u|u8|U|L)?\"",
"beginCaptures": {
"0": {
"name": "punctuation.definition.string.begin.cpp"
},
"1": {
"name": "meta.encoding.cpp"
}
},
"end": "\"",
"endCaptures": {
"0": {
"name": "punctuation.definition.string.end.cpp"
}
},
"name": "string.quoted.double.cpp",
"patterns": [
{
"match": "\\\\u\\h{4}|\\\\U\\h{8}",
"name": "constant.character.escape.cpp"
},
{
"match": "\\\\['\"?\\\\abfnrtv]",
"name": "constant.character.escape.cpp"
},
{
"match": "\\\\[0-7]{1,3}",
"name": "constant.character.escape.cpp"
},
{
"match": "\\\\x\\h+",
"name": "constant.character.escape.cpp"
},
{
"include": "source.c#string_placeholder"
}
]
},
{
"begin": "(u|u8|U|L)?R\"(?:([^ ()\\\\\\t]{0,16})|([^ ()\\\\\\t]*))\\(",
"beginCaptures": {
"0": {
"name": "punctuation.definition.string.begin.cpp"
},
"1": {
"name": "meta.encoding.cpp"
},
"3": {
"name": "invalid.illegal.delimiter-too-long.cpp"
}
},
"end": "\\)\\2(\\3)\"",
"endCaptures": {
"0": {
"name": "punctuation.definition.string.end.cpp"
},
"1": {
"name": "invalid.illegal.delimiter-too-long.cpp"
}
},
"name": "string.quoted.double.raw.cpp"
}
]
},
@@ -1954,6 +2140,233 @@
"include": "#block_innards"
}
]
},
"angle_brackets": {
"begin": "<",
"end": ">",
"name": "meta.angle-brackets.cpp",
"patterns": [
{
"include": "#angle_brackets"
},
{
"include": "$base"
}
]
},
"constructor": {
"patterns": [
{
"begin": "(?x)\n(?:^\\s*) # beginning of line\n((?!while|for|do|if|else|switch|catch|enumerate|r?iterate)[A-Za-z_][A-Za-z0-9_:]*) # actual name\n\\s*(\\() # opening bracket",
"beginCaptures": {
"1": {
"name": "entity.name.function.cpp"
},
"2": {
"name": "punctuation.definition.parameters.begin.c"
}
},
"end": "\\)",
"endCaptures": {
"0": {
"name": "punctuation.definition.parameters.end.c"
}
},
"name": "meta.function.constructor.cpp",
"patterns": [
{
"include": "$base"
}
]
},
{
"begin": "(?x)\n(:)\n(\n (?=\n \\s*[A-Za-z_][A-Za-z0-9_:]* # actual name\n \\s* (\\() # opening bracket\n )\n)",
"beginCaptures": {
"1": {
"name": "punctuation.definition.parameters.c"
}
},
"end": "(?=\\{)",
"name": "meta.function.constructor.initializer-list.cpp",
"patterns": [
{
"include": "$base"
}
]
}
]
},
"special_block": {
"patterns": [
{
"begin": "\\b(using)\\b\\s*(namespace)\\b\\s*((?:[_A-Za-z][_A-Za-z0-9]*\\b(::)?)*)",
"beginCaptures": {
"1": {
"name": "keyword.control.cpp"
},
"2": {
"name": "storage.type.cpp"
},
"3": {
"name": "entity.name.type.cpp"
}
},
"end": "(;)",
"name": "meta.using-namespace-declaration.cpp"
},
{
"begin": "\\b(namespace)\\b\\s*([_A-Za-z][_A-Za-z0-9]*\\b)?+",
"beginCaptures": {
"1": {
"name": "storage.type.cpp"
},
"2": {
"name": "entity.name.type.cpp"
}
},
"captures": {
"1": {
"name": "keyword.control.namespace.$2"
}
},
"end": "(?<=\\})|(?=(;|,|\\(|\\)|>|\\[|\\]|=))",
"name": "meta.namespace-block.cpp",
"patterns": [
{
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.definition.scope.cpp"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.scope.cpp"
}
},
"patterns": [
{
"include": "#special_block"
},
{
"include": "#constructor"
},
{
"include": "$base"
}
]
},
{
"include": "$base"
}
]
},
{
"begin": "\\b(class|struct)\\b\\s*([_A-Za-z][_A-Za-z0-9]*\\b)?+(\\s*:\\s*(public|protected|private)\\s*([_A-Za-z][_A-Za-z0-9]*\\b)((\\s*,\\s*(public|protected|private)\\s*[_A-Za-z][_A-Za-z0-9]*\\b)*))?",
"beginCaptures": {
"1": {
"name": "storage.type.cpp"
},
"2": {
"name": "entity.name.type.cpp"
},
"4": {
"name": "storage.type.modifier.cpp"
},
"5": {
"name": "entity.name.type.inherited.cpp"
},
"6": {
"patterns": [
{
"match": "(public|protected|private)",
"name": "storage.type.modifier.cpp"
},
{
"match": "[_A-Za-z][_A-Za-z0-9]*",
"name": "entity.name.type.inherited.cpp"
}
]
}
},
"end": "(?<=\\})|(?=(;|\\(|\\)|>|\\[|\\]|=))",
"name": "meta.class-struct-block.cpp",
"patterns": [
{
"include": "#angle_brackets"
},
{
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.section.block.begin.bracket.curly.cpp"
}
},
"end": "(\\})(\\s*\\n)?",
"endCaptures": {
"1": {
"name": "punctuation.section.block.end.bracket.curly.cpp"
},
"2": {
"name": "invalid.illegal.you-forgot-semicolon.cpp"
}
},
"patterns": [
{
"include": "#special_block"
},
{
"include": "#constructor"
},
{
"include": "$base"
}
]
},
{
"include": "$base"
}
]
},
{
"begin": "\\b(extern)(?=\\s*\")",
"beginCaptures": {
"1": {
"name": "storage.modifier.cpp"
}
},
"end": "(?<=\\})|(?=\\w)|(?=\\s*#\\s*endif\\b)",
"name": "meta.extern-block.cpp",
"patterns": [
{
"begin": "\\{",
"beginCaptures": {
"0": {
"name": "punctuation.section.block.begin.bracket.curly.c"
}
},
"end": "\\}|(?=\\s*#\\s*endif\\b)",
"endCaptures": {
"0": {
"name": "punctuation.section.block.end.bracket.curly.c"
}
},
"patterns": [
{
"include": "#special_block"
},
{
"include": "$base"
}
]
},
{
"include": "$base"
}
]
}
]
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "arduino-ide-extension",
"version": "0.0.1",
"version": "0.0.2",
"description": "An extension for Theia building the Arduino IDE",
"license": "MIT",
"engines": {
@@ -8,29 +8,58 @@
},
"dependencies": {
"@grpc/grpc-js": "^0.4.0",
"@theia/application-package": "next",
"@theia/core": "next",
"@theia/editor": "next",
"@theia/filesystem": "next",
"@theia/git": "next",
"@theia/languages": "next",
"@theia/markers": "next",
"@theia/monaco": "next",
"@theia/outline-view": "next",
"@theia/workspace": "next",
"p-queue": "^5.0.0"
"@theia/navigator": "next",
"@theia/terminal": "next",
"@theia/search-in-workspace": "next",
"@theia/cpp": "next",
"@types/ps-tree": "^1.1.0",
"@types/which": "^1.3.1",
"@types/react-select": "^3.0.0",
"@types/google-protobuf": "^3.7.1",
"css-element-queries": "^1.2.0",
"react-select": "^3.0.4",
"p-queue": "^5.0.0",
"ps-tree": "^1.2.0",
"string-natural-compare": "^2.0.3",
"tree-kill": "^1.2.1",
"upath": "^1.1.2",
"which": "^1.3.1"
},
"scripts": {
"generate-protoc": "./scripts/generate-protoc.sh && node ./scripts/patch-grpc-js.js",
"prepare": "yarn run clean && yarn run build",
"prepare": "yarn download-cli && yarn download-ls && yarn run clean && yarn run build",
"clean": "rimraf lib",
"download-cli": "node ./scripts/download-cli.js",
"download-ls": "node ./scripts/download-ls.js",
"generate-protocol": "node ./scripts/generate-protocol.js",
"lint": "tslint -c ./tslint.json --project ./tsconfig.json",
"build": "tsc && cp -rf src/node/cli-protocol lib/node && yarn lint",
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
"watch": "tsc -w"
},
"devDependencies": {
"@types/google-protobuf": "^3.2.7",
"decompress": "^4.2.0",
"decompress-targz": "^4.1.1",
"decompress-unzip": "^4.0.1",
"download": "^7.1.0",
"grpc-tools": "^1.7.3",
"grpc_tools_node_protoc_ts": "^2.5.0",
"moment": "^2.24.0",
"ncp": "^2.0.0",
"rimraf": "^2.6.1",
"shelljs": "^0.8.3",
"tslint": "^5.5.0",
"typescript": "2.9.1"
"typescript": "2.9.1",
"uuid": "^3.2.1",
"yargs": "^11.1.0"
},
"files": [
"lib",
@@ -42,6 +71,10 @@
{
"backend": "lib/node/arduino-backend-module",
"frontend": "lib/browser/arduino-frontend-module"
},
{
"frontend": "lib/browser/menu/browser-arduino-menu-module",
"frontendElectron": "lib/electron-browser/electron-arduino-menu-module"
}
]
}
}

View File

@@ -0,0 +1,62 @@
// @ts-check
// The links to the downloads as of today (02.09.) are the followings:
// In order to get the latest nightly build for your platform use the following links replacing <DATE> with the current date, using the format YYYYMMDD (i.e for 2019/Aug/06 use 20190806 )
// Linux 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_64bit.tar.gz
// Linux ARM 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Linux_ARM64.tar.gz
// Windows 64 bit: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_Windows_64bit.zip
// Mac OSX: https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-<DATE>_macOS_64bit.tar.gz
// [...]
// 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
(() => {
const DEFAULT_VERSION = 'latest'; // require('moment')().format('YYYYMMDD');
const os = require('os');
const path = require('path');
const shell = require('shelljs');
const downloader = require('./downloader');
const yargs = require('yargs')
.option('cli-version', {
alias: 'cv',
default: DEFAULT_VERSION,
describe: `The version of the 'arduino-cli' to download with the YYYYMMDD format, or 'latest'. Defaults to ${DEFAULT_VERSION}.`
})
.option('force-download', {
alias: 'fd',
default: false,
describe: `If set, this script force downloads the 'arduino-cli' even if it already exists on the file system.`
})
.version(false).parse();
const version = yargs['cli-version'];
const force = yargs['force-download'];
const { platform, arch } = process;
const build = path.join(__dirname, '..', 'build');
const cli = path.join(build, `arduino-cli${os.platform() === 'win32' ? '.exe' : ''}`);
const suffix = (() => {
switch (platform) {
case 'darwin': return 'macOS_64bit.tar.gz';
case 'win32': return 'Windows_64bit.zip';
case 'linux': {
switch (arch) {
case 'arm64': return 'Linux_ARM64.tar.gz';
case 'x64': return 'Linux_64bit.tar.gz';
default: return undefined;
}
}
default: return undefined;
}
})();
if (!suffix) {
shell.echo(`The CLI is not available for ${platform} ${arch}.`);
shell.exit(1);
}
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
downloader.download(url, cli, 'arduino-cli', force);
})();

View File

@@ -0,0 +1,72 @@
// @ts-check
// The links to the downloads as of today (28.08.2019) are the following:
// - https://downloads.arduino.cc/arduino-language-server/nightly/arduino-language-server_${SUFFIX}
// - https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${VERSION}_${SUFFIX}
(() => {
const DEFAULT_ALS_VERSION = 'nightly';
const DEFAULT_CLANGD_VERSION = '8.0.1';
const os = require('os');
const path = require('path');
const shell = require('shelljs');
const downloader = require('./downloader');
const yargs = require('yargs')
.option('ls-version', {
alias: 'lv',
default: DEFAULT_ALS_VERSION,
choices: ['nightly'],
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`
})
.option('clangd-version', {
alias: 'cv',
default: DEFAULT_CLANGD_VERSION,
choices: ['8.0.1'],
describe: `The version of 'clangd' to download. Defaults to ${DEFAULT_CLANGD_VERSION}.`
})
.option('force-download', {
alias: 'fd',
default: false,
describe: `If set, this script force downloads the 'arduino-language-server' even if it already exists on the file system.`
})
.version(false).parse();
const alsVersion = yargs['ls-version'];
const clangdVersion = yargs['clangd-version']
const force = yargs['force-download'];
const { platform, arch } = process;
const build = path.join(__dirname, '..', 'build');
const als = path.join(build, `arduino-language-server${os.platform() === 'win32' ? '.exe' : ''}`);
const clangd = path.join(build, `clangd${os.platform() === 'win32' ? '.exe' : ''}`);
let alsSuffix, clangdSuffix;
switch (platform) {
case 'darwin':
alsSuffix = 'Darwin_amd64.zip';
clangdSuffix = 'macos.zip';
break;
case 'win32':
alsSuffix = 'Windows_NT_amd64.zip';
clangdSuffix = 'windows.zip';
break;
case 'linux':
alsSuffix = 'Linux_amd64.zip';
break;
}
if (!alsSuffix) {
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}_${alsSuffix}`;
downloader.download(alsUrl, als, 'arduino-language-server', force);
if (clangdSuffix) {
const clangdUrl = `https://downloads.arduino.cc/arduino-language-server/clangd/clangd_${clangdVersion}_${clangdSuffix}`;
downloader.download(clangdUrl, clangd, 'clangd', force);
}
})();

View File

@@ -0,0 +1,71 @@
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');
process.on('unhandledRejection', (reason, _) => {
shell.echo(String(reason));
shell.exit(1);
throw reason;
});
process.on('uncaughtException', error => {
shell.echo(String(error));
shell.exit(1);
throw error;
});
exports.download = async (url, targetFile, filePrefix, force) => {
const { platform, arch } = process;
if (fs.existsSync(targetFile) && !force) {
shell.echo(`Skipping download because file already exists: ${targetFile}`);
return;
}
if (!fs.existsSync(path.dirname(targetFile))) {
if (shell.mkdir('-p', path.dirname(targetFile)).code !== 0) {
shell.echo('Could not create new directory.');
shell.exit(1);
}
}
const downloads = path.join(__dirname, '..', 'downloads');
if (shell.rm('-rf', targetFile, downloads).code !== 0) {
shell.exit(1);
}
shell.echo(`>>> Downloading from '${url}'...`);
const data = await download(url);
shell.echo(`<<< Download succeeded.`);
shell.echo('>>> Decompressing...');
const files = await decompress(data, downloads, {
plugins: [
unzip(),
untargz()
]
});
if (files.length === 0) {
shell.echo('Error ocurred while decompressing the archive.');
shell.exit(1);
}
const fileIndex = files.findIndex(f => f.path.startsWith(filePrefix));
if (fileIndex === -1) {
shell.echo(`The downloaded artifact does not contain any file with prefix ${filePrefix}.`);
shell.exit(1);
}
shell.echo('<<< Decompressing succeeded.');
if (shell.mv('-f', path.join(downloads, files[fileIndex].path), targetFile).code !== 0) {
shell.echo(`Could not move file to target path: ${targetFile}`);
shell.exit(1);
}
if (!fs.existsSync(targetFile)) {
shell.echo(`Could not find file: ${targetFile}`);
shell.exit(1);
} else {
shell.echo(`Done: ${targetFile}`);
}
}

View File

@@ -1,43 +0,0 @@
#!/bin/bash
WORKDIR=/tmp/arduino-cli-protoc
echo "Working in $WORKDIR"
# this could be a Git submodule, but that feels to clunky for just building the protobuf stuff
mkdir -p $WORKDIR
pushd $WORKDIR
if [ ! -d arduino-cli ]; then
git clone https://github.com/typefox/arduino-cli
cd arduino-cli
git checkout daemon
cd -
mkdir -p go/src/github.com/arduino
ln -s $PWD/arduino-cli go/src/github.com/arduino
export GOPATH=$PWD/go
cd go/src/github.com/arduino/arduino-cli
GOOS=linux go build -o arduino-cli.linux
# GOOS=darwin go build -o arduino-cli.darwin
fi
popd
# make sure the output path exists
mkdir -p src/node/cli-protocol
export PATH=$PATH:$PWD/node_modules/.bin
# generate js codes via grpc-tools
grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./src/node/cli-protocol \
--grpc_out=./src/node/cli-protocol \
--plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
-I /usr/lib/protoc/include \
-I $WORKDIR/arduino-cli/rpc \
$WORKDIR/arduino-cli/rpc/*.proto
# generate d.ts codes
protoc \
--plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
--ts_out=./src/node/cli-protocol \
-I /usr/lib/protoc/include \
-I $WORKDIR/arduino-cli/rpc \
$WORKDIR/arduino-cli/rpc/*.proto

View File

@@ -0,0 +1,81 @@
// @ts-check
(async () => {
const DEFAULT_VERSION = 'nightly'; // '0.3.7-alpha.preview';
const os = require('os');
const path = require('path');
const { v4 } = require('uuid');
const shell = require('shelljs');
shell.env.PATH = `${shell.env.PATH}${path.delimiter}${path.join(__dirname, '..', 'node_modules', '.bin')}`;
const yargs = require('yargs')
.option('cli-version', {
alias: 'cv',
default: DEFAULT_VERSION,
choices: [
// 'latest', // TODO: How do we get the source for `latest`. Currently, `latest` is the `0.3.7-alpha.preview`.
'nightly'
],
describe: `The version of the 'arduino-cli' to download. Specify either an existing version or use 'nightly'. Defaults to ${DEFAULT_VERSION}.`
})
.version(false).parse();
const version = yargs['cli-version'];
if (version !== 'nightly') {
shell.echo(`Only 'nightly' version is supported.`);
shell.exit(1);
}
const repository = path.join(os.tmpdir(), `${v4()}-arduino-cli`);
if (shell.mkdir('-p', repository).code !== 0) {
shell.exit(1);
}
if (shell.exec(`git clone https://github.com/arduino/arduino-cli.git ${repository}`).code !== 0) {
shell.exit(1);
}
if (version !== 'nightly') {
if (shell.exec(`git -C ${repository} checkout tags/${version} -b ${version}`).code !== 0) {
shell.exit(1);
}
}
shell.echo('Generating TS/JS API from:');
if (shell.exec(`git -C ${repository} rev-parse --abbrev-ref HEAD`).code !== 0) {
shell.exit(1);
}
if (shell.exec(`git -C ${repository} rev-parse --short HEAD`).code !== 0) {
shell.exit(1);
}
const pluginExec = shell.which('grpc_tools_node_protoc_plugin');
if (!pluginExec || pluginExec.code !== 0) {
shell.exit(1);
}
const plugin = pluginExec.stdout.trim();
const rpc = path.join(repository, 'rpc');
const out = path.join(__dirname, '..', 'src', 'node', 'cli-protocol');
// Generate JS code from the `.proto` files.
if (shell.exec(`grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:${out} \
--grpc_out=${out} \
--plugin=protoc-gen-grpc=${plugin} \
-I ${rpc} \
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
shell.exit(1);
}
// Generate the `.d.ts` files for JS.
if (shell.exec(`protoc \
--plugin=protoc-gen-ts=${path.resolve(__dirname, '..', 'node_modules', '.bin', 'protoc-gen-ts')} \
--ts_out=${out} \
-I ${rpc} \
${path.join(rpc, '/**/*.proto')}`).code !== 0) {
shell.exit(1);
}
const { patch } = require('./patch-grpc-js');
patch([out])
shell.echo('Done.');
})();

View File

@@ -1,25 +1,38 @@
// Use `@grpc/grpc-js` instead of `grpc` at runtime.
// https://github.com/grpc/grpc-node/issues/624
(() => {
const fs = require('fs');
const path = require('path');
const roots = ['src']; // XXX: patch the `lib` instead?
console.info("🔧 >>> Patching code. Switching from 'grpc' to '@grpc/grpc-js'...");
for (const root of roots) {
const cliProtocolPath = path.resolve(__dirname, '..', root, 'node', 'cli-protocol');
for (const fileName of fs.readdirSync(cliProtocolPath)) {
const filePath = path.resolve(cliProtocolPath, fileName);
let content = fs.readFileSync(filePath, { encoding: 'utf8' });
if (content.indexOf("require('grpc')") !== -1) {
console.info(`Updated require('grpc') to require('@grpc/grpc-js') in ${filePath}.`);
fs.writeFileSync(filePath, content.replace("require('grpc')", "require('@grpc/grpc-js')"));
}
content = fs.readFileSync(filePath, { encoding: 'utf8' });
if (content.indexOf('import * as grpc from "grpc"') !== -1) {
console.info(`Updated import * as grpc from "grpc" to import * as grpc from "@grpc/grpc-js" in ${filePath}.`);
fs.writeFileSync(filePath, content.replace('import * as grpc from "grpc"', 'import * as grpc from "@grpc/grpc-js"'));
// https://github.com/grpc/grpc-node/issues/931
const fs = require('fs');
const path = require('path');
module.exports.patch = function (roots = [path.join(__dirname, '..', 'src', 'node')]) {
console.info('🔧 <<< Patching code...');
patch(roots);
console.info('👌 <<< Done. The code has been patched.');
};
function patch(paths) {
for (const p of paths) {
const exist = fs.existsSync(p);
if (exist) {
const stat = fs.statSync(p);
if (stat.isDirectory()) {
console.info(`🔧 >>> Scanning code in ${p}...`);
patch(fs.readdirSync(p).map(name => path.join(p, name)));
} else {
let content = fs.readFileSync(p, { encoding: 'utf8' });
if (content.indexOf("require('grpc')") !== -1) {
console.info(`Updated require('grpc') to require('@grpc/grpc-js') in ${p}.`);
fs.writeFileSync(p, content.replace("require('grpc')", "require('@grpc/grpc-js')"));
}
content = fs.readFileSync(p, { encoding: 'utf8' });
if (content.indexOf('import * as grpc from "grpc"') !== -1) {
console.info(`Updated import * as grpc from "grpc" to import * as grpc from "@grpc/grpc-js" in ${p}.`);
fs.writeFileSync(p, content.replace('import * as grpc from "grpc"', 'import * as grpc from "@grpc/grpc-js"'));
}
}
} else {
console.warn(`${p} does not exist. Skipping.`);
}
}
console.info('👌 <<< Done. The code has been patched.');
})();
}

View File

@@ -12,10 +12,34 @@ export namespace ArduinoCommands {
label: 'Upload Sketch'
}
export const SHOW_OPEN_CONTEXT_MENU: Command = {
id: 'arduino-show-open-context-menu',
label: 'Open Sketch'
}
export const OPEN_FILE_NAVIGATOR: Command = {
id: 'arduino-open-file-navigator'
}
export const OPEN_SKETCH: Command = {
id: 'arduino-open-file'
}
export const SAVE_SKETCH: Command = {
id: 'arduino-save-file'
}
export const NEW_SKETCH: Command = {
id: "arduino-new-sketch",
label: 'New Sketch',
category: 'File'
}
export const OPEN_BOARDS_DIALOG: Command = {
id: "arduino-open-boards-dialog"
}
export const TOGGLE_ADVANCED_MODE: Command = {
id: "arduino-toggle-advanced-mode"
}
}

View File

@@ -1,15 +0,0 @@
import { injectable } from "inversify";
import { MenuContribution, MenuModelRegistry } from "@theia/core";
import { CommonMenus } from "@theia/core/lib/browser";
import { ArduinoCommands } from "./arduino-commands";
@injectable()
export class ArduinoFileMenuContribution implements MenuContribution {
registerMenus(registry: MenuModelRegistry) {
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sletch'], {
commandId: ArduinoCommands.NEW_SKETCH.id
})
}
}

View File

@@ -3,36 +3,83 @@ import { injectable, inject, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { MessageService } from '@theia/core/lib/common/message-service';
import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { DefaultFrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { BoardsService } from '../common/protocol/boards-service';
import { ArduinoCommands } from './arduino-commands';
import { ConnectedBoards } from './components/connected-boards';
import { CoreService } from '../common/protocol/core-service';
import { WorkspaceServiceExt } from './workspace-service-ext';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { ConfirmDialog } from '@theia/core/lib/browser';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
import { BoardsNotificationService } from './boards-notification-service';
import { WorkspaceRootUriAwareCommandHandler } from '@theia/workspace/lib/browser/workspace-commands';
import { SelectionService } from '@theia/core';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { SketchFactory } from './sketch-factory';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath } from '@theia/core';
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
import { EditorManager, EditorMainMenu } from '@theia/editor/lib/browser';
import {
ContextMenuRenderer,
OpenerService,
Widget,
StatusBar,
ShellLayoutRestorer,
StatusBarAlignment,
QuickOpenService
} from '@theia/core/lib/browser';
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
import { Sketch, SketchesService } from '../common/protocol/sketches-service';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution';
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
import { MaybePromise } from '@theia/core/lib/common/types';
import { BoardsConfigDialog } from './boards/boards-config-dialog';
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { BoardsConfig } from './boards/boards-config';
import { MonitorService } from '../common/protocol/monitor-service';
import { ConfigService } from '../common/protocol/config-service';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { ArduinoWorkspaceService } from './arduino-workspace-service';
export namespace ArduinoMenus {
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
export const TOOLS = [...MAIN_MENU_BAR, '4_tools'];
}
export namespace ArduinoToolbarContextMenu {
export const OPEN_SKETCH_PATH: MenuPath = ['arduino-open-sketch-context-menu'];
export const OPEN_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '1_open'];
export const WS_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '2_sketches'];
export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '3_examples'];
}
export namespace ArduinoAdvancedMode {
export const LS_ID = 'arduino-advanced-mode';
export const TOGGLED: boolean = (() => {
const advancedModeStr = window.localStorage.getItem(LS_ID);
return advancedModeStr === 'true';
})();
}
@injectable()
export class ArduinoFrontendContribution extends DefaultFrontendApplicationContribution implements TabBarToolbarContribution, CommandContribution {
export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution, MenuContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardService: BoardsService;
protected readonly boardsService: BoardsService;
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(MonitorService)
protected readonly monitorService: MonitorService;
@inject(WorkspaceServiceExt)
protected readonly workspaceServiceExt: WorkspaceServiceExt;
@@ -45,72 +92,138 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsListWidgetFrontendContribution: BoardsListWidgetFrontendContribution;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
@inject(SketchFactory)
protected readonly sketchFactory: SketchFactory;
@inject(EditorManager)
protected readonly editorManager: EditorManager;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(BoardsConfigDialog)
protected readonly boardsConfigDialog: BoardsConfigDialog;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(CommandRegistry)
protected readonly commands: CommandRegistry;
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(ShellLayoutRestorer)
protected readonly layoutRestorer: ShellLayoutRestorer;
@inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService;
@inject(ArduinoWorkspaceService)
protected readonly workspaceService: ArduinoWorkspaceService;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
protected boardsToolbarItem: BoardsToolBarItem | null;
protected wsSketchCount: number = 0;
@postConstruct()
protected async init(): Promise<void> {
// This is a hack. Otherwise, the backend services won't bind.
await this.workspaceServiceExt.roots();
const updateStatusBar = (config: BoardsConfig.Config) => {
this.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: BoardsConfig.Config.toString(config)
});
}
this.boardsServiceClient.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClient.boardsConfig);
this.registerSketchesInMenu(this.menuRegistry);
Promise.all([
this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts()
]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports));
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: ArduinoCommands.VERIFY.id,
command: ArduinoCommands.VERIFY.id,
tooltip: 'Verify',
group: 'arduino',
text: '$(check)'
tooltip: 'Verify'
});
registry.registerItem({
id: ArduinoCommands.UPLOAD.id,
command: ArduinoCommands.UPLOAD.id,
tooltip: 'Upload',
group: 'arduino',
text: '$(arrow-right)'
tooltip: 'Upload'
});
registry.registerItem({
id: ConnectedBoards.TOOLBAR_ID,
render: () => <ConnectedBoards
boardsService={this.boardService}
boardsNotificationService={this.boardsNotificationService}
quickPickService={this.quickPickService}
onNoBoardsInstalled={this.onNoBoardsInstalled.bind(this)}
onUnknownBoard={this.onUnknownBoard.bind(this)} />,
isVisible: widget => this.isArduinoEditor(widget)
})
id: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
command: ArduinoCommands.SHOW_OPEN_CONTEXT_MENU.id,
tooltip: 'Open'
});
registry.registerItem({
id: ArduinoCommands.SAVE_SKETCH.id,
command: ArduinoCommands.SAVE_SKETCH.id,
tooltip: 'Save'
});
registry.registerItem({
id: BoardsToolBarItem.TOOLBAR_ID,
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
ref={ref => this.boardsToolbarItem = ref}
commands={this.commands}
boardsServiceClient={this.boardsServiceClient}
boardService={this.boardsService} />,
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left'
});
registry.registerItem({
id: 'toggle-serial-monitor',
command: MonitorViewContribution.OPEN_SERIAL_MONITOR,
tooltip: 'Toggle Serial Monitor',
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
});
registry.registerItem({
id: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
command: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
tooltip: 'Toggle Advanced Mode',
text: (ArduinoAdvancedMode.TOGGLED ? '$(toggle-on)' : '$(toggle-off)'),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(ArduinoCommands.VERIFY, {
isVisible: widget => this.isArduinoEditor(widget),
isEnabled: widget => this.isArduinoEditor(widget),
execute: async widget => {
if (widget instanceof EditorWidget) {
await widget.saveable.save();
}
const uri = this.toUri(widget);
if (uri) {
const result = await this.coreService.compile({ uri: uri.toString() });
console.log('compile result', result);
}
}
});
registry.registerCommand(ArduinoCommands.UPLOAD, {
isVisible: widget => this.isArduinoEditor(widget),
isEnabled: widget => this.isArduinoEditor(widget),
execute: async widget => {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => true,
execute: async () => {
const widget = this.getCurrentWidget();
if (widget instanceof EditorWidget) {
await widget.saveable.save();
}
@@ -121,47 +234,273 @@ export class ArduinoFrontendContribution extends DefaultFrontendApplicationContr
}
try {
await this.coreService.upload({ uri: uri.toString() });
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for ${boardsConfig.selectedBoard.name}. Please install the board.`);
}
await this.coreService.compile({ uri: uri.toString(), board: boardsConfig.selectedBoard });
} catch (e) {
new ConfirmDialog({ title: "Error during upload", msg: e.toString(), ok: "Ok" }).open();
await this.messageService.error(e.toString());
}
}
});
registry.registerCommand(ArduinoCommands.UPLOAD, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => true,
execute: async () => {
const widget = this.getCurrentWidget();
if (widget instanceof EditorWidget) {
await widget.saveable.save();
}
const uri = this.toUri(widget);
if (!uri) {
return;
}
const connectionConfig = this.monitorConnection.connectionConfig;
await this.monitorConnection.disconnect();
try {
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
const { selectedPort } = boardsConfig;
if (!selectedPort) {
throw new Error('No ports selected. Please select a port.');
}
await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort.address });
} catch (e) {
await this.messageService.error(e.toString());
} finally {
if (connectionConfig) {
await this.monitorConnection.connect(connectionConfig);
}
}
}
});
registry.registerCommand(ArduinoCommands.SHOW_OPEN_CONTEXT_MENU, {
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (widget: Widget, target: EventTarget) => {
if (this.wsSketchCount) {
const el = (target as HTMLElement).parentElement;
if (el) {
this.contextMenuRenderer.render(ArduinoToolbarContextMenu.OPEN_SKETCH_PATH, {
x: el.getBoundingClientRect().left,
y: el.getBoundingClientRect().top + el.offsetHeight
});
}
} else {
this.commands.executeCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR.id);
}
}
});
registry.registerCommand(ArduinoCommands.OPEN_FILE_NAVIGATOR, {
isEnabled: () => true,
execute: () => this.doOpenFile()
})
registry.registerCommand(ArduinoCommands.OPEN_SKETCH, {
isEnabled: () => true,
execute: async (sketch: Sketch) => {
this.workspaceService.open(new URI(sketch.uri));
}
})
registry.registerCommand(ArduinoCommands.SAVE_SKETCH, {
isEnabled: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
execute: async (sketch: Sketch) => {
registry.executeCommand(CommonCommands.SAVE_ALL.id);
}
})
registry.registerCommand(ArduinoCommands.NEW_SKETCH, new WorkspaceRootUriAwareCommandHandler(this.workspaceService, this.selectionService, {
execute: async uri => {
try {
await this.sketchFactory.createNewSketch(uri);
// hack: sometimes we don't get the workspace root, but the currently active file: correct for that
if (uri.path.ext !== "") {
uri = uri.withPath(uri.path.dir.dir);
}
const sketch = await this.sketchService.createNewSketch(uri.toString());
this.workspaceService.open(new URI(sketch.uri));
} catch (e) {
new ConfirmDialog({ title: "Cannot create new sketch", msg: e.toString(), ok: "Ok" }).open();
}
await this.messageService.error(e.toString());
}
}
}))
}));
registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, {
isEnabled: () => true,
execute: async () => {
const boardsConfig = await this.boardsConfigDialog.open();
if (boardsConfig) {
this.boardsServiceClient.boardsConfig = boardsConfig;
}
}
})
registry.registerCommand(ArduinoCommands.TOGGLE_ADVANCED_MODE, {
execute: () => {
const oldModeState = ArduinoAdvancedMode.TOGGLED;
window.localStorage.setItem(ArduinoAdvancedMode.LS_ID, oldModeState ? 'false' : 'true');
registry.executeCommand('reset.layout');
},
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right',
isToggled: () => ArduinoAdvancedMode.TOGGLED
})
}
private async onNoBoardsInstalled() {
const action = await this.messageService.info("You have no boards installed. Use the boards mangager to install one.", "Open Boards Manager");
if (!action) {
return;
registerMenus(registry: MenuModelRegistry) {
if (!ArduinoAdvancedMode.TOGGLED) {
// If are not in pro-mode, we have to disable the context menu for the tabs.
// Such as `Close`, `Close All`, etc.
for (const command of [
CommonCommands.CLOSE_TAB,
CommonCommands.CLOSE_OTHER_TABS,
CommonCommands.CLOSE_RIGHT_TABS,
CommonCommands.CLOSE_ALL_TABS,
CommonCommands.COLLAPSE_PANEL,
CommonCommands.TOGGLE_MAXIMIZED
]) {
registry.unregisterMenuAction(command);
}
registry.unregisterMenuAction(FileSystemCommands.UPLOAD);
registry.unregisterMenuAction(FileDownloadCommands.DOWNLOAD);
registry.unregisterMenuAction(WorkspaceCommands.NEW_FOLDER);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_FOLDER);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_WORKSPACE);
registry.unregisterMenuAction(WorkspaceCommands.OPEN_RECENT_WORKSPACE);
registry.unregisterMenuAction(WorkspaceCommands.SAVE_WORKSPACE_AS);
registry.unregisterMenuAction(WorkspaceCommands.CLOSE);
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(MonacoMenus.SELECTION));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(EditorMainMenu.GO));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(TerminalMenus.TERMINAL));
registry.getMenu(MAIN_MENU_BAR).removeNode(this.getMenuId(CommonMenus.VIEW));
}
this.boardsListWidgetFrontendContribution.openView({reveal: true});
registry.registerSubmenu(ArduinoMenus.SKETCH, 'Sketch');
registry.registerMenuAction(ArduinoMenus.SKETCH, {
commandId: ArduinoCommands.VERIFY.id,
label: 'Verify/Compile',
order: '1'
});
registry.registerMenuAction(ArduinoMenus.SKETCH, {
commandId: ArduinoCommands.UPLOAD.id,
label: 'Upload',
order: '2'
});
registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_GROUP, {
commandId: ArduinoCommands.OPEN_FILE_NAVIGATOR.id,
label: 'Open...'
});
registry.registerSubmenu(ArduinoMenus.TOOLS, 'Tools');
registry.registerMenuAction(CommonMenus.HELP, {
commandId: ArduinoCommands.TOGGLE_ADVANCED_MODE.id,
label: 'Advanced Mode'
});
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sketch'], {
commandId: ArduinoCommands.NEW_SKETCH.id
});
}
private async onUnknownBoard() {
const action = await this.messageService.warn("There's a board connected for which you need to install software." +
" If this were not a PoC we would offer you the right package now.", "Open Boards Manager");
if (!action) {
return;
}
this.boardsListWidgetFrontendContribution.openView({reveal: true});
protected getMenuId(menuPath: string[]): string {
const index = menuPath.length - 1;
const menuId = menuPath[index];
return menuId;
}
private isArduinoEditor(maybeEditorWidget: any): boolean {
if (maybeEditorWidget instanceof EditorWidget) {
return maybeEditorWidget.editor.uri.toString().endsWith('.ino');
protected async registerSketchesInMenu(registry: MenuModelRegistry): Promise<void> {
this.sketchService.getSketches().then(sketches => {
this.wsSketchCount = sketches.length;
sketches.forEach(sketch => {
const command: Command = {
id: 'openSketch' + sketch.name
}
this.commands.registerCommand(command, {
execute: () => this.commands.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch)
});
registry.registerMenuAction(ArduinoToolbarContextMenu.WS_SKETCHES_GROUP, {
commandId: command.id,
label: sketch.name
});
})
})
}
async openSketchFiles(uri: string): Promise<void> {
this.sketchService.getSketchFiles(uri).then(uris => {
for (const uri of uris) {
this.editorManager.open(new URI(uri));
}
});
}
/**
* Opens a file after prompting the `Open File` dialog. Resolves to `undefined`, if
* - the workspace root is not set,
* - the file to open does not exist, or
* - it was not a file, but a directory.
*
* Otherwise, resolves to the URI of the file.
*/
protected async doOpenFile(): Promise<URI | undefined> {
const props: OpenFileDialogProps = {
title: WorkspaceCommands.OPEN_FILE.dialogLabel,
canSelectFolders: false,
canSelectFiles: true
};
const [rootStat] = await this.workspaceService.roots;
const destinationFileUri = await this.fileDialogService.showOpenDialog(props, rootStat);
if (destinationFileUri) {
const destinationFile = await this.fileSystem.getFileStat(destinationFileUri.toString());
if (destinationFile && !destinationFile.isDirectory) {
const message = await this.validate(destinationFile);
if (!message) {
await this.workspaceService.open(destinationFileUri);
return destinationFileUri;
} else {
this.messageService.warn(message);
}
}
}
return false;
return undefined;
}
protected getCurrentWidget(): EditorWidget | undefined {
let widget = this.editorManager.currentEditor;
if (!widget) {
const visibleWidgets = this.editorManager.all.filter(w => w.isVisible);
if (visibleWidgets.length > 0) {
widget = visibleWidgets[0];
}
}
return widget;
}
/**
* Returns `undefined` if the `file` is valid. Otherwise, returns with the validation error message.
*/
protected validate(file: FileStat): MaybePromise<string | undefined> {
const uri = new URI(file.uri);
const path = uri.path;
const { name, ext, dir } = path;
if (ext !== '.ino') {
return "Only sketches with '.ino' extension can be opened.";
}
if (name !== dir.name) {
return `The file "${name}${ext}" needs to be inside a sketch folder named "${name}".`;
}
return undefined;
}
private toUri(arg: any): URI | undefined {

View File

@@ -5,14 +5,16 @@ import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'
import { FrontendApplicationContribution, FrontendApplication } from '@theia/core/lib/browser/frontend-application'
import { LanguageGrammarDefinitionContribution } from '@theia/monaco/lib/browser/textmate';
import { LanguageClientContribution } from '@theia/languages/lib/browser';
import { ArduinoLanguageClientContribution } from './language/arduino-language-client-contribution';
import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
import { ArduinoFrontendContribution, ArduinoAdvancedMode } from './arduino-frontend-contribution';
import { ArduinoLanguageGrammarContribution } from './language/arduino-language-grammar-contribution';
import { LibraryService, LibraryServicePath } from '../common/protocol/library-service';
import { BoardsService, BoardsServicePath } from '../common/protocol/boards-service';
import { LibraryListWidgetFrontendContribution } from './library/list-widget-frontend-contribution';
import { BoardsService, BoardsServicePath, BoardsServiceClient } from '../common/protocol/boards-service';
import { SketchesService, SketchesServicePath } from '../common/protocol/sketches-service';
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
import { BoardsListWidget } from './boards/boards-list-widget';
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
@@ -21,24 +23,72 @@ import { WorkspaceServiceExtImpl } from './workspace-service-ext-impl';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { ToolOutputService } from '../common/protocol/tool-output-service';
import { ToolOutputServiceClientImpl } from './tool-output/client-service-impl';
import { BoardsNotificationService } from './boards-notification-service';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { AWorkspaceService } from './arduino-workspace-service';
import { ArduinoWorkspaceService } from './arduino-workspace-service';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { ArduinoTheme } from './arduino-theme';
import { ArduinoFileMenuContribution } from './arduino-file-menu';
import { MenuContribution } from '@theia/core';
import { SketchFactory } from './sketch-factory';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { SilentOutlineViewContribution } from './customization/silent-outline-contribution';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { SilentProblemContribution } from './customization/silent-problem-contribution';
import { SilentNavigatorContribution } from './customization/silent-navigator-contribution';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { ArduinoToolbarContribution } from './toolbar/arduino-toolbar-contribution';
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { ArduinoOutputToolContribution } from './customization/silent-output-tool-contribution';
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { ArduinoEditorContribution } from './customization/arduino-editor-contribution';
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
import { ArduinoMonacoStatusBarContribution } from './customization/arduino-monaco-status-bar-contribution';
import { ApplicationShell } from '@theia/core/lib/browser';
import { ArduinoApplicationShell } from './customization/arduino-application-shell';
import { ArduinoFrontendApplication } from './customization/arduino-frontend-application';
import { BoardsConfigDialog, BoardsConfigDialogProps } from './boards/boards-config-dialog';
import { BoardsConfigDialogWidget } from './boards/boards-config-dialog-widget';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { SilentScmContribution } from './customization/silent-scm-contribution';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { SilentSearchInWorkspaceContribution } from './customization/silent-search-in-workspace-contribution';
import { LibraryListWidgetFrontendContribution } from './library/library-widget-frontend-contribution';
import { LibraryItemRenderer } from './library/library-item-renderer';
import { BoardItemRenderer } from './boards/boards-item-renderer';
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 { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { ArduinoMonacoEditorProvider } from './editor/arduino-monaco-editor-provider';
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ArduinoTabBarDecoratorService } from './shell/arduino-tab-bar-decorator';
import { ProblemManager } from '@theia/markers/lib/browser';
import { ArduinoProblemManager } from './markers/arduino-problem-manager';
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
import { AboutDialog } from '@theia/core/lib/browser/about-dialog';
import { ArduinoAboutDialog } from './customization/arduino-about-dialog';
const ElementQueries = require('css-element-queries/src/ElementQueries');
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
ElementQueries.listen();
ElementQueries.init();
// Commands and toolbar items
bind(ArduinoFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).toService(ArduinoFrontendContribution);
bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution);
bind(MenuContribution).to(ArduinoFileMenuContribution).inSingletonScope();
bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution);
// `ino` TextMate grammar
bind(ArduinoToolbarContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution);
// `ino` TextMate grammar and language client
bind(LanguageGrammarDefinitionContribution).to(ArduinoLanguageGrammarContribution).inSingletonScope();
bind(LanguageClientContribution).to(ArduinoLanguageClientContribution).inSingletonScope();
// Library service
bind(LibraryService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, LibraryServicePath)).inSingletonScope();
@@ -51,13 +101,31 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
createWidget: () => context.container.get(LibraryListWidget)
}));
bind(FrontendApplicationContribution).toService(LibraryListWidgetFrontendContribution);
bind(LibraryItemRenderer).toSelf().inSingletonScope();
// Boards Notification service for updating boards list
// TODO (post-PoC): move this to boards service/backend
bind(BoardsNotificationService).toSelf().inSingletonScope();
// Sketch list service
bind(SketchesService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, SketchesServicePath)).inSingletonScope();
// Config service
bind(ConfigService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ConfigServicePath)).inSingletonScope();
// Boards service
bind(BoardsService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath)).inSingletonScope();
bind(BoardsService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(BoardsServiceClientImpl);
return connection.createProxy(BoardsServicePath, client);
}).inSingletonScope();
// Boards service client to receive and delegate notifications from the backend.
bind(BoardsServiceClientImpl).toSelf().inSingletonScope();
bind(BoardsServiceClient).toDynamicValue(context => {
const client = context.container.get(BoardsServiceClientImpl);
WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client);
return client;
}).inSingletonScope();
// boards auto-installer
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);
// Boards list widget
bind(BoardsListWidget).toSelf();
@@ -67,6 +135,14 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
createWidget: () => context.container.get(BoardsListWidget)
}));
bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution);
bind(BoardItemRenderer).toSelf().inSingletonScope();
// Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
bind(BoardsConfigDialog).toSelf().inSingletonScope();
bind(BoardsConfigDialogProps).toConstantValue({
title: 'Select Board'
})
// Core service
bind(CoreService)
@@ -88,12 +164,84 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
container.get(CoreService);
container.get(LibraryService);
container.get(BoardsService);
container.get(SketchesService);
return workspaceServiceExt;
});
rebind(WorkspaceService).to(AWorkspaceService).inSingletonScope();
bind(SketchFactory).toSelf().inSingletonScope();
// Serial Monitor
bind(MonitorModel).toSelf().inSingletonScope();
bind(MonitorWidget).toSelf();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue(context => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget)
}));
// Frontend binding for the 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();
// 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);
WebSocketConnectionProvider.createProxy(context.container, MonitorServicePath, client);
return client;
}).inSingletonScope();
bind(ArduinoWorkspaceService).toSelf().inSingletonScope();
rebind(WorkspaceService).to(ArduinoWorkspaceService).inSingletonScope();
const themeService = ThemeService.get();
themeService.register(...ArduinoTheme.themes);
// Customizing default Theia layout
if (!ArduinoAdvancedMode.TOGGLED) {
unbind(OutlineViewContribution);
bind(OutlineViewContribution).to(SilentOutlineViewContribution).inSingletonScope();
unbind(ProblemContribution);
bind(ProblemContribution).to(SilentProblemContribution).inSingletonScope();
unbind(FileNavigatorContribution);
bind(FileNavigatorContribution).to(SilentNavigatorContribution).inSingletonScope();
unbind(OutputToolbarContribution);
bind(OutputToolbarContribution).to(ArduinoOutputToolContribution).inSingletonScope();
unbind(EditorContribution);
bind(EditorContribution).to(ArduinoEditorContribution).inSingletonScope();
unbind(MonacoStatusBarContribution);
bind(MonacoStatusBarContribution).to(ArduinoMonacoStatusBarContribution).inSingletonScope();
unbind(ApplicationShell);
bind(ApplicationShell).to(ArduinoApplicationShell).inSingletonScope();
unbind(ScmContribution);
bind(ScmContribution).to(SilentScmContribution).inSingletonScope();
unbind(SearchInWorkspaceFrontendContribution);
bind(SearchInWorkspaceFrontendContribution).to(SilentSearchInWorkspaceContribution).inSingletonScope();
} else {
// We use this CSS class on the body to modify the visibility of the close button for the editors and views.
document.body.classList.add(ArduinoAdvancedMode.LS_ID);
}
unbind(FrontendApplication);
bind(FrontendApplication).to(ArduinoFrontendApplication).inSingletonScope();
// Monaco customizations
unbind(MonacoEditorProvider);
bind(ArduinoMonacoEditorProvider).toSelf().inSingletonScope();
bind(MonacoEditorProvider).toService(ArduinoMonacoEditorProvider);
// Decorator customizations
unbind(TabBarDecoratorService);
bind(ArduinoTabBarDecoratorService).toSelf().inSingletonScope();
bind(TabBarDecoratorService).toService(ArduinoTabBarDecoratorService);
// Problem markers
unbind(ProblemManager);
bind(ArduinoProblemManager).toSelf().inSingletonScope();
bind(ProblemManager).toService(ArduinoProblemManager);
// About dialog to show the CLI version
unbind(AboutDialog);
bind(ArduinoAboutDialog).toSelf().inSingletonScope();
bind(AboutDialog).toService(ArduinoAboutDialog);
});

View File

@@ -19,9 +19,10 @@ export class ArduinoTheme {
deactivate() {
ARDUINO_CSS.unuse();
}
}
};
static readonly themes: Theme[] = [
ArduinoTheme.arduino
]
}
];
}

View File

@@ -0,0 +1,68 @@
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 exists in 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.bind(this)(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;
}
}

View File

@@ -1,50 +1,56 @@
import { WorkspaceService } from "@theia/workspace/lib/browser/workspace-service";
import { injectable, inject } from "inversify";
import { WorkspaceServer } from "@theia/workspace/lib/common";
import { FileSystem, FileStat } from "@theia/filesystem/lib/common";
import URI from "@theia/core/lib/common/uri";
import { SketchFactory } from "./sketch-factory";
import { injectable, inject } from 'inversify';
import { LabelProvider } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ConfigService } from '../common/protocol/config-service';
import { SketchesService } from '../common/protocol/sketches-service';
import { ArduinoWorkspaceRootResolver } from './arduino-workspace-resolver';
import { ArduinoAdvancedMode } from './arduino-frontend-contribution';
/**
* This is workaround to have custom frontend binding for the default workspace, although we
* already have a custom binding for the backend.
*/
@injectable()
export class AWorkspaceService extends WorkspaceService {
export class ArduinoWorkspaceService extends WorkspaceService {
@inject(WorkspaceServer)
protected readonly workspaceServer: WorkspaceServer;
@inject(SketchesService)
protected readonly sketchService: SketchesService;
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(SketchFactory)
protected readonly sketchFactory: SketchFactory;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
protected async getDefaultWorkspacePath(): Promise<string | undefined> {
let result = await super.getDefaultWorkspacePath();
if (!result) {
const userHome = await this.fileSystem.getCurrentUserHome();
if (!userHome) {
return;
}
// The backend has created this location if it was missing.
result = new URI(userHome.uri).resolve('Arduino-PoC').resolve('Sketches').toString();
async getDefaultWorkspacePath(): Promise<string | undefined> {
const [hash, recentWorkspaces, recentSketches] = await Promise.all([
window.location.hash,
this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)),
this.server.getRecentWorkspaces()
]);
const toOpen = await new ArduinoWorkspaceRootResolver({
isValid: this.isValid.bind(this)
}).resolve({
hash,
recentWorkspaces,
recentSketches
});
if (toOpen) {
const { uri } = toOpen;
await this.server.setMostRecentlyUsedWorkspace(uri);
return toOpen.uri;
}
const stat = await this.fileSystem.getFileStat(result);
if (!stat) {
// workspace does not exist yet, create it
await this.fileSystem.createFolder(result);
await this.sketchFactory.createNewSketch(new URI(result));
}
return result;
return (await this.sketchService.createNewSketch()).uri;
}
protected async setWorkspace(workspaceStat: FileStat | undefined): Promise<void> {
await super.setWorkspace(workspaceStat);
private async isValid(uri: string): Promise<boolean> {
const exists = await this.fileSystem.exists(uri);
if (!exists) {
return false;
}
// The workspace root location must exist. However, when opening a workspace root in pro-mode,
// the workspace root must not be a sketch folder. It can be the default sketch directory, or any other directories, for instance.
if (!ArduinoAdvancedMode.TOGGLED) {
return true;
}
const sketchFolder = await this.sketchService.isSketchFolder(uri);
return sketchFolder;
}
}
}

View File

@@ -1,19 +0,0 @@
import { EventEmitter } from "events";
import { injectable } from "inversify";
// TODO (post-PoC): move this to the backend / BoardsService
@injectable()
export class BoardsNotificationService {
protected readonly emitter = new EventEmitter();
public on(event: 'boards-installed', listener: (...args: any[]) => void): this {
this.emitter.on(event, listener);
return this;
}
public notifyBoardsInstalled() {
this.emitter.emit('boards-installed');
}
}

View File

@@ -0,0 +1,63 @@
import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { BoardsService, Board } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
import { InstallationProgressDialog } from '../components/installation-progress-dialog';
import { BoardsConfig } from './boards-config';
/**
* Listens on `BoardsConfig.Config` changes, if a board is selected which does not
* have the corresponding core installed, it proposes the user to install the core.
*/
@injectable()
export class BoardsAutoInstaller implements FrontendApplicationContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(BoardsListWidgetFrontendContribution)
protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution;
onStart(): void {
this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this));
this.ensureCoreExists(this.boardsServiceClient.boardsConfig);
}
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
if (selectedBoard) {
this.boardsService.search({}).then(({ items }) => {
const candidates = items
.filter(item => item.boards.some(board => Board.sameAs(board, selectedBoard)))
.filter(({ installable, installedVersion }) => installable && !installedVersion);
for (const candidate of candidates) {
// tslint:disable-next-line:max-line-length
this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Yes', 'Install Manually').then(async answer => {
if (answer === 'Yes') {
const dialog = new InstallationProgressDialog(candidate.name);
dialog.open();
try {
await this.boardsService.install(candidate);
} finally {
dialog.close();
}
}
if (answer) {
this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase()));
}
});
}
})
}
}
}

View File

@@ -0,0 +1,55 @@
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 { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly onBoardConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
protected focusNode: HTMLElement | undefined;
constructor() {
super();
this.id = 'select-board-dialog';
}
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
boardsService={this.boardsService}
boardsServiceClient={this.boardsServiceClient}
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode} />
</div>;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
}
}

View File

@@ -0,0 +1,113 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@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(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
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();
}));
}
protected createDescription(): HTMLElement {
const head = document.createElement('div');
head.classList.add('head');
const title = document.createElement('div');
title.textContent = 'Select Other Board & Port';
title.classList.add('title');
head.appendChild(title);
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);
for (const paragraph of [
'Select both a Board and a Port if you want to upload a sketch.',
'If you only select a Board you will be able just to compile, but not to upload your sketch.'
]) {
const p = document.createElement('p');
p.textContent = paragraph;
text.appendChild(p);
}
return head;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(this.widget.onBoardConfigChanged(config => {
this.config = config;
this.update();
}));
super.onAfterAttach(msg);
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;
}
}

View File

@@ -0,0 +1,295 @@
import * as React from 'react';
import { DisposableCollection } from '@theia/core';
import { BoardsService, Board, Port, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
export namespace BoardsConfig {
export interface Config {
selectedBoard?: Board;
selectedPort?: Port;
}
export interface Props {
readonly boardsService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
}
export interface State extends Config {
searchResults: Array<Board & { packageName: string }>;
knownPorts: Port[];
showAllPorts: boolean;
}
}
export abstract class Item<T> extends React.Component<{
item: T,
label: string,
selected: boolean,
onClick: (item: T) => void,
missing?: boolean,
detail?: string
}> {
render(): React.ReactNode {
const { selected, label, missing, detail } = 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}${!detail ? '' : detail}`}>
<div className='label'>
{label}
</div>
{!detail ? '' : <div className='detail'>{detail}</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.boardsServiceClient;
this.state = {
searchResults: [],
knownPorts: [],
showAllPorts: false,
...boardsConfig
}
}
componentDidMount() {
this.updateBoards();
this.props.boardsService.getAvailablePorts().then(({ ports }) => this.updatePorts(ports));
const { boardsServiceClient: client } = this.props;
this.toDispose.pushAll([
client.onBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
})
]);
}
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()
).trim();
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<{ searchResults: Array<Board & { packageName: string }> }> => {
const { boardsService } = this.props;
const query = (options.query || '').toLocaleLowerCase();
return new Promise<{ searchResults: Array<Board & { packageName: string }> }>(resolve => {
boardsService.search(options)
.then(({ items }) => items
.map(item => item.boards.map(board => ({ ...board, packageName: item.name })))
.reduce((acc, curr) => acc.concat(curr), [])
.filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1)
.sort(Board.compare))
.then(searchResults => resolve({ searchResults }));
});
}
protected get attachedBoards(): Promise<{ boards: Board[] }> {
return this.props.boardsService.getAttachedBoards();
}
protected get availablePorts(): Promise<{ ports: Port[] }> {
return this.props.boardsService.getAvailablePorts();
}
protected queryPorts = (availablePorts: Promise<{ ports: Port[] }> = this.availablePorts) => {
return new Promise<{ knownPorts: Port[] }>(resolve => {
availablePorts
.then(({ ports }) => ports
.sort(Port.compare))
.then(knownPorts => resolve({ knownPorts }));
});
}
protected toggleFilterPorts = () => {
this.setState({ showAllPorts: !this.state.showAllPorts });
}
protected selectPort = (selectedPort: Port | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
}
protected selectBoard = (selectedBoard: Board & { packageName: string } | 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 } = this.state;
// Board names are not unique. We show the corresponding core name as a detail.
// https://github.com/arduino/arduino-cli/pull/294#issuecomment-513764948
const distinctBoardNames = new Map<string, number>();
for (const { name } of searchResults) {
const counter = distinctBoardNames.get(name) || 0;
distinctBoardNames.set(name, counter + 1);
}
// Due to the non-unique board names, we have to check the package name as well.
const selected = (board: Board & { packageName: string }) => {
if (!!selectedBoard) {
if (Board.equals(board, selectedBoard)) {
if ('packageName' in selectedBoard) {
return board.packageName === (selectedBoard as any).packageName;
}
return true;
}
}
return false;
}
return <React.Fragment>
<div className='search'>
<input type='search' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
<i className='fa fa-search'></i>
</div>
<div className='boards list'>
{this.state.searchResults.map(board => <Item<Board & { packageName: string }>
key={`${board.name}-${board.packageName}`}
item={board}
label={board.name}
detail={(distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined}
selected={selected(board)}
onClick={this.selectBoard}
missing={!Board.installed(board)}
/>)}
</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 | AttachedSerialBoard): boolean {
const { selectedBoard, selectedPort } = config;
if (AttachedSerialBoard.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) : ''}`;
}
}
}

View File

@@ -0,0 +1,54 @@
import * as React from 'react';
import { injectable } from 'inversify';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
import { BoardPackage } from '../../common/protocol/boards-service';
@injectable()
export class BoardItemRenderer extends ListItemRenderer<BoardPackage> {
renderItem(item: BoardPackage, install: (item: BoardPackage) => Promise<void>): React.ReactNode {
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>{item.author}</span>;
const installedVersion = !!item.installedVersion && <div className='version-info'>
<span className='version'>Version {item.installedVersion}</span>
<span className='installed'>INSTALLED</span>
</div>;
const summary = <div className='summary'>{item.summary}</div>;
const description = <div className='summary'>{item.description}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const installButton = item.installable && !item.installedVersion &&
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
const versions = (() => {
const { availableVersions } = item;
if (!!item.installedVersion || availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
}
})();
return <div className='component-list-item noselect'>
<div className='header'>
<span>{name} by {author}</span>
{installedVersion}
</div>
<div className='content'>
{summary}
{description}
</div>
<div className='info'>
{moreInfo}
</div>
<div className='footer'>
{installButton}
{versions}
</div>
</div>;
}
}

View File

@@ -0,0 +1,27 @@
import { inject, injectable } from 'inversify';
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
import { ListWidget } from '../components/component-list/list-widget';
import { BoardItemRenderer } from './boards-item-renderer';
@injectable()
export class BoardsListWidget extends ListWidget<BoardPackage> {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
constructor(
@inject(BoardsService) protected service: BoardsService,
@inject(BoardItemRenderer) protected itemRenderer: BoardItemRenderer) {
super({
id: BoardsListWidget.WIDGET_ID,
label: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip',
searchable: service,
installable: service,
itemLabel: (item: BoardPackage) => item.name,
itemRenderer
});
}
}

View File

@@ -1,16 +0,0 @@
import { ListWidget } from './list-widget';
export class BoardsListWidget extends ListWidget {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
protected widgetProps(): ListWidget.Props {
return {
id: BoardsListWidget.WIDGET_ID,
title: BoardsListWidget.WIDGET_LABEL,
iconClass: 'fa fa-microchip'
}
}
}

View File

@@ -0,0 +1,122 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { RecursiveRequired } from '../../common/types';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
@injectable()
export class BoardsServiceClientImpl implements BoardsServiceClient {
@inject(ILogger)
protected logger: ILogger;
@inject(LocalStorageService)
protected storageService: LocalStorageService;
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
* It happens with certain boards on Windows. For example, the `MKR1000` boards is selected on post `COM5` on Windows,
* perform an upload, the board automatically disconnects and reconnects, but on another port, `COM10`.
* We have to listen on such changes and auto-reconnect the same board on another port.
* See: https://arduino.slack.com/archives/CJJHJCJSJ/p1568645417013000?thread_ts=1568640504.009400&cid=CJJHJCJSJ
*/
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
protected _boardsConfig: BoardsConfig.Config = {};
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
@postConstruct()
protected init(): void {
this.loadState();
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
const { detached, attached } = AttachedBoardsChangeEvent.diff(event);
const { selectedPort, selectedBoard } = this.boardsConfig;
this.onAttachedBoardsChangedEmitter.fire(event);
// Dynamically unset the port if is not available anymore. A port can be "detached" when removing a board.
if (detached.ports.some(port => Port.equals(selectedPort, port))) {
this.boardsConfig = {
selectedBoard,
selectedPort: undefined
};
}
// Try to reconnect.
this.tryReconnect(attached.boards, attached.ports);
}
async tryReconnect(attachedBoards: Board[], availablePorts: Port[]): Promise<boolean> {
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
this.boardsConfig = this.latestValidBoardsConfig;
return true;
}
}
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
// See documentation on `latestValidBoardsConfig`.
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name) {
this.boardsConfig = {
...this.latestValidBoardsConfig,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
};
return true;
}
}
}
return false;
}
notifyBoardInstalled(event: BoardInstalledEvent): void {
this.logger.info('Board installed: ', JSON.stringify(event));
this.onBoardInstalledEmitter.fire(event);
}
set boardsConfig(config: BoardsConfig.Config) {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
if (this.canUploadTo(this._boardsConfig)) {
this.latestValidBoardsConfig = this._boardsConfig;
}
this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig));
}
get boardsConfig(): BoardsConfig.Config {
return this._boardsConfig;
}
protected saveState(): Promise<void> {
return this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
}
protected async loadState(): Promise<void> {
const storedValidBoardsConfig = await this.storageService.getData<RecursiveRequired<BoardsConfig.Config>>('latest-valid-boards-config');
if (storedValidBoardsConfig) {
this.latestValidBoardsConfig = storedValidBoardsConfig;
}
}
protected canVerify(config: BoardsConfig.Config | undefined): config is BoardsConfig.Config & { selectedBoard: Board } {
return !!config && !!config.selectedBoard;
}
protected canUploadTo(config: BoardsConfig.Config | undefined): config is RecursiveRequired<BoardsConfig.Config> {
return this.canVerify(config) && !!config.selectedPort && !!config.selectedBoard.fqbn;
}
}

View File

@@ -0,0 +1,202 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { CommandRegistry, DisposableCollection } from '@theia/core';
import { BoardsService, Board, AttachedSerialBoard, Port } from '../../common/protocol/boards-service';
import { ArduinoCommands } from '../arduino-commands';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsConfig } from './boards-config';
export interface BoardsDropDownListCoords {
readonly top: number;
readonly left: number;
readonly width: number;
readonly paddingTop: number;
}
export namespace BoardsDropDown {
export interface Props {
readonly coords: BoardsDropDownListCoords | 'hidden';
readonly items: Item[];
readonly openBoardsConfig: () => void;
}
export interface Item {
readonly label: string;
readonly selected: boolean;
readonly onClick: () => void;
}
}
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
protected dropdownElement: HTMLElement;
constructor(props: BoardsDropDown.Props) {
super(props);
let list = document.getElementById('boards-dropdown-container');
if (!list) {
list = document.createElement('div');
list.id = 'boards-dropdown-container';
document.body.appendChild(list);
this.dropdownElement = list;
}
}
render(): React.ReactNode {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
protected renderNode(): React.ReactNode {
const { coords, items } = this.props;
if (coords === 'hidden') {
return '';
}
items.push({
label: 'Select Other Board & Port',
selected: false,
onClick: () => this.props.openBoardsConfig()
})
return <div className='arduino-boards-dropdown-list'
style={{
position: 'absolute',
...coords
}}>
{items.map(this.renderItem)}
</div>
}
protected renderItem(item: BoardsDropDown.Item): React.ReactNode {
const { label, selected, onClick } = item;
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
<div>
{label}
</div>
{selected ? <span className='fa fa-check'/> : ''}
</div>
}
}
export namespace BoardsToolBarItem {
export interface Props {
readonly boardService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly commands: CommandRegistry;
}
export interface State {
boardsConfig: BoardsConfig.Config;
attachedBoards: Board[];
availablePorts: Port[];
coords: BoardsDropDownListCoords | 'hidden';
}
}
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
static TOOLBAR_ID: 'boards-toolbar';
protected readonly toDispose: DisposableCollection = new DisposableCollection();
constructor(props: BoardsToolBarItem.Props) {
super(props);
this.state = {
boardsConfig: this.props.boardsServiceClient.boardsConfig,
attachedBoards: [],
availablePorts: [],
coords: 'hidden'
};
document.addEventListener('click', () => {
this.setState({ coords: 'hidden' });
});
}
componentDidMount() {
const { boardsServiceClient: client, boardService } = this.props;
this.toDispose.pushAll([
client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })),
client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards, availablePorts: newState.ports }))
]);
Promise.all([
boardService.getAttachedBoards(),
boardService.getAvailablePorts()
]).then(([{boards: attachedBoards}, { ports: availablePorts }]) => {
this.setState({ attachedBoards, availablePorts })
});
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
const rect = element.getBoundingClientRect();
this.setState({
coords: {
top: rect.top,
left: rect.left,
width: rect.width,
paddingTop: rect.height
}
});
} else {
this.setState({ coords: 'hidden'});
}
}
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
};
render(): React.ReactNode {
const { boardsConfig, coords, attachedBoards, availablePorts } = this.state;
const title = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
const configuredBoard = attachedBoards
.filter(AttachedSerialBoard.is)
.filter(board => availablePorts.some(port => Port.sameAs(port, board.port)))
.filter(board => BoardsConfig.Config.sameAs(boardsConfig, board)).shift();
const items = attachedBoards.filter(AttachedSerialBoard.is).map(board => ({
label: `${board.name} at ${board.port}`,
selected: configuredBoard === board,
onClick: () => {
this.props.boardsServiceClient.boardsConfig = {
selectedBoard: board,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
}
}
}));
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={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
<div className='label noWrapInfo'>
<div className='noWrapInfo noselect'>
{title}
</div>
</div>
<span className='fa fa-caret-down caret'/>
</div>
</div>
</div>
<BoardsDropDown
coords={coords}
items={items}
openBoardsConfig={this.openDialog}>
</BoardsDropDown>
</React.Fragment>;
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ coords: 'hidden' });
};
}

View File

@@ -1,20 +1,14 @@
import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ListWidget } from './list-widget';
import { MenuModelRegistry } from '@theia/core';
import { BoardsListWidget } from './boards-list-widget';
import { ArduinoMenus } from '../arduino-frontend-contribution';
import { BoardPackage } from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../components/component-list/list-widget-frontend-contribution';
@injectable()
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution {
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardPackage> {
async initializeLayout(): Promise<void> {
await this.openView();
}
}
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution {
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;
constructor() {
super({
@@ -24,9 +18,18 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
area: 'left',
rank: 600
},
toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`,
toggleCommandId: BoardsListWidgetFrontendContribution.OPEN_MANAGER,
toggleKeybinding: 'ctrlcmd+shift+b'
});
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Boards Manager...'
});
}
}
}

View File

@@ -1,87 +0,0 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { BoardsService, Board, BoardPackage } from '../../common/protocol/boards-service';
import { BoardsNotificationService } from '../boards-notification-service';
@injectable()
export abstract class ListWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
constructor() {
super();
const { id, title, iconClass } = this.widgetProps();
this.id = id;
this.title.label = title;
this.title.caption = title;
this.title.iconClass = iconClass;
this.title.closable = true;
this.addClass(ListWidget.Styles.LIST_WIDGET_CLASS);
this.node.tabIndex = 0; // To be able to set the focus on the widget.
}
protected abstract widgetProps(): ListWidget.Props;
@postConstruct()
protected init(): void {
this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.focus();
this.render();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
render(): React.ReactNode {
const boardsServiceDelegate = this.boardsService;
const boardsService: BoardsService = {
getAttachedBoards: () => boardsServiceDelegate.getAttachedBoards(),
selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board),
getSelectBoard: () => boardsServiceDelegate.getSelectBoard(),
search: (options: { query?: string }) => boardsServiceDelegate.search(options),
install: async (item: BoardPackage) => {
await boardsServiceDelegate.install(item);
this.boardsNotificationService.notifyBoardsInstalled();
}
}
return <FilterableListContainer
service={boardsService}
windowService={this.windowService}
/>;
}
}
export namespace ListWidget {
/**
* Props for customizing the abstract list widget.
*/
export interface Props {
readonly id: string;
readonly title: string;
readonly iconClass: string;
}
export namespace Styles {
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
}
}

View File

@@ -1,78 +1,25 @@
import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListItemRenderer } from './list-item-renderer';
export class ComponentListItem extends React.Component<ComponentListItem.Props> {
export class ComponentListItem<T> extends React.Component<ComponentListItem.Props<T>> {
private onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.props.windowService.openNewWindow(target.href);
event.nativeEvent.preventDefault();
}
}
private async install(item: ArduinoComponent) {
protected async install(item: T): Promise<void> {
await this.props.install(item);
}
render(): React.ReactNode {
const { item } = this.props;
const style = ComponentListItem.Styles;
const name = <span className={style.NAME_CLASS}>{item.name}</span>;
const author = <span className={style.AUTHOR_CLASS}>{item.author}</span>;
const installedVersion = !!item.installedVersion && <div className={style.VERSION_INFO_CLASS}>
<span className={style.VERSION_CLASS}>Version {item.installedVersion}</span>
<span className={style.INSTALLED_CLASS}>INSTALLED</span>
</div>;
const summary = <div className={style.SUMMARY_CLASS}>{item.summary}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const install = this.props.install && item.installable && !item.installedVersion &&
<button className={style.INSTALL_BTN_CLASS} onClick={this.install.bind(this, item)}>INSTALL</button>;
return <div className={[style.LIST_ITEM_CLASS, style.NO_SELECT_CLASS].join(' ')}>
<div className={style.HEADER_CLASS}>
<span>{name} by {author}</span>
{installedVersion}
</div>
<div className={style.CONTENT_CLASS}>
{summary}
</div>
<div className={style.FOOTER_CLASS}>
{moreInfo}
{install}
</div>
</div>;
const { item, itemRenderer, install } = this.props;
return itemRenderer.renderItem(item, install.bind(this));
}
}
export namespace ComponentListItem {
export interface Props {
readonly item: ArduinoComponent;
readonly windowService: WindowService;
readonly install: (comp: ArduinoComponent) => Promise<void>;
}
export namespace Styles {
export const LIST_ITEM_CLASS = 'component-list-item';
export const HEADER_CLASS = 'header';
export const VERSION_INFO_CLASS = 'version-info';
export const CONTENT_CLASS = 'content';
export const FOOTER_CLASS = 'footer';
export const INSTALLED_CLASS = 'installed';
export const NO_SELECT_CLASS = 'noselect';
export const NAME_CLASS = 'name';
export const AUTHOR_CLASS = 'author';
export const VERSION_CLASS = 'version';
export const SUMMARY_CLASS = 'summary';
export const DESCRIPTION_CLASS = 'description';
export const INSTALL_BTN_CLASS = 'install';
export interface Props<T> {
readonly item: T;
readonly install: (item: T) => Promise<void>;
readonly itemRenderer: ListItemRenderer<T>;
}
}

View File

@@ -1,24 +1,47 @@
import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ComponentListItem } from './component-list-item';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListItemRenderer } from './list-item-renderer';
export class ComponentList extends React.Component<ComponentList.Props> {
export class ComponentList<T> extends React.Component<ComponentList.Props<T>> {
protected container?: HTMLElement;
render(): React.ReactNode {
return <div>
{this.props.items.map((item, idx) => <ComponentListItem key={idx} item={item} windowService={this.props.windowService} install={this.props.install} />)}
return <div
className={'items-container'}
ref={this.setRef}>
{this.props.items.map(item => this.createItem(item))}
</div>;
}
componentDidMount(): void {
if (this.container && this.props.resolveContainer) {
this.props.resolveContainer(this.container);
}
}
protected setRef = (element: HTMLElement | null) => {
this.container = element || undefined;
}
protected createItem(item: T): React.ReactNode {
return <ComponentListItem<T>
key={this.props.itemLabel(item)}
item={item}
itemRenderer={this.props.itemRenderer}
install={this.props.install} />
}
}
export namespace ComponentList {
export interface Props {
readonly items: ArduinoComponent[];
readonly windowService: WindowService;
readonly install: (comp: ArduinoComponent) => Promise<void>;
export interface Props<T> {
readonly items: T[];
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
readonly install: (item: T) => Promise<void>;
readonly resolveContainer: (element: HTMLElement) => void;
}
}

View File

@@ -1,64 +1,88 @@
import * as React from 'react';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { ComponentList } from './component-list';
import { SearchBar } from './search-bar';
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import debounce = require('lodash.debounce');
import { Event } from '@theia/core/lib/common/event';
import { Searchable } from '../../../common/protocol/searchable';
import { Installable } from '../../../common/protocol/installable';
import { InstallationProgressDialog } from '../installation-progress-dialog';
import { SearchBar } from './search-bar';
import { ComponentList } from './component-list';
import { ListItemRenderer } from './list-item-renderer';
export class FilterableListContainer extends React.Component<FilterableListContainer.Props, FilterableListContainer.State> {
export class FilterableListContainer<T> extends React.Component<FilterableListContainer.Props<T>, FilterableListContainer.State<T>> {
constructor(props: Readonly<FilterableListContainer.Props>) {
constructor(props: Readonly<FilterableListContainer.Props<T>>) {
super(props);
this.state = {
filterText: '',
items: []
};
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
}
componentWillMount(): void {
this.search = debounce(this.search, 500);
this.handleFilterTextChange('');
this.props.filterTextChangeEvent(this.handleFilterTextChange.bind(this));
}
render(): React.ReactNode {
return <div className={FilterableListContainer.Styles.FILTERABLE_LIST_CONTAINER_CLASS}>
<SearchBar
filterText={this.state.filterText}
onFilterTextChanged={this.handleFilterTextChange}
/>
<ComponentList
items={this.state.items}
install={this.install.bind(this)}
windowService={this.props.windowService}
/>
return <div className={'filterable-list-container'}>
{this.renderSearchFilter()}
{this.renderSearchBar()}
{this.renderComponentList()}
</div>
}
private handleFilterTextChange(filterText: string): void {
this.props.service.search({ query: filterText }).then(result => {
protected renderSearchFilter(): React.ReactNode {
return undefined;
}
protected renderSearchBar(): React.ReactNode {
return <SearchBar
resolveFocus={this.props.resolveFocus}
filterText={this.state.filterText}
onFilterTextChanged={this.handleFilterTextChange}
/>
}
protected renderComponentList(): React.ReactNode {
const { itemLabel, resolveContainer, itemRenderer } = this.props;
return <ComponentList<T>
items={this.state.items}
itemLabel={itemLabel}
itemRenderer={itemRenderer}
install={this.install.bind(this)}
resolveContainer={resolveContainer}
/>
}
protected handleFilterTextChange = (filterText: string) => {
this.setState({ filterText });
this.search(filterText);
}
protected search(query: string): void {
const { searchable } = this.props;
searchable.search({ query: query.trim() }).then(result => {
const { items } = result;
this.setState({
filterText,
items: items.sort((a, b) => {
if (a.name < b.name) {
return -1;
} else if (a.name === b.name) {
return 0;
} else {
return 1;
}
})
items: this.sort(items)
});
});
}
protected async install(comp: ArduinoComponent): Promise<void> {
const dialog = new InstallationProgressDialog(comp.name);
protected sort(items: T[]): T[] {
const { itemLabel } = this.props;
return items.sort((left, right) => itemLabel(left).localeCompare(itemLabel(right)));
}
protected async install(item: T): Promise<void> {
const { installable, searchable, itemLabel } = this.props;
const dialog = new InstallationProgressDialog(itemLabel(item));
dialog.open();
try {
await this.props.service.install(comp);
const { items } = await this.props.service.search({ query: this.state.filterText });
this.setState({ items });
await installable.install(item);
const { items } = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
dialog.close();
}
@@ -68,24 +92,19 @@ export class FilterableListContainer extends React.Component<FilterableListConta
export namespace FilterableListContainer {
export interface Props {
readonly service: ComponentSource;
readonly windowService: WindowService;
export interface Props<T> {
readonly installable: Installable<T>;
readonly searchable: Searchable<T>;
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
readonly resolveContainer: (element: HTMLElement) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
readonly filterTextChangeEvent: Event<string>;
}
export interface State {
export interface State<T> {
filterText: string;
items: ArduinoComponent[];
}
export namespace Styles {
export const FILTERABLE_LIST_CONTAINER_CLASS = 'filterable-list-container';
}
export interface ComponentSource {
search(req: { query: string }): Promise<{ items: ArduinoComponent[] }>
install(board: ArduinoComponent): Promise<void>;
items: T[];
}
}

View File

@@ -0,0 +1,21 @@
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
@injectable()
export abstract class ListItemRenderer<T> {
@inject(WindowService)
protected windowService: WindowService;
protected onClick = (event: React.SyntheticEvent<HTMLAnchorElement, Event>) => {
const { target } = event.nativeEvent;
if (target instanceof HTMLAnchorElement) {
this.windowService.openNewWindow(target.href, { external: true });
event.nativeEvent.preventDefault();
}
}
abstract renderItem(item: T, install: (item: T) => Promise<void>): React.ReactNode;
}

View File

@@ -0,0 +1,12 @@
import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ListWidget } from './list-widget';
@injectable()
export abstract class ListWidgetFrontendContribution<T> extends AbstractViewContribution<ListWidget<T>> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
}
}

View File

@@ -0,0 +1,89 @@
import * as React from 'react';
import { injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Emitter } from '@theia/core/lib/common/event';
import { MaybePromise } from '@theia/core/lib/common/types';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { Installable } from '../../../common/protocol/installable';
import { Searchable } from '../../../common/protocol/searchable';
import { FilterableListContainer } from './filterable-list-container';
import { ListItemRenderer } from './list-item-renderer';
@injectable()
export abstract class ListWidget<T> extends ReactWidget {
/**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/
protected focusNode: HTMLElement | undefined;
protected readonly deferredContainer = new Deferred<HTMLElement>();
protected readonly filterTextChangeEmitter = new Emitter<string>();
constructor(protected options: ListWidget.Options<T>) {
super();
const { id, label, iconClass } = options;
this.id = id;
this.title.label = label;
this.title.caption = label;
this.title.iconClass = iconClass
this.title.closable = true;
this.addClass('arduino-list-widget');
this.node.tabIndex = 0; // To be able to set the focus on the widget.
this.scrollOptions = {
suppressScrollX: true
}
this.toDispose.push(this.filterTextChangeEmitter);
}
@postConstruct()
protected init(): void {
this.update();
}
protected getScrollContainer(): MaybePromise<HTMLElement> {
return this.deferredContainer.promise;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
this.focusNode = element;
}
render(): React.ReactNode {
return <FilterableListContainer<T>
resolveContainer={this.deferredContainer.resolve}
resolveFocus={this.onFocusResolved}
searchable={this.options.searchable}
installable={this.options.installable}
itemLabel={this.options.itemLabel}
itemRenderer={this.options.itemRenderer}
filterTextChangeEvent={this.filterTextChangeEmitter.event}/>;
}
refresh(filterText: string): void {
this.deferredContainer.promise.then(() => this.filterTextChangeEmitter.fire(filterText));
}
}
export namespace ListWidget {
export interface Options<T> {
readonly id: string;
readonly label: string;
readonly iconClass: string;
readonly installable: Installable<T>;
readonly searchable: Searchable<T>;
readonly itemLabel: (item: T) => string;
readonly itemRenderer: ListItemRenderer<T>;
}
}

View File

@@ -9,15 +9,22 @@ export class SearchBar extends React.Component<SearchBar.Props> {
render(): React.ReactNode {
return <input
ref={this.setRef}
className={SearchBar.Styles.SEARCH_BAR_CLASS}
type='text'
placeholder='Search'
placeholder='Filter your search...'
size={1}
value={this.props.filterText}
onChange={this.handleFilterTextChange}
/>;
}
private setRef = (element: HTMLElement | null) => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
}
private handleFilterTextChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.props.onFilterTextChanged(event.target.value);
}
@@ -29,6 +36,7 @@ export namespace SearchBar {
export interface Props {
filterText: string;
onFilterTextChanged(filterText: string): void;
readonly resolveFocus?: (element: HTMLElement | undefined) => void;
}
export namespace Styles {

View File

@@ -1,147 +0,0 @@
import * as React from 'react';
import { BoardsService, Board } from '../../common/protocol/boards-service';
// import { SelectBoardDialog } from './select-board-dialog';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import { BoardsNotificationService } from '../boards-notification-service';
export class ConnectedBoards extends React.Component<ConnectedBoards.Props, ConnectedBoards.State> {
static TOOLBAR_ID: 'connected-boards-toolbar';
constructor(props: ConnectedBoards.Props) {
super(props);
this.state = { boardsLoading: false };
props.boardsNotificationService.on('boards-installed', () => this.onBoardsInstalled());
}
render(): React.ReactNode {
let content = [];
if (!!this.state.boards && this.state.boards.length > 0) {
content = this.state.boards.map((b, i) => <option value={i} key={i}>{b.name}</option>);
} else {
let label;
if (this.state.boardsLoading) {
label = "Loading ...";
} else {
label = "No board attached";
}
content = [ <option key="loading" value="0">{label}</option> ];
}
return <div className={ConnectedBoards.Styles.CONNECTED_BOARDS_CLASS}>
<select disabled={!this.state.boards}
onChange={this.onBoardSelect.bind(this)}
value={this.state.selection}>
<optgroup label="Attached boards">
{ content }
</optgroup>
<optgroup label="_________">
{ !!this.state.otherBoard && <option value="selected-other" key="selected-other">{this.state.otherBoard.name} (not attached)</option> }
<option value="select-other" key="select-other">Select other Board</option>
</optgroup>
</select>
</div>;
}
componentDidMount(): void {
this.reloadBoards();
}
protected onBoardsInstalled() {
if (!!this.findUnknownBoards()) {
this.reloadBoards();
}
}
protected findUnknownBoards(): Board[] {
if (!this.state || !this.state.boards) {
return [];
}
return this.state.boards.filter(b => !b.fqbn || b.name === "unknown");
}
protected async reloadBoards() {
const prevSelection = this.state.selection;
this.setState({ boardsLoading: true, boards: undefined, selection: "loading" });
const { boards } = await this.props.boardsService.getAttachedBoards()
this.setState({ boards, boardsLoading: false, selection: prevSelection });
if (boards) {
this.setState({ selection: "0" });
await this.props.boardsService.selectBoard(boards[0]);
const unknownBoards = this.findUnknownBoards();
if (unknownBoards && unknownBoards.length > 0) {
this.props.onUnknownBoard(unknownBoards[0]);
}
}
}
protected async onBoardSelect(evt: React.ChangeEvent<HTMLSelectElement>) {
const selection = evt.target.value;
if (selection === "select-other" || selection === "selected-other") {
let selectedBoard = this.state.otherBoard;
if (selection === "select-other" || !selectedBoard) {
selectedBoard = await this.selectedInstalledBoard();
}
if (!selectedBoard) {
return;
}
await this.props.boardsService.selectBoard(selectedBoard);
this.setState({otherBoard: selectedBoard, selection: "selected-other"});
return;
}
const selectedBoard = (this.state.boards || [])[parseInt(selection, 10)];
if (!selectedBoard) {
return;
}
await this.props.boardsService.selectBoard(selectedBoard);
this.setState({selection});
}
protected async selectedInstalledBoard(): Promise<Board | undefined> {
const {items} = await this.props.boardsService.search({});
const idx = new Map<string, Board>();
items.filter(pkg => !!pkg.installedVersion).forEach(pkg => pkg.boards.forEach(brd => idx.set(`${brd.name}`, brd) ));
if (idx.size === 0) {
this.props.onNoBoardsInstalled();
return;
}
const selection = await this.props.quickPickService.show(Array.from(idx.keys()));
if (!selection) {
return;
}
return idx.get(selection);
}
}
export namespace ConnectedBoards {
export interface Props {
readonly boardsService: BoardsService;
readonly boardsNotificationService: BoardsNotificationService;
readonly quickPickService: QuickPickService;
readonly onNoBoardsInstalled: () => void;
readonly onUnknownBoard: (board: Board) => void;
}
export interface State {
boardsLoading: boolean;
boards?: Board[];
otherBoard?: Board;
selection?: string;
}
export namespace Styles {
export const CONNECTED_BOARDS_CLASS = 'connected-boards';
}
}

View File

@@ -1,12 +1,12 @@
import { AbstractDialog } from "@theia/core/lib/browser";
import { AbstractDialog } from '@theia/core/lib/browser';
export class InstallationProgressDialog extends AbstractDialog<undefined> {
export class InstallationProgressDialog extends AbstractDialog<string> {
readonly value: "does-not-matter";
readonly value = undefined;
constructor(componentName: string) {
super({ title: 'Installation in progress' });
this.contentNode.textContent = `Installing ${componentName}. Please wait.`;
}
}
}

View File

@@ -0,0 +1,25 @@
import { injectable, inject, postConstruct } from 'inversify';
import { AboutDialog, ABOUT_CONTENT_CLASS } from '@theia/core/lib/browser/about-dialog';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoAboutDialog extends AboutDialog {
@inject(ConfigService)
protected readonly configService: ConfigService;
@postConstruct()
protected async init(): Promise<void> {
const [, version] = await Promise.all([super.init(), this.configService.getVersion()]);
if (version) {
const { firstChild } = this.contentNode;
if (firstChild instanceof HTMLElement && firstChild.classList.contains(ABOUT_CONTENT_CLASS)) {
const cliVersion = document.createElement('div');
cliVersion.textContent = version;
firstChild.appendChild(cliVersion);
// TODO: anchor to the commit in the `arduino-cli` repository.
}
}
}
}

View File

@@ -0,0 +1,33 @@
import { ApplicationShell, Widget, Saveable, FocusTracker, Message } from '@theia/core/lib/browser';
import { EditorWidget } from '@theia/editor/lib/browser';
export class ArduinoApplicationShell extends ApplicationShell {
protected refreshBottomPanelToggleButton() {
}
protected async track(widget: Widget): Promise<void> {
const tracker = (this as any).tracker as FocusTracker<Widget>;
tracker.add(widget);
this.disableClose(Saveable.apply(widget));
if (ApplicationShell.TrackableWidgetProvider.is(widget)) {
for (const toTrack of await widget.getTrackableWidgets()) {
tracker.add(toTrack);
this.disableClose(Saveable.apply(toTrack));
}
if (widget.onDidChangeTrackableWidgets) {
widget.onDidChangeTrackableWidgets(widgets => widgets.forEach(w => this.track(w)));
}
}
}
private disableClose(widget: Widget | undefined): void {
if (widget instanceof EditorWidget) {
const onCloseRequest = (_: Message) => {
// NOOP
};
(widget as any).onCloseRequest = onCloseRequest.bind(widget);
}
}
}

View File

@@ -0,0 +1,23 @@
import { EditorContribution } from '@theia/editor/lib/browser/editor-contribution';
import { TextEditor } from '@theia/editor/lib/browser';
import { StatusBarAlignment } from '@theia/core/lib/browser';
export class ArduinoEditorContribution extends EditorContribution {
protected updateLanguageStatus(editor: TextEditor | undefined): void {
}
protected setCursorPositionStatus(editor: TextEditor | undefined): void {
if (!editor) {
this.statusBar.removeElement('editor-status-cursor-position');
return;
}
const { cursor } = editor;
this.statusBar.setElement('editor-status-cursor-position', {
text: `${cursor.line + 1}`,
alignment: StatusBarAlignment.LEFT,
priority: 100
});
}
}

View File

@@ -0,0 +1,11 @@
import { injectable } from 'inversify';
import { FileMenuContribution } from '@theia/workspace/lib/browser';
import { MenuModelRegistry } from '@theia/core';
@injectable()
export class ArduinoFileMenuContribution extends FileMenuContribution {
registerMenus(registry: MenuModelRegistry) {
}
}

View File

@@ -0,0 +1,38 @@
import { injectable, inject } from 'inversify';
import { FileSystem } from '@theia/filesystem/lib/common/filesystem';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
import { ArduinoFrontendContribution, ArduinoAdvancedMode } from '../arduino-frontend-contribution';
@injectable()
export class ArduinoFrontendApplication extends FrontendApplication {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(ArduinoFrontendContribution)
protected readonly frontendContribution: ArduinoFrontendContribution;
protected async initializeLayout(): Promise<void> {
super.initializeLayout().then(() => {
// If not in PRO mode, we open the sketch file with all the related files.
// Otherwise, we reuse the workbench's restore functionality and we do not open anything at all.
// TODO: check `otherwise`. Also, what if we check for opened editors, instead of blindly opening them?
if (!ArduinoAdvancedMode.TOGGLED) {
this.workspaceService.roots.then(roots => {
for (const root of roots) {
this.fileSystem.exists(root.uri).then(exists => {
if (exists) {
this.frontendContribution.openSketchFiles(root.uri);
}
});
}
});
}
});
}
}

View File

@@ -0,0 +1,11 @@
import { MonacoStatusBarContribution } from '@theia/monaco/lib/browser/monaco-status-bar-contribution';
export class ArduinoMonacoStatusBarContribution extends MonacoStatusBarContribution {
protected setConfigTabSizeWidget() {
}
protected setLineEndingWidget() {
}
}

View File

@@ -0,0 +1,11 @@
import { injectable } from 'inversify';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { FrontendApplication } from '@theia/core/lib/browser';
@injectable()
export class SilentNavigatorContribution extends FileNavigatorContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
}
}

View File

@@ -0,0 +1,12 @@
import { injectable } from 'inversify';
import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution';
import { FrontendApplication } from '@theia/core/lib/browser';
@injectable()
export class SilentOutlineViewContribution extends OutlineViewContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
}
}

View File

@@ -0,0 +1,11 @@
import { OutputToolbarContribution } from '@theia/output/lib/browser/output-toolbar-contribution';
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { injectable } from 'inversify';
@injectable()
export class ArduinoOutputToolContribution extends OutputToolbarContribution {
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
}
}

View File

@@ -0,0 +1,15 @@
import { injectable } from 'inversify';
import { ProblemContribution } from '@theia/markers/lib/browser/problem/problem-contribution';
import { ProblemStat } from '@theia/markers/lib/browser/problem/problem-manager';
import { FrontendApplication } from '@theia/core/lib/browser';
@injectable()
export class SilentProblemContribution extends ProblemContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
}
protected setStatusBarElement(problemStat: ProblemStat) {
}
}

View File

@@ -0,0 +1,14 @@
import { injectable } from 'inversify';
import { ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { StatusBarEntry } from '@theia/core/lib/browser';
@injectable()
export class SilentScmContribution extends ScmContribution {
async initializeLayout(): Promise<void> {
}
protected setStatusBarEntry(id: string, entry: StatusBarEntry): void {
}
}

View File

@@ -0,0 +1,11 @@
import { injectable } from 'inversify';
import { SearchInWorkspaceFrontendContribution } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { FrontendApplication } from '@theia/core/lib/browser';
@injectable()
export class SilentSearchInWorkspaceContribution extends SearchInWorkspaceFrontendContribution {
async initializeLayout(app: FrontendApplication): Promise<void> {
}
}

View File

@@ -2,493 +2,89 @@
"tokenColors": [
{
"settings": {
"background": "#FFFFFF",
"foreground": "#000000"
"foreground": "#434f54"
}
},
{
"name": "Source",
"scope": "source",
"settings": {
"background": "#FFFFFF",
"fontStyle": ""
}
},
{
"name": "Comment",
"name": "Comments",
"scope": "comment",
"settings": {
"fontStyle": "italic",
"foreground": "#91a6a6"
"foreground": "#95a5a6cc"
}
},
{
"name": "Docblock",
"scope": "comment.block",
"settings": {
"fontStyle": "",
"foreground": "#91a6a6"
}
},
{
"name": "Docblock tag",
"scope": "keyword.other.phpdoc",
"settings": {
"fontStyle": "",
"foreground": "#91a6a6"
}
},
{
"name": "Core function",
"scope": "support.function",
"settings": {
"fontStyle": "",
"foreground": "#6f8100"
}
},
{
"name": "Keyword",
"name": "Keywords Attributes",
"scope": [
"keyword",
"storage"
"storage",
"support",
"string.quoted.single.c"
],
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
"foreground": "#00979D"
}
},
{
"name": "Keyword operator",
"scope": "keyword.operator.assignment",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "keyword like new",
"scope": "keyword.other.special-method",
"settings": {
"fontStyle": ""
}
},
{
"name": "Function (definition)",
"name": "literal",
"scope": [
"meta.function.c",
"entity.name.function",
"keyword.other.name-of-parameter.objc",
"support.type.exception.python"
"meta.function-call.c"
],
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
"foreground": "#D35400"
}
},
{
"name": "Class (definition)",
"scope": "entity.name",
"settings": {
"fontStyle": "bold",
"foreground": "#445588"
}
},
{
"name": "Number",
"scope": "constant.numeric",
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
}
},
{
"name": "Variable",
"name": "punctuation",
"scope": [
"variable.language",
"variable.other"
"punctuation.section",
"meta.function-call.c",
"meta.block.c",
"meta.function.c",
"entity.name.function.preprocessor.c",
"meta.preprocessor.macro.c"
],
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
"foreground": "#434f54"
}
},
{
"name": "Built-in constant",
"scope": "constant.language",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "Variable",
"scope": "variable.other.constant.ruby",
"settings": {
"foreground": "#0F8787"
}
},
{
"name": "Built-in constant",
"name": "strings",
"scope": [
"constant.language",
"support.function.construct"
"string.quoted.double"
],
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
"foreground": "#005C5F"
}
},
{
"name": "String",
"scope": "string",
"settings": {
"background": "#EB104512",
"foreground": "#006062"
}
},
{
"name": "Inherited class",
"scope": "entity.other.inherited-class",
"settings": {
"fontStyle": "",
"foreground": "#0F8787"
}
},
{
"name": "Inherited class seperator",
"scope": "punctuation.separator.inheritance",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "punctuation block",
"scope": "punctuation.separator.variable",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "array brackets",
"scope": "punctuation.section.array",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "hash separator",
"scope": "punctuation.separator.key-value",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "() brackets",
"scope": "punctuation.section.function",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Method call",
"scope": "meta.function-call",
"settings": {
"fontStyle": "",
"foreground": "#108888"
}
},
{
"name": "Method call",
"scope": "meta.function-call.python",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "hash brackets",
"scope": "punctuation.section.scope",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Special ruby method",
"scope": "keyword.other.special-method.ruby",
"settings": {
"fontStyle": "bold"
}
},
{
"name": "symbol",
"scope": "constant.other.symbol",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Support class",
"scope": "support.class.ruby",
"settings": {
"fontStyle": "",
"foreground": "#008080"
}
},
{
"name": "String.regexp",
"scope": "string.regexp",
"settings": {
"foreground": "#009926"
}
},
{
"name": "String embedded source",
"scope": "string.quoted source",
"settings": {
"foreground": "#000000"
}
},
{
"name": "doctype punctation",
"scope": "meta.tag.sgml.html",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "doctype declaration",
"scope": "entity.name.tag.doctype",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "doctype string",
"scope": "string.quoted.double.doctype",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "html tag punctuation",
"scope": "punctuation.definition.tag",
"settings": {
"fontStyle": "",
"foreground": "#121289"
}
},
{
"name": "html tag punctuation",
"scope": "entity.name.tag",
"settings": {
"fontStyle": "",
"foreground": "#121289"
}
},
{
"name": "html attribute",
"scope": "entity.other.attribute-name",
"settings": {
"fontStyle": "",
"foreground": "#0A8585"
}
},
{
"name": "html attribute punctation",
"scope": "entity.other.attribute-name",
"settings": {
"fontStyle": "",
"foreground": "#0A8585"
}
},
{
"name": "erb tags",
"scope": "punctuation.section.embedded.ruby",
"settings": {
"fontStyle": "bold",
"foreground": "#999999"
}
},
{
"name": "ruby string interpolation",
"scope": "source.ruby.embedded.source punctuation.section.embedded.ruby",
"settings": {
"foreground": "#CF1040"
}
},
{
"name": "css brackets",
"scope": "punctuation.section.property-list.css",
"settings": {
"fontStyle": ""
}
},
{
"name": "css property",
"scope": "support.type.property-name.css",
"settings": {
"fontStyle": "bold"
}
},
{
"name": "css property punctuation",
"scope": "punctuation.separator.key-value.css",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css property value",
"name": "meta keywords",
"scope": [
"meta.property-value",
"constant.other.color"
"keyword.control",
"meta.preprocessor.c"
],
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
"foreground": "#728E00"
}
},
{
"name": "css ending",
"scope": "punctuation.terminator.rule.css",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css font",
"scope": "support.constant.font-name",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css keyword",
"scope": "keyword.other.unit",
"settings": {
"fontStyle": "",
"foreground": "#00b0b3"
}
},
{
"name": "pseudo class",
"scope": "entity.other.attribute-name.pseudo-class",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "css constant property value",
"scope": "support.constant.property-value",
"settings": {
"fontStyle": "bold",
"foreground": "#000000"
}
},
{
"name": "css attribute class",
"scope": "entity.other.attribute-name.class",
"settings": {
"fontStyle": "bold",
"foreground": "#445588"
}
},
{
"name": "css attribute id",
"scope": "entity.other.attribute-name.id",
"settings": {
"fontStyle": "bold",
"foreground": "#990000"
}
},
{
"name": "diff header from",
"scope": "meta.diff.header.from-file",
"settings": {
"background": "#FFDDDD",
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "diff header to",
"scope": "meta.diff.header.to-file",
"settings": {
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "diff inserted",
"scope": "markup.inserted.diff",
"settings": {
"fontStyle": ""
}
},
{
"name": "diff deleted",
"scope": "markup.deleted.diff",
"settings": {
"background": "#FFDDDD",
"fontStyle": "",
"foreground": "#000000"
}
},
{
"name": "Function Parameters",
"scope": "variable.parameter",
"settings": {
"fontStyle": "italic"
}
},
{
"name": "Library object",
"name": "numeric preprocessor",
"scope": [
"support.function",
"support.variable",
"support.constant",
"support.class",
"support.type"
"meta.preprocessor.macro.c",
"constant.numeric.preprocessor.c",
"meta.preprocessor.c"
],
"settings": {
"fontStyle": "bold",
"foreground": "#4183c4"
}
},
{
"name": "Function Meta Call Arguments",
"scope": "meta.function-call.arguments",
"settings": {
"fontStyle": "italic"
}
},
{
"name": "Function Call",
"scope": "meta.function-call",
"settings": {
"fontStyle": ""
"foreground": "#434f54"
}
}
],
"colors": {
"editor.background": "#FFFFFF",
"editorCursor.foreground": "#000000",
"editor.foreground": "#000000",
"editorCursor.foreground": "#434f54",
"editor.foreground": "#434f54",
"editorWhitespace.foreground": "#BFBFBF",
"editor.lineHighlightBackground": "#00000010",
"editor.lineHighlightBackground": "#434f5410",
"editor.selectionBackground": "#ffcb00"
},
"name": "Arduino"

View File

@@ -0,0 +1,34 @@
import { inject, injectable } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoMonacoEditorProvider extends MonacoEditorProvider {
@inject(ConfigService)
protected readonly configService: ConfigService;
protected dataDirUri: string | undefined;
protected async getModel(uri: URI, toDispose: DisposableCollection): Promise<MonacoEditorModel> {
// `createMonacoEditorOptions` is not `async` so we ask the `dataDirUri` here.
// https://github.com/eclipse-theia/theia/issues/6234
const { dataDirUri } = await this.configService.getConfiguration()
this.dataDirUri = dataDirUri;
return super.getModel(uri, toDispose);
}
protected createMonacoEditorOptions(model: MonacoEditorModel): MonacoEditor.IOptions {
const options = this.createOptions(this.preferencePrefixes, model.uri, model.languageId);
options.model = model.textEditorModel;
options.readOnly = model.readOnly;
if (this.dataDirUri) {
options.readOnly = new URI(this.dataDirUri).isEqualOrParent(new URI(model.uri));
}
return options;
}
}

View File

@@ -0,0 +1,869 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="198px"
height="99px"
viewBox="0 0 198 99"
enable-background="new 0 0 198 99"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="buttons.svg"><metadata
id="metadata327"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs325" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1215"
inkscape:window-height="1000"
id="namedview323"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="4"
inkscape:cx="45.252385"
inkscape:cy="36.224987"
inkscape:window-x="65"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><g
id="g5"
transform="translate(-0.12000084,0)"><circle
cx="16.620001"
cy="16.608999"
r="12"
id="circle7"
style="fill:#ffcc00" /><polyline
stroke-miterlimit="10"
points="21.453,12.745 15.788,20.571 11.775,16.658 "
id="polyline9"
style="fill:none;stroke:#655100;stroke-width:2.30489993;stroke-miterlimit:10" /></g><g
id="g11"
transform="translate(-0.12000084,0)"><circle
cx="16.620001"
cy="49.465"
r="12"
id="circle13"
style="fill:#ffffff" /><polyline
stroke-miterlimit="10"
points="21.453,45.601 15.788,53.427 11.775,49.514 "
id="polyline15"
style="fill:none;stroke:#006468;stroke-width:2.30489993;stroke-miterlimit:10" /></g><g
id="g17"
transform="translate(-0.12000084,0)"><circle
cx="16.620001"
cy="82.476997"
r="12"
id="circle19"
style="fill:#4db7bb" /><polyline
stroke-miterlimit="10"
points="21.453,78.612 15.788,86.438 11.775,82.525 "
id="polyline21"
style="fill:none;stroke:#006468;stroke-width:2.30489993;stroke-miterlimit:10" /></g><g
id="g23"
transform="translate(-0.26100159,0)"><circle
cx="49.761002"
cy="16.608999"
r="12"
id="circle25"
style="fill:#ffcc00" /><polygon
points="45.412,15.313 49.307,15.313 49.307,11.53 54.701,16.875 49.331,22.245 49.331,18.563 45.412,18.539 "
id="polygon27"
style="fill:#655100" /></g><g
id="g29"
transform="translate(-0.26100159,0)"><circle
cx="49.761002"
cy="49.465"
r="12"
id="circle31"
style="fill:#ffffff" /><polygon
points="45.412,48.169 49.307,48.169 49.307,44.386 54.701,49.731 49.331,55.101 49.331,51.419 45.412,51.394 "
id="polygon33"
style="fill:#006468" /></g><g
id="g35"
transform="translate(-0.26100159,0)"><circle
cx="49.761002"
cy="82.476997"
r="12"
id="circle37"
style="fill:#4db7bb" /><polygon
points="45.412,81.18 49.307,81.18 49.307,77.397 54.701,82.743 49.331,88.113 49.331,84.43 45.412,84.406 "
id="polygon39"
style="fill:#006468" /></g><g
id="g41"
transform="translate(-0.54399872,0)"><rect
x="105.544"
y="6.1090002"
width="21"
height="21"
id="rect43"
style="fill:#ffcc00" /><polygon
points="114.44,19.083 114.44,15.116 110.586,15.116 116.032,9.621 121.502,15.091 117.751,15.091 117.726,19.083 "
id="polygon45"
style="fill:#655100" /><rect
x="110.511"
y="22.193001"
width="1"
height="1"
id="rect47"
style="fill:#655100" /><rect
x="112.518"
y="22.193001"
width="1"
height="1"
id="rect49"
style="fill:#655100" /><rect
x="114.517"
y="22.193001"
width="1"
height="1"
id="rect51"
style="fill:#655100" /><rect
x="116.525"
y="22.193001"
width="1"
height="1"
id="rect53"
style="fill:#655100" /><rect
x="118.524"
y="22.193001"
width="1"
height="1"
id="rect55"
style="fill:#655100" /><rect
x="120.531"
y="22.193001"
width="1"
height="1"
id="rect57"
style="fill:#655100" /></g><g
id="g59"
transform="translate(-0.68600464,0)"><rect
x="138.686"
y="6.1090002"
width="21"
height="21"
id="rect61"
style="fill:#ffcc00" /><polygon
points="150.79,9.621 150.79,13.588 154.644,13.588 149.198,19.083 143.728,13.612 147.479,13.613 147.504,9.621 "
id="polygon63"
style="fill:#655100" /><rect
x="143.65199"
y="22.193001"
width="1"
height="1"
id="rect65"
style="fill:#655100" /><rect
x="145.66"
y="22.193001"
width="1"
height="1"
id="rect67"
style="fill:#655100" /><rect
x="147.659"
y="22.193001"
width="1"
height="1"
id="rect69"
style="fill:#655100" /><rect
x="149.666"
y="22.193001"
width="1"
height="1"
id="rect71"
style="fill:#655100" /><rect
x="151.666"
y="22.193001"
width="1"
height="1"
id="rect73"
style="fill:#655100" /><rect
x="153.673"
y="22.193001"
width="1"
height="1"
id="rect75"
style="fill:#655100" /></g><g
id="g77"
transform="translate(-0.54399872,0)"><rect
x="105.544"
y="38.965"
width="21"
height="21"
id="rect79"
style="fill:#ffffff" /><polygon
points="114.44,51.939 114.44,47.971 110.586,47.971 116.032,42.476 121.502,47.947 117.751,47.947 117.726,51.939 "
id="polygon81"
style="fill:#006468" /><rect
x="110.511"
y="55.049"
width="1"
height="1"
id="rect83"
style="fill:#006468" /><rect
x="112.518"
y="55.049"
width="1"
height="1"
id="rect85"
style="fill:#006468" /><rect
x="114.517"
y="55.049"
width="1"
height="1"
id="rect87"
style="fill:#006468" /><rect
x="116.525"
y="55.049"
width="1"
height="1"
id="rect89"
style="fill:#006468" /><rect
x="118.524"
y="55.049"
width="1"
height="1"
id="rect91"
style="fill:#006468" /><rect
x="120.531"
y="55.049"
width="1"
height="1"
id="rect93"
style="fill:#006468" /></g><g
id="g95"
transform="translate(-0.68600464,0)"><rect
x="138.686"
y="38.965"
width="21"
height="21"
id="rect97"
style="fill:#ffffff" /><polygon
points="150.79,42.476 150.79,46.444 154.644,46.444 149.198,51.939 143.728,46.468 147.479,46.468 147.504,42.477 "
id="polygon99"
style="fill:#006468" /><rect
x="143.65199"
y="55.049"
width="1"
height="1"
id="rect101"
style="fill:#006468" /><rect
x="145.66"
y="55.049"
width="1"
height="1"
id="rect103"
style="fill:#006468" /><rect
x="147.659"
y="55.049"
width="1"
height="1"
id="rect105"
style="fill:#006468" /><rect
x="149.666"
y="55.049"
width="1"
height="1"
id="rect107"
style="fill:#006468" /><rect
x="151.666"
y="55.049"
width="1"
height="1"
id="rect109"
style="fill:#006468" /><rect
x="153.673"
y="55.049"
width="1"
height="1"
id="rect111"
style="fill:#006468" /></g><g
id="g113"
transform="translate(-0.54399872,0)"><rect
x="105.544"
y="71.976997"
width="21"
height="21"
id="rect115"
style="fill:#4db7bb" /><polygon
points="114.44,84.95 114.44,80.983 110.586,80.983 116.032,75.488 121.502,80.959 117.751,80.958 117.726,84.95 "
id="polygon117"
style="fill:#006468" /><rect
x="110.511"
y="88.060997"
width="1"
height="1"
id="rect119"
style="fill:#006468" /><rect
x="112.518"
y="88.060997"
width="1"
height="1"
id="rect121"
style="fill:#006468" /><rect
x="114.517"
y="88.060997"
width="1"
height="1"
id="rect123"
style="fill:#006468" /><rect
x="116.525"
y="88.060997"
width="1"
height="1"
id="rect125"
style="fill:#006468" /><rect
x="118.524"
y="88.060997"
width="1"
height="1"
id="rect127"
style="fill:#006468" /><rect
x="120.531"
y="88.060997"
width="1"
height="1"
id="rect129"
style="fill:#006468" /></g><g
id="g131"
transform="translate(-0.68600464,0)"><rect
x="138.686"
y="71.976997"
width="21"
height="21"
id="rect133"
style="fill:#4db7bb" /><polygon
points="150.79,75.488 150.79,79.455 154.644,79.455 149.198,84.95 143.728,79.48 147.479,79.48 147.504,75.488 "
id="polygon135"
style="fill:#006468" /><rect
x="143.65199"
y="88.060997"
width="1"
height="1"
id="rect137"
style="fill:#006468" /><rect
x="145.66"
y="88.060997"
width="1"
height="1"
id="rect139"
style="fill:#006468" /><rect
x="147.659"
y="88.060997"
width="1"
height="1"
id="rect141"
style="fill:#006468" /><rect
x="149.666"
y="88.060997"
width="1"
height="1"
id="rect143"
style="fill:#006468" /><rect
x="151.666"
y="88.060997"
width="1"
height="1"
id="rect145"
style="fill:#006468" /><rect
x="153.673"
y="88.060997"
width="1"
height="1"
id="rect147"
style="fill:#006468" /></g><g
id="g149"
transform="translate(-0.40299988,0)"><path
d="M 87.445,22.097"
id="path151"
inkscape:connector-curvature="0"
style="fill:#655100" /><rect
x="72.403"
y="6.1090002"
width="21"
height="21"
id="rect153"
style="fill:#ffcc00" /><polygon
points="83.44,10.094 84.441,10.094 88.421,14.079 88.421,15.057 87.445,15.057 83.44,15.057 "
id="polygon155"
style="fill:#655100" /><polygon
points="78.404,11.093 78.404,22.097 87.445,22.097 87.445,14.87 88.421,14.87 88.421,23.134 77.399,23.134 77.399,10.094 83.562,10.094 83.568,11.093 "
id="polygon157"
style="fill:#655100" /><rect
x="79.399002"
y="12.111"
width="0.995"
height="0.99599999"
id="rect159"
style="fill:#655100" /><rect
x="81.394997"
y="12.111"
width="0.995"
height="0.99599999"
id="rect161"
style="fill:#655100" /><rect
x="79.399002"
y="14.103"
width="0.995"
height="0.99599999"
id="rect163"
style="fill:#655100" /><rect
x="81.394997"
y="14.103"
width="0.995"
height="0.99599999"
id="rect165"
style="fill:#655100" /><rect
x="79.399002"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect167"
style="fill:#655100" /><rect
x="81.394997"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect169"
style="fill:#655100" /><rect
x="83.403"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect171"
style="fill:#655100" /><rect
x="85.400002"
y="16.115999"
width="0.995"
height="0.99599999"
id="rect173"
style="fill:#655100" /><rect
x="79.399002"
y="18.118"
width="0.995"
height="0.99599999"
id="rect175"
style="fill:#655100" /><rect
x="81.394997"
y="18.118"
width="0.995"
height="0.99599999"
id="rect177"
style="fill:#655100" /><rect
x="79.399002"
y="20.132"
width="0.995"
height="0.99599999"
id="rect179"
style="fill:#655100" /><rect
x="81.394997"
y="20.132"
width="0.995"
height="0.99599999"
id="rect181"
style="fill:#655100" /><rect
x="83.403"
y="18.118"
width="0.995"
height="0.99599999"
id="rect183"
style="fill:#655100" /><rect
x="85.400002"
y="18.118"
width="0.995"
height="0.99599999"
id="rect185"
style="fill:#655100" /><rect
x="83.403"
y="20.132"
width="0.995"
height="0.99599999"
id="rect187"
style="fill:#655100" /><rect
x="85.400002"
y="20.132"
width="0.995"
height="0.99599999"
id="rect189"
style="fill:#655100" /></g><g
id="g191"
transform="translate(-0.40299988,0)"><path
d="M 87.445,54.953"
id="path193"
inkscape:connector-curvature="0"
style="fill:#006468" /><rect
x="72.403"
y="38.965"
width="21"
height="21"
id="rect195"
style="fill:#ffffff" /><polygon
points="83.44,42.95 84.441,42.95 88.421,46.935 88.421,47.912 87.445,47.912 83.44,47.912 "
id="polygon197"
style="fill:#006468" /><polygon
points="78.404,43.949 78.404,54.953 87.445,54.953 87.445,47.726 88.421,47.726 88.421,55.99 77.399,55.99 77.399,42.95 83.586,42.95 83.599,43.951 "
id="polygon199"
style="fill:#006468" /><rect
x="79.399002"
y="44.966999"
width="0.995"
height="0.99599999"
id="rect201"
style="fill:#006468" /><rect
x="81.394997"
y="44.966999"
width="0.995"
height="0.99599999"
id="rect203"
style="fill:#006468" /><rect
x="79.399002"
y="46.959"
width="0.995"
height="0.99599999"
id="rect205"
style="fill:#006468" /><rect
x="81.394997"
y="46.959"
width="0.995"
height="0.99599999"
id="rect207"
style="fill:#006468" /><rect
x="79.399002"
y="48.972"
width="0.995"
height="0.99599999"
id="rect209"
style="fill:#006468" /><rect
x="81.394997"
y="48.972"
width="0.995"
height="0.99599999"
id="rect211"
style="fill:#006468" /><rect
x="83.403"
y="48.972"
width="0.995"
height="0.99599999"
id="rect213"
style="fill:#006468" /><rect
x="85.400002"
y="48.972"
width="0.995"
height="0.99599999"
id="rect215"
style="fill:#006468" /><rect
x="79.399002"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect217"
style="fill:#006468" /><rect
x="81.394997"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect219"
style="fill:#006468" /><rect
x="79.399002"
y="52.987"
width="0.995"
height="0.99599999"
id="rect221"
style="fill:#006468" /><rect
x="81.394997"
y="52.987"
width="0.995"
height="0.99599999"
id="rect223"
style="fill:#006468" /><rect
x="83.403"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect225"
style="fill:#006468" /><rect
x="85.400002"
y="50.973999"
width="0.995"
height="0.99599999"
id="rect227"
style="fill:#006468" /><rect
x="83.403"
y="52.987"
width="0.995"
height="0.99599999"
id="rect229"
style="fill:#006468" /><rect
x="85.400002"
y="52.987"
width="0.995"
height="0.99599999"
id="rect231"
style="fill:#006468" /></g><g
id="g233"
transform="translate(-0.40299988,0)"><path
d="M 87.445,87.965"
id="path235"
inkscape:connector-curvature="0"
style="fill:#006468" /><rect
x="72.403"
y="71.976997"
width="21"
height="21"
id="rect237"
style="fill:#4db7bb" /><polygon
points="83.44,75.961 84.441,75.961 88.421,79.946 88.421,80.924 87.445,80.924 83.44,80.924 "
id="polygon239"
style="fill:#006468" /><polygon
points="78.404,76.961 78.404,87.965 87.445,87.965 87.445,80.738 88.421,80.738 88.421,89.002 77.399,89.002 77.399,75.961 83.586,75.961 83.599,76.963 "
id="polygon241"
style="fill:#006468" /><rect
x="79.399002"
y="77.977997"
width="0.995"
height="0.99599999"
id="rect243"
style="fill:#006468" /><rect
x="81.394997"
y="77.977997"
width="0.995"
height="0.99599999"
id="rect245"
style="fill:#006468" /><rect
x="79.399002"
y="79.970001"
width="0.995"
height="0.99599999"
id="rect247"
style="fill:#006468" /><rect
x="81.394997"
y="79.970001"
width="0.995"
height="0.99599999"
id="rect249"
style="fill:#006468" /><rect
x="79.399002"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect251"
style="fill:#006468" /><rect
x="81.394997"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect253"
style="fill:#006468" /><rect
x="83.403"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect255"
style="fill:#006468" /><rect
x="85.400002"
y="81.984001"
width="0.995"
height="0.99599999"
id="rect257"
style="fill:#006468" /><rect
x="79.399002"
y="83.986"
width="0.995"
height="0.99599999"
id="rect259"
style="fill:#006468" /><rect
x="81.394997"
y="83.986"
width="0.995"
height="0.99599999"
id="rect261"
style="fill:#006468" /><rect
x="79.399002"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect263"
style="fill:#006468" /><rect
x="81.394997"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect265"
style="fill:#006468" /><rect
x="83.403"
y="83.986"
width="0.995"
height="0.99599999"
id="rect267"
style="fill:#006468" /><rect
x="85.400002"
y="83.986"
width="0.995"
height="0.99599999"
id="rect269"
style="fill:#006468" /><rect
x="83.403"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect271"
style="fill:#006468" /><rect
x="85.400002"
y="85.999001"
width="0.995"
height="0.99599999"
id="rect273"
style="fill:#006468" /></g><g
id="g275"
transform="translate(-0.82800293,0)"><rect
x="171.828"
y="6.1090002"
width="21"
height="21"
id="rect277"
style="fill:#ffcc00" /><rect
x="187.819"
y="16.101"
width="0.99900001"
height="0.99800003"
id="rect279"
style="fill:#655100" /><rect
x="189.825"
y="16.101"
width="0.99900001"
height="0.99800003"
id="rect281"
style="fill:#655100" /><rect
x="174.83299"
y="16.101"
width="0.99900001"
height="0.99800003"
id="rect283"
style="fill:#655100" /><circle
stroke-miterlimit="10"
cx="181.80299"
cy="16.101"
r="4.0900002"
id="circle285"
style="fill:none;stroke:#655100;stroke-width:1.95439994;stroke-miterlimit:10" /><rect
x="175.895"
y="18.427999"
transform="matrix(0.6915,0.7224,-0.7224,0.6915,69.5827,-121.6599)"
width="2.6800001"
height="4.4229999"
id="rect287"
style="fill:#655100" /><rect
x="180.789"
y="15.103"
width="2.0280001"
height="1.9960001"
id="rect289"
style="fill:#655100" /></g><g
id="g291"
transform="translate(-0.82800293,0)"><rect
x="171.828"
y="38.965"
width="21"
height="21"
id="rect293"
style="fill:#ffffff" /><rect
x="187.819"
y="48.957001"
width="0.99900001"
height="0.99800003"
id="rect295"
style="fill:#006468" /><rect
x="189.825"
y="48.957001"
width="0.99900001"
height="0.99800003"
id="rect297"
style="fill:#006468" /><rect
x="174.83299"
y="48.957001"
width="0.99900001"
height="0.99800003"
id="rect299"
style="fill:#006468" /><circle
stroke-miterlimit="10"
cx="181.80299"
cy="48.957001"
r="4.0900002"
id="circle301"
style="fill:none;stroke:#006468;stroke-width:1.95439994;stroke-miterlimit:10" /><rect
x="175.895"
y="51.284"
transform="matrix(0.6915,0.7224,-0.7224,0.6915,93.3163,-111.5246)"
width="2.6800001"
height="4.4229999"
id="rect303"
style="fill:#006468" /><rect
x="180.789"
y="47.959"
width="2.0280001"
height="1.9960001"
id="rect305"
style="fill:#006468" /></g><g
id="g307"
transform="translate(-0.82800293,0)"><rect
x="171.828"
y="71.976997"
width="21"
height="21"
id="rect309"
style="fill:#4db7bb" /><rect
x="187.819"
y="81.969002"
width="0.99900001"
height="0.99800003"
id="rect311"
style="fill:#006468" /><rect
x="189.825"
y="81.969002"
width="0.99900001"
height="0.99800003"
id="rect313"
style="fill:#006468" /><rect
x="174.83299"
y="81.969002"
width="0.99900001"
height="0.99800003"
id="rect315"
style="fill:#006468" /><circle
stroke-miterlimit="10"
cx="181.80299"
cy="81.969002"
r="4.0900002"
id="circle317"
style="fill:none;stroke:#006468;stroke-width:1.95439994;stroke-miterlimit:10" /><rect
x="175.895"
y="84.295998"
transform="matrix(0.6915,0.7224,-0.7224,0.6915,117.1625,-101.3412)"
width="2.6800001"
height="4.4229999"
id="rect319"
style="fill:#006468" /><rect
x="180.789"
y="80.970001"
width="2.0280001"
height="1.9960001"
id="rect321"
style="fill:#006468" /></g></svg>

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)-->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" id="Layer_1" width="198px" height="99px" x="0px" y="0px" enable-background="new 0 0 198 99" version="1.1" viewBox="0 0 198 99" inkscape:version="0.91 r13725" sodipodi:docname="buttons.svg" xml:space="preserve">
<metadata id="metadata327">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>
image/svg+xml
</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<defs id="defs325"/>
<sodipodi:namedview id="namedview323" bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" objecttolerance="10" pagecolor="#ffffff" showgrid="false" showguides="true" inkscape:current-layer="Layer_1" inkscape:cx="45.252385" inkscape:cy="36.224987" inkscape:guide-bbox="true" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="1000" inkscape:window-maximized="1" inkscape:window-width="1215" inkscape:window-x="65" inkscape:window-y="24" inkscape:zoom="4"/>
<g id="g5" transform="translate(-0.12000084,0)">
<polyline id="polyline9" stroke-miterlimit="10" points="21.453,12.745 15.788,20.571 11.775,16.658 " style="fill:none;stroke:#000000;stroke-width:2.30489993;stroke-miterlimit:10"/>
</g>
<g id="g23" transform="translate(-0.26100159,0)">
<polygon id="polygon27" points="45.412,15.313 49.307,15.313 49.307,11.53 54.701,16.875 49.331,22.245 49.331,18.563 45.412,18.539 " style="fill:#000000"/>
</g>
<g id="g41" transform="translate(-0.54399872,0)">
<polygon id="polygon45" points="114.44,19.083 114.44,15.116 110.586,15.116 116.032,9.621 121.502,15.091 117.751,15.091 117.726,19.083 " style="fill:#000000"/>
<rect id="rect47" width="1" height="1" x="110.511" y="22.193001" style="fill:#000000"/>
<rect id="rect49" width="1" height="1" x="112.518" y="22.193001" style="fill:#000000"/>
<rect id="rect51" width="1" height="1" x="114.517" y="22.193001" style="fill:#000000"/>
<rect id="rect53" width="1" height="1" x="116.525" y="22.193001" style="fill:#000000"/>
<rect id="rect55" width="1" height="1" x="118.524" y="22.193001" style="fill:#000000"/>
<rect id="rect57" width="1" height="1" x="120.531" y="22.193001" style="fill:#000000"/>
</g>
<g id="g59" transform="translate(-0.68600464,0)">
<polygon id="polygon63" points="150.79,9.621 150.79,13.588 154.644,13.588 149.198,19.083 143.728,13.612 147.479,13.613 147.504,9.621 " style="fill:#000000"/>
<rect id="rect65" width="1" height="1" x="143.65199" y="22.193001" style="fill:#000000"/>
<rect id="rect67" width="1" height="1" x="145.66" y="22.193001" style="fill:#000000"/>
<rect id="rect69" width="1" height="1" x="147.659" y="22.193001" style="fill:#000000"/>
<rect id="rect71" width="1" height="1" x="149.666" y="22.193001" style="fill:#000000"/>
<rect id="rect73" width="1" height="1" x="151.666" y="22.193001" style="fill:#000000"/>
<rect id="rect75" width="1" height="1" x="153.673" y="22.193001" style="fill:#000000"/>
</g>
<g id="g149" transform="translate(-0.40299988,0)">
<path id="path151" d="M 87.445,22.097" style="fill:#000000" inkscape:connector-curvature="0"/>
<polygon id="polygon155" points="83.44,10.094 84.441,10.094 88.421,14.079 88.421,15.057 87.445,15.057 83.44,15.057 " style="fill:#000000"/>
<polygon id="polygon157" points="78.404,11.093 78.404,22.097 87.445,22.097 87.445,14.87 88.421,14.87 88.421,23.134 77.399,23.134 77.399,10.094 83.562,10.094 83.568,11.093 " style="fill:#000000"/>
<rect id="rect159" width="0.995" height="0.99599999" x="79.399002" y="12.111" style="fill:#000000"/>
<rect id="rect161" width="0.995" height="0.99599999" x="81.394997" y="12.111" style="fill:#000000"/>
<rect id="rect163" width="0.995" height="0.99599999" x="79.399002" y="14.103" style="fill:#000000"/>
<rect id="rect165" width="0.995" height="0.99599999" x="81.394997" y="14.103" style="fill:#000000"/>
<rect id="rect167" width="0.995" height="0.99599999" x="79.399002" y="16.115999" style="fill:#000000"/>
<rect id="rect169" width="0.995" height="0.99599999" x="81.394997" y="16.115999" style="fill:#000000"/>
<rect id="rect171" width="0.995" height="0.99599999" x="83.403" y="16.115999" style="fill:#000000"/>
<rect id="rect173" width="0.995" height="0.99599999" x="85.400002" y="16.115999" style="fill:#000000"/>
<rect id="rect175" width="0.995" height="0.99599999" x="79.399002" y="18.118" style="fill:#000000"/>
<rect id="rect177" width="0.995" height="0.99599999" x="81.394997" y="18.118" style="fill:#000000"/>
<rect id="rect179" width="0.995" height="0.99599999" x="79.399002" y="20.132" style="fill:#000000"/>
<rect id="rect181" width="0.995" height="0.99599999" x="81.394997" y="20.132" style="fill:#000000"/>
<rect id="rect183" width="0.995" height="0.99599999" x="83.403" y="18.118" style="fill:#000000"/>
<rect id="rect185" width="0.995" height="0.99599999" x="85.400002" y="18.118" style="fill:#000000"/>
<rect id="rect187" width="0.995" height="0.99599999" x="83.403" y="20.132" style="fill:#000000"/>
<rect id="rect189" width="0.995" height="0.99599999" x="85.400002" y="20.132" style="fill:#000000"/>
</g>
<g id="g275" transform="translate(-0.82800293,0)">
<rect id="rect279" width="0.99900001" height="0.99800003" x="187.819" y="16.101" style="fill:#000000"/>
<rect id="rect281" width="0.99900001" height="0.99800003" x="189.825" y="16.101" style="fill:#000000"/>
<rect id="rect283" width="0.99900001" height="0.99800003" x="174.83299" y="16.101" style="fill:#000000"/>
<circle id="circle285" cx="181.80299" cy="16.101" r="4.0900002" stroke-miterlimit="10" style="fill:none;stroke:#000000;stroke-width:1.95439994;stroke-miterlimit:10"/>
<rect id="rect287" width="2.6800001" height="4.4229999" x="175.895" y="18.427999" style="fill:#000000" transform="matrix(0.6915,0.7224,-0.7224,0.6915,69.5827,-121.6599)"/>
<rect id="rect289" width="2.0280001" height="1.9960001" x="180.789" y="15.103" style="fill:#000000"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,40 @@
import { injectable, inject, postConstruct } from 'inversify';
import { BaseLanguageClientContribution } from '@theia/languages/lib/browser';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
@injectable()
export class ArduinoLanguageClientContribution extends BaseLanguageClientContribution {
readonly id = 'ino';
readonly name = 'Arduino';
protected get documentSelector(): string[] {
return ['ino'];
}
protected get globPatterns() {
return ['**/*.ino'];
}
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected boardConfig?: BoardsConfig.Config;
@postConstruct()
protected init() {
this.boardsServiceClient.onBoardsConfigChanged(this.selectBoard.bind(this));
}
selectBoard(config: BoardsConfig.Config): void {
this.boardConfig = config;
// Force a restart to send the new board config to the language server
this.restart();
}
protected getStartParameters(): BoardsConfig.Config | undefined {
return this.boardConfig;
}
}

View File

@@ -0,0 +1,52 @@
import * as React from 'react';
import { injectable } from 'inversify';
import { Library } from '../../common/protocol/library-service';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class LibraryItemRenderer extends ListItemRenderer<Library> {
renderItem(item: Library, install: (item: Library) => Promise<void>): React.ReactNode {
const name = <span className='name'>{item.name}</span>;
const author = <span className='author'>by {item.author}</span>;
const installedVersion = !!item.installedVersion && <div className='version-info'>
<span className='version'>Version {item.installedVersion}</span>
<span className='installed'>INSTALLED</span>
</div>;
const summary = <div className='summary'>{item.summary}</div>;
const moreInfo = !!item.moreInfoLink && <a href={item.moreInfoLink} onClick={this.onClick}>More info</a>;
const installButton = item.installable && !item.installedVersion &&
<button className='install' onClick={install.bind(this, item)}>INSTALL</button>;
const versions = (() => {
const { availableVersions } = item;
if (!!item.installedVersion || availableVersions.length === 0) {
return undefined;
} else if (availableVersions.length === 1) {
return <label>{availableVersions[0]}</label>
} else {
return <select>{item.availableVersions.map(version => <option value={version} key={version}>{version}</option>)}</select>;
}
})();
return <div className='component-list-item noselect'>
<div className='header'>
<span>{name} {author}</span>
{installedVersion}
</div>
<div className='content'>
{summary}
</div>
<div className='info'>
{moreInfo}
</div>
<div className='footer'>
{installButton}
{versions}
</div>
</div>;
}
}

View File

@@ -0,0 +1,27 @@
import { inject, injectable } from 'inversify';
import { Library, LibraryService } from '../../common/protocol/library-service';
import { ListWidget } from '../components/component-list/list-widget';
import { LibraryItemRenderer } from './library-item-renderer';
@injectable()
export class LibraryListWidget extends ListWidget<Library> {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
constructor(
@inject(LibraryService) protected service: LibraryService,
@inject(LibraryItemRenderer) protected itemRenderer: LibraryItemRenderer) {
super({
id: LibraryListWidget.WIDGET_ID,
label: LibraryListWidget.WIDGET_LABEL,
iconClass: 'library-tab-icon',
searchable: service,
installable: service,
itemLabel: (item: Library) => item.name,
itemRenderer
});
}
}

View File

@@ -1,16 +0,0 @@
import { ListWidget } from './list-widget';
export class LibraryListWidget extends ListWidget {
static WIDGET_ID = 'library-list-widget';
static WIDGET_LABEL = 'Library Manager';
protected widgetProps(): ListWidget.Props {
return {
id: LibraryListWidget.WIDGET_ID,
title: LibraryListWidget.WIDGET_LABEL,
iconClass: 'library-tab-icon'
}
}
}

View File

@@ -1,20 +1,12 @@
import { injectable } from 'inversify';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ListWidget } from './list-widget';
import { MenuModelRegistry } from '@theia/core';
import { LibraryListWidget } from './library-list-widget';
import { ArduinoMenus } from '../arduino-frontend-contribution';
@injectable()
export abstract class ListWidgetFrontendContribution extends AbstractViewContribution<ListWidget> implements FrontendApplicationContribution {
async initializeLayout(): Promise<void> {
await this.openView();
}
}
@injectable()
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution {
export class LibraryListWidgetFrontendContribution extends AbstractViewContribution<LibraryListWidget> implements FrontendApplicationContribution {
constructor() {
super({
@@ -29,4 +21,17 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
});
}
initializeLayout(): void {
// NOOP
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.SKETCH, {
commandId: this.toggleCommand.id,
label: 'Manage Libraries...'
});
}
}
}

View File

@@ -1,72 +0,0 @@
import * as React from 'react';
import { inject, injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { LibraryService } from '../../common/protocol/library-service';
@injectable()
export abstract class ListWidget extends ReactWidget {
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@inject(WindowService)
protected readonly windowService: WindowService;
constructor() {
super();
const { id, title, iconClass } = this.widgetProps();
this.id = id;
this.title.label = title;
this.title.caption = title;
this.title.iconClass = iconClass;
this.title.closable = true;
this.addClass(ListWidget.Styles.LIST_WIDGET_CLASS);
this.node.tabIndex = 0; // To be able to set the focus on the widget.
}
protected abstract widgetProps(): ListWidget.Props;
@postConstruct()
protected init(): void {
this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.focus();
this.render();
}
protected onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
this.render();
}
render(): React.ReactNode {
return <FilterableListContainer
service={this.libraryService}
windowService={this.windowService}
/>;
}
}
export namespace ListWidget {
/**
* Props for customizing the abstract list widget.
*/
export interface Props {
readonly id: string;
readonly title: string;
readonly iconClass: string;
}
export namespace Styles {
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
}
}

View File

@@ -0,0 +1,28 @@
import { inject, injectable, postConstruct } from 'inversify';
import { Diagnostic } from 'vscode-languageserver-types';
import URI from '@theia/core/lib/common/uri';
import { Marker } from '@theia/markers/lib/common/marker';
import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
import { ConfigService } from '../../common/protocol/config-service';
@injectable()
export class ArduinoProblemManager extends ProblemManager {
@inject(ConfigService)
protected readonly configService: ConfigService;
protected dataDirUri: URI | undefined;
@postConstruct()
protected init(): void {
super.init();
this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri));
}
setMarkers(uri: URI, owner: string, data: Diagnostic[]): Marker<Diagnostic>[] {
if (this.dataDirUri && this.dataDirUri.isEqualOrParent(uri)) {
return [];
}
return super.setMarkers(uri, owner, data);
}
}

View File

@@ -0,0 +1,13 @@
import { injectable } from 'inversify';
import { FrontendApplication } from '@theia/core/lib/browser';
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
@injectable()
export class ArduinoMenuContribution extends BrowserMenuBarContribution {
onStart(app: FrontendApplication): void {
const menu = this.factory.createMenuBar();
app.shell.addWidget(menu, { area: 'top' });
}
}

View File

@@ -0,0 +1,10 @@
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { ArduinoMenuContribution } from './arduino-menu-contribution';
import { ContainerModule, interfaces } from 'inversify';
import '../../../src/browser/style/browser-menu.css'
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind) => {
unbind(BrowserMenuBarContribution);
bind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
});

View File

@@ -0,0 +1,53 @@
import { injectable, inject } from "inversify";
import { MonitorService, ConnectionConfig } from "../../common/protocol/monitor-service";
import { Emitter, Event } from "@theia/core";
@injectable()
export class MonitorConnection {
@inject(MonitorService)
protected readonly monitorService: MonitorService;
connectionId: string | undefined;
protected _connectionConfig: ConnectionConfig | undefined;
protected readonly onConnectionChangedEmitter = new Emitter<string | undefined>();
readonly onConnectionChanged: Event<string | undefined> = this.onConnectionChangedEmitter.event;
get connectionConfig(): ConnectionConfig | undefined {
return this._connectionConfig;
}
async connect(config: ConnectionConfig): Promise<string | undefined> {
if (this.connectionId) {
await this.disconnect();
}
const { connectionId } = await this.monitorService.connect(config);
this.connectionId = connectionId;
this._connectionConfig = config;
this.onConnectionChangedEmitter.fire(this.connectionId);
return connectionId;
}
async disconnect(): Promise<boolean> {
let result = true;
const connections = await this.monitorService.getConnectionIds();
if (this.connectionId && connections.findIndex(id => id === this.connectionId) >= 0) {
console.log('>>> Disposing existing monitor connection before establishing a new one...');
result = await this.monitorService.disconnect(this.connectionId);
if (!result) {
// TODO: better!!!
console.error(`Could not close connection: ${this.connectionId}. Check the backend logs.`);
} else {
console.log(`<<< Disposed ${this.connectionId} connection.`);
this.connectionId = undefined;
this._connectionConfig = undefined;
this.onConnectionChangedEmitter.fire(this.connectionId);
}
}
return result;
}
}

View File

@@ -0,0 +1,58 @@
import { injectable } from "inversify";
import { Emitter } from "@theia/core";
export namespace MonitorModel {
export interface Data {
autoscroll: boolean,
timestamp: boolean,
baudRate: number,
lineEnding: string
}
}
@injectable()
export class MonitorModel {
protected readonly onChangeEmitter = new Emitter<void>();
readonly onChange = this.onChangeEmitter.event;
protected _autoscroll: boolean = true;
protected _timestamp: boolean = false;
baudRate: number;
lineEnding: string = '\n';
get autoscroll(): boolean {
return this._autoscroll;
}
get timestamp(): boolean {
return this._timestamp;
}
toggleAutoscroll(): void {
this._autoscroll = !this._autoscroll;
this.onChangeEmitter.fire(undefined);
}
toggleTimestamp(): void {
this._timestamp = !this._timestamp;
this.onChangeEmitter.fire(undefined);
}
restore(model: MonitorModel.Data) {
this._autoscroll = model.autoscroll;
this._timestamp = model.timestamp;
this.baudRate = model.baudRate;
this.lineEnding = model.lineEnding;
}
store(): MonitorModel.Data {
return {
autoscroll: this._autoscroll,
timestamp: this._timestamp,
baudRate: this.baudRate,
lineEnding: this.lineEnding
}
}
}

View File

@@ -0,0 +1,23 @@
import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { MonitorServiceClient, MonitorReadEvent, MonitorError } from '../../common/protocol/monitor-service';
@injectable()
export class MonitorServiceClientImpl implements MonitorServiceClient {
protected readonly onReadEmitter = new Emitter<MonitorReadEvent>();
protected readonly onErrorEmitter = new Emitter<MonitorError>();
readonly onRead = this.onReadEmitter.event;
readonly onError = this.onErrorEmitter.event;
notifyRead(event: MonitorReadEvent): void {
this.onReadEmitter.fire(event);
const { connectionId, data } = event;
console.log(`Received data from ${connectionId}: ${data}`);
}
notifyError(error: MonitorError): void {
this.onErrorEmitter.fire(error);
}
}

View File

@@ -0,0 +1,127 @@
import * as React from 'react';
import { injectable, inject } from "inversify";
import { AbstractViewContribution } from "@theia/core/lib/browser";
import { MonitorWidget } from "./monitor-widget";
import { MenuModelRegistry, Command, CommandRegistry } from "@theia/core";
import { ArduinoMenus } from "../arduino-frontend-contribution";
import { TabBarToolbarContribution, TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
import { MonitorModel } from './monitor-model';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
export namespace SerialMonitor {
export namespace Commands {
export const AUTOSCROLL: Command = {
id: 'serial-monitor-autoscroll',
label: 'Autoscroll'
}
export const TIMESTAMP: Command = {
id: 'serial-monitor-timestamp',
label: 'Timestamp'
}
export const CLEAR_OUTPUT: Command = {
id: 'serial-monitor-clear-output',
label: 'Clear Output',
iconClass: 'clear-all'
}
}
}
@injectable()
export class MonitorViewContribution extends AbstractViewContribution<MonitorWidget> implements TabBarToolbarContribution {
static readonly OPEN_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
@inject(MonitorModel) protected readonly model: MonitorModel;
constructor() {
super({
widgetId: MonitorWidget.ID,
widgetName: 'Serial Monitor',
defaultWidgetOptions: {
area: 'bottom'
},
toggleCommandId: MonitorViewContribution.OPEN_SERIAL_MONITOR,
toggleKeybinding: 'ctrl+shift+m'
})
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Serial Monitor'
});
}
}
async registerToolbarItems(registry: TabBarToolbarRegistry) {
registry.registerItem({
id: 'monitor-autoscroll',
render: () => this.renderAutoScrollButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange
});
registry.registerItem({
id: 'monitor-timestamp',
render: () => this.renderTimestampButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
command: SerialMonitor.Commands.CLEAR_OUTPUT.id,
tooltip: 'Clear Output'
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: widget => widget instanceof MonitorWidget,
isVisible: widget => widget instanceof MonitorWidget,
execute: widget => {
if (widget instanceof MonitorWidget) {
widget.clear();
}
}
});
if (this.toggleCommand) {
commands.registerCommand(this.toggleCommand, {
execute: () => this.openView({
toggle: true,
activate: true
}),
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'right'
});
}
}
protected renderAutoScrollButton(): React.ReactNode {
return <React.Fragment key='autoscroll-toolbar-item'>
<div
title='Toggle Autoscroll'
className={`item enabled fa fa-angle-double-down arduino-monitor ${this.model.autoscroll ? 'toggled' : ''}`}
onClick={this.toggleAutoScroll}
></div>
</React.Fragment>;
}
protected readonly toggleAutoScroll = () => this.doToggleAutoScroll();
protected async doToggleAutoScroll() {
this.model.toggleAutoscroll();
}
protected renderTimestampButton(): React.ReactNode {
return <React.Fragment key='line-ending-toolbar-item'>
<div
title='Toggle Timestamp'
className={`item enabled fa fa-clock-o arduino-monitor ${this.model.timestamp ? 'toggled' : ''}`}
onClick={this.toggleTimestamp}
></div>
</React.Fragment>;
}
protected readonly toggleTimestamp = () => this.doToggleTimestamp();
protected async doToggleTimestamp() {
this.model.toggleTimestamp();
}
}

View File

@@ -0,0 +1,398 @@
import { ReactWidget, Message, Widget, StatefulWidget } from "@theia/core/lib/browser";
import { postConstruct, injectable, inject } from "inversify";
import * as React from 'react';
import Select, { components } from 'react-select';
import { Styles } from "react-select/src/styles";
import { ThemeConfig } from "react-select/src/theme";
import { OptionsType } from "react-select/src/types";
import { MonitorServiceClientImpl } from "./monitor-service-client-impl";
import { MessageService } from "@theia/core";
import { ConnectionConfig, MonitorService } from "../../common/protocol/monitor-service";
import { MonitorConnection } from "./monitor-connection";
import { BoardsServiceClientImpl } from "../boards/boards-service-client-impl";
import { AttachedSerialBoard, BoardsService, Board } from "../../common/protocol/boards-service";
import { BoardsConfig } from "../boards/boards-config";
import { MonitorModel } from "./monitor-model";
export namespace SerialMonitorSendField {
export interface Props {
onSend: (text: string) => void
}
export interface State {
value: string;
}
}
export class SerialMonitorSendField extends React.Component<SerialMonitorSendField.Props, SerialMonitorSendField.State> {
protected inputField: HTMLInputElement | null;
constructor(props: SerialMonitorSendField.Props) {
super(props);
this.state = { value: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
if (this.inputField) {
this.inputField.focus();
}
}
render() {
return <React.Fragment>
<form onSubmit={this.handleSubmit}>
<input
tabIndex={-1}
ref={ref => this.inputField = ref}
type='text' id='serial-monitor-send'
autoComplete='off'
value={this.state.value}
onChange={this.handleChange} />
<input className="btn" type="submit" value="Submit" />
</form>
</React.Fragment>
}
protected handleChange(event: React.ChangeEvent<HTMLInputElement>) {
this.setState({ value: event.target.value });
}
protected handleSubmit(event: React.FormEvent<HTMLFormElement>) {
this.props.onSend(this.state.value);
this.setState({ value: '' });
event.preventDefault();
}
}
export namespace SerialMonitorOutput {
export interface Props {
lines: string[];
model: MonitorModel;
}
}
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props> {
protected theEnd: HTMLDivElement | null;
render() {
let result = '';
const style: React.CSSProperties = {
whiteSpace: 'pre',
fontFamily: 'monospace',
};
for (const text of this.props.lines) {
result += text;
}
return <React.Fragment>
<div style={style}>{result}</div>
<div style={{ float: "left", clear: "both" }}
ref={(el) => { this.theEnd = el; }}>
</div>
</React.Fragment>;
}
protected scrollToBottom() {
if (this.theEnd) {
this.theEnd.scrollIntoView();
}
}
componentDidMount() {
if (this.props.model.autoscroll) {
this.scrollToBottom();
}
}
componentDidUpdate() {
if (this.props.model.autoscroll) {
this.scrollToBottom();
}
}
}
export interface SelectOption {
label: string;
value: string | number;
}
@injectable()
export class MonitorWidget extends ReactWidget implements StatefulWidget {
static readonly ID = 'serial-monitor';
protected lines: string[];
protected tempData: string;
protected widgetHeight: number;
protected continuePreviousConnection: boolean;
constructor(
@inject(MonitorServiceClientImpl) protected readonly serviceClient: MonitorServiceClientImpl,
@inject(MonitorConnection) protected readonly connection: MonitorConnection,
@inject(MonitorService) protected readonly monitorService: MonitorService,
@inject(BoardsServiceClientImpl) protected readonly boardsServiceClient: BoardsServiceClientImpl,
@inject(MessageService) protected readonly messageService: MessageService,
@inject(BoardsService) protected readonly boardsService: BoardsService,
@inject(MonitorModel) protected readonly model: MonitorModel
) {
super();
this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor';
this.title.iconClass = 'arduino-serial-monitor-tab-icon';
this.lines = [];
this.tempData = '';
this.scrollOptions = undefined;
this.toDisposeOnDetach.push(serviceClient.onRead(({ data, connectionId }) => {
this.tempData += data;
if (this.tempData.endsWith('\n')) {
if (this.model.timestamp) {
const nu = new Date();
const h = (100 + nu.getHours()).toString().substr(1)
const min = (100 + nu.getMinutes()).toString().substr(1)
const sec = (100 + nu.getSeconds()).toString().substr(1)
const ms = (1000 + nu.getMilliseconds()).toString().substr(1);
this.tempData = `${h}:${min}:${sec}.${ms} -> ` + this.tempData;
}
this.lines.push(this.tempData);
this.tempData = '';
this.update();
}
}));
// TODO onError
}
@postConstruct()
protected init(): void {
this.update();
}
clear(): void {
this.lines = [];
this.update();
}
storeState(): MonitorModel.Data {
return this.model.store();
}
restoreState(oldState: MonitorModel.Data): void {
this.model.restore(oldState);
}
protected onAfterAttach(msg: Message) {
super.onAfterAttach(msg);
this.clear();
this.connect();
this.toDisposeOnDetach.push(
this.boardsServiceClient.onBoardsChanged(async states => {
const currentConnectionConfig = this.connection.connectionConfig;
const connectedBoard = states.newState.boards
.filter(AttachedSerialBoard.is)
.find(board => {
const potentiallyConnected = currentConnectionConfig && currentConnectionConfig.board;
if (AttachedSerialBoard.is(potentiallyConnected)) {
return Board.equals(board, potentiallyConnected) && board.port === potentiallyConnected.port;
}
return false;
});
if (connectedBoard && currentConnectionConfig) {
this.continuePreviousConnection = true;
this.connection.connect(currentConnectionConfig);
}
})
);
this.toDisposeOnDetach.push(
this.boardsServiceClient.onBoardsConfigChanged(async boardConfig => {
this.connect();
})
)
this.toDisposeOnDetach.push(this.connection.onConnectionChanged(() => {
if (!this.continuePreviousConnection) {
this.clear();
} else {
this.continuePreviousConnection = false;
}
}));
}
protected onBeforeDetach(msg: Message) {
super.onBeforeDetach(msg);
this.connection.disconnect();
}
protected onResize(msg: Widget.ResizeMessage) {
super.onResize(msg);
this.widgetHeight = msg.height;
this.update();
}
protected async connect() {
const config = await this.getConnectionConfig();
if (config) {
this.connection.connect(config);
}
}
protected async getConnectionConfig(): Promise<ConnectionConfig | undefined> {
const baudRate = this.model.baudRate;
const { boardsConfig } = this.boardsServiceClient;
const { selectedBoard, selectedPort } = boardsConfig;
if (!selectedBoard) {
this.messageService.warn('No boards selected.');
return;
}
const { name } = selectedBoard;
if (!selectedPort) {
this.messageService.warn(`No ports selected for board: '${name}'.`);
return;
}
const attachedBoards = await this.boardsService.getAttachedBoards();
const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
if (!connectedBoard) {
return;
}
return {
baudRate,
board: selectedBoard,
port: selectedPort.address
}
}
protected getLineEndings(): OptionsType<SelectOption> {
return [
{
label: 'No Line Ending',
value: ''
},
{
label: 'Newline',
value: '\n'
},
{
label: 'Carriage Return',
value: '\r'
},
{
label: 'Both NL & CR',
value: '\r\n'
}
]
}
protected getBaudRates(): OptionsType<SelectOption> {
const baudRates = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
return baudRates.map<SelectOption>(baudRate => ({ label: baudRate + ' baud', value: baudRate }))
}
protected render(): React.ReactNode {
const le = this.getLineEndings();
const br = this.getBaudRates();
const leVal = this.model.lineEnding && le.find(val => val.value === this.model.lineEnding);
const brVal = this.model.baudRate && br.find(val => val.value === this.model.baudRate);
return <React.Fragment>
<div className='serial-monitor-container'>
<div className='head'>
<div className='send'>
<SerialMonitorSendField onSend={this.onSend} />
</div>
<div className='config'>
{this.renderSelectField('arduino-serial-monitor-line-endings', le, leVal || le[1], this.onChangeLineEnding)}
{this.renderSelectField('arduino-serial-monitor-baud-rates', br, brVal || br[4], this.onChangeBaudRate)}
</div>
</div>
<div id='serial-monitor-output-container'>
<SerialMonitorOutput model={this.model} lines={this.lines} />
</div>
</div>
</React.Fragment>;
}
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string) {
const { connectionId } = this.connection;
if (connectionId) {
this.monitorService.send(connectionId, value + this.model.lineEnding);
}
}
protected readonly onChangeLineEnding = (le: SelectOption) => {
this.model.lineEnding = typeof le.value === 'string' ? le.value : '\n';
}
protected readonly onChangeBaudRate = async (br: SelectOption) => {
await this.connection.disconnect();
this.model.baudRate = typeof br.value === 'number' ? br.value : 9600;
this.clear();
const config = await this.getConnectionConfig();
if (config) {
await this.connection.connect(config);
}
}
protected renderSelectField(id: string, options: OptionsType<SelectOption>, defaultVal: SelectOption, onChange: (v: SelectOption) => void): React.ReactNode {
const height = 25;
const selectStyles: Styles = {
control: (provided, state) => ({
...provided,
width: 200,
border: 'none'
}),
dropdownIndicator: (p, s) => ({
...p,
padding: 0
}),
indicatorSeparator: (p, s) => ({
display: 'none'
}),
indicatorsContainer: (p, s) => ({
padding: '0 5px'
}),
menu: (p, s) => ({
...p,
marginTop: 0
})
};
const theme: ThemeConfig = theme => ({
...theme,
borderRadius: 0,
spacing: {
controlHeight: height,
baseUnit: 2,
menuGutter: 4
}
});
const DropdownIndicator = (
props: React.Props<typeof components.DropdownIndicator>
) => {
return (
<span className='fa fa-caret-down caret'></span>
);
};
return <Select
options={options}
defaultValue={defaultVal}
onChange={onChange}
components={{ DropdownIndicator }}
theme={theme}
styles={selectStyles}
maxMenuHeight={this.widgetHeight - 40}
classNamePrefix='sms'
className='serial-monitor-select'
id={id}
/>
}
}

View File

@@ -0,0 +1,32 @@
import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { Title, Widget } from '@phosphor/widgets';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ConfigService } from '../../common/protocol/config-service';
import { EditorWidget } from '@theia/editor/lib/browser';
@injectable()
export class ArduinoTabBarDecoratorService extends TabBarDecoratorService {
@inject(ConfigService)
protected readonly configService: ConfigService;
protected dataDirUri: URI | undefined;
@postConstruct()
protected init(): void {
super.init();
this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri));
}
getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {
if (title.owner instanceof EditorWidget) {
const editor = title.owner.editor;
if (this.dataDirUri && this.dataDirUri.isEqualOrParent(editor.uri)) {
return [];
}
}
return super.getDecorations(title);
}
}

View File

@@ -1,57 +0,0 @@
import { injectable, inject } from "inversify";
import URI from "@theia/core/lib/common/uri";
import { OpenerService } from "@theia/core/lib/browser";
import { FileSystem } from "@theia/filesystem/lib/common";
@injectable()
export class SketchFactory {
@inject(FileSystem)
protected readonly fileSystem: FileSystem;
@inject(OpenerService)
protected readonly openerService: OpenerService;
public async createNewSketch(parent: URI): Promise<void> {
const monthNames = ["january", "february", "march", "april", "may", "june",
"july", "august", "september", "october", "november", "december"
];
const today = new Date();
const sketchBaseName = `sketch_${monthNames[today.getMonth()]}${today.getDay()}`;
let sketchName: string | undefined;
for (let i = 97; i < 97 + 26; i++) {
let sketchNameCandidate = `${sketchBaseName}${String.fromCharCode(i)}`;
if (await this.fileSystem.exists(parent.resolve(sketchNameCandidate).toString())) {
continue;
}
sketchName = sketchNameCandidate;
break;
}
if (!sketchName) {
throw new Error("Cannot create a unique sketch name");
}
try {
const sketchDir = parent.resolve(sketchName);
const sketchFile = sketchDir.resolve(`${sketchName}.ino`);
this.fileSystem.createFolder(sketchDir.toString());
this.fileSystem.createFile(sketchFile.toString(), { content: `
void setup() {
}
void loop() {
}
` });
const opener = await this.openerService.getOpener(sketchFile)
opener.open(sketchFile, { reveal: true });
} catch (e) {
throw new Error("Cannot create new sketch: " + e);
}
}
}

View File

@@ -18,44 +18,57 @@ is not optimized for dense, information rich UIs.
:root {
/* Custom Theme Colors */
--theia-arduino-light: rgb(0, 102, 105);
--theia-arduino-light1: rgb(0, 164, 167);
--theia-arduino-light: rgb(0, 100, 104);
--theia-arduino-light1: rgb(23, 161, 165);
--theia-arduino-light2: rgb(218, 226, 228);
--theia-arduino-light3: rgb(237, 241, 242);
--theia-arduino-terminal: rgb(0, 0, 0);
/* Borders: Width and color (bright to dark) */
--theia-border-width: 1px;
--theia-panel-border-width: 2px;
--theia-border-color0: var(--md-grey-100);
--theia-border-color1: var(--md-grey-200);
--theia-border-color2: var(--md-grey-300);
--theia-border-color3: var(--md-grey-500);
--theia-border-color3: var(--md-grey-400);
/* UI fonts: Family, size and color (dark to bright)
---------------------------------------------------
The UI font CSS variables are used for the typography all of the Theia
user interface elements that are not directly user-generated content.
*/
--theia-ui-font-scale-factor: 1.2;
--theia-ui-font-size0: calc(var(--theia-ui-font-size1) / var(--theia-ui-font-scale-factor));
--theia-ui-font-size1: 13px;
/* Base font size */
--theia-ui-font-size1: 13px; /* Base font size */
--theia-ui-font-size2: calc(var(--theia-ui-font-size1) * var(--theia-ui-font-scale-factor));
--theia-ui-font-size3: calc(var(--theia-ui-font-size2) * var(--theia-ui-font-scale-factor));
--theia-ui-icon-font-size: 14px;
/* Ensures px perfect FontAwesome icons */
--theia-ui-icon-font-size: 14px; /* Ensures px perfect FontAwesome icons */
--theia-ui-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
--theia-ui-font-color0: var(--md-grey-900);
--theia-ui-font-color1: var(--md-grey-700);
--theia-ui-font-color2: var(--md-grey-500);
--theia-ui-font-color3: var(--md-grey-300);
/* Special font colors */
--theia-ui-icon-font-color: #ffffff;
--theia-ui-bar-font-color0: var(--theia-ui-font-color0);
--theia-ui-bar-font-color1: var(--theia-ui-font-color1);
--theia-ui-bar-font-color1: var(--theia-inverse-ui-font-color0); /* var(--theia-ui-font-color1); */
/* Use the inverse UI colors against the brand/accent/warn/error colors. */
--theia-inverse-ui-font-color0: rgba(255, 255, 255, 1.0);
--theia-inverse-ui-font-color2: rgba(255, 255, 255, 0.7);
--theia-inverse-ui-font-color3: rgba(255, 255, 255, 0.5);
/* Content fonts: Family, size and color (dark to bright)
Content font variables are used for typography of user-generated content.
*/
--theia-content-font-size: 13px;
--theia-content-line-height: 1.5;
--theia-content-font-color0: black;
@@ -68,75 +81,101 @@ is not optimized for dense, information rich UIs.
--theia-code-font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
--theia-terminal-font-family: monospace;
--theia-ui-padding: 6px;
/* Tab Icon Colors */
--theia-tab-icon-color: var(--theia-ui-font-color1);
--theia-tab-font-color: #000;
/* Main layout colors (bright to dark)
------------------------------------ */
------------------------------------ */
--theia-layout-color0: #ffffff;
--theia-layout-color1: #f3f3f3;
--theia-layout-color1: #f7f9f9;
--theia-layout-color2: #ececec;
--theia-layout-color3: #dcdcdc;
--theia-layout-color3: var(--theia-arduino-light2);
--theia-layout-color4: #dcdcdc;
/* Brand colors */
--theia-brand-color0: var(--md-blue-700);
--theia-brand-color1: var(--md-blue-500);
--theia-brand-color2: var(--md-blue-300);
--theia-brand-color3: var(--md-blue-100);
--theia-brand-color0: var(--theia-arduino-light);
--theia-brand-color1: var(--theia-arduino-light1);
--theia-brand-color2: var(--theia-arduino-light2);
--theia-brand-color3: var(--theia-arduino-light3);
/* Secondary Brand colors */
--theia-secondary-brand-color0: var(--md-grey-700);
--theia-secondary-brand-color1: var(--md-grey-500);
--theia-secondary-brand-color2: var(--md-grey-300);
--theia-secondary-brand-color3: var(--md-grey-100);
--theia-secondary-brand-color1: #b5c8c9;
--theia-secondary-brand-color2: var(--theia-arduino-light2);
--theia-secondary-brand-color3: var(--theia-arduino-light3);
/* Accent colors (dark to bright): Use these to create contrast to layout colors. */
--theia-accent-color0: rgb(0, 102, 105);
--theia-accent-color1: rgb(0, 164, 167, 1.0);
--theia-accent-color2: rgb(0, 164, 167, 0.8);
--theia-accent-color3: rgb(0, 164, 167, 0.6);
--theia-accent-color4: rgb(0, 164, 167, 0.4);
--theia-accent-color0: var(--theia-arduino-light);
--theia-accent-color1: rgb(77, 183, 187, 1.0);
--theia-accent-color2: rgb(77, 183, 187, 0.8);
--theia-accent-color3: rgb(77, 183, 187, 0.6);
--theia-accent-color4: rgba(77, 183, 187, 0.4);
/* Accent colors with opacity */
--theia-transparent-accent-color0: hsla(210, 63%, 46%, 0.3);
--theia-transparent-accent-color1: hsla(207, 66%, 56%, 0.3);
--theia-transparent-accent-color2: hsla(207, 64%, 85%, 0.3);
--theia-transparent-accent-color3: hsla(205, 70%, 91%, 0.3);
/* State colors (warn, error, success, info)
------------------------------------------ */
--theia-warn-color0: var(--md-amber-500);
--theia-warn-color1: var(--md-amber-400);
--theia-warn-color2: var(--md-amber-300);
--theia-warn-color3: var(--md-amber-200);
--theia-warn-font-color0: var(--md-grey-900);
--theia-error-color0: var(--md-red-400);
--theia-error-color1: var(--md-red-300);
--theia-error-color1: #da5b4a;
--theia-error-color2: var(--md-red-200);
--theia-error-color3: var(--md-red-100);
--theia-error-font-color0: var(--md-grey-300);
--theia-success-color0: var(--md-green-500);
--theia-success-color1: var(--md-green-300);
--theia-success-color2: var(--md-green-100);
--theia-success-color3: var(--md-green-50);
--theia-success-font-color0: var(--md-grey-300);
--theia-info-color0: var(--md-cyan-700);
--theia-info-color1: var(--md-cyan-500);
--theia-info-color2: var(--md-cyan-300);
--theia-info-color3: var(--md-cyan-100);
--theia-info-font-color0: var(--md-grey-300);
--theia-disabled-color0: var(--md-grey-500);
--theia-disabled-color1: var(--md-grey-300);
--theia-disabled-color2: var(--md-grey-200);
--theia-disabled-color3: var(--md-grey-50);
--theia-added-color0: rgba(0, 255, 0, 0.8);
--theia-removed-color0: rgba(230, 0, 0, 0.8);
--theia-modified-color0: rgba(0, 100, 150, 0.8);
/* Background for selected text */
--theia-selected-text-background: var(--theia-accent-color3);
/* Colors to highlight words in widgets like tree or editors */
--theia-word-highlight-color0: rgba(168, 172, 148, 0.7);
--theia-word-highlight-color1: rgba(253, 255, 0, 0.2);
--theia-word-highlight-match-color0: rgba(234, 92, 0, 0.33);
--theia-word-highlight-match-color1: rgba(234, 92, 0, 0.5);
--theia-word-highlight-replace-color0: rgba(155, 185, 85, 0.2);
/* Scroll-bars */
--theia-scrollbar-width: 10px;
--theia-scrollbar-rail-width: 10px;
--theia-scrollbar-thumb-color: hsla(0, 0%, 61%, .4);
@@ -144,19 +183,19 @@ is not optimized for dense, information rich UIs.
--theia-scrollbar-active-thumb-color: hsla(0, 0%, 39%, .4);
--theia-scrollbar-active-rail-color: transparent;
/* Menu */
--theia-menu-color0: var(--theia-layout-color3);
--theia-menu-color0: var(--theia-arduino-light);
--theia-menu-color1: var(--theia-layout-color0);
--theia-menu-color2: var(--theia-layout-color3);
--theia-menu-color2: #dae3e3;
/* Statusbar */
--theia-statusbar-color: var(--theia-arduino-light);
--theia-statusBar-font-color: var(--theia-inverse-ui-font-color0);
--theia-statusBar-font-size: 12px;
/* Buttons */
--theia-ui-button-color: var(--theia-arduino-light);
--theia-ui-button-color-hover: var(--theia-arduino-light1);
--theia-ui-button-font-color: var(--theia-inverse-ui-font-color0);
--theia-ui-button-color: var(--theia-accent-color1);
--theia-ui-button-color-hover: var(--theia-accent-color2);
--theia-ui-button-font-color: var(--theia-arduino-light);
--theia-ui-button-color-secondary: var(--theia-secondary-brand-color1);
--theia-ui-button-color-secondary-hover: var(--theia-secondary-brand-color0);
--theia-ui-button-color-secondary-hover: var(--theia-menu-color2);
--theia-ui-button-font-color-secondary: var(--theia-inverse-ui-font-color0);
--theia-ui-button-color-disabled: var(--theia-accent-color3);
--theia-ui-button-font-color-disabled: var(--theia-ui-font-color2);
@@ -168,8 +207,8 @@ is not optimized for dense, information rich UIs.
/* Dialogs */
--theia-ui-dialog-header-color: var(--theia-arduino-light);
--theia-ui-dialog-header-font-color: var(--theia-inverse-ui-font-color0);
--theia-ui-dialog-color: var(--theia-layout-color0);
--theia-ui-dialog-font-color: var(--theia-ui-font-color1);
--theia-ui-dialog-color: rgb(236, 241, 241);
--theia-ui-dialog-font-color: black;
/* Variables */
--theia-variable-name-color: #9B46B0;
--theia-variable-value-color: rgba(108, 108, 108, 0.8);
@@ -194,4 +233,8 @@ is not optimized for dense, information rich UIs.
--theia-ansi-magenta-background-color: darkmagenta;
--theia-ansi-cyan-background-color: darkcyan;
--theia-ansi-white-background-color: #BDBDBD;
/* Output */
--theia-output-font-color: var(--theia-ui-font-color3);
}

View File

@@ -0,0 +1,225 @@
div#select-board-dialog {
margin: 5px 20px 50px 20px;
}
div#select-board-dialog .selectBoardContainer .body {
display: flex;
overflow: hidden;
}
div.dialogContent.select-board-dialog > div.head {
padding-left: 21px;
}
div.dialogContent.select-board-dialog > div.head .title {
font-weight: 400;
letter-spacing: .02em;
font-size: 1.2em;
color: #00979d;
margin: 17px 0;
}
div#select-board-dialog .selectBoardContainer .head .text {
margin-bottom: 21px;
}
div#select-board-dialog .selectBoardContainer .body .list .item.selected {
background: var(--theia-ui-button-color-secondary-hover);
}
div#select-board-dialog .selectBoardContainer .body .list .item.selected i{
color: var(--theia-arduino-light);
}
#select-board-dialog .selectBoardContainer .search,
#select-board-dialog .selectBoardContainer .search input,
#select-board-dialog .selectBoardContainer .list,
#select-board-dialog .selectBoardContainer .list {
background: white; /* TODO find a theia color instead! */
}
#select-board-dialog .selectBoardContainer .body .search input {
border: none;
width: 100%;
height: auto;
max-height: 37px;
padding: 10px 5px 10px 10px;
margin: 0;
vertical-align: top;
display: flex;
color: var(--theia-content-font-color0);
}
#select-board-dialog .selectBoardContainer .body .search input:focus {
box-shadow: none;
}
#select-board-dialog .selectBoardContainer .body .container {
flex: 1;
padding: 0px 10px 0px 0px;
}
#select-board-dialog .selectBoardContainer .body .left.container .content {
margin: 0 5px 0 0;
}
#select-board-dialog .selectBoardContainer .body .right.container .content {
margin: 0 0 0 5px;
}
#select-board-dialog .selectBoardContainer .body .container .content .title {
color: #7f8c8d;
padding: 0px 0px 10px 0px;
text-transform: uppercase;
}
#select-board-dialog .selectBoardContainer .body .container .content .footer {
padding: 10px 5px 10px 0px;
}
#select-board-dialog .selectBoardContainer .body .container .content .loading {
font-size: var(--theia-ui-font-size1);
color: #7f8c8d;
padding: 10px 5px 10px 10px;
text-transform: uppercase;
/* The max, min-height comes from `.body .list` 265px + 47px top padding - 2 * 10px top padding */
max-height: 292px;
min-height: 292px;
}
#select-board-dialog .selectBoardContainer .body .list .item {
padding: 10px 5px 10px 10px;
display: flex;
justify-content: end;
}
#select-board-dialog .selectBoardContainer .body .list .item .selected-icon {
margin-left: auto;
}
#select-board-dialog .selectBoardContainer .body .list .item .detail {
font-size: var(--theia-ui-font-size1);
color: var(--theia-disabled-color0);
width: 155px; /* used heuristics for the calculation */
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
}
#select-board-dialog .selectBoardContainer .body .list .item.missing {
color: var(--theia-disabled-color0);
}
#select-board-dialog .selectBoardContainer .body .list .item:hover {
background: var(--theia-ui-button-color-secondary-hover);
}
#select-board-dialog .selectBoardContainer .body .list {
max-height: 265px;
min-height: 265px;
overflow-y: auto;
}
#select-board-dialog .selectBoardContainer .body .ports.list {
margin: 47px 0px 0px 0px /* 47 is 37 as input height for the `Boards`, plus 10 margin bottom. */
}
#select-board-dialog .selectBoardContainer .body .search {
margin-bottom: 10px;
display: flex;
align-items: center;
padding-right: 5px;
}
.p-Widget.dialogOverlay .dialogContent.select-board-dialog {
width: 740px;
}
button.theia-button {
height: 31px;
}
button.theia-button.secondary {
background-color: #b5c8c9;
color: #000;
box-shadow: 0 4px #95a5a6;
}
button.theia-button.main {
color: #fff;
/* background-color: #00979c; */
box-shadow: 0 4px var(--theia-accent-color0);
}
.dialogControl {
margin: 0 20px 30px 0;
}
.arduino-boards-toolbar-item-container {
margin-left: 3px;
}
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container {
display: flex;
align-items: baseline;
width: 100%;
}
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .notAttached {
width: 10px;
height: 10px;
color: red;
margin: 0 5px;
}
.arduino-boards-toolbar-item-container {
display: flex;
align-items: center;
width: 220px;
}
.arduino-boards-toolbar-item .label {
height: 100%;
display: flex;
align-items: center;
margin: 0 5px;
width: 100%;
}
.arduino-boards-toolbar-item .caret {
width: 10px;
margin-right: 5px;
}
.arduino-boards-toolbar-item {
background: var(--theia-layout-color1);
height: 22px;
display: flex;
width: 100%;
overflow: hidden;
}
.arduino-boards-dropdown-list {
border: 3px solid var(--theia-border-color2);
margin: -3px;
z-index: 1;
}
.arduino-boards-dropdown-item {
font-size: var(--theia-ui-font-size1);
display: flex;
padding: 10px;
cursor: pointer;
color: var(--theia-ui-font-color1);
background: var(--theia-layout-color1);
}
.arduino-boards-dropdown-item .fa-check {
color: var(--theia-accent-color1);
align-self: center;
}
.arduino-boards-dropdown-item.selected,
.arduino-boards-dropdown-item:hover {
background: var(--theia-layout-color3);
}

View File

@@ -0,0 +1,15 @@
#theia-top-panel {
min-height: 64px;
flex-direction: column;
}
#theia-top-panel .p-TabBar-toolbar {
justify-content: flex-end;
margin: 0;
padding-left: 10px;
width: 100%;
}
.p-MenuBar-item.p-mod-active {
color: var(--theia-ui-bar-font-color0);
}

View File

@@ -0,0 +1,13 @@
/* Do not show the `close` icon for editor, but show the dirty state if not in pro-mode. */
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable:hover > .p-TabBar-tabCloseIcon,
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-current > .p-TabBar-tabCloseIcon {
background-image: none;
cursor: pointer;
}
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty:hover > .p-TabBar-tabCloseIcon,
body:not(.arduino-advanced-mode) .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty > .p-TabBar-tabCloseIcon:hover {
background-size: 10px;
background-image: var(--theia-icon-circle);
cursor: pointer;
}

View File

@@ -1,2 +1,5 @@
@import './list-widget.css';
@import './select-board-dialog.css';
@import './board-select-dialog.css';
@import './main.css';
@import './editor.css';
@import './monitor.css';

View File

@@ -7,15 +7,58 @@
color: var(--theia-ui-font-color1);
}
.arduino-list-widget .search-bar > input {
margin: 0px 5px 0px 5px;
width: 95%;
.arduino-list-widget .search-bar {
margin: 0px 10px 10px 15px;
border-color: var(--theia-border-color3);
}
.arduino-list-widget .search-filters {
margin: 0px 10px 0px 15px;
border-color: var(--theia-border-color3);
}
.arduino-list-widget .search-bar:focus {
border-color: var(--theia-accent-color3);
}
.arduino-list-widget .filterable-list-container .search-filters .filter {
margin: 0px 0px 10px 0px;
display: flex;
}
.arduino-list-widget .filterable-list-container .search-filters .filter .title {
margin: 0px 10px 0px 0px;
align-self: center;
text-transform: uppercase;
font-size: var(--theia-ui-font-size0);
}
.arduino-list-widget .filterable-list-container .search-filters .filter > select {
width: 100%;
}
.filterable-list-container {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%; /* This has top be 100% down to the `scrollContainer`. */
}
.filterable-list-container .items-container {
height: 100%; /* This has to be propagated down from the widget. */
position: relative; /* To fix the `top` of the vertical toolbar. */
}
.filterable-list-container .items-container > div:nth-child(odd) {
background-color: var(--theia-layout-color0);
}
.filterable-list-container .items-container > div:nth-child(even) {
background-color: var(--theia-layout-color1);
}
.filterable-list-container .items-container > div:hover {
background-color: var(--theia-layout-color2);
}
.component-list-item {
@@ -57,6 +100,10 @@
color: var(--theia-ui-font-color2);
}
.component-list-item:hover .header .author {
color: var(--theia-ui-font-color1);
}
.component-list-item .header .version {
color: var(--theia-ui-font-color2);
}
@@ -64,7 +111,7 @@
.component-list-item .header .installed {
margin-left: 4px;
justify-self: end;
background-color: var(--theia-accent-color2);
background-color: var(--theia-accent-color1);
padding: 2px 4px 2px 4px;
font-size: 10px;
font-weight: bold;
@@ -72,18 +119,35 @@
color: var(--theia-inverse-ui-font-color0);
}
.component-list-item .footer {
padding-top: 5px;
.component-list-item[min-width~="170px"] .footer {
padding: 5px 5px 0px 0px;
min-height: 26px; /* 21 + 5 from the footer margin top */
display: flex;
flex-direction: row-reverse;
}
.component-list-item .footer a {
.component-list-item .footer {
flex-direction: column-reverse;
}
.component-list-item .footer > * {
display: none
}
.component-list-item:hover .footer > * {
display: block;
margin: 5px 0px 0px 10px;
}
.component-list-item:hover .footer > label {
display: block;
align-self: center;
margin: 5px 0px 0px 10px;
}
.component-list-item .info a {
color: var(--theia-brand-color1);
text-decoration: none;
font-weight: 600;
}
.component-list-item .footer .install {
margin-left: auto;
}
.component-list-item a:hover {
@@ -92,4 +156,4 @@
.component-list-item strong.installed {
color: rgb(0, 151, 157)
}
}

View File

@@ -0,0 +1,145 @@
.p-TabBar[data-orientation='horizontal'].theia-app-bottom {
background: var(--theia-layout-color1);
}
.theia-output {
background: var(--theia-arduino-terminal);
}
#outputView {
cursor: text;
}
.p-TabBar-toolbar .item.arduino-tool-item {
margin-left: 3px;
}
.p-TabBar-toolbar .item.arduino-tool-item > div {
display: flex;
justify-content: center;
align-items: center;
height: 24px;
width: 24px;
background: var(--theia-ui-button-color);
}
.p-TabBar-toolbar .item.arduino-tool-item > div:hover {
background: var(--theia-ui-button-color-hover);
}
.arduino-verify, .arduino-upload {
border-radius: 12px;
}
.arduino-tool-icon {
height: 24px;
width: 24px;
background: var(--theia-ui-button-font-color);
-webkit-mask: url(../icons/mask-buttons.svg);
mask: url(../icons/mask-buttons.svg);
-webkit-mask-size: 800%;
mask-size: 800%;
}
.arduino-save-file-icon {
-webkit-mask-position: 59px -4px;
mask-position: 59px -4px;
}
.arduino-verify-icon {
-webkit-mask-position: 188px -4px;
mask-position: 188px -4px;
}
.arduino-upload-icon {
-webkit-mask-position: 156px -4px;
mask-position: 156px -4px;
}
.arduino-show-open-context-menu-icon {
-webkit-mask-position: 92px -4px;
mask-position: 92px -4px;
}
.toggle-serial-monitor-icon {
-webkit-mask-position: 28px -4px;
mask-position: 28px -4px;
}
#arduino-toolbar-container {
display: flex;
width: 100%;
}
.p-TabBar-toolbar.theia-arduino-toolbar {
flex: 1;
z-index: 0;
}
#theia-top-panel .p-TabBar-toolbar.theia-arduino-toolbar.right {
justify-content: flex-start;
min-width: 190px;
}
#theia-top-panel .p-TabBar-toolbar.theia-arduino-toolbar.left {
min-width: 398px;
justify-content: flex-end;
}
.arduino-tool-item.item.connected-boards {
opacity: 1;
}
.arduino-tool-item.item.connected-boards select {
line-height: var(--theia-content-line-height);
font-size: var(--theia-ui-font-size1);
color: var(--theia-ui-font-color1);
-webkit-appearance: none;
-moz-appearance: none;
background-image: linear-gradient(45deg, transparent 50%, var(--theia-ui-font-color1) 50%), linear-gradient(135deg, var(--theia-ui-font-color1) 50%, transparent 50%);
background-position: calc(100% - 6px) 8px, calc(100% - 2px) 8px, 100% 0;
background-size: 4px 5px;
background-repeat: no-repeat;
padding-left: 3px;
padding-right: 15px;
}
.arduino-toolbar-tooltip {
margin-left: 10px;
display: flex;
align-items: center;
color: var(--theia-ui-font-color3);
}
.p-TabBar-toolbar .item > div.arduino-toggle-advanced-mode {
display: flex;
width: 24px;
height: 24px;
justify-content: center;
align-items: center;
}
.arduino-toggle-advanced-mode-icon {
mask: none;
-webkit-mask: none;
background: none;
display: flex;
justify-content: center;
align-items: center;
color: var(--theia-ui-button-font-color);
}
.monaco-editor .margin {
border-right: 2px solid var(--theia-border-color1);
box-sizing: border-box;
}
.noWrapInfo {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theia-sidepanel-toolbar .theia-sidepanel-title {
margin-left: 10px;
}

View File

@@ -0,0 +1,126 @@
.p-TabBar.theia-app-centers .p-TabBar-tabIcon.arduino-serial-monitor-tab-icon {
background: url(../icons/buttons.svg);
background-size: 800%;
background-position-y: 41px;
background-position-x: 19px;
}
.serial-monitor-container {
height: 100%;
display: flex;
flex-direction: column;
}
.serial-monitor-container .head {
display: flex;
padding: 5px;
background: var(--theia-layout-color0);
height: 27px;
}
.serial-monitor-container .head .send {
display: flex;
flex:1;
}
.serial-monitor-container .head .send .btn {
display: flex;
padding: 0 5px;
align-items: center;
color: var(--theia-ui-font-color1);
opacity: 0.7;
}
.serial-monitor-container .head .send .btn:hover {
opacity: 1;
cursor: pointer;
}
.serial-monitor-container .head .send form {
flex: 1;
display: flex;
background: var(--theia-layout-color2);
}
.serial-monitor-container .head .send input#serial-monitor-send {
color: var(--theia-ui-font-color1);
flex: 1;
}
.serial-monitor-container .head .send input:focus {
outline: none;
}
.serial-monitor-container .head .config {
display: flex;
}
.serial-monitor-container .head .config .serial-monitor-select {
margin-left: 5px;
}
#serial-monitor-output-container {
overflow: auto;
flex: 1;
padding: 6px;
}
.p-TabBar-toolbar .item.arduino-monitor {
width: 24px;
justify-content: center;
font-size: medium;
box-sizing: border-box;
}
.p-TabBar-toolbar .item.arduino-monitor.toggled {
background: var(--theia-secondary-brand-color1);
}
.p-TabBar-toolbar .item .clear-all {
background: var(--theia-icon-clear) no-repeat;
}
/* React Select Styles */
.serial-monitor-select .sms__control {
border: var(--theia-border-color1) var(--theia-border-width) solid;
background: var(--theia-layout-color2);
}
.serial-monitor-select .sms__control--is-focused {
border-color: var(--theia-border-color2) !important;
box-shadow: none !important;
}
.sms__control--is-focused:hover {
border-color: var(--theia-border-color2) !important;
}
.serial-monitor-select .sms__option--is-selected {
background-color: var(--theia-ui-button-color-secondary-hover);
color: var(--theia-content-font-color0);
}
.serial-monitor-select .sms__option--is-focused {
background-color: var(--theia-ui-button-color-secondary-hover);
}
.serial-monitor-select .sms__menu {
background-color: var(--theia-layout-color1);
border: 1px solid var(--theia-border-color2);
border-top: none;
box-shadow: none;
}
.serial-monitor-select .sms__control.sms__control--menu-is-open {
border: 1px solid;
border-color: var(--theia-border-color2) !important;
border-bottom: none;
}
.serial-monitor-select .sms__value-container .sms__single-value {
color: var(--theia-ui-font-color1);
}
.sms__menu-list {
padding-top: 0 !important;
padding-bottom: 0 !important;
}

View File

@@ -1,13 +0,0 @@
.select-board-dialog {
width: 600px;
}
.select-board-dialog input {
width: calc(100% - 8px);
margin-bottom: 5px;
}
.select-board-dialog select {
width: 100%;
}

View File

@@ -0,0 +1,45 @@
import { FrontendApplicationContribution, FrontendApplication, Widget, Message } from "@theia/core/lib/browser";
import { injectable, inject } from "inversify";
import { ArduinoToolbar } from "./arduino-toolbar";
import { TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
import { CommandRegistry } from "@theia/core";
import { LabelParser } from "@theia/core/lib/browser/label-parser";
export class ArduinoToolbarContainer extends Widget {
protected toolbars: ArduinoToolbar[];
constructor(...toolbars: ArduinoToolbar[]) {
super();
this.id = 'arduino-toolbar-container';
this.toolbars = toolbars;
}
onAfterAttach(msg: Message) {
for (const toolbar of this.toolbars) {
Widget.attach(toolbar, this.node);
}
}
}
@injectable()
export class ArduinoToolbarContribution implements FrontendApplicationContribution {
protected arduinoToolbarContainer: ArduinoToolbarContainer;
constructor(
@inject(TabBarToolbarRegistry) protected tabBarToolBarRegistry: TabBarToolbarRegistry,
@inject(CommandRegistry) protected commandRegistry: CommandRegistry,
@inject(LabelParser) protected labelParser: LabelParser) {
const leftToolbarWidget = new ArduinoToolbar(tabBarToolBarRegistry, commandRegistry, labelParser, 'left');
const rightToolbarWidget = new ArduinoToolbar(tabBarToolBarRegistry, commandRegistry, labelParser, 'right');
this.arduinoToolbarContainer = new ArduinoToolbarContainer(leftToolbarWidget, rightToolbarWidget);
}
onStart(app: FrontendApplication) {
app.shell.addWidget(this.arduinoToolbarContainer, {
area: 'top'
});
}
}

View File

@@ -0,0 +1,140 @@
import * as React from 'react';
import { TabBarToolbar, TabBarToolbarRegistry, TabBarToolbarItem, ReactTabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { ReactWidget } from '@theia/core/lib/browser';
import { LabelParser, LabelIcon } from '@theia/core/lib/browser/label-parser';
export const ARDUINO_TOOLBAR_ITEM_CLASS = 'arduino-tool-item';
export namespace ArduinoToolbarComponent {
export interface Props {
side: 'left' | 'right',
items: (TabBarToolbarItem | ReactTabBarToolbarItem)[],
commands: CommandRegistry,
labelParser: LabelParser,
commandIsEnabled: (id: string) => boolean,
executeCommand: (e: React.MouseEvent<HTMLElement>) => void
}
export interface State {
tooltip: string
}
}
export class ArduinoToolbarComponent extends React.Component<ArduinoToolbarComponent.Props, ArduinoToolbarComponent.State> {
constructor(props: ArduinoToolbarComponent.Props) {
super(props);
this.state = { tooltip: '' };
}
protected renderItem = (item: TabBarToolbarItem) => {
let innerText = '';
let className = `arduino-tool-icon ${item.id}-icon`;
if (item.text) {
for (const labelPart of this.props.labelParser.parse(item.text)) {
if (typeof labelPart !== 'string' && LabelIcon.is(labelPart)) {
className += ` fa fa-${labelPart.name}`;
} else {
innerText = labelPart;
}
}
}
const command = this.props.commands.getCommand(item.command);
const cls = `${ARDUINO_TOOLBAR_ITEM_CLASS} ${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} ${command && this.props.commandIsEnabled(command.id) ? ' enabled' : ''}`
return <div key={item.id} className={cls} >
<div className={item.id}>
<div
key={item.id + '-icon'}
id={item.id}
className={className}
onClick={this.props.executeCommand}
onMouseOver={() => this.setState({ tooltip: item.tooltip || '' })}
onMouseOut={() => this.setState({ tooltip: '' })}
title={item.tooltip}>
{innerText}
</div>
</div>
</div>
}
render(): React.ReactNode {
const tooltip = <div key='arduino-toolbar-tooltip' className={'arduino-toolbar-tooltip'}>{this.state.tooltip}</div>;
const items = [
<React.Fragment key={this.props.side + '-arduino-toolbar-tooltip'}>
{[...this.props.items].map(item => TabBarToolbarItem.is(item) ? this.renderItem(item) : item.render())}
</React.Fragment>
]
if (this.props.side === 'left') {
items.unshift(tooltip);
} else {
items.push(tooltip)
}
return items;
}
}
export class ArduinoToolbar extends ReactWidget {
protected items = new Map<string, TabBarToolbarItem | ReactTabBarToolbarItem>();
constructor(
protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry,
protected readonly commands: CommandRegistry,
protected readonly labelParser: LabelParser,
public readonly side: 'left' | 'right'
) {
super();
this.id = side + '-arduino-toolbar';
this.addClass(TabBarToolbar.Styles.TAB_BAR_TOOLBAR);
this.init();
this.tabBarToolbarRegistry.onDidChange(() => this.updateToolbar());
}
protected updateItems(items: Array<TabBarToolbarItem | ReactTabBarToolbarItem>): void {
this.items.clear();
const revItems = items.reverse();
for (const item of revItems) {
this.items.set(item.id, item);
}
this.update();
}
protected updateToolbar(): void {
const items = this ? this.tabBarToolbarRegistry.visibleItems(this) : [];
this.updateItems(items);
}
protected init(): void {
this.node.classList.add('theia-arduino-toolbar', this.side);
this.update();
}
protected readonly doCommandIsEnabled = (id: string) => this.commandIsEnabled(id);
protected commandIsEnabled(command: string): boolean {
return this.commands.isEnabled(command, this);
}
protected render(): React.ReactNode {
return <ArduinoToolbarComponent
key='arduino-toolbar-component'
side={this.side}
labelParser={this.labelParser}
items={[...this.items.values()]}
commands={this.commands}
commandIsEnabled={this.doCommandIsEnabled}
executeCommand={this.executeCommand}
/>
}
protected executeCommand = (e: React.MouseEvent<HTMLElement>) => {
const item = this.items.get(e.currentTarget.id);
if (TabBarToolbarItem.is(item)) {
this.commands.executeCommand(item.command, this, e.target);
}
}
}
export namespace ArduinoToolbar {
export function is(maybeToolbarWidget: any): maybeToolbarWidget is ArduinoToolbar {
return maybeToolbarWidget instanceof ArduinoToolbar;
}
}

View File

@@ -1,14 +1,169 @@
import { ArduinoComponent } from "./arduino-component";
import { isWindows, isOSX } from '@theia/core/lib/common/os';
import { JsonRpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
const naturalCompare: (left: string, right: string) => number = require('string-natural-compare').caseInsensitive;
export interface AttachedBoardsChangeEvent {
readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>;
readonly newState: Readonly<{ boards: Board[], ports: Port[] }>;
}
export namespace AttachedBoardsChangeEvent {
export function diff(event: AttachedBoardsChangeEvent): Readonly<{
attached: {
boards: Board[],
ports: Port[]
},
detached: {
boards: Board[],
ports: Port[]
}
}> {
const diff = <T>(left: T[], right: T[]) => {
return left.filter(item => right.indexOf(item) === -1);
}
const { boards: newBoards } = event.newState;
const { boards: oldBoards } = event.oldState;
const { ports: newPorts } = event.newState;
const { ports: oldPorts } = event.oldState;
return {
detached: {
boards: diff(oldBoards, newBoards),
ports: diff(oldPorts, newPorts)
},
attached: {
boards: diff(newBoards, oldBoards),
ports: diff(newPorts, oldPorts)
}
};
}
}
export interface BoardInstalledEvent {
readonly pkg: Readonly<BoardPackage>;
}
export const BoardsServiceClient = Symbol('BoardsServiceClient');
export interface BoardsServiceClient {
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void;
notifyBoardInstalled(event: BoardInstalledEvent): void
}
export const BoardsServicePath = '/services/boards-service';
export const BoardsService = Symbol('BoardsService');
export interface BoardsService {
export interface BoardsService extends Installable<BoardPackage>, Searchable<BoardPackage>, JsonRpcServer<BoardsServiceClient> {
getAttachedBoards(): Promise<{ boards: Board[] }>;
selectBoard(board: Board): Promise<void>;
getSelectBoard(): Promise<Board | undefined>;
getAvailablePorts(): Promise<{ ports: Port[] }>;
}
export interface Port {
readonly address: string;
readonly protocol: Port.Protocol;
/**
* Optional label for the protocol. For example: `Serial Port (USB)`.
*/
readonly label?: string;
}
export namespace Port {
export type Protocol = 'serial' | 'network' | 'unknown';
export namespace Protocol {
export function toProtocol(protocol: string | undefined): Protocol {
if (protocol === 'serial') {
return 'serial';
} else if (protocol === 'network') {
return 'network';
} else {
return 'unknown';
}
}
}
export function toString(port: Port, options: { useLabel: boolean } = { useLabel: false }): string {
if (options.useLabel && port.label) {
return `${port.address} ${port.label}`
}
return port.address;
}
export function compare(left: Port, right: Port): number {
// Board ports have higher priorities, they come first.
if (isBoardPort(left) && !isBoardPort(right)) {
return -1;
}
if (!isBoardPort(left) && isBoardPort(right)) {
return 1;
}
let result = left.protocol.toLocaleLowerCase().localeCompare(right.protocol.toLocaleLowerCase());
if (result !== 0) {
return result;
}
result = naturalCompare(left.address, right.address);
if (result !== 0) {
return result;
}
return (left.label || '').localeCompare(right.label || '');
}
export function equals(left: Port | undefined, right: Port | undefined): boolean {
if (left && right) {
return left.address === right.address
&& left.protocol === right.protocol
&& (left.label || '') === (right.label || '');
}
return left === right;
}
// Based on: https://github.com/arduino/Arduino/blob/93581b03d723e55c60caedb4729ffc6ea808fe78/arduino-core/src/processing/app/SerialPortList.java#L48-L74
export function isBoardPort(port: Port): boolean {
const address = port.address.toLocaleLowerCase();
if (isWindows) {
// `COM1` seems to be the default serial port on Windows.
return address !== 'COM1'.toLocaleLowerCase();
}
// On macOS and Linux, the port should start with `/dev/`.
if (!address.startsWith('/dev/')) {
return false
}
if (isOSX) {
// Example: `/dev/cu.usbmodem14401`
if (/(tty|cu)\..*/.test(address.substring('/dev/'.length))) {
return [
'/dev/cu.MALS',
'/dev/cu.SOC',
'/dev/cu.Bluetooth-Incoming-Port'
].map(a => a.toLocaleLowerCase()).every(a => a !== address);
}
}
// Example: `/dev/ttyACM0`
if (/(ttyS|ttyUSB|ttyACM|ttyAMA|rfcomm|ttyO)[0-9]{1,3}/.test(address.substring('/dev/'.length))) {
// Default ports were `/dev/ttyS0` -> `/dev/ttyS31` on Ubuntu 16.04.2.
if (address.startsWith('/dev/ttyS')) {
const index = Number.parseInt(address.substring('/dev/ttyS'.length), 10);
if (!Number.isNaN(index) && 0 <= index && 31 >= index) {
return false;
}
}
return true;
}
return false;
}
export function sameAs(left: Port | undefined, right: string | undefined) {
if (left && right) {
if (left.protocol !== 'serial') {
console.log(`Unexpected protocol for port: ${JSON.stringify(left)}. Ignoring protocol, comparing addresses with ${right}.`);
}
return left.address === right;
}
return false;
}
search(options: { query?: string }): Promise<{ items: BoardPackage[] }>;
install(item: BoardPackage): Promise<void>;
}
export interface BoardPackage extends ArduinoComponent {
@@ -21,33 +176,58 @@ export interface Board {
fqbn?: string
}
export namespace Board {
export function is(board: any): board is Board {
return !!board && 'name' in board;
}
export function equals(left: Board, right: Board): boolean {
return left.name === right.name && left.fqbn === right.fqbn;
}
export function sameAs(left: Board, right: string | Board): boolean {
// How to associate a selected board with one of the available cores: https://typefox.slack.com/archives/CJJHJCJSJ/p1571142327059200
// 1. How to use the FQBN if any and infer the package ID from it: https://typefox.slack.com/archives/CJJHJCJSJ/p1571147549069100
// 2. How to trim the `/Genuino` from the name: https://arduino.slack.com/archives/CJJHJCJSJ/p1571146951066800?thread_ts=1571142327.059200&cid=CJJHJCJSJ
const other = typeof right === 'string' ? { name: right } : right;
if (left.fqbn && other.fqbn) {
return left.fqbn === other.fqbn;
}
return left.name.replace('/Genuino', '') === other.name.replace('/Genuino', '');
}
export function compare(left: Board, right: Board): number {
let result = left.name.localeCompare(right.name);
if (result === 0) {
result = (left.fqbn || '').localeCompare(right.fqbn || '');
}
return result;
}
export function installed(board: Board): boolean {
return !!board.fqbn;
}
}
export interface AttachedSerialBoard extends Board {
port: string;
serialNumber: string;
productID: string;
vendorID: string;
}
export namespace AttachedSerialBoard {
export function is(b: Board): b is AttachedSerialBoard {
return 'port' in b
&& 'serialNumber' in b
&& 'productID' in b
&& 'vendorID' in b;
export function is(b: Board | any): b is AttachedSerialBoard {
return !!b && 'port' in b;
}
}
export interface AttachedNetworkBoard extends Board {
info: string;
address: string;
port: number;
port: string;
}
export namespace AttachedNetworkBoard {
export function is(b: Board): b is AttachedNetworkBoard {
return 'name' in b
&& 'info' in b
&& 'address' in b
&& 'port' in b;
return 'address' in b && 'port' in b;
}
}

View File

@@ -0,0 +1,14 @@
export const ConfigServicePath = '/services/config-service';
export const ConfigService = Symbol('ConfigService');
export interface ConfigService {
getVersion(): Promise<string>;
getConfiguration(): Promise<Config>;
isInDataDir(uri: string): Promise<boolean>;
isInSketchDir(uri: string): Promise<boolean>;
}
export interface Config {
sketchDirUri: string;
dataDirUri: string;
}

View File

@@ -1,3 +1,5 @@
import { Board } from "./boards-service";
export const CoreServicePath = '/services/core-service';
export const CoreService = Symbol('CoreService');
export interface CoreService {
@@ -10,12 +12,15 @@ export namespace CoreService {
export namespace Upload {
export interface Options {
readonly uri: string;
readonly board: Board;
readonly port: string;
}
}
export namespace Compile {
export interface Options {
readonly uri: string;
readonly board: Board;
}
}
}

View File

@@ -0,0 +1,3 @@
export interface Installable<T> {
install(item: T): Promise<void>;
}

View File

@@ -1,9 +1,10 @@
import { ArduinoComponent } from "./arduino-component";
import { Searchable } from './searchable';
import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component';
export const LibraryServicePath = '/services/library-service';
export const LibraryService = Symbol('LibraryService');
export interface LibraryService {
search(options: { query?: string }): Promise<{ items: Library[] }>;
export interface LibraryService extends Installable<Library>, Searchable<Library> {
install(library: Library): Promise<void>;
}

View File

@@ -0,0 +1,44 @@
import { JsonRpcServer } from '@theia/core';
import { Board } from './boards-service';
export interface MonitorError {
readonly message: string;
readonly code: number
}
export interface MonitorReadEvent {
readonly connectionId: string;
readonly data: string;
}
export const MonitorServiceClient = Symbol('MonitorServiceClient');
export interface MonitorServiceClient {
notifyRead(event: MonitorReadEvent): void;
notifyError(event: MonitorError): void;
}
export const MonitorServicePath = '/services/serial-monitor';
export const MonitorService = Symbol('MonitorService');
export interface MonitorService extends JsonRpcServer<MonitorServiceClient> {
connect(config: ConnectionConfig): Promise<{ connectionId: string }>;
disconnect(connectionId: string): Promise<boolean>;
send(connectionId: string, data: string | Uint8Array): Promise<void>;
getConnectionIds(): Promise<string[]>;
}
export interface ConnectionConfig {
readonly board: Board;
readonly port: string;
/**
* Defaults to [`SERIAL`](ConnectionType#SERIAL).
*/
readonly type?: ConnectionType;
/**
* Defaults to `9600`.
*/
readonly baudRate?: number;
}
export enum ConnectionType {
SERIAL = 0
}

View File

@@ -0,0 +1,11 @@
export interface Searchable<T> {
search(options: Searchable.Options): Promise<{ items: T[] }>;
}
export namespace Searchable {
export interface Options {
/**
* Defaults to empty an empty string.
*/
readonly query?: string;
}
}

View File

@@ -0,0 +1,21 @@
export const SketchesServicePath = '/services/sketches-service';
export const SketchesService = Symbol('SketchesService');
export interface SketchesService {
/**
* Returns with the direct sketch folders from the location of the `fileStat`.
* The sketches returns with inverse-chronological order, the first item is the most recent one.
*/
getSketches(uri?: string): Promise<Sketch[]>
getSketchFiles(uri: string): Promise<string[]>
/**
* Creates a new sketch folder in the `parentUri` location. If `parentUri` is not specified,
* it falls back to the default `sketchDirUri` from the CLI.
*/
createNewSketch(parentUri?: string): Promise<Sketch>
isSketchFolder(uri: string): Promise<boolean>
}
export interface Sketch {
readonly name: string;
readonly uri: string
}

View File

@@ -0,0 +1,3 @@
export type RecursiveRequired<T> = {
[P in keyof T]-?: RecursiveRequired<T[P]>;
};

View File

@@ -0,0 +1,12 @@
import { injectable } from 'inversify';
import { ElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
@injectable()
export class ElectronArduinoMenuContribution extends ElectronMenuContribution {
protected hideTopPanel(): void {
// NOOP
// We reuse the `div` for the Arduino toolbar.
}
}

View File

@@ -0,0 +1,8 @@
import { ContainerModule } from 'inversify';
import { ElectronMenuContribution } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution'
import { ElectronArduinoMenuContribution } from './electron-arduino-menu-contribution';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ElectronArduinoMenuContribution).toSelf().inSingletonScope();
rebind(ElectronMenuContribution).to(ElectronArduinoMenuContribution);
});

Some files were not shown because too many files have changed in this diff Show More