mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-12 21:08:32 +00:00
Compare commits
256 Commits
2.0.0-rc9.
...
2.1.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2c74ad6437 | ||
![]() |
9fff553f1a | ||
![]() |
4c2f0fb841 | ||
![]() |
54f210d4de | ||
![]() |
5540170341 | ||
![]() |
7cc252fc36 | ||
![]() |
cb2a371263 | ||
![]() |
96da5bb5ea | ||
![]() |
ef5762599a | ||
![]() |
3aee575a35 | ||
![]() |
80d5b5afa7 | ||
![]() |
ab5c63c4b7 | ||
![]() |
0a53550b0b | ||
![]() |
f5c98c8400 | ||
![]() |
eb1f247296 | ||
![]() |
6e72be1b4c | ||
![]() |
e4beb03a40 | ||
![]() |
39ab836880 | ||
![]() |
dafb2454fd | ||
![]() |
9b49712669 | ||
![]() |
0ab28266df | ||
![]() |
b09ae48536 | ||
![]() |
2aad0e3b16 | ||
![]() |
58aac236bf | ||
![]() |
ec24b6813d | ||
![]() |
d398ed1345 | ||
![]() |
fb10de1446 | ||
![]() |
24dc0bbc88 | ||
![]() |
fa9777e529 | ||
![]() |
77213507fb | ||
![]() |
bfec85c352 | ||
![]() |
f3d3d40c75 | ||
![]() |
5bf38d804e | ||
![]() |
9dec9c5a18 | ||
![]() |
43b5d4e22f | ||
![]() |
fe19e0ef26 | ||
![]() |
c0af297f48 | ||
![]() |
c97e34aa04 | ||
![]() |
01ee045beb | ||
![]() |
cf6f83c8a2 | ||
![]() |
4deaf4fb76 | ||
![]() |
d68bc4abdb | ||
![]() |
4f07515ee8 | ||
![]() |
25b545d4c4 | ||
![]() |
79b6b7ecc0 | ||
![]() |
5d264ef5b6 | ||
![]() |
f63ee85fa3 | ||
![]() |
083a7069f0 | ||
![]() |
f5621db85d | ||
![]() |
658f117e93 | ||
![]() |
6140ae525c | ||
![]() |
afb02da806 | ||
![]() |
692f29fe1a | ||
![]() |
40e797966f | ||
![]() |
a15a94a339 | ||
![]() |
ca687cfe40 | ||
![]() |
32e17745f1 | ||
![]() |
432f3654df | ||
![]() |
197cea2a60 | ||
![]() |
b2bf368db9 | ||
![]() |
287b2e3f41 | ||
![]() |
da0fecfd0f | ||
![]() |
76f9f635d8 | ||
![]() |
3f05396222 | ||
![]() |
644e6079b3 | ||
![]() |
1d342cdbd0 | ||
![]() |
908ec4c544 | ||
![]() |
7c86f1f9d3 | ||
![]() |
f8c01e379c | ||
![]() |
af468a73bc | ||
![]() |
d3a863911c | ||
![]() |
c4172ee8e1 | ||
![]() |
ed8ed15168 | ||
![]() |
32f0426f01 | ||
![]() |
200c00244b | ||
![]() |
1104467329 | ||
![]() |
5695fd8afb | ||
![]() |
d0e383853f | ||
![]() |
3bc412b42f | ||
![]() |
f553d6919d | ||
![]() |
d6a4b0f910 | ||
![]() |
c0488d1f64 | ||
![]() |
81195431b0 | ||
![]() |
87109e6559 | ||
![]() |
c0af1e62e8 | ||
![]() |
ac9cce16f7 | ||
![]() |
3ad660927f | ||
![]() |
8778d70ad7 | ||
![]() |
fe3fbb189c | ||
![]() |
23c7f5f848 | ||
![]() |
f1144efb93 | ||
![]() |
9cec643cab | ||
![]() |
1a7784a540 | ||
![]() |
d24a3911f8 | ||
![]() |
3735553003 | ||
![]() |
f6d112e1f6 | ||
![]() |
cc2d557706 | ||
![]() |
103acc4b7e | ||
![]() |
c3dc7c6307 | ||
![]() |
7d6a2d5e33 | ||
![]() |
6984c52b92 | ||
![]() |
3a70547770 | ||
![]() |
8a85b5c3d8 | ||
![]() |
b998d35524 | ||
![]() |
ddec64c4a5 | ||
![]() |
8fed08003e | ||
![]() |
8454c625f7 | ||
![]() |
60df322f09 | ||
![]() |
8bfb140e7c | ||
![]() |
260227e79a | ||
![]() |
cc310bf1a5 | ||
![]() |
dbd52e2f34 | ||
![]() |
9cd03bec46 | ||
![]() |
c29452a858 | ||
![]() |
7d91f2d8cb | ||
![]() |
f6275f9f62 | ||
![]() |
0d0550974a | ||
![]() |
4e882d25d9 | ||
![]() |
f93f78039b | ||
![]() |
2b2463b834 | ||
![]() |
0773c3915c | ||
![]() |
2f5afe0d9c | ||
![]() |
b8370686ec | ||
![]() |
3b2d12eff9 | ||
![]() |
cdaaa5584d | ||
![]() |
3476de27f7 | ||
![]() |
b55cfc2052 | ||
![]() |
44751c370b | ||
![]() |
32d904ca36 | ||
![]() |
5424dfcf70 | ||
![]() |
b8bf1eefa2 | ||
![]() |
93291b6811 | ||
![]() |
87ebcbe77e | ||
![]() |
99b10942bb | ||
![]() |
960a2d0634 | ||
![]() |
e577de4e8e | ||
![]() |
f3ef95cfe2 | ||
![]() |
bc264d1adf | ||
![]() |
5444395f34 | ||
![]() |
2d2be1f6d0 | ||
![]() |
1e269ac83d | ||
![]() |
0c49709f26 | ||
![]() |
019b2d5588 | ||
![]() |
aa0807ca3f | ||
![]() |
61a11a0857 | ||
![]() |
0c20ae0e28 | ||
![]() |
945a8f4841 | ||
![]() |
ae76432944 | ||
![]() |
40807db65e | ||
![]() |
da22f1ed11 | ||
![]() |
32b70efd5c | ||
![]() |
6f07717369 | ||
![]() |
d6cb23f782 | ||
![]() |
9ac2638335 | ||
![]() |
96cf09d594 | ||
![]() |
8380c82028 | ||
![]() |
5eb2926407 | ||
![]() |
a4ab204400 | ||
![]() |
6416c431c6 | ||
![]() |
8f88aa69bf | ||
![]() |
3c2b2a0734 | ||
![]() |
39538f163f | ||
![]() |
9ef04bb8d6 | ||
![]() |
707f3bef61 | ||
![]() |
878395221a | ||
![]() |
6a35bbfa7e | ||
![]() |
42f6f43870 | ||
![]() |
6983c5bf7f | ||
![]() |
b3ab5cbd2a | ||
![]() |
8a5995920a | ||
![]() |
8de6cf84d9 | ||
![]() |
f5c36bb691 | ||
![]() |
364f8b8e51 | ||
![]() |
671d2eabd4 | ||
![]() |
9a65ef6ea8 | ||
![]() |
4e590ab618 | ||
![]() |
026e80e7fc | ||
![]() |
fdf6f0f9c8 | ||
![]() |
0151e4c224 | ||
![]() |
e8b0ea4f2d | ||
![]() |
7c1ca04c75 | ||
![]() |
0ba88d5ab6 | ||
![]() |
96e229d803 | ||
![]() |
d07d83fdfe | ||
![]() |
5f82577bc1 | ||
![]() |
35fcfb89c1 | ||
![]() |
6e3fe08c4c | ||
![]() |
7f06b148f4 | ||
![]() |
bf303d1b2f | ||
![]() |
59ca91d805 | ||
![]() |
69bb0aa385 | ||
![]() |
565970e779 | ||
![]() |
fec3b1138b | ||
![]() |
dcc0c0aa5d | ||
![]() |
76673cb553 | ||
![]() |
8f95fd6ca6 | ||
![]() |
4907ef2a47 | ||
![]() |
9ae3402631 | ||
![]() |
d0dfc656e6 | ||
![]() |
df3a34eec6 | ||
![]() |
20cc34ca9d | ||
![]() |
1b7f86b231 | ||
![]() |
0d545bea0e | ||
![]() |
204d71b2dd | ||
![]() |
5cb9166c83 | ||
![]() |
7828cc11ac | ||
![]() |
34a7fdb733 | ||
![]() |
7c361cf2d1 | ||
![]() |
8beade0867 | ||
![]() |
3afc2d7e4b | ||
![]() |
d40401437a | ||
![]() |
10ac7fd50a | ||
![]() |
07962e81d4 | ||
![]() |
785775327b | ||
![]() |
80dfa5b7dd | ||
![]() |
40425d49e0 | ||
![]() |
0c87fa9877 | ||
![]() |
5b79320302 | ||
![]() |
1da2dfc349 | ||
![]() |
d7bbfc515d | ||
![]() |
0c22884729 | ||
![]() |
fc9107c084 | ||
![]() |
474d5e5975 | ||
![]() |
f7f644cf36 | ||
![]() |
b5f9aa0f15 | ||
![]() |
cc5cf3b165 | ||
![]() |
125bd64c91 | ||
![]() |
ca47e8a09a | ||
![]() |
52804a5b52 | ||
![]() |
3ec62642dd | ||
![]() |
1281ad1932 | ||
![]() |
de32bddc20 | ||
![]() |
79ea0fa9a6 | ||
![]() |
683219dc1c | ||
![]() |
d674ab9b73 | ||
![]() |
5be1f9d7fe | ||
![]() |
9e2b73a045 | ||
![]() |
75e00c2bae | ||
![]() |
989300f25d | ||
![]() |
5226636fed | ||
![]() |
8b3f3c69fc | ||
![]() |
a39ab47e70 | ||
![]() |
9cabd40429 | ||
![]() |
6e3681896c | ||
![]() |
8a1cabd2bc | ||
![]() |
7a3e6789d1 | ||
![]() |
92bc5ecf7b | ||
![]() |
aebec0f942 | ||
![]() |
54db9bbce8 | ||
![]() |
676eb2f588 | ||
![]() |
ce273adf77 | ||
![]() |
0b33b51700 | ||
![]() |
36ac47b975 | ||
![]() |
bf193b1cac | ||
![]() |
879aedeaa3 | ||
![]() |
d556ee95c0 |
@@ -15,9 +15,7 @@ module.exports = {
|
||||
'.browser_modules/*',
|
||||
'docs/*',
|
||||
'scripts/*',
|
||||
'electron/*',
|
||||
'electron-app/*',
|
||||
'browser-app/*',
|
||||
'plugins/*',
|
||||
'arduino-ide-extension/src/node/cli-protocol',
|
||||
],
|
||||
|
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -30,7 +30,7 @@ body:
|
||||
description: |
|
||||
Which version of the Arduino IDE are you using?
|
||||
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
|
||||
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
|
||||
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds).
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -68,7 +68,7 @@ body:
|
||||
options:
|
||||
- label: I searched for previous reports in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
|
||||
required: true
|
||||
- label: I verified the problem still occurs when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
|
||||
- label: I verified the problem still occurs when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds)
|
||||
required: true
|
||||
- label: My report contains all necessary details
|
||||
required: true
|
||||
|
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -8,6 +8,12 @@ contact_links:
|
||||
- name: Support request
|
||||
url: https://forum.arduino.cc/
|
||||
about: We can help you out on the Arduino Forum!
|
||||
- name: Issue report guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/contributor-guide/issues.md#issue-report-guide
|
||||
about: Learn about submitting issue reports to this repository.
|
||||
- name: Contributor guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide
|
||||
about: Learn about contributing to this project.
|
||||
- name: Discuss development work on the project
|
||||
url: https://groups.google.com/a/arduino.cc/g/developers
|
||||
about: Arduino Developers Mailing List
|
||||
|
4
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -25,7 +25,7 @@ body:
|
||||
description: |
|
||||
Which version of the Arduino IDE are you using?
|
||||
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
|
||||
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
|
||||
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds).
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -63,7 +63,7 @@ body:
|
||||
options:
|
||||
- label: I searched for previous requests in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
|
||||
required: true
|
||||
- label: I verified the feature was still missing when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
|
||||
- label: I verified the feature was still missing when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds)
|
||||
required: true
|
||||
- label: My request contains all necessary details
|
||||
required: true
|
||||
|
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# See: https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#about-the-dependabotyml-file
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
# Configure check for outdated GitHub Actions actions in workflows.
|
||||
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/dependabot/README.md
|
||||
# See: https://docs.github.com/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||
- package-ecosystem: github-actions
|
||||
directory: / # Check the repository's workflows under /.github/workflows/
|
||||
assignees:
|
||||
- per1234
|
||||
schedule:
|
||||
interval: daily
|
||||
labels:
|
||||
- "topic: infrastructure"
|
3
.github/label-configuration-files/labels.yml
vendored
3
.github/label-configuration-files/labels.yml
vendored
@@ -7,6 +7,9 @@
|
||||
- name: "topic: CLI"
|
||||
color: "00ffff"
|
||||
description: Related to Arduino CLI
|
||||
- name: "topic: cloud"
|
||||
color: "00ffff"
|
||||
description: Related to Arduino Cloud and cloud sketches
|
||||
- name: "topic: debugger"
|
||||
color: "00ffff"
|
||||
description: Related to the integrated debugger
|
||||
|
131
.github/tools/fetch_athena_stats.py
vendored
131
.github/tools/fetch_athena_stats.py
vendored
@@ -1,131 +0,0 @@
|
||||
import boto3
|
||||
import semver
|
||||
import os
|
||||
import logging
|
||||
import uuid
|
||||
import time
|
||||
|
||||
|
||||
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
||||
log = logging.getLogger()
|
||||
logging.getLogger("boto3").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
||||
|
||||
|
||||
def execute(client, statement, dest_s3_output_location):
|
||||
log.info("execute query: {} dumping in {}".format(statement, dest_s3_output_location))
|
||||
result = client.start_query_execution(
|
||||
QueryString=statement,
|
||||
ClientRequestToken=str(uuid.uuid4()),
|
||||
ResultConfiguration={
|
||||
"OutputLocation": dest_s3_output_location,
|
||||
},
|
||||
)
|
||||
execution_id = result["QueryExecutionId"]
|
||||
log.info("wait for query {} completion".format(execution_id))
|
||||
wait_for_query_execution_completion(client, execution_id)
|
||||
log.info("operation successful")
|
||||
return execution_id
|
||||
|
||||
|
||||
def wait_for_query_execution_completion(client, query_execution_id):
|
||||
query_ended = False
|
||||
while not query_ended:
|
||||
query_execution = client.get_query_execution(QueryExecutionId=query_execution_id)
|
||||
state = query_execution["QueryExecution"]["Status"]["State"]
|
||||
if state == "SUCCEEDED":
|
||||
query_ended = True
|
||||
elif state in ["FAILED", "CANCELLED"]:
|
||||
raise BaseException(
|
||||
"query failed or canceled: {}".format(query_execution["QueryExecution"]["Status"]["StateChangeReason"])
|
||||
)
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def valid(key):
|
||||
split = key.split("_")
|
||||
if len(split) < 1:
|
||||
return False
|
||||
try:
|
||||
semver.parse(split[0])
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_results(client, execution_id):
|
||||
results_paginator = client.get_paginator("get_query_results")
|
||||
results_iter = results_paginator.paginate(QueryExecutionId=execution_id, PaginationConfig={"PageSize": 1000})
|
||||
res = {}
|
||||
for results_page in results_iter:
|
||||
for row in results_page["ResultSet"]["Rows"][1:]:
|
||||
# Loop through the JSON objects
|
||||
key = row["Data"][0]["VarCharValue"]
|
||||
if valid(key):
|
||||
res[key] = row["Data"][1]["VarCharValue"]
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def convert_data(data):
|
||||
result = []
|
||||
for key, value in data.items():
|
||||
# 0.18.0_macOS_64bit.tar.gz
|
||||
split_key = key.split("_")
|
||||
if len(split_key) != 3:
|
||||
continue
|
||||
(version, os_version, arch) = split_key
|
||||
arch_split = arch.split(".")
|
||||
if len(arch_split) < 1:
|
||||
continue
|
||||
arch = arch_split[0]
|
||||
if len(arch) > 10:
|
||||
# This can't be an architecture really.
|
||||
# It's an ugly solution but works for now so deal with it.
|
||||
continue
|
||||
repo = os.environ["GITHUB_REPOSITORY"].split("/")[1]
|
||||
result.append(
|
||||
{
|
||||
"type": "gauge",
|
||||
"name": "arduino.downloads.total",
|
||||
"value": value,
|
||||
"host": os.environ["GITHUB_REPOSITORY"],
|
||||
"tags": [
|
||||
f"version:{version}",
|
||||
f"os:{os_version}",
|
||||
f"arch:{arch}",
|
||||
"cdn:downloads.arduino.cc",
|
||||
f"project:{repo}",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
DEST_S3_OUTPUT = os.environ["AWS_ATHENA_OUTPUT_LOCATION"]
|
||||
AWS_ATHENA_SOURCE_TABLE = os.environ["AWS_ATHENA_SOURCE_TABLE"]
|
||||
|
||||
session = boto3.session.Session(region_name="us-east-1")
|
||||
athena_client = session.client("athena")
|
||||
|
||||
# Load all partitions before querying downloads
|
||||
execute(athena_client, f"MSCK REPAIR TABLE {AWS_ATHENA_SOURCE_TABLE};", DEST_S3_OUTPUT)
|
||||
|
||||
query = f"""SELECT replace(json_extract_scalar(url_decode(url_decode(querystring)),
|
||||
'$.data.url'), 'https://downloads.arduino.cc/arduino-ide/arduino-ide_', '')
|
||||
AS flavor, count(json_extract(url_decode(url_decode(querystring)),'$')) AS gauge
|
||||
FROM {AWS_ATHENA_SOURCE_TABLE}
|
||||
WHERE json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
|
||||
LIKE 'https://downloads.arduino.cc/arduino-ide/arduino-ide_%'
|
||||
AND json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
|
||||
NOT LIKE '%latest%' -- exclude latest redirect
|
||||
group by 1 ;"""
|
||||
exec_id = execute(athena_client, query, DEST_S3_OUTPUT)
|
||||
results = get_results(athena_client, exec_id)
|
||||
result_json = convert_data(results)
|
||||
|
||||
print(f"::set-output name=result::{result_json}")
|
57
.github/workflows/arduino-stats.yaml
vendored
57
.github/workflows/arduino-stats.yaml
vendored
@@ -1,57 +0,0 @@
|
||||
name: arduino-stats
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every day at 07:00 AM, 03:00 PM and 11:00 PM
|
||||
- cron: "0 7,15,23 * * *"
|
||||
workflow_dispatch:
|
||||
repository_dispatch:
|
||||
|
||||
jobs:
|
||||
push-stats:
|
||||
# This workflow is only of value to the arduino/arduino-ide repository and
|
||||
# would always fail in forks
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Fetch downloads count form Arduino CDN using AWS Athena
|
||||
id: fetch
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.STATS_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.STATS_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ATHENA_SOURCE_TABLE: ${{ secrets.STATS_AWS_ATHENA_SOURCE_TABLE }}
|
||||
AWS_ATHENA_OUTPUT_LOCATION: ${{ secrets.STATS_AWS_ATHENA_OUTPUT_LOCATION }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
pip install boto3 semver
|
||||
python .github/tools/fetch_athena_stats.py
|
||||
|
||||
- name: Send metrics
|
||||
uses: masci/datadog@v1
|
||||
with:
|
||||
api-key: ${{ secrets.DD_API_KEY }}
|
||||
# Metrics input expects YAML but JSON will work just right.
|
||||
metrics: ${{steps.fetch.outputs.result}}
|
||||
|
||||
- name: Report failure
|
||||
if: failure()
|
||||
uses: masci/datadog@v1
|
||||
with:
|
||||
api-key: ${{ secrets.DD_API_KEY }}
|
||||
events: |
|
||||
- title: "Arduino IDE stats failing"
|
||||
text: "Stats collection failed"
|
||||
alert_type: "error"
|
||||
host: ${{ github.repository }}
|
||||
tags:
|
||||
- "project:arduino-ide"
|
||||
- "cdn:downloads.arduino.cc"
|
||||
- "workflow:${{ github.workflow }}"
|
67
.github/workflows/build.yml
vendored
67
.github/workflows/build.yml
vendored
@@ -28,6 +28,8 @@ on:
|
||||
- cron: '0 3 * * *' # run every day at 3AM (https://docs.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule)
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.19"
|
||||
JOB_TRANSFER_ARTIFACT: build-artifacts
|
||||
CHANGELOG_ARTIFACTS: changelog
|
||||
|
||||
@@ -41,7 +43,7 @@ jobs:
|
||||
certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX # Name of the secret that contains the certificate.
|
||||
certificate-password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD # Name of the secret that contains the certificate password.
|
||||
certificate-extension: pfx # File extension for the certificate.
|
||||
- os: ubuntu-18.04 # https://github.com/arduino/arduino-ide/issues/259
|
||||
- os: ubuntu-20.04
|
||||
- os: macos-latest
|
||||
# APPLE_SIGNING_CERTIFICATE_P12 secret was produced by following the procedure from:
|
||||
# https://www.kencochrane.com/2020/08/01/build-and-sign-golang-binaries-for-macos-with-github-actions/#exporting-the-developer-certificate
|
||||
@@ -53,19 +55,30 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js 14.x
|
||||
uses: actions/setup-node@v1
|
||||
- name: Install Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14.x'
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Python 3.x
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Taskfile
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Package
|
||||
shell: bash
|
||||
env:
|
||||
@@ -86,6 +99,7 @@ jobs:
|
||||
export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}"
|
||||
echo "${{ secrets[matrix.config.certificate-secret] }}" | base64 --decode > "$CSC_LINK"
|
||||
export CSC_KEY_PASSWORD="${{ secrets[matrix.config.certificate-password-secret] }}"
|
||||
export CSC_FOR_PULL_REQUEST=true
|
||||
fi
|
||||
|
||||
if [ "${{ runner.OS }}" = "Windows" ]; then
|
||||
@@ -96,7 +110,7 @@ jobs:
|
||||
yarn --cwd ./electron/packager/ package
|
||||
|
||||
- name: Upload [GitHub Actions]
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: electron/build/dist/build-artifacts/
|
||||
@@ -127,13 +141,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download job transfer artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
|
||||
- name: Upload tester build artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact.name }}
|
||||
path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }}
|
||||
@@ -145,7 +159,7 @@ jobs:
|
||||
BODY: ${{ steps.changelog.outputs.BODY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # To fetch all history for all branches and tags.
|
||||
|
||||
@@ -167,15 +181,19 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
echo -e "$BODY"
|
||||
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
|
||||
echo "::set-output name=BODY::$OUTPUT_SAFE_BODY"
|
||||
|
||||
# Set workflow step output
|
||||
# See: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
DELIMITER="$RANDOM"
|
||||
echo "BODY<<$DELIMITER" >> $GITHUB_OUTPUT
|
||||
echo "$BODY" >> $GITHUB_OUTPUT
|
||||
echo "$DELIMITER" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "$BODY" > CHANGELOG.txt
|
||||
|
||||
- name: Upload Changelog [GitHub Actions]
|
||||
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: CHANGELOG.txt
|
||||
@@ -186,7 +204,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download [GitHub Actions]
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
@@ -207,7 +225,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download [GitHub Actions]
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
path: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
@@ -215,10 +233,10 @@ jobs:
|
||||
- name: Get Tag
|
||||
id: tag_name
|
||||
run: |
|
||||
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
|
||||
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Publish Release [GitHub]
|
||||
uses: svenstaro/upload-release-action@2.2.0
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
|
||||
@@ -227,6 +245,15 @@ jobs:
|
||||
file_glob: true
|
||||
body: ${{ needs.changelog.outputs.BODY }}
|
||||
|
||||
# Temporary measure to prevent release update offers before the manually produced builds are uploaded.
|
||||
# The step must be removed once fully automated builds are regained.
|
||||
- name: Remove "channel update info files" related to manual builds
|
||||
run: |
|
||||
# See: https://github.com/arduino/arduino-ide/issues/2018
|
||||
rm "${{ env.JOB_TRANSFER_ARTIFACT }}/stable-linux.yml"
|
||||
# See: https://github.com/arduino/arduino-ide/issues/408
|
||||
rm "${{ env.JOB_TRANSFER_ARTIFACT }}/stable-mac.yml"
|
||||
|
||||
- name: Publish Release [S3]
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
uses: docker://plugins/s3
|
||||
@@ -250,6 +277,6 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Remove unneeded job transfer artifact
|
||||
uses: geekyeggo/delete-artifact@v1
|
||||
uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
name: ${{ env.JOB_TRANSFER_ARTIFACT }}
|
||||
|
7
.github/workflows/check-certificates.yml
vendored
7
.github/workflows/check-certificates.yml
vendored
@@ -59,7 +59,9 @@ jobs:
|
||||
(
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-noout -passin env:CERTIFICATE_PASSWORD
|
||||
-legacy \
|
||||
-noout \
|
||||
-passin env:CERTIFICATE_PASSWORD
|
||||
) || (
|
||||
echo "::error::Verification of ${{ matrix.certificate.identifier }} failed!!!"
|
||||
exit 1
|
||||
@@ -87,6 +89,7 @@ jobs:
|
||||
openssl pkcs12 \
|
||||
-in "${{ env.CERTIFICATE_PATH }}" \
|
||||
-clcerts \
|
||||
-legacy \
|
||||
-nodes \
|
||||
-passin env:CERTIFICATE_PASSWORD
|
||||
) | (
|
||||
@@ -108,7 +111,7 @@ jobs:
|
||||
echo "Certificate expiration date: $EXPIRATION_DATE"
|
||||
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
|
||||
|
||||
echo "::set-output name=days::$DAYS_BEFORE_EXPIRATION"
|
||||
echo "days=$DAYS_BEFORE_EXPIRATION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if expiration notification period has been reached
|
||||
id: check-expiration
|
||||
|
25
.github/workflows/check-i18n-task.yml
vendored
25
.github/workflows/check-i18n-task.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Check Internationalization
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on:
|
||||
push:
|
||||
@@ -23,16 +27,29 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
- name: Install Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14.x'
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Taskfile
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check for errors
|
||||
run: yarn i18n:check
|
||||
|
@@ -8,7 +8,7 @@ on:
|
||||
env:
|
||||
CHANGELOG_ARTIFACTS: changelog
|
||||
# See: https://github.com/actions/setup-node/#readme
|
||||
NODE_VERSION: 14.x
|
||||
NODE_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
create-changelog:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Get Tag
|
||||
id: tag_name
|
||||
run: |
|
||||
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
|
||||
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create full changelog
|
||||
id: full-changelog
|
96
.github/workflows/github-stats.yaml
vendored
96
.github/workflows/github-stats.yaml
vendored
@@ -1,96 +0,0 @@
|
||||
name: github-stats
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every 30 minutes
|
||||
- cron: "*/30 * * * *"
|
||||
workflow_dispatch:
|
||||
repository_dispatch:
|
||||
|
||||
jobs:
|
||||
push-stats:
|
||||
# This workflow is only of value to the arduino/arduino-ide repository and
|
||||
# would always fail in forks
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Fetch downloads count
|
||||
id: fetch
|
||||
uses: actions/github-script@v4
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
let metrics = []
|
||||
|
||||
// Get a list of releases
|
||||
const opts = github.repos.listReleases.endpoint.merge({
|
||||
...context.repo
|
||||
})
|
||||
const releases = await github.paginate(opts)
|
||||
|
||||
// Get download stats for every release
|
||||
for (const rel of releases) {
|
||||
// Names for assets are like `arduino-ide_2.0.0-beta.12_Linux_64bit.zip`,
|
||||
// we'll use this later to split the asset file name more easily
|
||||
const baseName = `arduino-ide_${rel.name}_`
|
||||
|
||||
// Get a list of assets for this release
|
||||
const opts = github.repos.listReleaseAssets.endpoint.merge({
|
||||
...context.repo,
|
||||
release_id: rel.id
|
||||
})
|
||||
const assets = await github.paginate(opts)
|
||||
|
||||
for (const asset of assets) {
|
||||
// Ignore files that are not arduino-ide packages
|
||||
if (!asset.name.startsWith(baseName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Strip the base and remove file extension to get `Linux_32bit`
|
||||
systemArch = asset.name.replace(baseName, "").split(".")[0].split("_")
|
||||
|
||||
// Add a metric object to the list of gathered metrics
|
||||
metrics.push({
|
||||
"type": "gauge",
|
||||
"name": "arduino.downloads.total",
|
||||
"value": asset.download_count,
|
||||
"host": "${{ github.repository }}",
|
||||
"tags": [
|
||||
`version:${rel.name}`,
|
||||
`os:${systemArch[0]}`,
|
||||
`arch:${systemArch[1]}`,
|
||||
"cdn:github.com",
|
||||
"project:arduino-ide"
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The action will put whatever we return from this function in
|
||||
// `outputs.result`, JSON encoded. So we just return the array
|
||||
// of objects and GitHub will do the rest.
|
||||
return metrics
|
||||
|
||||
- name: Send metrics
|
||||
uses: masci/datadog@v1
|
||||
with:
|
||||
api-key: ${{ secrets.DD_API_KEY }}
|
||||
# Metrics input expects YAML but JSON will work just right.
|
||||
metrics: ${{steps.fetch.outputs.result}}
|
||||
|
||||
- name: Report failure
|
||||
if: failure()
|
||||
uses: masci/datadog@v1
|
||||
with:
|
||||
api-key: ${{ secrets.DD_API_KEY }}
|
||||
events: |
|
||||
- title: "Arduino IDE stats failing"
|
||||
text: "Stats collection failed"
|
||||
alert_type: "error"
|
||||
host: ${{ github.repository }}
|
||||
tags:
|
||||
- "project:arduino-ide"
|
||||
- "cdn:github.com"
|
||||
- "workflow:${{ github.workflow }}"
|
23
.github/workflows/i18n-nightly-push.yml
vendored
23
.github/workflows/i18n-nightly-push.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: i18n-nightly-push
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every day at 1AM
|
||||
@@ -10,14 +14,25 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
- name: Install Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14.x'
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
|
25
.github/workflows/i18n-weekly-pull.yml
vendored
25
.github/workflows/i18n-weekly-pull.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: i18n-weekly-pull
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every monday at 2AM
|
||||
@@ -10,14 +14,25 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js 14.x
|
||||
uses: actions/setup-node@v2
|
||||
- name: Install Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14.x'
|
||||
node-version: '16.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
@@ -30,7 +45,7 @@ jobs:
|
||||
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
commit-message: Updated translation files
|
||||
title: Update translation files
|
||||
|
16
.github/workflows/sync-labels.yml
vendored
16
.github/workflows/sync-labels.yml
vendored
@@ -27,11 +27,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download JSON schema for labels configuration file
|
||||
id: download-schema
|
||||
uses: carlosperate/download-file-action@v1
|
||||
uses: carlosperate/download-file-action@v2
|
||||
with:
|
||||
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/arduino-tooling-gh-label-configuration-schema.json
|
||||
location: ${{ runner.temp }}/label-configuration-schema
|
||||
@@ -66,12 +66,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Download
|
||||
uses: carlosperate/download-file-action@v1
|
||||
uses: carlosperate/download-file-action@v2
|
||||
with:
|
||||
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }}
|
||||
|
||||
- name: Pass configuration files to next job via workflow artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
*.yaml
|
||||
@@ -103,19 +103,19 @@ jobs:
|
||||
run: |
|
||||
# Use of this flag in the github-label-sync command will cause it to only check the validity of the
|
||||
# configuration.
|
||||
echo "::set-output name=flag::--dry-run"
|
||||
echo "flag=--dry-run" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download configuration files artifact
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
|
||||
path: ${{ env.CONFIGURATIONS_FOLDER }}
|
||||
|
||||
- name: Remove unneeded artifact
|
||||
uses: geekyeggo/delete-artifact@v1
|
||||
uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
name: ${{ env.CONFIGURATIONS_ARTIFACT }}
|
||||
|
||||
|
17
.github/workflows/themes-weekly-pull.yml
vendored
17
.github/workflows/themes-weekly-pull.yml
vendored
@@ -7,7 +7,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 14.x
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.19"
|
||||
NODE_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
pull-from-jsonbin:
|
||||
@@ -22,6 +24,17 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Install Task
|
||||
uses: arduino/setup-task@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
@@ -41,7 +54,7 @@ jobs:
|
||||
run: yarn run themes:generate
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
commit-message: Updated themes
|
||||
title: Update themes
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -4,10 +4,10 @@ node_modules/
|
||||
lib/
|
||||
downloads/
|
||||
build/
|
||||
Examples/
|
||||
arduino-ide-extension/Examples/
|
||||
!electron/build/
|
||||
src-gen/
|
||||
!webpack.config.js
|
||||
webpack.config.js
|
||||
gen-webpack.config.js
|
||||
.DS_Store
|
||||
# switching from `electron` to `browser` in dev mode.
|
||||
@@ -15,11 +15,11 @@ gen-webpack.config.js
|
||||
yarn*.log
|
||||
# For the VS Code extensions used by Theia.
|
||||
plugins
|
||||
# the config files for the CLI
|
||||
arduino-ide-extension/data/cli/config
|
||||
# the tokens folder for the themes
|
||||
scripts/themes/tokens
|
||||
# environment variables
|
||||
.env
|
||||
# content trace files for electron
|
||||
electron-app/traces
|
||||
# any Arduino LS generated log files
|
||||
inols*.log
|
||||
|
40
.vscode/launch.json
vendored
40
.vscode/launch.json
vendored
@@ -14,15 +14,14 @@
|
||||
".",
|
||||
"--log-level=debug",
|
||||
"--hostname=localhost",
|
||||
"--no-cluster",
|
||||
"--app-project-path=${workspaceRoot}/electron-app",
|
||||
"--remote-debugging-port=9222",
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:../plugins",
|
||||
"--hosted-plugin-inspect=9339",
|
||||
"--nosplash",
|
||||
"--content-trace",
|
||||
"--open-devtools"
|
||||
"--open-devtools",
|
||||
"--no-ping-timeout",
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
@@ -52,12 +51,12 @@
|
||||
".",
|
||||
"--log-level=debug",
|
||||
"--hostname=localhost",
|
||||
"--no-cluster",
|
||||
"--app-project-path=${workspaceRoot}/electron-app",
|
||||
"--remote-debugging-port=9222",
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:../plugins",
|
||||
"--hosted-plugin-inspect=9339"
|
||||
"--hosted-plugin-inspect=9339",
|
||||
"--no-ping-timeout",
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
@@ -81,37 +80,6 @@
|
||||
"port": 9222,
|
||||
"webRoot": "${workspaceFolder}/electron-app"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "App (Browser)",
|
||||
"program": "${workspaceRoot}/browser-app/src-gen/backend/main.js",
|
||||
"args": [
|
||||
"--hostname=0.0.0.0",
|
||||
"--port=3000",
|
||||
"--no-cluster",
|
||||
"--no-app-auto-install",
|
||||
"--plugins=local-dir:plugins"
|
||||
],
|
||||
"windows": {
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"NODE_PRESERVE_SYMLINKS": "1"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"outFiles": [
|
||||
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
|
||||
"${workspaceRoot}/browser-app/lib/**/*.js",
|
||||
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
|
||||
],
|
||||
"smartStep": true,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -2,6 +2,9 @@
|
||||
"files.exclude": {
|
||||
"**/lib": false
|
||||
},
|
||||
"search.exclude": {
|
||||
"arduino-ide-extension/src/test/node/__test_sketchbook__": true
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
|
30
.vscode/tasks.json
vendored
30
.vscode/tasks.json
vendored
@@ -12,17 +12,6 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Start Browser App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app start",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch IDE Extension",
|
||||
"type": "shell",
|
||||
@@ -34,17 +23,6 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch Browser App",
|
||||
"type": "shell",
|
||||
"command": "yarn --cwd ./browser-app watch",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new",
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch Electron App",
|
||||
"type": "shell",
|
||||
@@ -56,14 +34,6 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch All [Browser]",
|
||||
"type": "shell",
|
||||
"dependsOn": [
|
||||
"Arduino IDE - Watch IDE Extension",
|
||||
"Arduino IDE - Watch Browser App"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Arduino IDE - Watch All [Electron]",
|
||||
"type": "shell",
|
||||
|
154
BUILDING.md
154
BUILDING.md
@@ -1,153 +1,3 @@
|
||||
# Development
|
||||
|
||||
This page includes technical documentation for developers who want to build the IDE locally and contribute to the project.
|
||||
|
||||
## Architecture overview
|
||||
|
||||
The IDE consists of three major parts:
|
||||
- the _Electron main_ process,
|
||||
- the _backend_, and
|
||||
- the _frontend_.
|
||||
|
||||
The _Electron main_ process is responsible for:
|
||||
- creating the application,
|
||||
- managing the application lifecycle via listeners, and
|
||||
- creating and managing the web pages for the app.
|
||||
|
||||
In Electron, the process that runs the main entry JavaScript file is called the main process. The _Electron main_ process can display a GUI by creating web pages. An Electron app always has exactly one main process.
|
||||
|
||||
By default, whenever the _Electron main_ process creates a web page, it will instantiate a new `BrowserWindow` instance. Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process. Each `BrowserWindow` instance runs the web page in its own renderer process. When a `BrowserWindow` instance is destroyed, the corresponding renderer process is also terminated. The main process manages all web pages and their corresponding renderer processes. Each renderer process is isolated and only cares about the web page running in it.<sup>[[1]]</sup>
|
||||
|
||||
In normal browsers, web pages usually run in a sandboxed environment, and accessing native resources are disallowed. However, Electron has the power to use Node.js APIs in the web pages allowing lower-level OS interactions. Due to security reasons, accessing native resources is an undesired behavior in the IDE. So by convention, we do not use Node.js APIs. (Note: the Node.js integration is [not yet disabled](https://github.com/eclipse-theia/theia/issues/2018) although it is not used). In the IDE, only the _backend_ allows OS interaction.
|
||||
|
||||
The _backend_ process is responsible for:
|
||||
- providing access to the filesystem,
|
||||
- communicating with the [Arduino CLI](https://github.com/arduino/arduino-cli) via gRPC,
|
||||
- running your terminal,
|
||||
- exposing additional RESTful APIs,
|
||||
- performing the Git commands in the local repositories,
|
||||
- hosting and running any VS Code extensions, or
|
||||
- executing VS Code tasks<sup>[[2]]</sup>.
|
||||
|
||||
The _Electron main_ process spawns the _backend_ process. There is always exactly one _backend_ process. However, due to performance considerations, the _backend_ spawns several sub-processes for the filesystem watching, Git repository discovery, etc. The communication between the _backend_ process and its sub-processes is established via IPC. Besides spawning sub-processes, the _backend_ will start an HTTP server on a random available port, and serves the web application as static content. When the sub-processes are up and running, and the HTTP server is also listening, the _backend_ process sends the HTTP server port to the _Electron main_ process via IPC. The _Electron main_ process will load the _backend_'s endpoint in the `BrowserWindow`.
|
||||
|
||||
The _frontend_ is running as an Electron renderer process and can invoke services implemented on the _backend_. The communication between the _backend_ and the _frontend_ is done via JSON-RPC over a websocket connection. This means, the services running in the _frontend_ are all proxies, and will ask the corresponding service implementation on the _backend_.
|
||||
|
||||
[1]: https://www.electronjs.org/docs/tutorial/application-architecture#differences-between-main-process-and-renderer-process
|
||||
[2]: https://code.visualstudio.com/Docs/editor/tasks
|
||||
|
||||
|
||||
## Build from source
|
||||
|
||||
If you’re familiar with TypeScript, the [Theia IDE](https://theia-ide.org/), and if you want to contribute to the
|
||||
project, you should be able to build the Arduino IDE locally.
|
||||
Please refer to the [Theia IDE prerequisites](https://github.com/theia-ide/theia/blob/master/doc/) documentation for the setup instructions.
|
||||
> **Note**: Node.js 14 must be used instead of the version 12 recommended at the link above.
|
||||
|
||||
Once you have all the tools installed, you can build the editor following these steps
|
||||
|
||||
1. Install the dependencies and build
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
|
||||
2. Rebuild the dependencies
|
||||
```sh
|
||||
yarn rebuild:browser
|
||||
```
|
||||
|
||||
3. Rebuild the electron dependencies
|
||||
```sh
|
||||
yarn rebuild:electron
|
||||
```
|
||||
|
||||
4. Start the application
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Notes for Windows contributors
|
||||
Windows requires the Microsoft Visual C++ (MSVC) compiler toolset to be installed on your development machine.
|
||||
|
||||
In case it's not already present, it can be downloaded from the "**Tools for Visual Studio 20XX**" section of the Visual Studio [downloads page](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) via the "**Build Tools for Visual Studio 20XX**" (e.g., "**Build Tools for Visual Studio 2022**") download link.
|
||||
|
||||
Select "**Desktop development with C++**" from the "**Workloads**" tab during the installation procedure.
|
||||
|
||||
### CI
|
||||
|
||||
This project is built on [GitHub Actions](https://github.com/arduino/arduino-ide/actions).
|
||||
|
||||
- _Snapshot_ builds run when changes are pushed to the `main` branch, or when a PR is created against the `main` branch. For the sake of the review and verification process, the build artifacts for each operating system can be downloaded from the GitHub Actions page.
|
||||
- _Nightly_ builds run every day at 03:00 GMT from the `main` branch.
|
||||
- _Release_ builds run when a new tag is pushed to the remote. The tag must follow the [semver](https://semver.org/). For instance, `1.2.3` is a correct tag, but `v2.3.4` won't work. Steps to trigger a new release build:
|
||||
- Create a local tag:
|
||||
```sh
|
||||
git tag -a 1.2.3 -m "Creating a new tag for the `1.2.3` release."
|
||||
```
|
||||
- Push it to the remote:
|
||||
```sh
|
||||
git push origin 1.2.3
|
||||
```
|
||||
|
||||
|
||||
## Notes for macOS contributors
|
||||
Beginning in macOS 10.14.5, the software [must be notarized to run](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution). The signing and notarization processes for the Arduino IDE are managed by our Continuous Integration (CI) workflows, implemented with GitHub Actions. On every push and pull request, the Arduino IDE is built and saved to a workflow artifact. These artifacts can be used by contributors and beta testers who don't want to set up a build system locally.
|
||||
For security reasons, signing and notarization are disabled for workflow runs for pull requests from forks of this repository. This means that macOS will block you from running those artifacts.
|
||||
Due to this limitation, Mac users have two options for testing contributions from forks:
|
||||
|
||||
### The Safe approach (recommended)
|
||||
|
||||
Follow [the instructions above](#build-from-source) to create the build environment locally, then build the code you want to test.
|
||||
|
||||
### The Risky approach
|
||||
|
||||
*Please note that this approach is risky as you are lowering the security on your system, therefore we strongly discourage you from following it.*
|
||||
1. Use [this guide](https://help.apple.com/xcode/mac/10.2/index.html?localePath=en.lproj#/dev9b7736b0e), in order to disable Gatekeeper (at your own risk!).
|
||||
1. Download the unsigned artifact provided by the CI workflow run related to the Pull Request at each push.
|
||||
1. Re-enable Gatekeeper after tests are done, following the guide linked above.
|
||||
|
||||
### Creating a release
|
||||
|
||||
You will not need to create a new release yourself as the Arduino team takes care of this on a regular basis, but we are documenting the process here. Let's assume the current version is `0.1.3` and you want to release `0.2.0`.
|
||||
|
||||
- Make sure the `main` state represents what you want to release and you're on `main`.
|
||||
- Prepare a release-candidate build on a branch:
|
||||
```bash
|
||||
git branch 0.2.0-rc \
|
||||
&& git checkout 0.2.0-rc
|
||||
```
|
||||
- Bump up the version number. It must be a valid [semver](https://semver.org/) and must be greater than the current one:
|
||||
```bash
|
||||
yarn update:version 0.2.0
|
||||
```
|
||||
- This should generate multiple outgoing changes with the version update.
|
||||
- Commit your changes and push to the remote:
|
||||
```bash
|
||||
git add . \
|
||||
&& git commit -s -m "Updated versions to 0.2.0" \
|
||||
&& git push
|
||||
```
|
||||
- Create the GH PR the workflow starts automatically.
|
||||
- Once you're happy with the RC, merge the changes to the `main`.
|
||||
- Create a tag and push it:
|
||||
```bash
|
||||
git tag -a 0.2.0 -m "0.2.0" \
|
||||
&& git push origin 0.2.0
|
||||
```
|
||||
- The release build starts automatically and uploads the artifacts with the changelog to the [release page](https://github.com/arduino/arduino-ide/releases).
|
||||
- If you do not want to release the `EXE` and `MSI` installers, wipe them manually.
|
||||
- If you do not like the generated changelog, modify it and update the GH release.
|
||||
|
||||
## FAQ
|
||||
|
||||
* *Can I manually change the version of the [`arduino-cli`](https://github.com/arduino/arduino-cli/) used by the IDE?*
|
||||
|
||||
Yes. It is possible but not recommended. The CLI exposes a set of functionality via [gRPC](https://github.com/arduino/arduino-cli/tree/master/rpc) and the IDE uses this API to communicate with the CLI. Before we build a new version of IDE, we pin a specific version of CLI and use the corresponding `proto` files to generate TypeScript modules for gRPC. This means, a particular version of IDE is compliant only with the pinned version of CLI. Mismatching IDE and CLI versions might not be able to communicate with each other. This could cause unpredictable IDE behavior.
|
||||
|
||||
* *I have understood that not all versions of the CLI are compatible with my version of IDE but how can I manually update the `arduino-cli` inside the IDE?*
|
||||
|
||||
[Get](https://arduino.github.io/arduino-cli/installation) the desired version of `arduino-cli` for your platform and manually replace the one inside the IDE. The CLI can be found inside the IDE at:
|
||||
- Windows: `C:\path\to\Arduino IDE\resources\app\node_modules\arduino-ide-extension\build\arduino-cli.exe`,
|
||||
- macOS: `/path/to/Arduino IDE.app/Contents/Resources/app/node_modules/arduino-ide-extension/build/arduino-cli`, and
|
||||
- Linux: `/path/to/Arduino IDE/resources/app/node_modules/arduino-ide-extension/build/arduino-cli`.
|
||||
# Development Guide
|
||||
|
||||
This documentation has been moved [**here**](docs/development.md#development-guide).
|
||||
|
51
README.md
51
README.md
@@ -1,45 +1,18 @@
|
||||
<img src="https://content.arduino.cc/website/Arduino_logo_teal.svg" height="100" align="right" />
|
||||
|
||||
# Arduino IDE 2.x (beta)
|
||||
# Arduino IDE 2.x
|
||||
|
||||
[](https://github.com/arduino/arduino-ide/actions?query=workflow%3A%22Arduino+IDE%22)
|
||||
|
||||
This repository contains the source code of the Arduino IDE 2.x, which is currently in beta stage. If you're looking for the stable IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
|
||||
This repository contains the source code of the Arduino IDE 2.x. If you're looking for the old IDE, go to the repository of the 1.x version at https://github.com/arduino/Arduino.
|
||||
|
||||
The Arduino IDE 2.x is a major rewrite, sharing no code with the IDE 1.x. It is based on the [Theia IDE](https://theia-ide.org/) framework and built with [Electron](https://www.electronjs.org/). The backend operations such as compilation and uploading are offloaded to an [arduino-cli](https://github.com/arduino/arduino-cli) instance running in daemon mode. This new IDE was developed with the goal of preserving the same interface and user experience of the previous major version in order to provide a frictionless upgrade.
|
||||
|
||||
> ⚠️ This is **beta** software. Help us test it!
|
||||
|
||||

|
||||
|
||||
## Download
|
||||
|
||||
You can download the latest version from the [software download page on the Arduino website](https://www.arduino.cc/en/software#experimental-software).
|
||||
|
||||
### Nightly builds
|
||||
|
||||
These builds are generated every day at 03:00 GMT from the `main` branch and
|
||||
should be considered unstable:
|
||||
|
||||
| Platform | 32 bit | 64 bit |
|
||||
| --------- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
| Linux | | [Nightly Linux AppImage 64 bit]<br />[Nightly Linux ZIP file 64 bit] |
|
||||
| Linux ARM | [🚧 Work in progress...] | [🚧 Work in progress...] |
|
||||
| Windows | | [Nightly Windows 64 bit installer]<br />[Nightly Windows 64 bit MSI]<br />[Nightly Windows 64 bit ZIP] |
|
||||
| macOS | | [Nightly macOS 64 bit] |
|
||||
|
||||
[🚧 work in progress...]: https://github.com/arduino/arduino-ide/issues/107
|
||||
[nightly linux appimage 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.AppImage
|
||||
[nightly linux zip file 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Linux_64bit.zip
|
||||
[nightly windows 64 bit installer]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.exe
|
||||
[nightly windows 64 bit msi]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.msi
|
||||
[nightly windows 64 bit zip]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_Windows_64bit.zip
|
||||
[nightly macos 64 bit]: https://downloads.arduino.cc/arduino-ide/nightly/arduino-ide_nightly-latest_macOS_64bit.dmg
|
||||
|
||||
> These links return an HTTP `302: Found` response, redirecting to latest
|
||||
> generated builds by replacing `latest` with the latest available build
|
||||
> date, using the format YYYYMMDD (i.e for 2019/Aug/06 `latest` is
|
||||
> replaced with `20190806`)
|
||||
You can download the latest release version and nightly builds from the [software download page on the Arduino website](https://www.arduino.cc/en/software).
|
||||
|
||||
## Support
|
||||
|
||||
@@ -47,10 +20,9 @@ If you need assistance, see the [Help Center](https://support.arduino.cc/hc/en-u
|
||||
|
||||
## Bugs & Issues
|
||||
|
||||
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository. A few rules apply:
|
||||
If you want to report an issue, you can submit it to the [issue tracker](https://github.com/arduino/arduino-ide/issues) of this repository.
|
||||
|
||||
- Before posting, please check if the same problem has been already reported by someone else to avoid duplicates.
|
||||
- Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board.
|
||||
See [**the issue report guide**](docs/contributor-guide/issues.md#issue-report-guide) for instructions.
|
||||
|
||||
### Security
|
||||
|
||||
@@ -62,16 +34,15 @@ e-mail contact: security@arduino.cc
|
||||
|
||||
## Contributions and development
|
||||
|
||||
Contributions are very welcome! You can browse the list of open issues to see what's needed and then you can submit your code using a Pull Request. Please provide detailed descriptions. We also appreciate any help in testing issues and patches contributed by other users.
|
||||
Contributions are very welcome! There are several ways to participate in this project, including:
|
||||
|
||||
This repository contains the main code, but two more repositories are included during the build process:
|
||||
- Fixing bugs
|
||||
- Beta testing
|
||||
- Translation
|
||||
|
||||
- [vscode-arduino-tools](https://github.com/arduino/vscode-arduino-tools): provides support for the language server and the debugger
|
||||
- [arduino-language-server](https://github.com/arduino/arduino-language-server): provides the language server that parses Arduino code
|
||||
See [**the contributor guide**](docs/CONTRIBUTING.md#contributor-guide) for more information.
|
||||
|
||||
See the [BUILDING.md](BUILDING.md) for a technical overview of the application and instructions for building the code.
|
||||
|
||||
You can help with the translation of the Arduino IDE to your language here: [Arduino IDE on Transifex](https://www.transifex.com/arduino-1/ide2/dashboard/).
|
||||
See the [**development guide**](docs/development.md) for a technical overview of the application and instructions for building the code.
|
||||
|
||||
## Donations
|
||||
|
||||
|
@@ -62,6 +62,15 @@ The Config Service knows about your system, like for example the default sketch
|
||||
- Some CLI updates can bring changes to the gRPC interfaces, as the API might change. gRPC interfaces can be updated running the command
|
||||
`yarn --cwd arduino-ide-extension generate-protocol`
|
||||
|
||||
### Update **clangd** and **ClangFormat**
|
||||
|
||||
The [**clangd** C++ language server](https://clangd.llvm.org/) and the [**ClangFormat** code formatter](https://clang.llvm.org/docs/ClangFormat.html) tool dependencies are managed in parallel. Updating them to a different version is done by the following procedure:
|
||||
|
||||
1. If the target version is not already [available from the `arduino/clang-static-binaries` repository](https://github.com/arduino/clang-static-binaries/releases), submit [an issue there](https://github.com/arduino/clang-static-binaries/issues) requesting a build and wait for that to be completed.
|
||||
1. Validate the **ClangFormat** configuration for the target version by following the instructions [**here**](https://github.com/arduino/tooling-project-assets/tree/main/other/clang-format-configuration#clangformat-version-updates)
|
||||
1. Submit a pull request in the `arduino/arduino-ide` repository to update the version in the `arduino.clangd.version` key of [`package.json`](package.json).
|
||||
1. Submit a pull request in [the `arduino/tooling-project-assets` repository](https://github.com/arduino/tooling-project-assets) to update the version in the `vars.DEFAULT_CLANG_FORMAT_VERSION` field of [`Taskfile.yml`](https://github.com/arduino/tooling-project-assets/blob/main/Taskfile.yml).
|
||||
|
||||
### Customize Icons
|
||||
ArduinoIde uses a customized version of FontAwesome.
|
||||
In order to update/replace icons follow the following steps:
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "2.0.0-rc9.1",
|
||||
"version": "2.1.0",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -17,73 +17,83 @@
|
||||
"build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint",
|
||||
"watch": "tsc -w",
|
||||
"test": "mocha \"./lib/test/**/*.test.js\"",
|
||||
"test:slow": "mocha \"./lib/test/**/*.slow-test.js\" --slow 5000",
|
||||
"test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.6.7",
|
||||
"@theia/application-package": "1.25.0",
|
||||
"@theia/core": "1.25.0",
|
||||
"@theia/editor": "1.25.0",
|
||||
"@theia/electron": "1.25.0",
|
||||
"@theia/filesystem": "1.25.0",
|
||||
"@theia/keymaps": "1.25.0",
|
||||
"@theia/markers": "1.25.0",
|
||||
"@theia/monaco": "1.25.0",
|
||||
"@theia/navigator": "1.25.0",
|
||||
"@theia/outline-view": "1.25.0",
|
||||
"@theia/output": "1.25.0",
|
||||
"@theia/preferences": "1.25.0",
|
||||
"@theia/search-in-workspace": "1.25.0",
|
||||
"@theia/terminal": "1.25.0",
|
||||
"@theia/workspace": "1.25.0",
|
||||
"@theia/application-package": "1.31.1",
|
||||
"@theia/core": "1.31.1",
|
||||
"@theia/debug": "1.31.1",
|
||||
"@theia/editor": "1.31.1",
|
||||
"@theia/electron": "1.31.1",
|
||||
"@theia/filesystem": "1.31.1",
|
||||
"@theia/keymaps": "1.31.1",
|
||||
"@theia/markers": "1.31.1",
|
||||
"@theia/messages": "1.31.1",
|
||||
"@theia/monaco": "1.31.1",
|
||||
"@theia/monaco-editor-core": "1.67.2",
|
||||
"@theia/navigator": "1.31.1",
|
||||
"@theia/outline-view": "1.31.1",
|
||||
"@theia/output": "1.31.1",
|
||||
"@theia/plugin-ext": "1.31.1",
|
||||
"@theia/preferences": "1.31.1",
|
||||
"@theia/scm": "1.31.1",
|
||||
"@theia/search-in-workspace": "1.31.1",
|
||||
"@theia/terminal": "1.31.1",
|
||||
"@theia/typehierarchy": "1.31.1",
|
||||
"@theia/workspace": "1.31.1",
|
||||
"@tippyjs/react": "^4.2.5",
|
||||
"@types/atob": "^2.1.2",
|
||||
"@types/auth0-js": "^9.14.0",
|
||||
"@types/btoa": "^1.2.3",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/deepmerge": "^2.2.0",
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/js-yaml": "^3.12.2",
|
||||
"@types/keytar": "^4.4.0",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/ncp": "^2.0.4",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/p-queue": "^2.3.1",
|
||||
"@types/ps-tree": "^1.1.0",
|
||||
"@types/react-select": "^3.0.0",
|
||||
"@types/react-tabs": "^2.3.2",
|
||||
"@types/temp": "^0.8.34",
|
||||
"@types/which": "^1.3.1",
|
||||
"ajv": "^6.5.3",
|
||||
"arduino-serial-plotter-webapp": "0.1.0",
|
||||
"@vscode/debugprotocol": "^1.51.0",
|
||||
"arduino-serial-plotter-webapp": "0.2.0",
|
||||
"async-mutex": "^0.3.0",
|
||||
"atob": "^2.1.2",
|
||||
"auth0-js": "^9.14.0",
|
||||
"btoa": "^1.2.1",
|
||||
"classnames": "^2.3.1",
|
||||
"cpy": "^8.1.2",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"dateformat": "^3.0.3",
|
||||
"deep-equal": "^2.0.5",
|
||||
"deepmerge": "2.0.1",
|
||||
"electron-updater": "^4.6.5",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"filename-reserved-regex": "^2.0.0",
|
||||
"glob": "^7.1.6",
|
||||
"google-protobuf": "^3.20.1",
|
||||
"hash.js": "^1.1.7",
|
||||
"is-valid-path": "^0.1.1",
|
||||
"is-online": "^9.0.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"jsonc-parser": "^2.2.0",
|
||||
"just-diff": "^5.1.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"keytar": "7.2.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"ncp": "^2.0.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"open": "^8.0.6",
|
||||
"p-queue": "^5.0.0",
|
||||
"p-debounce": "^2.1.0",
|
||||
"p-queue": "^2.4.2",
|
||||
"ps-tree": "^1.2.0",
|
||||
"query-string": "^7.0.1",
|
||||
"react-disable": "^0.1.0",
|
||||
"react-disable": "^0.1.1",
|
||||
"react-markdown": "^8.0.0",
|
||||
"react-select": "^3.0.4",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
"react-select": "^5.6.0",
|
||||
"react-tabs": "^3.1.2",
|
||||
"react-window": "^1.8.6",
|
||||
"semver": "^7.3.2",
|
||||
@@ -91,8 +101,6 @@
|
||||
"temp": "^0.9.1",
|
||||
"temp-dir": "^2.0.0",
|
||||
"tree-kill": "^1.2.1",
|
||||
"upath": "^1.1.2",
|
||||
"url": "^0.11.0",
|
||||
"which": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -101,11 +109,10 @@
|
||||
"@types/chai-string": "^1.4.2",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/sinon": "^10.0.6",
|
||||
"@types/sinon-chai": "^3.2.6",
|
||||
"chai": "^4.2.0",
|
||||
"chai-string": "^1.5.0",
|
||||
"decompress": "^4.2.0",
|
||||
"decompress-tarbz2": "^4.1.1",
|
||||
"decompress-targz": "^4.1.1",
|
||||
"decompress-unzip": "^4.0.1",
|
||||
"download": "^7.1.0",
|
||||
@@ -113,11 +120,9 @@
|
||||
"mocha": "^7.0.0",
|
||||
"mockdate": "^3.0.5",
|
||||
"moment": "^2.24.0",
|
||||
"ncp": "^2.0.0",
|
||||
"protoc": "^1.0.4",
|
||||
"shelljs": "^0.8.3",
|
||||
"sinon": "^12.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"typemoq": "^2.1.0",
|
||||
"uuid": "^3.2.1",
|
||||
"yargs": "^11.1.0"
|
||||
},
|
||||
@@ -147,25 +152,27 @@
|
||||
"frontend": "lib/browser/arduino-ide-frontend-module"
|
||||
},
|
||||
{
|
||||
"frontend": "lib/browser/theia/core/browser-menu-module",
|
||||
"frontendElectron": "lib/electron-browser/theia/core/electron-menu-module"
|
||||
},
|
||||
{
|
||||
"frontendElectron": "lib/electron-browser/theia/core/electron-window-module"
|
||||
},
|
||||
{
|
||||
"electronMain": "lib/electron-main/arduino-electron-main-module"
|
||||
}
|
||||
],
|
||||
"arduino": {
|
||||
"cli": {
|
||||
"version": "0.25.1"
|
||||
"version": "0.32.2"
|
||||
},
|
||||
"fwuploader": {
|
||||
"version": "2.2.0"
|
||||
"version": "2.2.2"
|
||||
},
|
||||
"clangd": {
|
||||
"version": "14.0.0"
|
||||
},
|
||||
"languageServer": {
|
||||
"version": "0.7.1"
|
||||
"version": "0.7.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@
|
||||
const semver = require('semver');
|
||||
const moment = require('moment');
|
||||
const downloader = require('./downloader');
|
||||
const { goBuildFromGit } = require('./utils');
|
||||
const { taskBuildFromGit } = require('./utils');
|
||||
|
||||
const version = (() => {
|
||||
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
||||
@@ -42,6 +42,9 @@
|
||||
const suffix = (() => {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
if (arch === 'arm64') {
|
||||
return 'macOS_ARM64.tar.gz';
|
||||
}
|
||||
return 'macOS_64bit.tar.gz';
|
||||
case 'win32':
|
||||
return 'Windows_64bit.zip';
|
||||
@@ -82,6 +85,6 @@
|
||||
shell.exit(1);
|
||||
}
|
||||
} else {
|
||||
goBuildFromGit(version, destinationPath, 'CLI');
|
||||
taskBuildFromGit(version, destinationPath, 'CLI');
|
||||
}
|
||||
})();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
// The version to use.
|
||||
const version = '1.9.1';
|
||||
const version = '1.10.0';
|
||||
|
||||
(async () => {
|
||||
const os = require('os');
|
||||
|
@@ -76,6 +76,12 @@
|
||||
lsSuffix = 'macOS_64bit.tar.gz';
|
||||
clangdSuffix = 'macOS_64bit';
|
||||
break;
|
||||
case 'darwin-arm64':
|
||||
clangdExecutablePath = path.join(build, 'clangd');
|
||||
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||
lsSuffix = 'macOS_ARM64.tar.gz';
|
||||
clangdSuffix = 'macOS_ARM64';
|
||||
break;
|
||||
case 'linux-x64':
|
||||
clangdExecutablePath = path.join(build, 'clangd');
|
||||
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||
|
@@ -1,3 +1,14 @@
|
||||
/**
|
||||
* Clones something from GitHub and builds it with [`Task`](https://taskfile.dev/).
|
||||
*
|
||||
* @param version {object} the version object.
|
||||
* @param destinationPath {string} the absolute path of the output binary. For example, `C:\\folder\\arduino-cli.exe` or `/path/to/arduino-language-server`
|
||||
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
|
||||
*/
|
||||
exports.taskBuildFromGit = (version, destinationPath, taskName) => {
|
||||
return buildFromGit('task', version, destinationPath, taskName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clones something from GitHub and builds it with `Golang`.
|
||||
*
|
||||
@@ -6,6 +17,13 @@
|
||||
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
|
||||
*/
|
||||
exports.goBuildFromGit = (version, destinationPath, taskName) => {
|
||||
return buildFromGit('go', version, destinationPath, taskName);
|
||||
};
|
||||
|
||||
/**
|
||||
* The `command` is either `go` or `task`.
|
||||
*/
|
||||
function buildFromGit(command, version, destinationPath, taskName) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const temp = require('temp');
|
||||
@@ -62,7 +80,7 @@ exports.goBuildFromGit = (version, destinationPath, taskName) => {
|
||||
}
|
||||
|
||||
shell.echo(`>>> Building the ${taskName}...`);
|
||||
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
|
||||
if (shell.exec(`${command} build`, { cwd: tempRepoPath }).code !== 0) {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`<<< Done ${taskName} build.`);
|
||||
@@ -89,4 +107,4 @@ exports.goBuildFromGit = (version, destinationPath, taskName) => {
|
||||
shell.exit(1);
|
||||
}
|
||||
shell.echo(`>>> Verified ${taskName}.`);
|
||||
};
|
||||
}
|
||||
|
@@ -5,18 +5,12 @@ import {
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { SketchesService } from '../common/protocol';
|
||||
import {
|
||||
MAIN_MENU_BAR,
|
||||
MenuContribution,
|
||||
MenuModelRegistry,
|
||||
} from '@theia/core';
|
||||
import {
|
||||
Dialog,
|
||||
FrontendApplication,
|
||||
FrontendApplicationContribution,
|
||||
OnWillStopAction,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
||||
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
@@ -34,14 +28,9 @@ import { EditorCommands, EditorMainMenu } from '@theia/editor/lib/browser';
|
||||
import { MonacoMenus } from '@theia/monaco/lib/browser/monaco-menu';
|
||||
import { FileNavigatorCommands } from '@theia/navigator/lib/browser/navigator-contribution';
|
||||
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../common/protocol/sketches-service-client-impl';
|
||||
import { ArduinoPreferences } from './arduino-preferences';
|
||||
import { ElectronWindowPreferences } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
|
||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||
import { BoardsToolBarItem } from './boards/boards-toolbar-item';
|
||||
import { SaveAsSketch } from './contributions/save-as-sketch';
|
||||
import { ArduinoMenus } from './menu/arduino-menus';
|
||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||
import { ArduinoToolbar } from './toolbar/arduino-toolbar';
|
||||
@@ -63,17 +52,11 @@ export class ArduinoFrontendContribution
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(SketchesService)
|
||||
private readonly sketchService: SketchesService;
|
||||
|
||||
@inject(CommandRegistry)
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
private readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
@inject(ElectronWindowPreferences)
|
||||
private readonly electronWindowPreferences: ElectronWindowPreferences;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
@@ -91,11 +74,11 @@ export class ArduinoFrontendContribution
|
||||
}
|
||||
}
|
||||
|
||||
async onStart(app: FrontendApplication): Promise<void> {
|
||||
this.arduinoPreferences.onPreferenceChanged((event) => {
|
||||
onStart(): void {
|
||||
this.electronWindowPreferences.onPreferenceChanged((event) => {
|
||||
if (event.newValue !== event.oldValue) {
|
||||
switch (event.preferenceName) {
|
||||
case 'arduino.window.zoomLevel':
|
||||
case 'window.zoomLevel':
|
||||
if (typeof event.newValue === 'number') {
|
||||
const webContents = remote.getCurrentWebContents();
|
||||
webContents.setZoomLevel(event.newValue || 0);
|
||||
@@ -105,16 +88,13 @@ export class ArduinoFrontendContribution
|
||||
}
|
||||
});
|
||||
this.appStateService.reachedState('ready').then(() =>
|
||||
this.arduinoPreferences.ready.then(() => {
|
||||
this.electronWindowPreferences.ready.then(() => {
|
||||
const webContents = remote.getCurrentWebContents();
|
||||
const zoomLevel = this.arduinoPreferences.get(
|
||||
'arduino.window.zoomLevel'
|
||||
);
|
||||
const zoomLevel =
|
||||
this.electronWindowPreferences.get('window.zoomLevel');
|
||||
webContents.setZoomLevel(zoomLevel);
|
||||
})
|
||||
);
|
||||
// Removes the _Settings_ (cog) icon from the left sidebar
|
||||
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
@@ -303,58 +283,4 @@ export class ArduinoFrontendContribution
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: should be handled by `Close` contribution. https://github.com/arduino/arduino-ide/issues/1016
|
||||
onWillStop(): OnWillStopAction {
|
||||
return {
|
||||
reason: 'temp-sketch',
|
||||
action: () => {
|
||||
return this.showTempSketchDialog();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async showTempSketchDialog(): Promise<boolean> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return true;
|
||||
}
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
if (!isTemp) {
|
||||
return true;
|
||||
}
|
||||
const messageBoxResult = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
message: nls.localize(
|
||||
'arduino/sketch/saveTempSketch',
|
||||
'Save your sketch to open it again later.'
|
||||
),
|
||||
title: nls.localize(
|
||||
'theia/core/quitTitle',
|
||||
'Are you sure you want to quit?'
|
||||
),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
Dialog.CANCEL,
|
||||
nls.localizeByDefault('Save As...'),
|
||||
nls.localizeByDefault("Don't Save"),
|
||||
],
|
||||
}
|
||||
);
|
||||
const result = messageBoxResult.response;
|
||||
if (result === 2) {
|
||||
return true;
|
||||
} else if (result === 1) {
|
||||
return !!(await this.commandRegistry.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
{
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: false,
|
||||
wipeOriginal: true,
|
||||
}
|
||||
));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,9 @@
|
||||
import '../../src/browser/style/index.css';
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { Container, ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
|
||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import {
|
||||
TabBarToolbarContribution,
|
||||
TabBarToolbarFactory,
|
||||
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
|
||||
import {
|
||||
FrontendApplicationContribution,
|
||||
@@ -26,7 +23,7 @@ import {
|
||||
SketchesService,
|
||||
SketchesServicePath,
|
||||
} from '../common/protocol/sketches-service';
|
||||
import { SketchesServiceClientImpl } from '../common/protocol/sketches-service-client-impl';
|
||||
import { SketchesServiceClientImpl } from './sketches-service-client-impl';
|
||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
@@ -53,8 +50,6 @@ import {
|
||||
DockPanelRenderer as TheiaDockPanelRenderer,
|
||||
TabBarRendererFactory,
|
||||
ContextMenuRenderer,
|
||||
createTreeContainer,
|
||||
TreeWidget,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||
import {
|
||||
@@ -84,12 +79,12 @@ import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browse
|
||||
import { ProblemManager } from './theia/markers/problem-manager';
|
||||
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
||||
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
|
||||
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import {
|
||||
MonacoThemeJson,
|
||||
MonacoThemingService,
|
||||
} from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
ArduinoComponentContextMenuRenderer,
|
||||
ListItemRenderer,
|
||||
} from './widgets/component-list/list-item-renderer';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
|
||||
import {
|
||||
ArduinoDaemonPath,
|
||||
ArduinoDaemon,
|
||||
@@ -98,6 +93,8 @@ import { EditorCommandContribution as TheiaEditorCommandContribution } from '@th
|
||||
import {
|
||||
FrontendConnectionStatusService,
|
||||
ApplicationConnectionStatusContribution,
|
||||
DaemonPort,
|
||||
IsOnline,
|
||||
} from './theia/core/connection-status-service';
|
||||
import {
|
||||
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
||||
@@ -105,7 +102,8 @@ import {
|
||||
} from '@theia/core/lib/browser/connection-status-service';
|
||||
import { BoardsDataMenuUpdater } from './boards/boards-data-menu-updater';
|
||||
import { BoardsDataStore } from './boards/boards-data-store';
|
||||
import { ILogger } from '@theia/core';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import {
|
||||
FileSystemExt,
|
||||
FileSystemExtPath,
|
||||
@@ -134,15 +132,12 @@ import { PreferencesContribution as TheiaPreferencesContribution } from '@theia/
|
||||
import { PreferencesContribution } from './theia/preferences/preferences-contribution';
|
||||
import { QuitApp } from './contributions/quit-app';
|
||||
import { SketchControl } from './contributions/sketch-control';
|
||||
import { Settings } from './contributions/settings';
|
||||
import { OpenSettings } from './contributions/open-settings';
|
||||
import { WorkspaceCommandContribution } from './theia/workspace/workspace-commands';
|
||||
import { WorkspaceDeleteHandler as TheiaWorkspaceDeleteHandler } from '@theia/workspace/lib/browser/workspace-delete-handler';
|
||||
import { WorkspaceDeleteHandler } from './theia/workspace/workspace-delete-handler';
|
||||
import { TabBarToolbar } from './theia/core/tab-bar-toolbar';
|
||||
import { EditorWidgetFactory as TheiaEditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory';
|
||||
import { EditorWidgetFactory } from './theia/editor/editor-widget-factory';
|
||||
import { OutputWidget as TheiaOutputWidget } from '@theia/output/lib/browser/output-widget';
|
||||
import { OutputWidget } from './theia/output/output-widget';
|
||||
import { BurnBootloader } from './contributions/burn-bootloader';
|
||||
import {
|
||||
ExamplesServicePath,
|
||||
@@ -184,8 +179,6 @@ import { EditorCommandContribution } from './theia/editor/editor-command';
|
||||
import { NavigatorTabBarDecorator as TheiaNavigatorTabBarDecorator } from '@theia/navigator/lib/browser/navigator-tab-bar-decorator';
|
||||
import { NavigatorTabBarDecorator } from './theia/navigator/navigator-tab-bar-decorator';
|
||||
import { Debug } from './contributions/debug';
|
||||
import { DebugSessionManager } from './theia/debug/debug-session-manager';
|
||||
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
|
||||
import { Sketchbook } from './contributions/sketchbook';
|
||||
import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution';
|
||||
import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
|
||||
@@ -208,14 +201,13 @@ import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } f
|
||||
import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution';
|
||||
import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager';
|
||||
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
|
||||
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
|
||||
import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget';
|
||||
import { SearchInWorkspaceFactory as TheiaSearchInWorkspaceFactory } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';
|
||||
import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory';
|
||||
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
|
||||
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
|
||||
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
|
||||
import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import {
|
||||
MonacoEditorFactory,
|
||||
MonacoEditorProvider as TheiaMonacoEditorProvider,
|
||||
} from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { StorageWrapper } from './storage-wrapper';
|
||||
import { NotificationManager } from './theia/messages/notifications-manager';
|
||||
import { NotificationManager as TheiaNotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||
@@ -245,9 +237,7 @@ import { UploadFirmware } from './contributions/upload-firmware';
|
||||
import {
|
||||
UploadFirmwareDialog,
|
||||
UploadFirmwareDialogProps,
|
||||
UploadFirmwareDialogWidget,
|
||||
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
|
||||
|
||||
import { UploadCertificate } from './contributions/upload-certificate';
|
||||
import {
|
||||
ArduinoFirmwareUploader,
|
||||
@@ -262,7 +252,6 @@ import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-c
|
||||
import {
|
||||
UserFieldsDialog,
|
||||
UserFieldsDialogProps,
|
||||
UserFieldsDialogWidget,
|
||||
} from './dialogs/user-fields/user-fields-dialog';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { IDEUpdaterCommands } from './ide-updater/ide-updater-commands';
|
||||
@@ -275,7 +264,6 @@ import { IDEUpdaterClientImpl } from './ide-updater/ide-updater-client-impl';
|
||||
import {
|
||||
IDEUpdaterDialog,
|
||||
IDEUpdaterDialogProps,
|
||||
IDEUpdaterDialogWidget,
|
||||
} from './dialogs/ide-updater/ide-updater-dialog';
|
||||
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
|
||||
import { MonitorModel } from './monitor-model';
|
||||
@@ -307,46 +295,72 @@ import { CoreErrorHandler } from './contributions/core-error-handler';
|
||||
import { CompilerErrors } from './contributions/compiler-errors';
|
||||
import { WidgetManager } from './theia/core/widget-manager';
|
||||
import { WidgetManager as TheiaWidgetManager } from '@theia/core/lib/browser/widget-manager';
|
||||
import { StartupTasks } from './widgets/sketchbook/startup-task';
|
||||
import { StartupTasks } from './contributions/startup-task';
|
||||
import { IndexesUpdateProgress } from './contributions/indexes-update-progress';
|
||||
import { Daemon } from './contributions/daemon';
|
||||
import { FirstStartupInstaller } from './contributions/first-startup-installer';
|
||||
import { OpenSketchFiles } from './contributions/open-sketch-files';
|
||||
import { InoLanguage } from './contributions/ino-language';
|
||||
import { SelectedBoard } from './contributions/selected-board';
|
||||
import { CheckForUpdates } from './contributions/check-for-updates';
|
||||
import { CheckForIDEUpdates } from './contributions/check-for-ide-updates';
|
||||
import { OpenBoardsConfig } from './contributions/open-boards-config';
|
||||
import { SketchFilesTracker } from './contributions/sketch-files-tracker';
|
||||
import { MonacoThemeServiceIsReady } from './utils/window';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { StatusBarImpl } from './theia/core/status-bar';
|
||||
import { StatusBarImpl as TheiaStatusBarImpl } from '@theia/core/lib/browser';
|
||||
|
||||
const registerArduinoThemes = () => {
|
||||
const themes: MonacoThemeJson[] = [
|
||||
{
|
||||
id: 'arduino-theme',
|
||||
label: 'Light (Arduino)',
|
||||
uiTheme: 'vs',
|
||||
json: require('../../src/browser/data/default.color-theme.json'),
|
||||
},
|
||||
{
|
||||
id: 'arduino-theme-dark',
|
||||
label: 'Dark (Arduino)',
|
||||
uiTheme: 'vs-dark',
|
||||
json: require('../../src/browser/data/dark.color-theme.json'),
|
||||
},
|
||||
];
|
||||
themes.forEach((theme) => MonacoThemingService.register(theme));
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const global = window as any;
|
||||
const ready = global[MonacoThemeServiceIsReady] as Deferred;
|
||||
if (ready) {
|
||||
ready.promise.then(registerArduinoThemes);
|
||||
} else {
|
||||
registerArduinoThemes();
|
||||
}
|
||||
import { EditorMenuContribution } from './theia/editor/editor-file';
|
||||
import { EditorMenuContribution as TheiaEditorMenuContribution } from '@theia/editor/lib/browser/editor-menu';
|
||||
import { PreferencesEditorWidget as TheiaPreferencesEditorWidget } from '@theia/preferences/lib/browser/views/preference-editor-widget';
|
||||
import { PreferencesEditorWidget } from './theia/preferences/preference-editor-widget';
|
||||
import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
|
||||
import { createPreferencesWidgetContainer } from '@theia/preferences/lib/browser/views/preference-widget-bindings';
|
||||
import {
|
||||
BoardsFilterRenderer,
|
||||
LibraryFilterRenderer,
|
||||
} from './widgets/component-list/filter-renderer';
|
||||
import { CheckForUpdates } from './contributions/check-for-updates';
|
||||
import { OutputEditorFactory } from './theia/output/output-editor-factory';
|
||||
import { StartupTaskProvider } from '../electron-common/startup-task';
|
||||
import { DeleteSketch } from './contributions/delete-sketch';
|
||||
import { UserFields } from './contributions/user-fields';
|
||||
import { UpdateIndexes } from './contributions/update-indexes';
|
||||
import { InterfaceScale } from './contributions/interface-scale';
|
||||
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
|
||||
import { NewCloudSketch } from './contributions/new-cloud-sketch';
|
||||
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
|
||||
import { WindowTitleUpdater } from './theia/core/window-title-updater';
|
||||
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
|
||||
import {
|
||||
MonacoThemingService,
|
||||
CleanupObsoleteThemes,
|
||||
ThemesRegistrationSummary,
|
||||
MonacoThemeRegistry,
|
||||
} from './theia/monaco/monaco-theming-service';
|
||||
import { MonacoThemeRegistry as TheiaMonacoThemeRegistry } from '@theia/monaco/lib/browser/textmate/monaco-theme-registry';
|
||||
import { MonacoThemingService as TheiaMonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
|
||||
import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarchy-service';
|
||||
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
|
||||
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution';
|
||||
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
|
||||
import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution';
|
||||
import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
|
||||
import { DebugToolbar } from './theia/debug/debug-toolbar-widget';
|
||||
import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
|
||||
import { PluginMenuCommandAdapter } from './theia/plugin-ext/plugin-menu-command-adapter';
|
||||
import { PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter } from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter';
|
||||
import { DebugSessionManager } from './theia/debug/debug-session-manager';
|
||||
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
|
||||
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
|
||||
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
|
||||
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
|
||||
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
|
||||
import { ConfigServiceClient } from './config/config-service-client';
|
||||
import { ValidateSketch } from './contributions/validate-sketch';
|
||||
import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
|
||||
import { CreateFeatures } from './create/create-features';
|
||||
import { Account } from './contributions/account';
|
||||
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
|
||||
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
|
||||
import { CreateCloudCopy } from './contributions/create-cloud-copy';
|
||||
import { FileResourceResolver } from './theia/filesystem/file-resource';
|
||||
import { FileResourceResolver as TheiaFileResourceResolver } from '@theia/filesystem/lib/browser/file-resource';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Commands and toolbar items
|
||||
@@ -362,6 +376,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
|
||||
// Renderer for both the library and the core widgets.
|
||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||
bind(LibraryFilterRenderer).toSelf().inSingletonScope();
|
||||
bind(BoardsFilterRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// Library service
|
||||
bind(LibraryService)
|
||||
@@ -383,6 +399,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
LibraryListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService)
|
||||
@@ -405,6 +422,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
bind(ConfigServiceClient).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
|
||||
|
||||
// Boards service
|
||||
bind(BoardsService)
|
||||
@@ -418,6 +437,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Boards service client to receive and delegate notifications from the backend.
|
||||
bind(BoardsServiceProvider).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsServiceProvider);
|
||||
bind(CommandContribution).toService(BoardsServiceProvider);
|
||||
|
||||
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
|
||||
bind(FrontendApplicationContribution)
|
||||
@@ -448,12 +468,16 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
BoardsListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialog).toSelf().inSingletonScope();
|
||||
bind(BoardsConfigDialogProps).toConstantValue({
|
||||
title: nls.localize('arduino/common/selectBoard', 'Select Board'),
|
||||
title: nls.localize(
|
||||
'arduino/board/boardConfigDialogTitle',
|
||||
'Select Other Board and Port'
|
||||
),
|
||||
});
|
||||
|
||||
// Core service
|
||||
@@ -475,15 +499,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
|
||||
bind(WidgetFactory).toDynamicValue((context) => ({
|
||||
id: MonitorWidget.ID,
|
||||
createWidget: () => {
|
||||
return new MonitorWidget(
|
||||
context.container.get<MonitorModel>(MonitorModel),
|
||||
context.container.get<MonitorManagerProxyClient>(
|
||||
MonitorManagerProxyClient
|
||||
),
|
||||
context.container.get<BoardsServiceProvider>(BoardsServiceProvider)
|
||||
);
|
||||
},
|
||||
createWidget: () => context.container.get(MonitorWidget),
|
||||
}));
|
||||
|
||||
bind(MonitorManagerProxyFactory).toFactory(
|
||||
@@ -563,16 +579,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.to(WorkspaceDeleteHandler)
|
||||
.inSingletonScope();
|
||||
rebind(TheiaEditorWidgetFactory).to(EditorWidgetFactory).inSingletonScope();
|
||||
rebind(TabBarToolbarFactory).toFactory(
|
||||
({ container: parentContainer }) =>
|
||||
() => {
|
||||
const container = parentContainer.createChild();
|
||||
container.bind(TabBarToolbar).toSelf().inSingletonScope();
|
||||
return container.get(TabBarToolbar);
|
||||
}
|
||||
);
|
||||
bind(OutputWidget).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputWidget).toService(OutputWidget);
|
||||
bind(OutputChannelManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
|
||||
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
|
||||
@@ -584,9 +590,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(MonacoEditorProvider).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoEditorProvider).toService(MonacoEditorProvider);
|
||||
|
||||
bind(SearchInWorkspaceWidget).toSelf();
|
||||
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
|
||||
|
||||
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
|
||||
bind(EditorManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorManager).toService(EditorManager);
|
||||
@@ -596,17 +599,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.to(SearchInWorkspaceFactory)
|
||||
.inSingletonScope();
|
||||
|
||||
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(
|
||||
({ container }) => {
|
||||
const childContainer = createTreeContainer(container);
|
||||
childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf();
|
||||
childContainer
|
||||
.rebind(TreeWidget)
|
||||
.toService(SearchInWorkspaceResultTreeWidget);
|
||||
return childContainer.get(SearchInWorkspaceResultTreeWidget);
|
||||
}
|
||||
);
|
||||
|
||||
// Show a disconnected status bar, when the daemon is not available
|
||||
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaApplicationConnectionStatusContribution).toService(
|
||||
@@ -637,6 +629,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(WindowContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaWindowContribution).toService(WindowContribution);
|
||||
|
||||
// To remove `File` > `Close Editor`.
|
||||
bind(EditorMenuContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorMenuContribution).toService(EditorMenuContribution);
|
||||
|
||||
// To disable the highlighting of non-unicode characters in the _Output_ view
|
||||
bind(OutputEditorFactory).toSelf().inSingletonScope();
|
||||
// Rebind to `TheiaOutputEditorFactory` when https://github.com/eclipse-theia/theia/pull/11615 is available.
|
||||
rebind(MonacoEditorFactory).toService(OutputEditorFactory);
|
||||
|
||||
bind(ArduinoDaemon)
|
||||
.toDynamicValue((context) =>
|
||||
WebSocketConnectionProvider.createProxy(
|
||||
@@ -702,7 +703,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, EditContributions);
|
||||
Contribution.configure(bind, QuitApp);
|
||||
Contribution.configure(bind, SketchControl);
|
||||
Contribution.configure(bind, Settings);
|
||||
Contribution.configure(bind, OpenSettings);
|
||||
Contribution.configure(bind, BurnBootloader);
|
||||
Contribution.configure(bind, BuiltInExamples);
|
||||
Contribution.configure(bind, LibraryExamples);
|
||||
@@ -728,9 +729,23 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, OpenSketchFiles);
|
||||
Contribution.configure(bind, InoLanguage);
|
||||
Contribution.configure(bind, SelectedBoard);
|
||||
Contribution.configure(bind, CheckForUpdates);
|
||||
Contribution.configure(bind, CheckForIDEUpdates);
|
||||
Contribution.configure(bind, OpenBoardsConfig);
|
||||
Contribution.configure(bind, SketchFilesTracker);
|
||||
Contribution.configure(bind, CheckForUpdates);
|
||||
Contribution.configure(bind, UserFields);
|
||||
Contribution.configure(bind, DeleteSketch);
|
||||
Contribution.configure(bind, UpdateIndexes);
|
||||
Contribution.configure(bind, InterfaceScale);
|
||||
Contribution.configure(bind, NewCloudSketch);
|
||||
Contribution.configure(bind, ValidateSketch);
|
||||
Contribution.configure(bind, RenameCloudSketch);
|
||||
Contribution.configure(bind, Account);
|
||||
Contribution.configure(bind, CloudSketchbookContribution);
|
||||
Contribution.configure(bind, CreateCloudCopy);
|
||||
|
||||
bindContributionProvider(bind, StartupTaskProvider);
|
||||
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
|
||||
|
||||
// Disabled the quick-pick customization from Theia when multiple formatters are available.
|
||||
// Use the default VS Code behavior, and pick the first one. In the IDE2, clang-format has `exclusive` selectors.
|
||||
@@ -812,9 +827,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(AboutDialog).toSelf().inSingletonScope();
|
||||
rebind(TheiaAboutDialog).toService(AboutDialog);
|
||||
|
||||
// To avoid running `Save All` when there are no dirty editors before starting the debug session.
|
||||
bind(DebugSessionManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
|
||||
// To remove the `Run` menu item from the application menu.
|
||||
bind(DebugFrontendApplicationContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaDebugFrontendApplicationContribution).toService(
|
||||
@@ -828,14 +840,22 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(WidgetManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaWidgetManager).toService(WidgetManager);
|
||||
|
||||
// To avoid running a status bar update on every single `keypress` event from the editor.
|
||||
bind(StatusBarImpl).toSelf().inSingletonScope();
|
||||
rebind(TheiaStatusBarImpl).toService(StatusBarImpl);
|
||||
|
||||
// Debounced update for the tab-bar toolbar when typing in the editor.
|
||||
bind(DockPanelRenderer).toSelf();
|
||||
rebind(TheiaDockPanelRenderer).toService(DockPanelRenderer);
|
||||
|
||||
// Avoid running the "reset scroll" interval tasks until the preference editor opens.
|
||||
rebind(PreferencesWidget)
|
||||
.toDynamicValue(({ container }) => {
|
||||
const child = createPreferencesWidgetContainer(container);
|
||||
child.bind(PreferencesEditorWidget).toSelf().inSingletonScope();
|
||||
child
|
||||
.rebind(TheiaPreferencesEditorWidget)
|
||||
.toService(PreferencesEditorWidget);
|
||||
return child.get(PreferencesWidget);
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
// Preferences
|
||||
bindArduinoPreferences(bind);
|
||||
|
||||
@@ -870,6 +890,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
id: 'arduino-sketchbook-widget',
|
||||
createWidget: () => container.get(SketchbookWidget),
|
||||
}));
|
||||
bind(SketchbookCompositeWidget).toSelf();
|
||||
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
|
||||
id: 'sketchbook-composite-widget',
|
||||
createWidget: () => ctx.container.get(SketchbookCompositeWidget),
|
||||
}));
|
||||
|
||||
bind(CloudSketchbookWidget).toSelf();
|
||||
rebind(SketchbookWidget).toService(CloudSketchbookWidget);
|
||||
@@ -878,6 +903,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
);
|
||||
bind(CreateApi).toSelf().inSingletonScope();
|
||||
bind(SketchCache).toSelf().inSingletonScope();
|
||||
bind(CreateFeatures).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(CreateFeatures);
|
||||
|
||||
bind(ShareSketchDialog).toSelf().inSingletonScope();
|
||||
bind(AuthenticationClientService).toSelf().inSingletonScope();
|
||||
@@ -894,17 +921,14 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(CreateFsProvider).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(CreateFsProvider);
|
||||
bind(FileServiceContribution).toService(CreateFsProvider);
|
||||
bind(CloudSketchbookContribution).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(CloudSketchbookContribution);
|
||||
bind(LocalCacheFsProvider).toSelf().inSingletonScope();
|
||||
bind(FileServiceContribution).toService(LocalCacheFsProvider);
|
||||
bind(CloudSketchbookCompositeWidget).toSelf();
|
||||
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
|
||||
bind(WidgetFactory).toDynamicValue((ctx) => ({
|
||||
id: 'cloud-sketchbook-composite-widget',
|
||||
createWidget: () => ctx.container.get(CloudSketchbookCompositeWidget),
|
||||
}));
|
||||
|
||||
bind(UploadFirmwareDialogWidget).toSelf().inSingletonScope();
|
||||
bind(UploadFirmwareDialog).toSelf().inSingletonScope();
|
||||
bind(UploadFirmwareDialogProps).toConstantValue({
|
||||
title: 'UploadFirmware',
|
||||
@@ -915,13 +939,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
title: 'UploadCertificate',
|
||||
});
|
||||
|
||||
bind(IDEUpdaterDialogWidget).toSelf().inSingletonScope();
|
||||
bind(IDEUpdaterDialog).toSelf().inSingletonScope();
|
||||
bind(IDEUpdaterDialogProps).toConstantValue({
|
||||
title: 'IDEUpdater',
|
||||
});
|
||||
|
||||
bind(UserFieldsDialogWidget).toSelf().inSingletonScope();
|
||||
bind(UserFieldsDialog).toSelf().inSingletonScope();
|
||||
bind(UserFieldsDialogProps).toConstantValue({
|
||||
title: 'UserFields',
|
||||
@@ -948,4 +970,77 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
rebind(TheiaHostedPluginSupport).toService(HostedPluginSupport);
|
||||
bind(HostedPluginEvents).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(HostedPluginEvents);
|
||||
|
||||
// custom window titles
|
||||
bind(WindowTitleUpdater).toSelf().inSingletonScope();
|
||||
rebind(TheiaWindowTitleUpdater).toService(WindowTitleUpdater);
|
||||
|
||||
// register Arduino themes
|
||||
bind(MonacoThemingService).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
|
||||
|
||||
// workaround for themes cannot be removed after registration
|
||||
// https://github.com/eclipse-theia/theia/issues/11151
|
||||
bind(CleanupObsoleteThemes).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
CleanupObsoleteThemes
|
||||
);
|
||||
bind(ThemesRegistrationSummary).toSelf().inSingletonScope();
|
||||
bind(MonacoThemeRegistry).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoThemeRegistry).toService(MonacoThemeRegistry);
|
||||
|
||||
// disable type-hierarchy support
|
||||
// https://github.com/eclipse-theia/theia/commit/16c88a584bac37f5cf3cc5eb92ffdaa541bda5be
|
||||
bind(TypeHierarchyServiceProvider).toSelf().inSingletonScope();
|
||||
rebind(TheiaTypeHierarchyServiceProvider).toService(
|
||||
TypeHierarchyServiceProvider
|
||||
);
|
||||
bind(TypeHierarchyContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution);
|
||||
|
||||
// patched the debugger for `cortex-debug@1.5.1`
|
||||
// https://github.com/eclipse-theia/theia/issues/11871
|
||||
// https://github.com/eclipse-theia/theia/issues/11879
|
||||
// https://github.com/eclipse-theia/theia/issues/11880
|
||||
// https://github.com/eclipse-theia/theia/issues/11885
|
||||
// https://github.com/eclipse-theia/theia/issues/11886
|
||||
// https://github.com/eclipse-theia/theia/issues/11916
|
||||
// based on: https://github.com/eclipse-theia/theia/compare/master...kittaakos:theia:%2311871
|
||||
bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
|
||||
rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory);
|
||||
bind(DebugSessionManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
|
||||
bind(DebugToolbar).toSelf().inSingletonScope();
|
||||
rebind(TheiaDebugToolbar).toService(DebugToolbar);
|
||||
bind(PluginMenuCommandAdapter).toSelf().inSingletonScope();
|
||||
rebind(TheiaPluginMenuCommandAdapter).toService(PluginMenuCommandAdapter);
|
||||
bind(WidgetFactory)
|
||||
.toDynamicValue(({ container }) => ({
|
||||
id: DebugWidget.ID,
|
||||
createWidget: () => {
|
||||
const child = new Container({ defaultScope: 'Singleton' });
|
||||
child.parent = container;
|
||||
child.bind(DebugViewModel).toSelf();
|
||||
child.bind(DebugToolbar).toSelf(); // patched toolbar
|
||||
child.bind(DebugSessionWidget).toSelf();
|
||||
child.bind(DebugConfigurationWidget).toSelf();
|
||||
child.bind(DebugWidget).toSelf();
|
||||
return child.get(DebugWidget);
|
||||
},
|
||||
}))
|
||||
.inSingletonScope();
|
||||
|
||||
bind(SidebarBottomMenuWidget).toSelf();
|
||||
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
|
||||
|
||||
bind(ArduinoComponentContextMenuRenderer).toSelf().inSingletonScope();
|
||||
|
||||
bind(DaemonPort).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(DaemonPort);
|
||||
bind(IsOnline).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(IsOnline);
|
||||
|
||||
// https://github.com/arduino/arduino-ide/issues/437
|
||||
bind(FileResourceResolver).toSelf().inSingletonScope();
|
||||
rebind(TheiaFileResourceResolver).toService(FileResourceResolver);
|
||||
});
|
||||
|
@@ -114,11 +114,12 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
},
|
||||
'arduino.window.zoomLevel': {
|
||||
type: 'number',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/window.zoomLevel',
|
||||
'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1) or below (e.g. -1) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.'
|
||||
),
|
||||
description: '',
|
||||
default: 0,
|
||||
deprecationMessage: nls.localize(
|
||||
'arduino/preferences/window.zoomLevel/deprecationMessage',
|
||||
"Deprecated. Use 'window.zoomLevel' instead."
|
||||
),
|
||||
},
|
||||
'arduino.ide.updateChannel': {
|
||||
type: 'string',
|
||||
@@ -241,6 +242,22 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
),
|
||||
default: false,
|
||||
},
|
||||
'arduino.checkForUpdates': {
|
||||
type: 'boolean',
|
||||
description: nls.localize(
|
||||
'arduino/preferences/checkForUpdate',
|
||||
"Receive notifications of available updates for the IDE, boards, and libraries. Requires an IDE restart after change. It's true by default."
|
||||
),
|
||||
default: true,
|
||||
},
|
||||
'arduino.sketch.inoBlueprint': {
|
||||
type: 'string',
|
||||
markdownDescription: nls.localize(
|
||||
'arduino/preferences/sketch/inoBlueprint',
|
||||
'Absolute filesystem path to the default `.ino` blueprint file. If specified, the content of the blueprint file will be used for every new sketch created by the IDE. The sketches will be generated with the default Arduino content if not specified. Unaccessible blueprint files are ignored. **A restart of the IDE is needed** for this setting to take effect.'
|
||||
),
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -254,7 +271,6 @@ export interface ArduinoConfiguration {
|
||||
'arduino.upload.verbose': boolean;
|
||||
'arduino.upload.verify': boolean;
|
||||
'arduino.window.autoScale': boolean;
|
||||
'arduino.window.zoomLevel': number;
|
||||
'arduino.ide.updateChannel': UpdateChannel;
|
||||
'arduino.ide.updateBaseUrl': string;
|
||||
'arduino.board.certificates': string;
|
||||
@@ -270,6 +286,8 @@ export interface ArduinoConfiguration {
|
||||
'arduino.auth.registerUri': string;
|
||||
'arduino.survey.notification': boolean;
|
||||
'arduino.cli.daemon.debug': boolean;
|
||||
'arduino.sketch.inoBlueprint': string;
|
||||
'arduino.checkForUpdates': boolean;
|
||||
}
|
||||
|
||||
export const ArduinoPreferences = Symbol('ArduinoPreferences');
|
||||
|
@@ -1,68 +0,0 @@
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { isWindows } from '@theia/core/lib/common/os';
|
||||
import { notEmpty } from '@theia/core/lib/common/objects';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
|
||||
/**
|
||||
* Class for determining the default workspace location from the
|
||||
* `location.hash`, the historical workspace locations, and recent sketch files.
|
||||
*
|
||||
* The following logic is used for determining the default workspace location:
|
||||
* - `hash` points to an existing location?
|
||||
* - Yes
|
||||
* - `validate location`. Is valid sketch location?
|
||||
* - Yes
|
||||
* - Done.
|
||||
* - No
|
||||
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
|
||||
* - No
|
||||
* - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`.
|
||||
*/
|
||||
namespace ArduinoWorkspaceRootResolver {
|
||||
export interface InitOptions {
|
||||
readonly isValid: (uri: string) => MaybePromise<boolean>;
|
||||
}
|
||||
export interface ResolveOptions {
|
||||
readonly hash?: string;
|
||||
readonly recentWorkspaces: string[];
|
||||
// Gathered from the default sketch folder. The default sketch folder is defined by the CLI.
|
||||
readonly recentSketches: string[];
|
||||
}
|
||||
}
|
||||
export class ArduinoWorkspaceRootResolver {
|
||||
constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {}
|
||||
|
||||
async resolve(
|
||||
options: ArduinoWorkspaceRootResolver.ResolveOptions
|
||||
): Promise<{ uri: string } | undefined> {
|
||||
const { hash, recentWorkspaces, recentSketches } = options;
|
||||
for (const uri of [
|
||||
this.hashToUri(hash),
|
||||
...recentWorkspaces,
|
||||
...recentSketches,
|
||||
].filter(notEmpty)) {
|
||||
const valid = await this.isValid(uri);
|
||||
if (valid) {
|
||||
return { uri };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected isValid(uri: string): MaybePromise<boolean> {
|
||||
return this.options.isValid(uri);
|
||||
}
|
||||
|
||||
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
|
||||
// This is important for Windows only and a NOOP on POSIX.
|
||||
// Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See:
|
||||
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and
|
||||
// - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423
|
||||
protected hashToUri(hash: string | undefined): string | undefined {
|
||||
if (hash && hash.length > 1 && hash.startsWith('#')) {
|
||||
const path = decodeURI(hash.slice(1)).replace(/\\/g, '/'); // Trim the leading `#`, decode the URI and replace Windows separators
|
||||
return URI.file(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)).toString();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
@@ -83,9 +83,13 @@ export class AuthenticationClientService
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(CloudUserCommands.LOGIN, {
|
||||
execute: () => this.service.login(),
|
||||
isEnabled: () => !this._session,
|
||||
isVisible: () => !this._session,
|
||||
});
|
||||
registry.registerCommand(CloudUserCommands.LOGOUT, {
|
||||
execute: () => this.service.logout(),
|
||||
isEnabled: () => !!this._session,
|
||||
isVisible: () => !!this._session,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
|
||||
export const LEARN_MORE_URL =
|
||||
'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync';
|
||||
|
||||
export namespace CloudUserCommands {
|
||||
export const LOGIN = Command.toLocalizedCommand(
|
||||
{
|
||||
@@ -16,9 +19,4 @@ export namespace CloudUserCommands {
|
||||
},
|
||||
'arduino/cloud/signOut'
|
||||
);
|
||||
|
||||
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
|
||||
id: 'arduino-cloud-sketchbook--open-profile-menu',
|
||||
label: 'Contextual menu',
|
||||
};
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import { Installable, ResponseServiceClient } from '../../common/protocol';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { InstallManually } from '../../common/nls';
|
||||
|
||||
interface AutoInstallPromptAction {
|
||||
// isAcceptance, whether or not the action indicates acceptance of auto-install proposal
|
||||
@@ -173,7 +174,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
// CLI returns the packages already sorted with the deprecated ones at the end of the list
|
||||
// in order to ensure the new ones are preferred
|
||||
const candidates = packagesForBoard.filter(
|
||||
({ installable, installedVersion }) => installable && !installedVersion
|
||||
({ installedVersion }) => !installedVersion
|
||||
);
|
||||
|
||||
return candidates[0];
|
||||
@@ -231,19 +232,18 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
candidate: BoardsPackage
|
||||
): AutoInstallPromptActions {
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
const manualInstall = nls.localize(
|
||||
'arduino/board/installManually',
|
||||
'Install Manually'
|
||||
);
|
||||
|
||||
const actions: AutoInstallPromptActions = [
|
||||
{
|
||||
key: manualInstall,
|
||||
key: InstallManually,
|
||||
handler: () => {
|
||||
this.boardsManagerFrontendContribution
|
||||
.openView({ reveal: true })
|
||||
.then((widget) =>
|
||||
widget.refresh(candidate.name.toLocaleLowerCase())
|
||||
widget.refresh({
|
||||
query: candidate.name.toLocaleLowerCase(),
|
||||
type: 'All',
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
injectable,
|
||||
inject,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
|
||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||
@@ -28,8 +32,9 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
@inject(BoardsConfigDialogProps)
|
||||
protected override readonly props: BoardsConfigDialogProps
|
||||
) {
|
||||
super(props);
|
||||
super({ ...props, maxWidth: 500 });
|
||||
|
||||
this.node.id = 'select-board-dialog-container';
|
||||
this.contentNode.classList.add('select-board-dialog');
|
||||
this.contentNode.appendChild(this.createDescription());
|
||||
|
||||
@@ -65,14 +70,6 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
const head = document.createElement('div');
|
||||
head.classList.add('head');
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.textContent = nls.localize(
|
||||
'arduino/board/configDialogTitle',
|
||||
'Select Other Board & Port'
|
||||
);
|
||||
title.classList.add('title');
|
||||
head.appendChild(title);
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.classList.add('text');
|
||||
head.appendChild(text);
|
||||
|
@@ -6,7 +6,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
Board,
|
||||
Port,
|
||||
AttachedBoardsChangeEvent,
|
||||
BoardConfig as ProtocolBoardConfig,
|
||||
BoardWithPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
@@ -19,10 +19,7 @@ import { nls } from '@theia/core/lib/common';
|
||||
import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state';
|
||||
|
||||
export namespace BoardsConfig {
|
||||
export interface Config {
|
||||
selectedBoard?: Board;
|
||||
selectedPort?: Port;
|
||||
}
|
||||
export type Config = ProtocolBoardConfig;
|
||||
|
||||
export interface Props {
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
@@ -113,11 +110,14 @@ export class BoardsConfig extends React.Component<
|
||||
);
|
||||
}
|
||||
}),
|
||||
this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
|
||||
this.updatePorts(
|
||||
event.newState.ports,
|
||||
AttachedBoardsChangeEvent.diff(event).detached.ports
|
||||
)
|
||||
this.props.boardsServiceProvider.onAvailablePortsChanged(
|
||||
({ newState, oldState }) => {
|
||||
const removedPorts = oldState.filter(
|
||||
(oldPort) =>
|
||||
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
|
||||
);
|
||||
this.updatePorts(newState, removedPorts);
|
||||
}
|
||||
),
|
||||
this.props.boardsServiceProvider.onBoardsConfigChanged(
|
||||
({ selectedBoard, selectedPort }) => {
|
||||
@@ -132,7 +132,7 @@ export class BoardsConfig extends React.Component<
|
||||
this.props.notificationCenter.onPlatformDidUninstall(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onIndexDidUpdate(() =>
|
||||
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onDaemonDidStart(() =>
|
||||
@@ -258,14 +258,17 @@ export class BoardsConfig extends React.Component<
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<div className="body">
|
||||
{this.renderContainer('boards', this.renderBoards.bind(this))}
|
||||
<>
|
||||
{this.renderContainer(
|
||||
'ports',
|
||||
nls.localize('arduino/board/boards', 'boards'),
|
||||
this.renderBoards.bind(this)
|
||||
)}
|
||||
{this.renderContainer(
|
||||
nls.localize('arduino/board/ports', 'ports'),
|
||||
this.renderPorts.bind(this),
|
||||
this.renderPortsFooter.bind(this)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -299,6 +302,18 @@ export class BoardsConfig extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
const boardsList = Array.from(distinctBoards.values()).map((board) => (
|
||||
<Item<BoardWithPackage>
|
||||
key={toKey(board)}
|
||||
item={board}
|
||||
label={board.name}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={board.missing}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="search">
|
||||
@@ -306,25 +321,26 @@ export class BoardsConfig extends React.Component<
|
||||
type="search"
|
||||
value={query}
|
||||
className="theia-input"
|
||||
placeholder="SEARCH BOARD"
|
||||
placeholder={nls.localize(
|
||||
'arduino/board/searchBoard',
|
||||
'Search board'
|
||||
)}
|
||||
onChange={this.updateBoards}
|
||||
ref={this.focusNodeSet}
|
||||
/>
|
||||
<i className="fa fa-search"></i>
|
||||
</div>
|
||||
<div className="boards list">
|
||||
{Array.from(distinctBoards.values()).map((board) => (
|
||||
<Item<BoardWithPackage>
|
||||
key={toKey(board)}
|
||||
item={board}
|
||||
label={board.name}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={board.missing}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{boardsList.length > 0 ? (
|
||||
<div className="boards list">{boardsList}</div>
|
||||
) : (
|
||||
<div className="no-result">
|
||||
{nls.localize(
|
||||
'arduino/board/noBoardsFound',
|
||||
'No boards found for "{0}"',
|
||||
query
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -334,27 +350,19 @@ export class BoardsConfig extends React.Component<
|
||||
if (this.state.showAllPorts) {
|
||||
ports = this.state.knownPorts;
|
||||
} else {
|
||||
ports = this.state.knownPorts.filter((port) => {
|
||||
if (port.protocol === 'serial') {
|
||||
return true;
|
||||
}
|
||||
// All other ports with different protocol are
|
||||
// only shown if there is a recognized board
|
||||
// connected
|
||||
for (const board of this.availableBoards) {
|
||||
if (board.port?.address === port.address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ports = this.state.knownPorts.filter(
|
||||
Port.visiblePorts(this.availableBoards)
|
||||
);
|
||||
}
|
||||
return !ports.length ? (
|
||||
<div className="loading noselect">No ports discovered</div>
|
||||
<div className="no-result">
|
||||
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="ports list">
|
||||
{ports.map((port) => (
|
||||
<Item<Port>
|
||||
key={`${port.id}`}
|
||||
key={`${Port.keyOf(port)}`}
|
||||
item={port}
|
||||
label={Port.toString(port)}
|
||||
selected={Port.sameAs(this.state.selectedPort, port)}
|
||||
@@ -379,7 +387,9 @@ export class BoardsConfig extends React.Component<
|
||||
defaultChecked={this.state.showAllPorts}
|
||||
onChange={this.toggleFilterPorts}
|
||||
/>
|
||||
<span>Show all ports</span>
|
||||
<span>
|
||||
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
@@ -418,53 +428,5 @@ export namespace BoardsConfig {
|
||||
const { name } = selectedBoard;
|
||||
return `${name}${port ? ` at ${port.address}` : ''}`;
|
||||
}
|
||||
|
||||
export function setConfig(
|
||||
config: Config | undefined,
|
||||
urlToAttachTo: URL
|
||||
): URL {
|
||||
const copy = new URL(urlToAttachTo.toString());
|
||||
if (!config) {
|
||||
copy.searchParams.delete('boards-config');
|
||||
return copy;
|
||||
}
|
||||
|
||||
const selectedBoard = config.selectedBoard
|
||||
? {
|
||||
name: config.selectedBoard.name,
|
||||
fqbn: config.selectedBoard.fqbn,
|
||||
}
|
||||
: undefined;
|
||||
const selectedPort = config.selectedPort
|
||||
? {
|
||||
protocol: config.selectedPort.protocol,
|
||||
address: config.selectedPort.address,
|
||||
}
|
||||
: undefined;
|
||||
const jsonConfig = JSON.stringify({ selectedBoard, selectedPort });
|
||||
copy.searchParams.set('boards-config', encodeURIComponent(jsonConfig));
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function getConfig(url: URL): Config | undefined {
|
||||
const encoded = url.searchParams.get('boards-config');
|
||||
if (!encoded) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const raw = decodeURIComponent(encoded);
|
||||
const candidate = JSON.parse(raw);
|
||||
if (typeof candidate === 'object') {
|
||||
return candidate;
|
||||
}
|
||||
console.warn(
|
||||
`Expected candidate to be an object. It was ${typeof candidate}. URL was: ${url}`
|
||||
);
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
console.log(`Could not get board config from URL: ${url}.`, e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -80,16 +80,16 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
string,
|
||||
Disposable & { label: string }
|
||||
>();
|
||||
let selectedValue = '';
|
||||
for (const value of values) {
|
||||
const id = `${fqbn}-${option}--${value.value}`;
|
||||
const command = { id };
|
||||
const selectedValue = value.value;
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.boardsDataStore.selectConfigOption({
|
||||
fqbn,
|
||||
option,
|
||||
selectedValue,
|
||||
selectedValue: value.value,
|
||||
}),
|
||||
isToggled: () => value.selected,
|
||||
};
|
||||
@@ -100,8 +100,14 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
{ label: value.label }
|
||||
)
|
||||
);
|
||||
if (value.selected) {
|
||||
selectedValue = value.label;
|
||||
}
|
||||
}
|
||||
this.menuRegistry.registerSubmenu(menuPath, label);
|
||||
this.menuRegistry.registerSubmenu(
|
||||
menuPath,
|
||||
`${label}${selectedValue ? `: "${selectedValue}"` : ''}`
|
||||
);
|
||||
this.toDisposeOnBoardChange.pushAll([
|
||||
...commands.values(),
|
||||
Disposable.create(() =>
|
||||
@@ -111,7 +117,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
const { label } = commands.get(commandId)!;
|
||||
this.menuRegistry.registerMenuAction(menuPath, {
|
||||
commandId,
|
||||
order: `${i}`,
|
||||
order: String(i).padStart(4),
|
||||
label,
|
||||
});
|
||||
return Disposable.create(() =>
|
||||
|
@@ -30,11 +30,11 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
@inject(LocalStorageService)
|
||||
protected readonly storageService: LocalStorageService;
|
||||
|
||||
protected readonly onChangedEmitter = new Emitter<void>();
|
||||
protected readonly onChangedEmitter = new Emitter<string[]>();
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
|
||||
let shouldFireChanged = false;
|
||||
const dataDidChangePerFqbn: string[] = [];
|
||||
for (const fqbn of item.boards
|
||||
.map(({ fqbn }) => fqbn)
|
||||
.filter(notEmpty)
|
||||
@@ -49,18 +49,18 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
data = details.configOptions;
|
||||
if (data.length) {
|
||||
await this.storageService.setData(key, data);
|
||||
shouldFireChanged = true;
|
||||
dataDidChangePerFqbn.push(fqbn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldFireChanged) {
|
||||
this.fireChanged();
|
||||
if (dataDidChangePerFqbn.length) {
|
||||
this.fireChanged(...dataDidChangePerFqbn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get onChanged(): Event<void> {
|
||||
get onChanged(): Event<string[]> {
|
||||
return this.onChangedEmitter.event;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
fqbn,
|
||||
data: { ...data, selectedProgrammer },
|
||||
});
|
||||
this.fireChanged();
|
||||
this.fireChanged(fqbn);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
return false;
|
||||
}
|
||||
await this.setData({ fqbn, data });
|
||||
this.fireChanged();
|
||||
this.fireChanged(fqbn);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -190,8 +190,8 @@ export class BoardsDataStore implements FrontendApplicationContribution {
|
||||
}
|
||||
}
|
||||
|
||||
protected fireChanged(): void {
|
||||
this.onChangedEmitter.fire();
|
||||
protected fireChanged(...fqbn: string[]): void {
|
||||
this.onChangedEmitter.fire(fqbn);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -4,22 +4,24 @@ import {
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
BoardsService,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { BoardsFilterRenderer } from '../widgets/component-list/filter-renderer';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
static WIDGET_ID = 'boards-list-widget';
|
||||
static WIDGET_LABEL = nls.localize('arduino/boardsManager', 'Boards Manager');
|
||||
|
||||
constructor(
|
||||
@inject(BoardsService) protected service: BoardsService,
|
||||
@inject(ListItemRenderer)
|
||||
protected itemRenderer: ListItemRenderer<BoardsPackage>
|
||||
@inject(BoardsService) service: BoardsService,
|
||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
|
||||
@inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
|
||||
) {
|
||||
super({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
@@ -28,8 +30,9 @@ export class BoardsListWidget extends ListWidget<BoardsPackage> {
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemDeprecated: (item: BoardsPackage) => item.deprecated,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All' },
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,12 @@
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
import {
|
||||
Command,
|
||||
CommandContribution,
|
||||
CommandRegistry,
|
||||
CommandService,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { RecursiveRequired } from '../../common/types';
|
||||
@@ -13,6 +18,7 @@ import {
|
||||
AttachedBoardsChangeEvent,
|
||||
BoardWithPackage,
|
||||
BoardUserField,
|
||||
AvailablePorts,
|
||||
} from '../../common/protocol';
|
||||
import { BoardsConfig } from './boards-config';
|
||||
import { naturalCompare } from '../../common/utils';
|
||||
@@ -21,9 +27,19 @@ import { StorageWrapper } from '../storage-wrapper';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { Unknown } from '../../common/nls';
|
||||
import {
|
||||
StartupTask,
|
||||
StartupTaskProvider,
|
||||
} from '../../electron-common/startup-task';
|
||||
|
||||
@injectable()
|
||||
export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
export class BoardsServiceProvider
|
||||
implements
|
||||
FrontendApplicationContribution,
|
||||
StartupTaskProvider,
|
||||
CommandContribution
|
||||
{
|
||||
@inject(ILogger)
|
||||
protected logger: ILogger;
|
||||
|
||||
@@ -47,7 +63,11 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
|
||||
AvailableBoard[]
|
||||
>();
|
||||
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
|
||||
protected readonly onAvailablePortsChangedEmitter = new Emitter<{
|
||||
newState: Port[];
|
||||
oldState: Port[];
|
||||
}>();
|
||||
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
|
||||
|
||||
/**
|
||||
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
|
||||
@@ -65,11 +85,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
protected _availablePorts: Port[] = [];
|
||||
protected _availableBoards: AvailableBoard[] = [];
|
||||
|
||||
private lastBoardsConfigOnUpload: BoardsConfig.Config | undefined;
|
||||
private lastAvailablePortsOnUpload: Port[] | undefined;
|
||||
private boardConfigToAutoSelect: BoardsConfig.Config | undefined;
|
||||
|
||||
/**
|
||||
* Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\
|
||||
* This even also fires, when the boards package was not available for the currently selected board,
|
||||
* Unlike `onAttachedBoardsChanged` this event fires when the user modifies the selected board in the IDE.\
|
||||
* This event also fires, when the boards package was not available for the currently selected board,
|
||||
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
|
||||
* currently selected board.\
|
||||
* currently selected board.
|
||||
*
|
||||
* This event is also emitted when the board package for the currently selected board was uninstalled.
|
||||
*/
|
||||
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
|
||||
@@ -91,14 +116,19 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
);
|
||||
|
||||
this.appStateService.reachedState('ready').then(async () => {
|
||||
const [attachedBoards, availablePorts] = await Promise.all([
|
||||
this.boardsService.getAttachedBoards(),
|
||||
this.boardsService.getAvailablePorts(),
|
||||
const [state] = await Promise.all([
|
||||
this.boardsService.getState(),
|
||||
this.loadState(),
|
||||
]);
|
||||
const { boards: attachedBoards, ports: availablePorts } =
|
||||
AvailablePorts.split(state);
|
||||
this._attachedBoards = attachedBoards;
|
||||
const oldState = this._availablePorts.slice();
|
||||
this._availablePorts = availablePorts;
|
||||
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||
this.onAvailablePortsChangedEmitter.fire({
|
||||
newState: this._availablePorts.slice(),
|
||||
oldState,
|
||||
});
|
||||
|
||||
await this.reconcileAvailableBoards();
|
||||
|
||||
@@ -107,10 +137,108 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
});
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(USE_INHERITED_CONFIG, {
|
||||
execute: (inheritedConfig: BoardsConfig.Config) =>
|
||||
this.inheritedConfig.resolve(inheritedConfig),
|
||||
});
|
||||
}
|
||||
|
||||
get reconciled(): Promise<void> {
|
||||
return this._reconciled.promise;
|
||||
}
|
||||
|
||||
snapshotBoardDiscoveryOnUpload(): void {
|
||||
this.lastBoardsConfigOnUpload = this._boardsConfig;
|
||||
this.lastAvailablePortsOnUpload = this._availablePorts;
|
||||
}
|
||||
|
||||
clearBoardDiscoverySnapshot(): void {
|
||||
this.lastBoardsConfigOnUpload = undefined;
|
||||
this.lastAvailablePortsOnUpload = undefined;
|
||||
}
|
||||
|
||||
attemptPostUploadAutoSelect(): void {
|
||||
setTimeout(() => {
|
||||
if (this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload) {
|
||||
this.attemptAutoSelect({
|
||||
ports: this._availablePorts,
|
||||
boards: this._availableBoards,
|
||||
});
|
||||
}
|
||||
}, 2000); // 2 second delay same as IDE 1.8
|
||||
}
|
||||
|
||||
private attemptAutoSelect(
|
||||
newState: AttachedBoardsChangeEvent['newState']
|
||||
): void {
|
||||
this.deriveBoardConfigToAutoSelect(newState);
|
||||
this.tryReconnect();
|
||||
}
|
||||
|
||||
private deriveBoardConfigToAutoSelect(
|
||||
newState: AttachedBoardsChangeEvent['newState']
|
||||
): void {
|
||||
if (!this.lastBoardsConfigOnUpload || !this.lastAvailablePortsOnUpload) {
|
||||
this.boardConfigToAutoSelect = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPorts = this.lastAvailablePortsOnUpload;
|
||||
const { ports: newPorts, boards: newBoards } = newState;
|
||||
|
||||
const appearedPorts =
|
||||
oldPorts.length > 0
|
||||
? newPorts.filter((newPort: Port) =>
|
||||
oldPorts.every((oldPort: Port) => !Port.sameAs(newPort, oldPort))
|
||||
)
|
||||
: newPorts;
|
||||
|
||||
for (const port of appearedPorts) {
|
||||
const boardOnAppearedPort = newBoards.find((board: Board) =>
|
||||
Port.sameAs(board.port, port)
|
||||
);
|
||||
|
||||
const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload;
|
||||
|
||||
if (boardOnAppearedPort && lastBoardsConfigOnUpload.selectedBoard) {
|
||||
const boardIsSameHardware = Board.hardwareIdEquals(
|
||||
boardOnAppearedPort,
|
||||
lastBoardsConfigOnUpload.selectedBoard
|
||||
);
|
||||
|
||||
const boardIsSameFqbn = Board.sameAs(
|
||||
boardOnAppearedPort,
|
||||
lastBoardsConfigOnUpload.selectedBoard
|
||||
);
|
||||
|
||||
if (!boardIsSameHardware && !boardIsSameFqbn) continue;
|
||||
|
||||
let boardToAutoSelect = boardOnAppearedPort;
|
||||
if (boardIsSameHardware && !boardIsSameFqbn) {
|
||||
const { name, fqbn } = lastBoardsConfigOnUpload.selectedBoard;
|
||||
|
||||
boardToAutoSelect = {
|
||||
...boardToAutoSelect,
|
||||
name:
|
||||
boardToAutoSelect.name === Unknown || !boardToAutoSelect.name
|
||||
? name
|
||||
: boardToAutoSelect.name,
|
||||
fqbn: boardToAutoSelect.fqbn || fqbn,
|
||||
};
|
||||
}
|
||||
|
||||
this.clearBoardDiscoverySnapshot();
|
||||
|
||||
this.boardConfigToAutoSelect = {
|
||||
selectedBoard: boardToAutoSelect,
|
||||
selectedPort: port,
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected notifyAttachedBoardsChanged(
|
||||
event: AttachedBoardsChangeEvent
|
||||
): void {
|
||||
@@ -119,10 +247,22 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
this.logger.info(AttachedBoardsChangeEvent.toString(event));
|
||||
this.logger.info('------------------------------------------');
|
||||
}
|
||||
|
||||
this._attachedBoards = event.newState.boards;
|
||||
const oldState = this._availablePorts.slice();
|
||||
this._availablePorts = event.newState.ports;
|
||||
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||
this.reconcileAvailableBoards().then(() => this.tryReconnect());
|
||||
this.onAvailablePortsChangedEmitter.fire({
|
||||
newState: this._availablePorts.slice(),
|
||||
oldState,
|
||||
});
|
||||
this.reconcileAvailableBoards().then(() => {
|
||||
const { uploadInProgress } = event;
|
||||
// avoid attempting "auto-selection" while an
|
||||
// upload is in progress
|
||||
if (!uploadInProgress) {
|
||||
this.attemptAutoSelect(event.newState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected notifyPlatformInstalled(event: { item: BoardsPackage }): void {
|
||||
@@ -199,8 +339,10 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
// it is just a FQBN, so we need to find the `selected` board among the `AvailableBoards`
|
||||
const selectedAvailableBoard = AvailableBoard.is(selectedBoard)
|
||||
? selectedBoard
|
||||
: this._availableBoards.find((availableBoard) =>
|
||||
Board.sameAs(availableBoard, selectedBoard)
|
||||
: this._availableBoards.find(
|
||||
(availableBoard) =>
|
||||
Board.hardwareIdEquals(availableBoard, selectedBoard) ||
|
||||
Board.sameAs(availableBoard, selectedBoard)
|
||||
);
|
||||
if (
|
||||
selectedAvailableBoard &&
|
||||
@@ -226,9 +368,28 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
|
||||
protected tryReconnect(): boolean {
|
||||
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
|
||||
// ** Reconnect to a board unplugged from, and plugged back into the same port
|
||||
for (const board of this.availableBoards.filter(
|
||||
({ state }) => state !== AvailableBoard.State.incomplete
|
||||
)) {
|
||||
if (
|
||||
Board.hardwareIdEquals(
|
||||
this.latestValidBoardsConfig.selectedBoard,
|
||||
board
|
||||
)
|
||||
) {
|
||||
const { name, fqbn } = this.latestValidBoardsConfig.selectedBoard;
|
||||
this.boardsConfig = {
|
||||
selectedBoard: {
|
||||
name: board.name === Unknown || !board.name ? name : board.name,
|
||||
fqbn: board.fqbn || fqbn,
|
||||
port: board.port,
|
||||
},
|
||||
selectedPort: board.port,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn &&
|
||||
this.latestValidBoardsConfig.selectedBoard.name === board.name &&
|
||||
@@ -238,24 +399,15 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
|
||||
// See documentation on `latestValidBoardsConfig`.
|
||||
for (const board of this.availableBoards.filter(
|
||||
({ state }) => state !== AvailableBoard.State.incomplete
|
||||
)) {
|
||||
if (
|
||||
this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn &&
|
||||
this.latestValidBoardsConfig.selectedBoard.name === board.name &&
|
||||
this.latestValidBoardsConfig.selectedPort.protocol ===
|
||||
board.port?.protocol
|
||||
) {
|
||||
this.boardsConfig = {
|
||||
...this.latestValidBoardsConfig,
|
||||
selectedPort: board.port,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// **
|
||||
|
||||
// ** Reconnect to a board whose port changed due to an upload
|
||||
if (!this.boardConfigToAutoSelect) return false;
|
||||
|
||||
this.boardsConfig = this.boardConfigToAutoSelect;
|
||||
this.boardConfigToAutoSelect = undefined;
|
||||
return true;
|
||||
// **
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -294,14 +446,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
}
|
||||
|
||||
async selectedBoardUserFields(): Promise<BoardUserField[]> {
|
||||
if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
|
||||
if (!this._boardsConfig.selectedBoard) {
|
||||
return [];
|
||||
}
|
||||
const fqbn = this._boardsConfig.selectedBoard.fqbn;
|
||||
if (!fqbn) {
|
||||
return [];
|
||||
}
|
||||
const protocol = this._boardsConfig.selectedPort.protocol;
|
||||
// Protocol must be set to `default` when uploading without a port selected:
|
||||
// https://arduino.github.io/arduino-cli/dev/platform-specification/#sketch-upload-configuration
|
||||
const protocol = this._boardsConfig.selectedPort?.protocol || 'default';
|
||||
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
|
||||
}
|
||||
|
||||
@@ -380,6 +534,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
return this._availableBoards;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Do not use this API, it will be removed. This is a hack to be able to set the missing port `properties` before an upload.
|
||||
*
|
||||
* See: https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236.
|
||||
*/
|
||||
// TODO: remove this API and fix the selected board config store/restore correctly.
|
||||
get availablePorts(): Port[] {
|
||||
return this._availablePorts.slice();
|
||||
}
|
||||
|
||||
async waitUntilAvailable(
|
||||
what: Board & { port: Port },
|
||||
timeout?: number
|
||||
@@ -436,28 +600,19 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
const currentAvailableBoards = this._availableBoards;
|
||||
const availableBoards: AvailableBoard[] = [];
|
||||
const attachedBoards = this._attachedBoards.filter(({ port }) => !!port);
|
||||
const availableBoardPorts = availablePorts.filter((port) => {
|
||||
if (port.protocol === 'serial') {
|
||||
// We always show all serial ports, even if there
|
||||
// is no recognized board connected to it
|
||||
return true;
|
||||
}
|
||||
|
||||
// All other ports with different protocol are
|
||||
// only shown if there is a recognized board
|
||||
// connected
|
||||
for (const board of attachedBoards) {
|
||||
if (board.port?.address === port.address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const availableBoardPorts = availablePorts.filter(
|
||||
Port.visiblePorts(attachedBoards)
|
||||
);
|
||||
|
||||
for (const boardPort of availableBoardPorts) {
|
||||
const board = attachedBoards.find(({ port }) =>
|
||||
Port.sameAs(boardPort, port)
|
||||
);
|
||||
// "board" will always be falsey for
|
||||
// port that was originally mapped
|
||||
// to unknown board and then selected
|
||||
// manually by user
|
||||
|
||||
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(
|
||||
boardPort
|
||||
);
|
||||
@@ -476,12 +631,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
availableBoard = {
|
||||
...lastSelectedBoard,
|
||||
state: AvailableBoard.State.guessed,
|
||||
selected: BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard),
|
||||
selected:
|
||||
BoardsConfig.Config.sameAs(boardsConfig, lastSelectedBoard) &&
|
||||
Port.sameAs(boardPort, boardsConfig.selectedPort), // to avoid double selection
|
||||
port: boardPort,
|
||||
};
|
||||
} else {
|
||||
availableBoard = {
|
||||
name: nls.localize('arduino/common/unknown', 'Unknown'),
|
||||
name: Unknown,
|
||||
port: boardPort,
|
||||
state: AvailableBoard.State.incomplete,
|
||||
};
|
||||
@@ -491,8 +648,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
|
||||
if (
|
||||
boardsConfig.selectedBoard &&
|
||||
!availableBoards.some(({ selected }) => selected)
|
||||
availableBoards.every(({ selected }) => !selected)
|
||||
) {
|
||||
let port = boardsConfig.selectedPort;
|
||||
// If the selected board has the same port of an unknown board
|
||||
// that is already in availableBoards we might get a duplicate port.
|
||||
// So we remove the one already in the array and add the selected one.
|
||||
@@ -500,11 +658,15 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
(board) => board.port?.address === boardsConfig.selectedPort?.address
|
||||
);
|
||||
if (found >= 0) {
|
||||
// get the "Unknown board port" that we will substitute,
|
||||
// then we can include it in the "availableBoard object"
|
||||
// pushed below; to ensure addressLabel is included
|
||||
port = availableBoards[found].port;
|
||||
availableBoards.splice(found, 1);
|
||||
}
|
||||
availableBoards.push({
|
||||
...boardsConfig.selectedBoard,
|
||||
port: boardsConfig.selectedPort,
|
||||
port,
|
||||
selected: true,
|
||||
state: AvailableBoard.State.incomplete,
|
||||
});
|
||||
@@ -570,11 +732,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
let storedLatestBoardsConfig = await this.getData<
|
||||
BoardsConfig.Config | undefined
|
||||
>('latest-boards-config');
|
||||
// Try to get from the URL if it was not persisted.
|
||||
// Try to get from the startup task. Wait for it, then timeout. Maybe it never arrives.
|
||||
if (!storedLatestBoardsConfig) {
|
||||
storedLatestBoardsConfig = BoardsConfig.Config.getConfig(
|
||||
new URL(window.location.href)
|
||||
);
|
||||
storedLatestBoardsConfig = await Promise.race([
|
||||
this.inheritedConfig.promise,
|
||||
new Promise<undefined>((resolve) =>
|
||||
setTimeout(() => resolve(undefined), 2_000)
|
||||
),
|
||||
]);
|
||||
}
|
||||
if (storedLatestBoardsConfig) {
|
||||
this.latestBoardsConfig = storedLatestBoardsConfig;
|
||||
@@ -597,8 +762,31 @@ export class BoardsServiceProvider implements FrontendApplicationContribution {
|
||||
key
|
||||
);
|
||||
}
|
||||
|
||||
tasks(): StartupTask[] {
|
||||
return [
|
||||
{
|
||||
command: USE_INHERITED_CONFIG.id,
|
||||
args: [this.boardsConfig],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It should be neither visible nor called from outside.
|
||||
*
|
||||
* This service creates a startup task with the current board config and
|
||||
* passes the task to the electron-main process so that the new window
|
||||
* can inherit the boards config state of this service.
|
||||
*
|
||||
* Note that the state is always set, but new windows might ignore it.
|
||||
* For example, the new window already has a valid boards config persisted to the local storage.
|
||||
*/
|
||||
const USE_INHERITED_CONFIG: Command = {
|
||||
id: 'arduino-use-inherited-boards-config',
|
||||
};
|
||||
|
||||
/**
|
||||
* Representation of a ready-to-use board, either the user has configured it or was automatically recognized by the CLI.
|
||||
* An available board was not necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`.
|
||||
|
@@ -138,7 +138,7 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
|
||||
{boardLabel}
|
||||
</div>
|
||||
<div className="arduino-boards-dropdown-item--port-label noWrapInfo noselect">
|
||||
{port.address}
|
||||
{port.addressLabel}
|
||||
</div>
|
||||
</div>
|
||||
{selected ? <div className="fa fa-check" /> : ''}
|
||||
|
@@ -1,10 +1,17 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
import { BoardsPackage } from '../../common/protocol/boards-service';
|
||||
import {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
BoardsPackage,
|
||||
BoardSearch
|
||||
> {
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: BoardsListWidget.WIDGET_ID,
|
||||
@@ -18,7 +25,16 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
});
|
||||
}
|
||||
|
||||
override async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
protected canParse(uri: URI): boolean {
|
||||
try {
|
||||
BoardSearch.UriParser.parse(uri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): BoardSearch | undefined {
|
||||
return BoardSearch.UriParser.parse(uri);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,102 @@
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { ConfigService, ConfigState } from '../../common/protocol';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
|
||||
@injectable()
|
||||
export class ConfigServiceClient implements FrontendApplicationContribution {
|
||||
@inject(ConfigService)
|
||||
private readonly delegate: ConfigService;
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
@inject(MessageService)
|
||||
private readonly messageService: MessageService;
|
||||
|
||||
private readonly didChangeSketchDirUriEmitter = new Emitter<
|
||||
URI | undefined
|
||||
>();
|
||||
private readonly didChangeDataDirUriEmitter = new Emitter<URI | undefined>();
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.didChangeSketchDirUriEmitter,
|
||||
this.didChangeDataDirUriEmitter
|
||||
);
|
||||
|
||||
private config: ConfigState | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.appStateService.reachedState('ready').then(async () => {
|
||||
const config = await this.delegate.getConfiguration();
|
||||
this.use(config);
|
||||
});
|
||||
}
|
||||
|
||||
onStart(): void {
|
||||
this.notificationCenter.onConfigDidChange((config) => this.use(config));
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get onDidChangeSketchDirUri(): Event<URI | undefined> {
|
||||
return this.didChangeSketchDirUriEmitter.event;
|
||||
}
|
||||
|
||||
get onDidChangeDataDirUri(): Event<URI | undefined> {
|
||||
return this.didChangeDataDirUriEmitter.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI config related error messages if any.
|
||||
*/
|
||||
tryGetMessages(): string[] | undefined {
|
||||
return this.config?.messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* `directories.user`
|
||||
*/
|
||||
tryGetSketchDirUri(): URI | undefined {
|
||||
return this.config?.config?.sketchDirUri
|
||||
? new URI(this.config?.config?.sketchDirUri)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* `directories.data`
|
||||
*/
|
||||
tryGetDataDirUri(): URI | undefined {
|
||||
return this.config?.config?.dataDirUri
|
||||
? new URI(this.config?.config?.dataDirUri)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private use(config: ConfigState): void {
|
||||
const oldConfig = deepClone(this.config);
|
||||
this.config = config;
|
||||
if (oldConfig?.config?.sketchDirUri !== this.config?.config?.sketchDirUri) {
|
||||
this.didChangeSketchDirUriEmitter.fire(this.tryGetSketchDirUri());
|
||||
}
|
||||
if (oldConfig?.config?.dataDirUri !== this.config?.config?.dataDirUri) {
|
||||
this.didChangeDataDirUriEmitter.fire(this.tryGetDataDirUri());
|
||||
}
|
||||
if (this.config.messages?.length) {
|
||||
const message = this.config.messages.join(' ');
|
||||
// toast the error later otherwise it might not show up in IDE2
|
||||
setTimeout(() => this.messageService.error(message), 1_000);
|
||||
}
|
||||
}
|
||||
}
|
@@ -41,22 +41,16 @@ export class About extends Contribution {
|
||||
}
|
||||
|
||||
async showAbout(): Promise<void> {
|
||||
const {
|
||||
version,
|
||||
commit,
|
||||
status: cliStatus,
|
||||
} = await this.configService.getVersion();
|
||||
const version = await this.configService.getVersion();
|
||||
const buildDate = this.buildDate;
|
||||
const detail = (showAll: boolean) =>
|
||||
nls.localize(
|
||||
'arduino/about/detail',
|
||||
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}{4} [{5}]\n\n{6}',
|
||||
'Version: {0}\nDate: {1}{2}\nCLI Version: {3}\n\n{4}',
|
||||
remote.app.getVersion(),
|
||||
buildDate ? buildDate : nls.localize('', 'dev build'),
|
||||
buildDate && showAll ? ` (${this.ago(buildDate)})` : '',
|
||||
version,
|
||||
cliStatus ? ` ${cliStatus}` : '',
|
||||
commit,
|
||||
nls.localize(
|
||||
'arduino/about/copyright',
|
||||
'Copyright © {0} Arduino SA',
|
||||
|
155
arduino-ide-extension/src/browser/contributions/account.ts
Normal file
155
arduino-ide-extension/src/browser/contributions/account.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MenuPath } from '@theia/core/lib/common/menu';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CloudUserCommands, LEARN_MORE_URL } from '../auth/cloud-user-commands';
|
||||
import { CreateFeatures } from '../create/create-features';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
Contribution,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
|
||||
export const accountMenu: SidebarMenu = {
|
||||
id: 'arduino-accounts-menu',
|
||||
iconClass: 'codicon codicon-account',
|
||||
title: nls.localize('arduino/account/menuTitle', 'Arduino Cloud'),
|
||||
menuPath: ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT,
|
||||
order: 0,
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class Account extends Contribution {
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
@inject(CreateFeatures)
|
||||
private readonly createFeatures: CreateFeatures;
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private app: FrontendApplication;
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.app = app;
|
||||
this.updateSidebarCommand();
|
||||
this.toDispose.push(
|
||||
this.createFeatures.onDidChangeEnabled((enabled) =>
|
||||
this.updateSidebarCommand(enabled)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
const openExternal = (url: string) =>
|
||||
this.windowService.openNewWindow(url, { external: true });
|
||||
const loggedIn = () => Boolean(this.createFeatures.session);
|
||||
const loggedInWithInternetConnection = () =>
|
||||
loggedIn() && this.connectionStatus.offlineStatus !== 'internet';
|
||||
registry.registerCommand(Account.Commands.LEARN_MORE, {
|
||||
execute: () => openExternal(LEARN_MORE_URL),
|
||||
isEnabled: () => !loggedIn(),
|
||||
isVisible: () => !loggedIn(),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_PROFILE, {
|
||||
execute: () => openExternal('https://id.arduino.cc/'),
|
||||
isEnabled: () => loggedInWithInternetConnection(),
|
||||
isVisible: () => loggedIn(),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, {
|
||||
execute: () => openExternal('https://create.arduino.cc/editor'),
|
||||
isEnabled: () => loggedInWithInternetConnection(),
|
||||
isVisible: () => loggedIn(),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, {
|
||||
execute: () => openExternal('https://create.arduino.cc/iot/'),
|
||||
isEnabled: () => loggedInWithInternetConnection(),
|
||||
isVisible: () => loggedIn(),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
const register = (
|
||||
menuPath: MenuPath,
|
||||
...commands: (Command | [command: Command, menuLabel: string])[]
|
||||
) =>
|
||||
commands.forEach((command, index) => {
|
||||
const commandId = Array.isArray(command) ? command[0].id : command.id;
|
||||
const label = Array.isArray(command) ? command[1] : command.label;
|
||||
registry.registerMenuAction(menuPath, {
|
||||
label,
|
||||
commandId,
|
||||
order: String(index),
|
||||
});
|
||||
});
|
||||
|
||||
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP, [
|
||||
CloudUserCommands.LOGIN,
|
||||
nls.localize('arduino/cloud/signInToCloud', 'Sign in to Arduino Cloud'),
|
||||
]);
|
||||
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP, [
|
||||
Account.Commands.LEARN_MORE,
|
||||
nls.localize('arduino/cloud/learnMore', 'Learn more'),
|
||||
]);
|
||||
register(
|
||||
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP,
|
||||
[
|
||||
Account.Commands.GO_TO_PROFILE,
|
||||
nls.localize('arduino/account/goToProfile', 'Go to Profile'),
|
||||
],
|
||||
[
|
||||
Account.Commands.GO_TO_CLOUD_EDITOR,
|
||||
nls.localize('arduino/account/goToCloudEditor', 'Go to Cloud Editor'),
|
||||
],
|
||||
[
|
||||
Account.Commands.GO_TO_IOT_CLOUD,
|
||||
nls.localize('arduino/account/goToIoTCloud', 'Go to IoT Cloud'),
|
||||
]
|
||||
);
|
||||
register(
|
||||
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP,
|
||||
CloudUserCommands.LOGOUT
|
||||
);
|
||||
}
|
||||
|
||||
private updateSidebarCommand(
|
||||
visible: boolean = this.preferences['arduino.cloud.enabled']
|
||||
): void {
|
||||
if (!this.app) {
|
||||
return;
|
||||
}
|
||||
const handler = this.app.shell.leftPanelHandler;
|
||||
if (visible) {
|
||||
handler.addBottomMenu(accountMenu);
|
||||
} else {
|
||||
handler.removeBottomMenu(accountMenu.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Account {
|
||||
export namespace Commands {
|
||||
export const GO_TO_PROFILE: Command = {
|
||||
id: 'arduino-go-to-profile',
|
||||
};
|
||||
export const GO_TO_CLOUD_EDITOR: Command = {
|
||||
id: 'arduino-go-to-cloud-editor',
|
||||
};
|
||||
export const GO_TO_IOT_CLOUD: Command = {
|
||||
id: 'arduino-go-to-iot-cloud',
|
||||
};
|
||||
export const LEARN_MORE: Command = {
|
||||
id: 'arduino-learn-more',
|
||||
};
|
||||
}
|
||||
}
|
@@ -7,15 +7,16 @@ import {
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
URI,
|
||||
Sketch,
|
||||
} from './contribution';
|
||||
import { FileDialogService } from '@theia/filesystem/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class AddFile extends SketchContribution {
|
||||
@inject(FileDialogService)
|
||||
protected readonly fileDialogService: FileDialogService;
|
||||
private readonly fileDialogService: FileDialogService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddFile.Commands.ADD_FILE, {
|
||||
@@ -31,7 +32,7 @@ export class AddFile extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
protected async addFile(): Promise<void> {
|
||||
private async addFile(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
@@ -41,13 +42,12 @@ export class AddFile extends SketchContribution {
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
modal: true,
|
||||
});
|
||||
if (!toAddUri) {
|
||||
return;
|
||||
}
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const filename = toAddUri.path.base;
|
||||
const targetUri = sketchUri.resolve('data').resolve(filename);
|
||||
const { uri: targetUri, filename } = this.resolveTarget(sketch, toAddUri);
|
||||
const exists = await this.fileService.exists(targetUri);
|
||||
if (exists) {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
@@ -79,6 +79,22 @@ export class AddFile extends SketchContribution {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// https://github.com/arduino/arduino-ide/issues/284#issuecomment-1364533662
|
||||
// File the file to add has one of the following extension, it goes to the sketch folder root: .ino, .h, .cpp, .c, .S
|
||||
// Otherwise, the files goes to the `data` folder inside the sketch folder root.
|
||||
private resolveTarget(
|
||||
sketch: Sketch,
|
||||
toAddUri: URI
|
||||
): { uri: URI; filename: string } {
|
||||
const path = toAddUri.path;
|
||||
const filename = path.base;
|
||||
let root = new URI(sketch.uri);
|
||||
if (!Sketch.Extensions.CODE_FILES.includes(path.ext)) {
|
||||
root = root.resolve('data');
|
||||
}
|
||||
return { uri: root.resolve(filename), filename: filename };
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AddFile {
|
||||
|
@@ -2,7 +2,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ConfirmDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { LibraryService, ResponseServiceClient } from '../../common/protocol';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
@@ -16,14 +15,11 @@ import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class AddZipLibrary extends SketchContribution {
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(ResponseServiceClient)
|
||||
protected readonly responseService: ResponseServiceClient;
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
|
||||
@@ -43,23 +39,26 @@ export class AddZipLibrary extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
async addZipLibrary(): Promise<void> {
|
||||
private async addZipLibrary(): Promise<void> {
|
||||
const homeUri = await this.envVariableServer.getHomeDirUri();
|
||||
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
|
||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
|
||||
title: nls.localize(
|
||||
'arduino/selectZip',
|
||||
"Select a zip file containing the library you'd like to add"
|
||||
),
|
||||
defaultPath,
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/library/zipLibrary', 'Library'),
|
||||
extensions: ['zip'],
|
||||
},
|
||||
],
|
||||
});
|
||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/selectZip',
|
||||
"Select a zip file containing the library you'd like to add"
|
||||
),
|
||||
defaultPath,
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/library/zipLibrary', 'Library'),
|
||||
extensions: ['zip'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
if (!canceled && filePaths.length) {
|
||||
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
|
||||
try {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
SketchContribution,
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class ArchiveSketch extends SketchContribution {
|
||||
@@ -28,11 +27,8 @@ export class ArchiveSketch extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
protected async archiveSketch(): Promise<void> {
|
||||
const [sketch, config] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
private async archiveSketch(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
@@ -40,16 +36,19 @@ export class ArchiveSketch extends SketchContribution {
|
||||
new Date(),
|
||||
'yymmdd'
|
||||
)}a.zip`;
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri).resolve(archiveBasename)
|
||||
const defaultContainerUri = await this.defaultUri();
|
||||
const defaultUri = defaultContainerUri.resolve(archiveBasename);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveSketchAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveSketchAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
});
|
||||
if (!filePath || canceled) {
|
||||
return;
|
||||
}
|
||||
@@ -57,7 +56,7 @@ export class ArchiveSketch extends SketchContribution {
|
||||
if (!destinationUri) {
|
||||
return;
|
||||
}
|
||||
await this.sketchService.archive(sketch, destinationUri.toString());
|
||||
await this.sketchesService.archive(sketch, destinationUri.toString());
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/sketch/createdArchive',
|
||||
|
@@ -5,7 +5,6 @@ import {
|
||||
DisposableCollection,
|
||||
Disposable,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { firstToUpperCase } from '../../common/utils';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { BoardsListWidget } from '../boards/boards-list-widget';
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
InstalledBoardWithPackage,
|
||||
AvailablePorts,
|
||||
Port,
|
||||
getBoardInfo,
|
||||
} from '../../common/protocol';
|
||||
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
@@ -50,52 +50,28 @@ export class BoardSelection extends SketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
|
||||
execute: async () => {
|
||||
const { selectedBoard, selectedPort } =
|
||||
this.boardsServiceProvider.boardsConfig;
|
||||
if (!selectedBoard) {
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/board/selectBoardForInfo',
|
||||
'Please select a board to obtain board info.'
|
||||
)
|
||||
);
|
||||
const boardInfo = await getBoardInfo(
|
||||
this.boardsServiceProvider.boardsConfig.selectedPort,
|
||||
this.boardsService.getState()
|
||||
);
|
||||
if (typeof boardInfo === 'string') {
|
||||
this.messageService.info(boardInfo);
|
||||
return;
|
||||
}
|
||||
if (!selectedBoard.fqbn) {
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/board/platformMissing',
|
||||
"The platform for the selected '{0}' board is not installed.",
|
||||
selectedBoard.name
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!selectedPort) {
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/board/selectPortForInfo',
|
||||
'Please select a port to obtain board info.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
const boardDetails = await this.boardsService.getBoardDetails({
|
||||
fqbn: selectedBoard.fqbn,
|
||||
});
|
||||
if (boardDetails) {
|
||||
const { VID, PID } = boardDetails;
|
||||
const detail = `BN: ${selectedBoard.name}
|
||||
const { BN, VID, PID, SN } = boardInfo;
|
||||
const detail = `
|
||||
BN: ${BN}
|
||||
VID: ${VID}
|
||||
PID: ${PID}`;
|
||||
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
type: 'info',
|
||||
detail,
|
||||
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
|
||||
});
|
||||
}
|
||||
PID: ${PID}
|
||||
SN: ${SN}
|
||||
`.trim();
|
||||
await remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
|
||||
type: 'info',
|
||||
detail,
|
||||
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -156,10 +132,7 @@ PID: ${PID}`;
|
||||
);
|
||||
|
||||
// Ports submenu
|
||||
const portsSubmenuPath = [
|
||||
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||
'2_ports',
|
||||
];
|
||||
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
|
||||
const portsSubmenuLabel = config.selectedPort?.address;
|
||||
this.menuModelRegistry.registerSubmenu(
|
||||
portsSubmenuPath,
|
||||
@@ -200,14 +173,15 @@ PID: ${PID}`;
|
||||
});
|
||||
|
||||
// Installed boards
|
||||
for (const board of installedBoards) {
|
||||
installedBoards.forEach((board, index) => {
|
||||
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
|
||||
|
||||
const packageLabel =
|
||||
packageName +
|
||||
`${manuallyInstalled
|
||||
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||
: ''
|
||||
`${
|
||||
manuallyInstalled
|
||||
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||
: ''
|
||||
}`;
|
||||
// Platform submenu
|
||||
const platformMenuPath = [...boardsPackagesGroup, packageId];
|
||||
@@ -240,14 +214,18 @@ PID: ${PID}`;
|
||||
};
|
||||
|
||||
// Board menu
|
||||
const menuAction = { commandId: id, label: name };
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label: name,
|
||||
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
|
||||
);
|
||||
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
|
||||
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
|
||||
}
|
||||
});
|
||||
|
||||
// Installed ports
|
||||
const registerPorts = (
|
||||
@@ -267,8 +245,12 @@ PID: ${PID}`;
|
||||
];
|
||||
const placeholder = new PlaceholderMenuNode(
|
||||
menuPath,
|
||||
`${firstToUpperCase(protocol)} ports`,
|
||||
{ order: protocolOrder.toString() }
|
||||
nls.localize(
|
||||
'arduino/board/typeOfPorts',
|
||||
'{0} ports',
|
||||
Port.Protocols.protocolLabel(protocol)
|
||||
),
|
||||
{ order: protocolOrder.toString().padStart(4) }
|
||||
);
|
||||
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -279,16 +261,18 @@ PID: ${PID}`;
|
||||
|
||||
// First we show addresses with recognized boards connected,
|
||||
// then all the rest.
|
||||
const sortedIDs = Object.keys(ports).sort((left: string, right: string): number => {
|
||||
const [, leftBoards] = ports[left];
|
||||
const [, rightBoards] = ports[right];
|
||||
return rightBoards.length - leftBoards.length;
|
||||
});
|
||||
const sortedIDs = Object.keys(ports).sort(
|
||||
(left: string, right: string): number => {
|
||||
const [, leftBoards] = ports[left];
|
||||
const [, rightBoards] = ports[right];
|
||||
return rightBoards.length - leftBoards.length;
|
||||
}
|
||||
);
|
||||
|
||||
for (let i = 0; i < sortedIDs.length; i++) {
|
||||
const portID = sortedIDs[i];
|
||||
const [port, boards] = ports[portID];
|
||||
let label = `${port.address}`;
|
||||
let label = `${port.addressLabel}`;
|
||||
if (boards.length) {
|
||||
const boardsList = boards.map((board) => board.name).join(', ');
|
||||
label = `${label} (${boardsList})`;
|
||||
@@ -319,7 +303,7 @@ PID: ${PID}`;
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label,
|
||||
order: `${protocolOrder + i + 1}`,
|
||||
order: String(protocolOrder + i + 1).padStart(4),
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -331,7 +315,7 @@ PID: ${PID}`;
|
||||
}
|
||||
};
|
||||
|
||||
const grouped = AvailablePorts.byProtocol(availablePorts);
|
||||
const grouped = AvailablePorts.groupByProtocol(availablePorts);
|
||||
let protocolOrder = 100;
|
||||
// We first show serial and network ports, then all the rest
|
||||
['serial', 'network'].forEach((protocol) => {
|
||||
@@ -351,7 +335,7 @@ PID: ${PID}`;
|
||||
}
|
||||
|
||||
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
|
||||
const allBoards = await this.boardsService.searchBoards({});
|
||||
const allBoards = await this.boardsService.getInstalledBoards();
|
||||
return allBoards.filter(InstalledBoardWithPackage.is);
|
||||
}
|
||||
}
|
||||
|
@@ -29,6 +29,7 @@ export class BurnBootloader extends CoreServiceContribution {
|
||||
}
|
||||
|
||||
private async burnBootloader(): Promise<void> {
|
||||
this.clearVisibleNotification();
|
||||
const options = await this.options();
|
||||
try {
|
||||
await this.doWithProgress({
|
||||
|
@@ -0,0 +1,69 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
IDEUpdater,
|
||||
SKIP_IDE_VERSION,
|
||||
} from '../../common/protocol/ide-updater';
|
||||
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class CheckForIDEUpdates extends Contribution {
|
||||
@inject(IDEUpdater)
|
||||
private readonly updater: IDEUpdater;
|
||||
|
||||
@inject(IDEUpdaterDialog)
|
||||
private readonly updaterDialog: IDEUpdaterDialog;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
private readonly localStorage: LocalStorageService;
|
||||
|
||||
override onStart(): void {
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, newValue, oldValue }) => {
|
||||
if (newValue !== oldValue) {
|
||||
switch (preferenceName) {
|
||||
case 'arduino.ide.updateChannel':
|
||||
case 'arduino.ide.updateBaseUrl':
|
||||
this.updater.init(
|
||||
this.preferences.get('arduino.ide.updateChannel'),
|
||||
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.updater
|
||||
.init(
|
||||
this.preferences.get('arduino.ide.updateChannel'),
|
||||
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||
)
|
||||
.then(() => {
|
||||
if (!this.preferences['arduino.checkForUpdates']) {
|
||||
return;
|
||||
}
|
||||
return this.updater.checkForUpdates(true);
|
||||
})
|
||||
.then(async (updateInfo) => {
|
||||
if (!updateInfo) return;
|
||||
const versionToSkip = await this.localStorage.getData<string>(
|
||||
SKIP_IDE_VERSION
|
||||
);
|
||||
if (versionToSkip === updateInfo.version) return;
|
||||
this.updaterDialog.open(updateInfo);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/ide-updater/errorCheckingForUpdates',
|
||||
'Error while checking for Arduino IDE updates.\n{0}',
|
||||
e.message
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,64 +1,221 @@
|
||||
import type { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { InstallManually, Later } from '../../common/nls';
|
||||
import {
|
||||
IDEUpdater,
|
||||
SKIP_IDE_VERSION,
|
||||
} from '../../common/protocol/ide-updater';
|
||||
import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog';
|
||||
import { Contribution } from './contribution';
|
||||
ArduinoComponent,
|
||||
BoardsPackage,
|
||||
BoardsService,
|
||||
LibraryPackage,
|
||||
LibraryService,
|
||||
ResponseServiceClient,
|
||||
Searchable,
|
||||
} from '../../common/protocol';
|
||||
import { Installable } from '../../common/protocol/installable';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
import { BoardsListWidgetFrontendContribution } from '../boards/boards-widget-frontend-contribution';
|
||||
import { LibraryListWidgetFrontendContribution } from '../library/library-widget-frontend-contribution';
|
||||
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||
import type { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { Command, CommandRegistry, Contribution } from './contribution';
|
||||
|
||||
const NoUpdates = nls.localize(
|
||||
'arduino/checkForUpdates/noUpdates',
|
||||
'There are no recent updates available.'
|
||||
);
|
||||
const PromptUpdateBoards = nls.localize(
|
||||
'arduino/checkForUpdates/promptUpdateBoards',
|
||||
'Updates are available for some of your boards.'
|
||||
);
|
||||
const PromptUpdateLibraries = nls.localize(
|
||||
'arduino/checkForUpdates/promptUpdateLibraries',
|
||||
'Updates are available for some of your libraries.'
|
||||
);
|
||||
const UpdatingBoards = nls.localize(
|
||||
'arduino/checkForUpdates/updatingBoards',
|
||||
'Updating boards...'
|
||||
);
|
||||
const UpdatingLibraries = nls.localize(
|
||||
'arduino/checkForUpdates/updatingLibraries',
|
||||
'Updating libraries...'
|
||||
);
|
||||
const InstallAll = nls.localize(
|
||||
'arduino/checkForUpdates/installAll',
|
||||
'Install All'
|
||||
);
|
||||
|
||||
interface Task<T extends ArduinoComponent> {
|
||||
readonly run: () => Promise<void>;
|
||||
readonly item: T;
|
||||
}
|
||||
|
||||
const Updatable = { type: 'Updatable' } as const;
|
||||
|
||||
@injectable()
|
||||
export class CheckForUpdates extends Contribution {
|
||||
@inject(IDEUpdater)
|
||||
private readonly updater: IDEUpdater;
|
||||
@inject(WindowServiceExt)
|
||||
private readonly windowService: WindowServiceExt;
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
@inject(BoardsService)
|
||||
private readonly boardsService: BoardsService;
|
||||
@inject(LibraryService)
|
||||
private readonly libraryService: LibraryService;
|
||||
@inject(BoardsListWidgetFrontendContribution)
|
||||
private readonly boardsContribution: BoardsListWidgetFrontendContribution;
|
||||
@inject(LibraryListWidgetFrontendContribution)
|
||||
private readonly librariesContribution: LibraryListWidgetFrontendContribution;
|
||||
|
||||
@inject(IDEUpdaterDialog)
|
||||
private readonly updaterDialog: IDEUpdaterDialog;
|
||||
|
||||
@inject(LocalStorageService)
|
||||
private readonly localStorage: LocalStorageService;
|
||||
|
||||
override onStart(): void {
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, newValue, oldValue }) => {
|
||||
if (newValue !== oldValue) {
|
||||
switch (preferenceName) {
|
||||
case 'arduino.ide.updateChannel':
|
||||
case 'arduino.ide.updateBaseUrl':
|
||||
this.updater.init(
|
||||
this.preferences.get('arduino.ide.updateChannel'),
|
||||
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
override registerCommands(register: CommandRegistry): void {
|
||||
register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
|
||||
execute: () => this.checkForUpdates(false),
|
||||
});
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.updater
|
||||
.init(
|
||||
this.preferences.get('arduino.ide.updateChannel'),
|
||||
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||
)
|
||||
.then(() => this.updater.checkForUpdates(true))
|
||||
.then(async (updateInfo) => {
|
||||
if (!updateInfo) return;
|
||||
const versionToSkip = await this.localStorage.getData<string>(
|
||||
SKIP_IDE_VERSION
|
||||
);
|
||||
if (versionToSkip === updateInfo.version) return;
|
||||
this.updaterDialog.open(updateInfo);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/ide-updater/errorCheckingForUpdates',
|
||||
'Error while checking for Arduino IDE updates.\n{0}',
|
||||
e.message
|
||||
)
|
||||
);
|
||||
override async onReady(): Promise<void> {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
if (checkForUpdates) {
|
||||
this.windowService.isFirstWindow().then((firstWindow) => {
|
||||
if (firstWindow) {
|
||||
this.checkForUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async checkForUpdates(silent = true) {
|
||||
const [boardsPackages, libraryPackages] = await Promise.all([
|
||||
this.boardsService.search(Updatable),
|
||||
this.libraryService.search(Updatable),
|
||||
]);
|
||||
this.promptUpdateBoards(boardsPackages);
|
||||
this.promptUpdateLibraries(libraryPackages);
|
||||
if (!libraryPackages.length && !boardsPackages.length && !silent) {
|
||||
this.messageService.info(NoUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private promptUpdateBoards(items: BoardsPackage[]): void {
|
||||
this.prompt({
|
||||
items,
|
||||
installable: this.boardsService,
|
||||
viewContribution: this.boardsContribution,
|
||||
viewSearchOptions: { query: '', ...Updatable },
|
||||
promptMessage: PromptUpdateBoards,
|
||||
updatingMessage: UpdatingBoards,
|
||||
});
|
||||
}
|
||||
|
||||
private promptUpdateLibraries(items: LibraryPackage[]): void {
|
||||
this.prompt({
|
||||
items,
|
||||
installable: this.libraryService,
|
||||
viewContribution: this.librariesContribution,
|
||||
viewSearchOptions: { query: '', topic: 'All', ...Updatable },
|
||||
promptMessage: PromptUpdateLibraries,
|
||||
updatingMessage: UpdatingLibraries,
|
||||
});
|
||||
}
|
||||
|
||||
private prompt<
|
||||
T extends ArduinoComponent,
|
||||
S extends Searchable.Options
|
||||
>(options: {
|
||||
items: T[];
|
||||
installable: Installable<T>;
|
||||
viewContribution: AbstractViewContribution<ListWidget<T, S>>;
|
||||
viewSearchOptions: S;
|
||||
promptMessage: string;
|
||||
updatingMessage: string;
|
||||
}): void {
|
||||
const {
|
||||
items,
|
||||
installable,
|
||||
viewContribution,
|
||||
promptMessage: message,
|
||||
viewSearchOptions,
|
||||
updatingMessage,
|
||||
} = options;
|
||||
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
this.messageService
|
||||
.info(message, Later, InstallManually, InstallAll)
|
||||
.then((answer) => {
|
||||
if (answer === InstallAll) {
|
||||
const tasks = items.map((item) =>
|
||||
this.createInstallTask(item, installable)
|
||||
);
|
||||
this.executeTasks(updatingMessage, tasks);
|
||||
} else if (answer === InstallManually) {
|
||||
viewContribution
|
||||
.openView({ reveal: true })
|
||||
.then((widget) => widget.refresh(viewSearchOptions));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async executeTasks(
|
||||
message: string,
|
||||
tasks: Task<ArduinoComponent>[]
|
||||
): Promise<void> {
|
||||
if (tasks.length) {
|
||||
return ExecuteWithProgress.withProgress(
|
||||
message,
|
||||
this.messageService,
|
||||
async (progress) => {
|
||||
try {
|
||||
const total = tasks.length;
|
||||
let count = 0;
|
||||
for (const { run, item } of tasks) {
|
||||
try {
|
||||
await run(); // runs update sequentially. // TODO: is parallel update desired?
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.messageService.error(
|
||||
`Failed to update ${item.name}. ${err}`
|
||||
);
|
||||
} finally {
|
||||
progress.report({ work: { total, done: ++count } });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
progress.cancel();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private createInstallTask<T extends ArduinoComponent>(
|
||||
item: T,
|
||||
installable: Installable<T>
|
||||
): Task<T> {
|
||||
const latestVersion = item.availableVersions[0];
|
||||
return {
|
||||
item,
|
||||
run: () =>
|
||||
Installable.installWithProgress({
|
||||
installable,
|
||||
item,
|
||||
version: latestVersion,
|
||||
messageService: this.messageService,
|
||||
responseService: this.responseService,
|
||||
keepOutput: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
export namespace CheckForUpdates {
|
||||
export namespace Commands {
|
||||
export const CHECK_FOR_UPDATES: Command = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-check-for-updates',
|
||||
label: 'Check for Arduino Updates',
|
||||
category: 'Arduino',
|
||||
},
|
||||
'arduino/checkForUpdates/checkForUpdates'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,14 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { toArray } from '@theia/core/shared/@phosphor/algorithm';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import type { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import type {
|
||||
FrontendApplication,
|
||||
OnWillStopAction,
|
||||
} from '@theia/core/lib/browser/frontend-application';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
SketchContribution,
|
||||
@@ -11,27 +16,48 @@ import {
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
Sketch,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
|
||||
/**
|
||||
* Closes the `current` closeable editor, or any closeable current widget from the main area, or the current sketch window.
|
||||
*/
|
||||
@injectable()
|
||||
export class Close extends SketchContribution {
|
||||
@inject(EditorManager)
|
||||
protected override readonly editorManager: EditorManager;
|
||||
private shell: ApplicationShell | undefined;
|
||||
|
||||
protected shell: ApplicationShell;
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
override onStart(app: FrontendApplication): MaybePromise<void> {
|
||||
this.shell = app.shell;
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Close.Commands.CLOSE, {
|
||||
execute: () => remote.getCurrentWindow().close()
|
||||
execute: () => {
|
||||
// Close current editor if closeable.
|
||||
const { currentEditor } = this.editorManager;
|
||||
if (currentEditor && currentEditor.title.closable) {
|
||||
currentEditor.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shell) {
|
||||
// Close current widget from the main area if possible.
|
||||
const { currentWidget } = this.shell;
|
||||
if (currentWidget) {
|
||||
const currentWidgetInMain = toArray(
|
||||
this.shell.mainPanel.widgets()
|
||||
).find((widget) => widget === currentWidget);
|
||||
if (currentWidgetInMain && currentWidgetInMain.title.closable) {
|
||||
return currentWidgetInMain.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
return remote.getCurrentWindow().close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,7 +65,7 @@ export class Close extends SketchContribution {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: Close.Commands.CLOSE.id,
|
||||
label: nls.localize('vscode/editor.contribution/close', 'Close'),
|
||||
order: '5',
|
||||
order: '6',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,6 +76,123 @@ export class Close extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
// `FrontendApplicationContribution#onWillStop`
|
||||
onWillStop(): OnWillStopAction {
|
||||
return {
|
||||
reason: 'save-sketch',
|
||||
action: () => {
|
||||
return this.showSaveSketchDialog();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If returns with `true`, IDE2 will close. Otherwise, it won't.
|
||||
*/
|
||||
private async showSaveSketchDialog(): Promise<boolean> {
|
||||
const sketch = await this.isCurrentSketchTemp();
|
||||
if (!sketch) {
|
||||
// Normal close workflow: if there are dirty editors prompt the user.
|
||||
if (!this.shell) {
|
||||
console.error(
|
||||
`Could not get the application shell. Something went wrong.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (this.shell.canSaveAll()) {
|
||||
const prompt = await this.prompt(false);
|
||||
switch (prompt) {
|
||||
case Prompt.DoNotSave:
|
||||
return true;
|
||||
case Prompt.Cancel:
|
||||
return false;
|
||||
case Prompt.Save: {
|
||||
await this.shell.saveAll();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unexpected prompt: ${prompt}`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If non of the sketch files were ever touched, do not prompt the save dialog. (#1274)
|
||||
const wereTouched = await Promise.all(
|
||||
Sketch.uris(sketch).map((uri) => this.wasTouched(uri))
|
||||
);
|
||||
if (wereTouched.every((wasTouched) => !Boolean(wasTouched))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const prompt = await this.prompt(true);
|
||||
switch (prompt) {
|
||||
case Prompt.DoNotSave:
|
||||
return true;
|
||||
case Prompt.Cancel:
|
||||
return false;
|
||||
case Prompt.Save: {
|
||||
// If `save as` was canceled by user, the result will be `undefined`, otherwise the new URI.
|
||||
const result = await this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
{
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: false,
|
||||
wipeOriginal: true,
|
||||
markAsRecentlyOpened: true,
|
||||
}
|
||||
);
|
||||
return !!result;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unexpected prompt: ${prompt}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async prompt(isTemp: boolean): Promise<Prompt> {
|
||||
const { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
message: nls.localize(
|
||||
'arduino/sketch/saveSketch',
|
||||
'Save your sketch to open it again later.'
|
||||
),
|
||||
title: nls.localize(
|
||||
'theia/core/quitTitle',
|
||||
'Are you sure you want to quit?'
|
||||
),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
nls.localizeByDefault("Don't Save"),
|
||||
Dialog.CANCEL,
|
||||
nls.localizeByDefault(isTemp ? 'Save As...' : 'Save'),
|
||||
],
|
||||
defaultId: 2, // `Save`/`Save As...` button index is the default.
|
||||
}
|
||||
);
|
||||
switch (response) {
|
||||
case 0:
|
||||
return Prompt.DoNotSave;
|
||||
case 1:
|
||||
return Prompt.Cancel;
|
||||
case 2:
|
||||
return Prompt.Save;
|
||||
default:
|
||||
throw new Error(`Unexpected response: ${response}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async isCurrentSketchTemp(): Promise<false | Sketch> {
|
||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||
if (CurrentSketch.isValid(currentSketch)) {
|
||||
const isTemp = await this.sketchesService.isTemp(currentSketch);
|
||||
if (isTemp) {
|
||||
return currentSketch;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the file was ever touched/modified. We get this based on the `version` of the monaco model.
|
||||
*/
|
||||
@@ -59,13 +202,23 @@ export class Close extends SketchContribution {
|
||||
const { editor } = editorWidget;
|
||||
if (editor instanceof MonacoEditor) {
|
||||
const versionId = editor.getControl().getModel()?.getVersionId();
|
||||
if (Number.isInteger(versionId) && versionId! > 1) {
|
||||
if (this.isInteger(versionId) && versionId > 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isInteger(arg: unknown): arg is number {
|
||||
return Number.isInteger(arg);
|
||||
}
|
||||
}
|
||||
|
||||
enum Prompt {
|
||||
Save,
|
||||
DoNotSave,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
export namespace Close {
|
||||
|
@@ -0,0 +1,121 @@
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateApi } from '../create/create-api';
|
||||
import { CreateFeatures } from '../create/create-features';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { Create, isNotFound } from '../create/typings';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
|
||||
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
|
||||
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
|
||||
import { SketchContribution } from './contribution';
|
||||
|
||||
export function sketchAlreadyExists(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/alreadyExists',
|
||||
"Cloud sketch '{0}' already exists.",
|
||||
input
|
||||
);
|
||||
}
|
||||
export function sketchNotFound(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/notFound',
|
||||
"Could not pull the cloud sketch '{0}'. It does not exist.",
|
||||
input
|
||||
);
|
||||
}
|
||||
export const synchronizingSketchbook = nls.localize(
|
||||
'arduino/cloudSketch/synchronizingSketchbook',
|
||||
'Synchronizing sketchbook...'
|
||||
);
|
||||
export function pullingSketch(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/pulling',
|
||||
"Synchronizing sketchbook, pulling '{0}'...",
|
||||
input
|
||||
);
|
||||
}
|
||||
export function pushingSketch(input: string): string {
|
||||
return nls.localize(
|
||||
'arduino/cloudSketch/pushing',
|
||||
"Synchronizing sketchbook, pushing '{0}'...",
|
||||
input
|
||||
);
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class CloudSketchContribution extends SketchContribution {
|
||||
@inject(SketchbookWidgetContribution)
|
||||
private readonly widgetContribution: SketchbookWidgetContribution;
|
||||
@inject(CreateApi)
|
||||
protected readonly createApi: CreateApi;
|
||||
@inject(CreateFeatures)
|
||||
protected readonly createFeatures: CreateFeatures;
|
||||
|
||||
protected async treeModel(): Promise<
|
||||
(CloudSketchbookTreeModel & { root: CompositeTreeNode }) | undefined
|
||||
> {
|
||||
const { enabled, session } = this.createFeatures;
|
||||
if (enabled && session) {
|
||||
const widget = await this.widgetContribution.widget;
|
||||
const treeModel = this.treeModelFrom(widget);
|
||||
if (treeModel) {
|
||||
const root = treeModel.root;
|
||||
if (CompositeTreeNode.is(root)) {
|
||||
return treeModel as CloudSketchbookTreeModel & {
|
||||
root: CompositeTreeNode;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async pull(
|
||||
sketch: Create.Sketch
|
||||
): Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (!treeModel) {
|
||||
return undefined;
|
||||
}
|
||||
const id = CreateUri.toUri(sketch).path.toString();
|
||||
const node = treeModel.getNode(id);
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
`Could not find cloud sketchbook tree node with ID: ${id}.`
|
||||
);
|
||||
}
|
||||
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||
throw new Error(
|
||||
`Cloud sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
|
||||
);
|
||||
}
|
||||
try {
|
||||
await treeModel.sketchbookTree().pull({ node }, true);
|
||||
return node;
|
||||
} catch (err) {
|
||||
if (isNotFound(err)) {
|
||||
await treeModel.refresh();
|
||||
this.messageService.error(sketchNotFound(sketch.name));
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private treeModelFrom(
|
||||
widget: SketchbookWidget
|
||||
): CloudSketchbookTreeModel | undefined {
|
||||
for (const treeWidget of widget.getTreeWidgets()) {
|
||||
if (treeWidget instanceof CloudSketchbookTreeWidget) {
|
||||
const model = treeWidget.model;
|
||||
if (model instanceof CloudSketchbookTreeModel) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
@@ -4,11 +4,13 @@ import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
Emitter,
|
||||
MaybeArray,
|
||||
MaybePromise,
|
||||
nls,
|
||||
notEmpty,
|
||||
} from '@theia/core';
|
||||
import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
|
||||
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
@@ -28,14 +30,15 @@ import * as monaco from '@theia/monaco-editor-core';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
|
||||
import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter';
|
||||
import { OutputUri } from '@theia/output/lib/common/output-uri';
|
||||
import { CoreError } from '../../common/protocol/core-service';
|
||||
import { ErrorRevealStrategy } from '../arduino-preferences';
|
||||
import { InoSelector } from '../ino-selectors';
|
||||
import { fullRange } from '../utils/monaco';
|
||||
import { ArduinoOutputSelector, InoSelector } from '../selectors';
|
||||
import { Contribution } from './contribution';
|
||||
import { CoreErrorHandler } from './core-error-handler';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
|
||||
interface ErrorDecoration {
|
||||
interface ErrorDecorationRef {
|
||||
/**
|
||||
* This is the unique ID of the decoration given by `monaco`.
|
||||
*/
|
||||
@@ -45,72 +48,89 @@ interface ErrorDecoration {
|
||||
*/
|
||||
readonly uri: string;
|
||||
}
|
||||
namespace ErrorDecoration {
|
||||
export function rangeOf(
|
||||
{ id, uri }: ErrorDecoration,
|
||||
editorProvider: (uri: string) => Promise<MonacoEditor | undefined>
|
||||
): Promise<monaco.Range | undefined>;
|
||||
export function rangeOf(
|
||||
{ id, uri }: ErrorDecoration,
|
||||
editorProvider: MonacoEditor
|
||||
): monaco.Range | undefined;
|
||||
export function rangeOf(
|
||||
{ id, uri }: ErrorDecoration,
|
||||
editorProvider:
|
||||
| ((uri: string) => Promise<MonacoEditor | undefined>)
|
||||
| MonacoEditor
|
||||
): MaybePromise<monaco.Range | undefined> {
|
||||
if (editorProvider instanceof MonacoEditor) {
|
||||
const control = editorProvider.getControl();
|
||||
const model = control.getModel();
|
||||
if (model) {
|
||||
return control
|
||||
.getDecorationsInRange(fullRange(model))
|
||||
?.find(({ id: candidateId }) => id === candidateId)?.range;
|
||||
}
|
||||
return undefined;
|
||||
export namespace ErrorDecorationRef {
|
||||
export function is(arg: unknown): arg is ErrorDecorationRef {
|
||||
if (typeof arg === 'object') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const object = arg as any;
|
||||
return (
|
||||
'uri' in object &&
|
||||
typeof object['uri'] === 'string' &&
|
||||
'id' in object &&
|
||||
typeof object['id'] === 'string'
|
||||
);
|
||||
}
|
||||
return editorProvider(uri).then((editor) => {
|
||||
if (editor) {
|
||||
return rangeOf({ id, uri }, editor);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// export async function rangeOf(
|
||||
// { id, uri }: ErrorDecoration,
|
||||
// editorProvider:
|
||||
// | ((uri: string) => Promise<MonacoEditor | undefined>)
|
||||
// | MonacoEditor
|
||||
// ): Promise<monaco.Range | undefined> {
|
||||
// const editor =
|
||||
// editorProvider instanceof MonacoEditor
|
||||
// ? editorProvider
|
||||
// : await editorProvider(uri);
|
||||
// if (editor) {
|
||||
// const control = editor.getControl();
|
||||
// const model = control.getModel();
|
||||
// if (model) {
|
||||
// return control
|
||||
// .getDecorationsInRange(fullRange(model))
|
||||
// ?.find(({ id: candidateId }) => id === candidateId)?.range;
|
||||
// }
|
||||
// }
|
||||
// return undefined;
|
||||
// }
|
||||
export function sameAs(
|
||||
left: ErrorDecoration,
|
||||
right: ErrorDecoration
|
||||
left: ErrorDecorationRef,
|
||||
right: ErrorDecorationRef
|
||||
): boolean {
|
||||
return left.id === right.id && left.uri === right.uri;
|
||||
}
|
||||
}
|
||||
|
||||
interface ErrorDecoration extends ErrorDecorationRef {
|
||||
/**
|
||||
* The range of the error location the error in the compiler output from the CLI.
|
||||
*/
|
||||
readonly rangesInOutput: monaco.Range[];
|
||||
}
|
||||
namespace ErrorDecoration {
|
||||
export function rangeOf(
|
||||
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||
decorations: ErrorDecoration
|
||||
): monaco.Range | undefined;
|
||||
export function rangeOf(
|
||||
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||
decorations: ErrorDecoration[]
|
||||
): (monaco.Range | undefined)[];
|
||||
export function rangeOf(
|
||||
editorOrModel: MonacoEditor | ITextModel | undefined,
|
||||
decorations: ErrorDecoration | ErrorDecoration[]
|
||||
): MaybePromise<MaybeArray<monaco.Range | undefined>> {
|
||||
if (editorOrModel) {
|
||||
const allDecorations = getAllDecorations(editorOrModel);
|
||||
if (allDecorations) {
|
||||
if (Array.isArray(decorations)) {
|
||||
return decorations.map(({ id: decorationId }) =>
|
||||
findRangeOf(decorationId, allDecorations)
|
||||
);
|
||||
} else {
|
||||
return findRangeOf(decorations.id, allDecorations);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.isArray(decorations)
|
||||
? decorations.map(() => undefined)
|
||||
: undefined;
|
||||
}
|
||||
function findRangeOf(
|
||||
decorationId: string,
|
||||
allDecorations: { id: string; range?: monaco.Range }[]
|
||||
): monaco.Range | undefined {
|
||||
return allDecorations.find(
|
||||
({ id: candidateId }) => candidateId === decorationId
|
||||
)?.range;
|
||||
}
|
||||
function getAllDecorations(
|
||||
editorOrModel: MonacoEditor | ITextModel
|
||||
): { id: string; range?: monaco.Range }[] {
|
||||
if (editorOrModel instanceof MonacoEditor) {
|
||||
const model = editorOrModel.getControl().getModel();
|
||||
if (!model) {
|
||||
return [];
|
||||
}
|
||||
return model.getAllDecorations();
|
||||
}
|
||||
return editorOrModel.getAllDecorations();
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class CompilerErrors
|
||||
extends Contribution
|
||||
implements monaco.languages.CodeLensProvider
|
||||
implements monaco.languages.CodeLensProvider, monaco.languages.LinkProvider
|
||||
{
|
||||
@inject(EditorManager)
|
||||
private readonly editorManager: EditorManager;
|
||||
@@ -119,11 +139,14 @@ export class CompilerErrors
|
||||
private readonly p2m: ProtocolToMonacoConverter;
|
||||
|
||||
@inject(MonacoToProtocolConverter)
|
||||
private readonly mp2: MonacoToProtocolConverter;
|
||||
private readonly m2p: MonacoToProtocolConverter;
|
||||
|
||||
@inject(CoreErrorHandler)
|
||||
private readonly coreErrorHandler: CoreErrorHandler;
|
||||
|
||||
private revealStrategy = ErrorRevealStrategy.Default;
|
||||
private experimental = false;
|
||||
|
||||
private readonly errors: ErrorDecoration[] = [];
|
||||
private readonly onDidChangeEmitter = new monaco.Emitter<this>();
|
||||
private readonly currentErrorDidChangEmitter = new Emitter<ErrorDecoration>();
|
||||
@@ -131,8 +154,8 @@ export class CompilerErrors
|
||||
this.currentErrorDidChangEmitter.event;
|
||||
private readonly toDisposeOnCompilerErrorDidChange =
|
||||
new DisposableCollection();
|
||||
|
||||
private shell: ApplicationShell | undefined;
|
||||
private revealStrategy = ErrorRevealStrategy.Default;
|
||||
private currentError: ErrorDecoration | undefined;
|
||||
private get currentErrorIndex(): number {
|
||||
const current = this.currentError;
|
||||
@@ -140,46 +163,75 @@ export class CompilerErrors
|
||||
return -1;
|
||||
}
|
||||
return this.errors.findIndex((error) =>
|
||||
ErrorDecoration.sameAs(error, current)
|
||||
ErrorDecorationRef.sameAs(error, current)
|
||||
);
|
||||
}
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.shell = app.shell;
|
||||
monaco.languages.registerCodeLensProvider(InoSelector, this);
|
||||
monaco.languages.registerLinkProvider(ArduinoOutputSelector, this);
|
||||
this.coreErrorHandler.onCompilerErrorsDidChange((errors) =>
|
||||
this.filter(errors).then(this.handleCompilerErrorsDidChange.bind(this))
|
||||
this.handleCompilerErrorsDidChange(errors)
|
||||
);
|
||||
this.onCurrentErrorDidChange(async (error) => {
|
||||
const range = await ErrorDecoration.rangeOf(error, (uri) =>
|
||||
this.monacoEditor(uri)
|
||||
);
|
||||
if (!range) {
|
||||
const monacoEditor = await this.monacoEditor(error.uri);
|
||||
const monacoRange = ErrorDecoration.rangeOf(monacoEditor, error);
|
||||
if (!monacoRange) {
|
||||
console.warn(
|
||||
'compiler-errors',
|
||||
`Could not find range of decoration: ${error.id}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const range = this.m2p.asRange(monacoRange);
|
||||
const editor = await this.revealLocationInEditor({
|
||||
uri: error.uri,
|
||||
range: this.mp2.asRange(range),
|
||||
range,
|
||||
});
|
||||
if (!editor) {
|
||||
console.warn(
|
||||
'compiler-errors',
|
||||
`Failed to mark error ${error.id} as the current one.`
|
||||
);
|
||||
} else {
|
||||
const monacoEditor = this.monacoEditor(editor);
|
||||
if (monacoEditor) {
|
||||
monacoEditor.cursor = range.start;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override onReady(): MaybePromise<void> {
|
||||
this.preferences.ready.then(() => {
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (preferenceName === 'arduino.compile.revealRange') {
|
||||
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
||||
? newValue
|
||||
: ErrorRevealStrategy.Default;
|
||||
this.experimental = Boolean(
|
||||
this.preferences['arduino.compile.experimental']
|
||||
);
|
||||
const strategy = this.preferences['arduino.compile.revealRange'];
|
||||
this.revealStrategy = ErrorRevealStrategy.is(strategy)
|
||||
? strategy
|
||||
: ErrorRevealStrategy.Default;
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, newValue, oldValue }) => {
|
||||
if (newValue === oldValue) {
|
||||
return;
|
||||
}
|
||||
switch (preferenceName) {
|
||||
case 'arduino.compile.revealRange': {
|
||||
this.revealStrategy = ErrorRevealStrategy.is(newValue)
|
||||
? newValue
|
||||
: ErrorRevealStrategy.Default;
|
||||
return;
|
||||
}
|
||||
case 'arduino.compile.experimental': {
|
||||
this.experimental = Boolean(newValue);
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,9 +248,13 @@ export class CompilerErrors
|
||||
}
|
||||
const nextError =
|
||||
this.errors[index === this.errors.length - 1 ? 0 : index + 1];
|
||||
this.markAsCurrentError(nextError);
|
||||
return this.markAsCurrentError(nextError, {
|
||||
forceReselect: true,
|
||||
reveal: true,
|
||||
});
|
||||
},
|
||||
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
||||
isEnabled: () =>
|
||||
this.experimental && !!this.currentError && this.errors.length > 1,
|
||||
});
|
||||
registry.registerCommand(CompilerErrors.Commands.PREVIOUS_ERROR, {
|
||||
execute: () => {
|
||||
@@ -212,9 +268,24 @@ export class CompilerErrors
|
||||
}
|
||||
const previousError =
|
||||
this.errors[index === 0 ? this.errors.length - 1 : index - 1];
|
||||
this.markAsCurrentError(previousError);
|
||||
return this.markAsCurrentError(previousError, {
|
||||
forceReselect: true,
|
||||
reveal: true,
|
||||
});
|
||||
},
|
||||
isEnabled: () => !!this.currentError && this.errors.length > 1,
|
||||
isEnabled: () =>
|
||||
this.experimental && !!this.currentError && this.errors.length > 1,
|
||||
});
|
||||
registry.registerCommand(CompilerErrors.Commands.MARK_AS_CURRENT, {
|
||||
execute: (arg: unknown) => {
|
||||
if (ErrorDecorationRef.is(arg)) {
|
||||
return this.markAsCurrentError(
|
||||
{ id: arg.id, uri: new URI(arg.uri).toString() }, // Make sure the URI fragments are encoded. On Windows, `C:` is encoded as `C%3A`.
|
||||
{ forceReselect: true, reveal: true }
|
||||
);
|
||||
}
|
||||
},
|
||||
isEnabled: () => !!this.errors.length,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,13 +300,13 @@ export class CompilerErrors
|
||||
): Promise<monaco.languages.CodeLensList> {
|
||||
const lenses: monaco.languages.CodeLens[] = [];
|
||||
if (
|
||||
this.experimental &&
|
||||
this.currentError &&
|
||||
this.currentError.uri === model.uri.toString() &&
|
||||
this.errors.length > 1
|
||||
) {
|
||||
const range = await ErrorDecoration.rangeOf(this.currentError, (uri) =>
|
||||
this.monacoEditor(uri)
|
||||
);
|
||||
const monacoEditor = await this.monacoEditor(model.uri);
|
||||
const range = ErrorDecoration.rangeOf(monacoEditor, this.currentError);
|
||||
if (range) {
|
||||
lenses.push(
|
||||
{
|
||||
@@ -268,14 +339,81 @@ export class CompilerErrors
|
||||
};
|
||||
}
|
||||
|
||||
async provideLinks(
|
||||
model: monaco.editor.ITextModel,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_token: monaco.CancellationToken
|
||||
): Promise<monaco.languages.ILinksList> {
|
||||
const links: monaco.languages.ILink[] = [];
|
||||
if (
|
||||
model.uri.scheme === OutputUri.SCHEME &&
|
||||
model.uri.path === '/Arduino'
|
||||
) {
|
||||
links.push(
|
||||
...this.errors
|
||||
.filter((decoration) => !!decoration.rangesInOutput.length)
|
||||
.map(({ rangesInOutput, id, uri }) =>
|
||||
rangesInOutput.map(
|
||||
(range) =>
|
||||
<monaco.languages.ILink>{
|
||||
range,
|
||||
url: monaco.Uri.parse(`command://`).with({
|
||||
query: JSON.stringify({ id, uri }),
|
||||
path: CompilerErrors.Commands.MARK_AS_CURRENT.id,
|
||||
}),
|
||||
tooltip: nls.localize(
|
||||
'arduino/editor/revealError',
|
||||
'Reveal Error'
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
.reduce((acc, curr) => acc.concat(curr), [])
|
||||
);
|
||||
} else {
|
||||
console.warn('unexpected URI: ' + model.uri.toString());
|
||||
}
|
||||
return { links };
|
||||
}
|
||||
|
||||
async resolveLink(
|
||||
link: monaco.languages.ILink,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_token: monaco.CancellationToken
|
||||
): Promise<monaco.languages.ILink | undefined> {
|
||||
if (!this.experimental) {
|
||||
return undefined;
|
||||
}
|
||||
const { url } = link;
|
||||
if (url) {
|
||||
const candidateUri = new URI(
|
||||
typeof url === 'string' ? url : url.toString()
|
||||
);
|
||||
const candidateId = candidateUri.path.toString();
|
||||
const error = this.errors.find((error) => error.id === candidateId);
|
||||
if (error) {
|
||||
const monacoEditor = await this.monacoEditor(error.uri);
|
||||
const range = ErrorDecoration.rangeOf(monacoEditor, error);
|
||||
if (range) {
|
||||
return {
|
||||
range,
|
||||
url: monaco.Uri.parse(error.uri),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async handleCompilerErrorsDidChange(
|
||||
errors: CoreError.ErrorLocation[]
|
||||
): Promise<void> {
|
||||
this.toDisposeOnCompilerErrorDidChange.dispose();
|
||||
const compilerErrorsPerResource = this.groupByResource(
|
||||
await this.filter(errors)
|
||||
const groupedErrors = this.groupBy(
|
||||
errors,
|
||||
(error: CoreError.ErrorLocation) => error.location.uri
|
||||
);
|
||||
const decorations = await this.decorateEditors(compilerErrorsPerResource);
|
||||
const decorations = await this.decorateEditors(groupedErrors);
|
||||
this.errors.push(...decorations.errors);
|
||||
this.toDisposeOnCompilerErrorDidChange.pushAll([
|
||||
Disposable.create(() => (this.errors.length = 0)),
|
||||
@@ -283,17 +421,17 @@ export class CompilerErrors
|
||||
...(await Promise.all([
|
||||
decorations.dispose,
|
||||
this.trackEditors(
|
||||
compilerErrorsPerResource,
|
||||
groupedErrors,
|
||||
(editor) =>
|
||||
editor.editor.onSelectionChanged((selection) =>
|
||||
editor.onSelectionChanged((selection) =>
|
||||
this.handleSelectionChange(editor, selection)
|
||||
),
|
||||
(editor) =>
|
||||
editor.onDidDispose(() =>
|
||||
this.handleEditorDidDispose(editor.editor.uri.toString())
|
||||
editor.onDispose(() =>
|
||||
this.handleEditorDidDispose(editor.uri.toString())
|
||||
),
|
||||
(editor) =>
|
||||
editor.editor.onDocumentContentChanged((event) =>
|
||||
editor.onDocumentContentChanged((event) =>
|
||||
this.handleDocumentContentChange(editor, event)
|
||||
)
|
||||
),
|
||||
@@ -301,24 +439,13 @@ export class CompilerErrors
|
||||
]);
|
||||
const currentError = this.errors[0];
|
||||
if (currentError) {
|
||||
await this.markAsCurrentError(currentError);
|
||||
await this.markAsCurrentError(currentError, {
|
||||
forceReselect: true,
|
||||
reveal: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async filter(
|
||||
errors: CoreError.ErrorLocation[]
|
||||
): Promise<CoreError.ErrorLocation[]> {
|
||||
if (!errors.length) {
|
||||
return [];
|
||||
}
|
||||
await this.preferences.ready;
|
||||
if (this.preferences['arduino.compile.experimental']) {
|
||||
return errors;
|
||||
}
|
||||
// Always shows maximum one error; hence the code lens navigation is unavailable.
|
||||
return [errors[0]];
|
||||
}
|
||||
|
||||
private async decorateEditors(
|
||||
errors: Map<string, CoreError.ErrorLocation[]>
|
||||
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||
@@ -342,11 +469,11 @@ export class CompilerErrors
|
||||
uri: string,
|
||||
errors: CoreError.ErrorLocation[]
|
||||
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
|
||||
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||
const editor = await this.monacoEditor(uri);
|
||||
if (!editor) {
|
||||
return { dispose: Disposable.NULL, errors: [] };
|
||||
}
|
||||
const oldDecorations = editor.editor.deltaDecorations({
|
||||
const oldDecorations = editor.deltaDecorations({
|
||||
oldDecorations: [],
|
||||
newDecorations: errors.map((error) =>
|
||||
this.compilerErrorDecoration(error.location.range)
|
||||
@@ -355,13 +482,19 @@ export class CompilerErrors
|
||||
return {
|
||||
dispose: Disposable.create(() => {
|
||||
if (editor) {
|
||||
editor.editor.deltaDecorations({
|
||||
editor.deltaDecorations({
|
||||
oldDecorations,
|
||||
newDecorations: [],
|
||||
});
|
||||
}
|
||||
}),
|
||||
errors: oldDecorations.map((id) => ({ id, uri })),
|
||||
errors: oldDecorations.map((id, index) => ({
|
||||
id,
|
||||
uri,
|
||||
rangesInOutput: errors[index].rangesInOutput.map((range) =>
|
||||
this.p2m.asRange(range)
|
||||
),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -371,7 +504,7 @@ export class CompilerErrors
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: 'compiler-error',
|
||||
stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges,
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -379,11 +512,10 @@ export class CompilerErrors
|
||||
/**
|
||||
* Tracks the selection in all editors that have an error. If the editor selection overlaps one of the compiler error's range, mark as current error.
|
||||
*/
|
||||
private handleSelectionChange(editor: EditorWidget, selection: Range): void {
|
||||
const monacoEditor = this.monacoEditor(editor);
|
||||
if (!monacoEditor) {
|
||||
return;
|
||||
}
|
||||
private handleSelectionChange(
|
||||
monacoEditor: MonacoEditor,
|
||||
selection: Range
|
||||
): void {
|
||||
const uri = monacoEditor.uri.toString();
|
||||
const monacoSelection = this.p2m.asRange(selection);
|
||||
console.log(
|
||||
@@ -418,12 +550,13 @@ export class CompilerErrors
|
||||
console.trace('No match');
|
||||
return undefined;
|
||||
};
|
||||
const error = this.errors
|
||||
.filter((error) => error.uri === uri)
|
||||
.map((error) => ({
|
||||
error,
|
||||
range: ErrorDecoration.rangeOf(error, monacoEditor),
|
||||
}))
|
||||
const errorsPerResource = this.errors.filter((error) => error.uri === uri);
|
||||
const rangesPerResource = ErrorDecoration.rangeOf(
|
||||
monacoEditor,
|
||||
errorsPerResource
|
||||
);
|
||||
const error = rangesPerResource
|
||||
.map((range, index) => ({ error: errorsPerResource[index], range }))
|
||||
.map(({ error, range }) => {
|
||||
if (range) {
|
||||
const priority = calculatePriority(range, monacoSelection);
|
||||
@@ -464,66 +597,77 @@ export class CompilerErrors
|
||||
}
|
||||
|
||||
/**
|
||||
* If a document change "destroys" the range of the decoration, the decoration must be removed.
|
||||
* If the text document changes in the line where compiler errors are, the compiler errors will be removed.
|
||||
*/
|
||||
private handleDocumentContentChange(
|
||||
editor: EditorWidget,
|
||||
monacoEditor: MonacoEditor,
|
||||
event: TextDocumentChangeEvent
|
||||
): void {
|
||||
const monacoEditor = this.monacoEditor(editor);
|
||||
if (!monacoEditor) {
|
||||
return;
|
||||
}
|
||||
// A decoration location can be "destroyed", hence should be deleted when:
|
||||
// - deleting range (start != end AND text is empty)
|
||||
// - inserting text into range (start != end AND text is not empty)
|
||||
// Filter unrelated delta changes to spare the CPU.
|
||||
const relevantChanges = event.contentChanges.filter(
|
||||
({ range: { start, end } }) =>
|
||||
start.line !== end.line || start.character !== end.character
|
||||
const errorsPerResource = this.errors.filter(
|
||||
(error) => error.uri === event.document.uri
|
||||
);
|
||||
if (!relevantChanges.length) {
|
||||
return;
|
||||
let editorOrModel: MonacoEditor | ITextModel = monacoEditor;
|
||||
const doc = event.document;
|
||||
if (doc instanceof MonacoEditorModel) {
|
||||
editorOrModel = doc.textEditorModel;
|
||||
}
|
||||
|
||||
const resolvedMarkers = this.errors
|
||||
.filter((error) => error.uri === event.document.uri)
|
||||
.map((error, index) => {
|
||||
const range = ErrorDecoration.rangeOf(error, monacoEditor);
|
||||
if (range) {
|
||||
return { error, range, index };
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(notEmpty);
|
||||
|
||||
const decorationIdsToRemove = relevantChanges
|
||||
const rangesPerResource = ErrorDecoration.rangeOf(
|
||||
editorOrModel,
|
||||
errorsPerResource
|
||||
);
|
||||
const resolvedDecorations = rangesPerResource.map((range, index) => ({
|
||||
error: errorsPerResource[index],
|
||||
range,
|
||||
}));
|
||||
const decoratorsToRemove = event.contentChanges
|
||||
.map(({ range }) => this.p2m.asRange(range))
|
||||
.map((changeRange) =>
|
||||
resolvedMarkers.filter(({ range: decorationRange }) =>
|
||||
changeRange.containsRange(decorationRange)
|
||||
)
|
||||
.map((changedRange) =>
|
||||
resolvedDecorations
|
||||
.filter(({ range: decorationRange }) => {
|
||||
if (!decorationRange) {
|
||||
return false;
|
||||
}
|
||||
const affects =
|
||||
changedRange.startLineNumber <= decorationRange.startLineNumber &&
|
||||
changedRange.endLineNumber >= decorationRange.endLineNumber;
|
||||
console.log(
|
||||
'compiler-errors',
|
||||
`decoration range: ${decorationRange.toString()}, change range: ${changedRange.toString()}, affects: ${affects}`
|
||||
);
|
||||
return affects;
|
||||
})
|
||||
.map(({ error }) => {
|
||||
const index = this.errors.findIndex((candidate) =>
|
||||
ErrorDecorationRef.sameAs(candidate, error)
|
||||
);
|
||||
return index !== -1 ? { error, index } : undefined;
|
||||
})
|
||||
.filter(notEmpty)
|
||||
)
|
||||
.reduce((acc, curr) => acc.concat(curr), [])
|
||||
.map(({ error, index }) => {
|
||||
this.errors.splice(index, 1);
|
||||
return error.id;
|
||||
});
|
||||
if (!decorationIdsToRemove.length) {
|
||||
return;
|
||||
.sort((left, right) => left.index - right.index); // highest index last
|
||||
|
||||
if (decoratorsToRemove.length) {
|
||||
let i = decoratorsToRemove.length;
|
||||
while (i--) {
|
||||
this.errors.splice(decoratorsToRemove[i].index, 1);
|
||||
}
|
||||
monacoEditor.getControl().deltaDecorations(
|
||||
decoratorsToRemove.map(({ error }) => error.id),
|
||||
[]
|
||||
);
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
}
|
||||
monacoEditor.getControl().deltaDecorations(decorationIdsToRemove, []);
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
}
|
||||
|
||||
private async trackEditors(
|
||||
errors: Map<string, CoreError.ErrorLocation[]>,
|
||||
...track: ((editor: EditorWidget) => Disposable)[]
|
||||
...track: ((editor: MonacoEditor) => Disposable)[]
|
||||
): Promise<Disposable> {
|
||||
return new DisposableCollection(
|
||||
...(await Promise.all(
|
||||
Array.from(errors.keys()).map(async (uri) => {
|
||||
const editor = await this.editorManager.getByUri(new URI(uri));
|
||||
const editor = await this.monacoEditor(uri);
|
||||
if (!editor) {
|
||||
return Disposable.NULL;
|
||||
}
|
||||
@@ -533,15 +677,18 @@ export class CompilerErrors
|
||||
);
|
||||
}
|
||||
|
||||
private async markAsCurrentError(error: ErrorDecoration): Promise<void> {
|
||||
private async markAsCurrentError(
|
||||
ref: ErrorDecorationRef,
|
||||
options?: { forceReselect?: boolean; reveal?: boolean }
|
||||
): Promise<void> {
|
||||
const index = this.errors.findIndex((candidate) =>
|
||||
ErrorDecoration.sameAs(candidate, error)
|
||||
ErrorDecorationRef.sameAs(candidate, ref)
|
||||
);
|
||||
if (index < 0) {
|
||||
console.warn(
|
||||
'compiler-errors',
|
||||
`Failed to mark error ${
|
||||
error.id
|
||||
ref.id
|
||||
} as the current one. Error is unknown. Known errors are: ${this.errors.map(
|
||||
({ id }) => id
|
||||
)}`
|
||||
@@ -550,15 +697,18 @@ export class CompilerErrors
|
||||
}
|
||||
const newError = this.errors[index];
|
||||
if (
|
||||
options?.forceReselect ||
|
||||
!this.currentError ||
|
||||
!ErrorDecoration.sameAs(this.currentError, newError)
|
||||
!ErrorDecorationRef.sameAs(this.currentError, newError)
|
||||
) {
|
||||
this.currentError = this.errors[index];
|
||||
console.log(
|
||||
'compiler-errors',
|
||||
`Current error changed to ${this.currentError.id}`
|
||||
);
|
||||
this.currentErrorDidChangEmitter.fire(this.currentError);
|
||||
if (options?.reveal) {
|
||||
this.currentErrorDidChangEmitter.fire(this.currentError);
|
||||
}
|
||||
this.onDidChangeEmitter.fire(this);
|
||||
}
|
||||
}
|
||||
@@ -593,32 +743,33 @@ export class CompilerErrors
|
||||
}
|
||||
console.warn(
|
||||
'compiler-errors',
|
||||
`could not found editor widget for URI: ${uri}`
|
||||
`could not find editor widget for URI: ${uri}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private groupByResource(
|
||||
errors: CoreError.ErrorLocation[]
|
||||
): Map<string, CoreError.ErrorLocation[]> {
|
||||
return errors.reduce((acc, curr) => {
|
||||
const {
|
||||
location: { uri },
|
||||
} = curr;
|
||||
let errors = acc.get(uri);
|
||||
if (!errors) {
|
||||
errors = [];
|
||||
acc.set(uri, errors);
|
||||
private groupBy<K, V>(
|
||||
elements: V[],
|
||||
extractKey: (element: V) => K
|
||||
): Map<K, V[]> {
|
||||
return elements.reduce((acc, curr) => {
|
||||
const key = extractKey(curr);
|
||||
let values = acc.get(key);
|
||||
if (!values) {
|
||||
values = [];
|
||||
acc.set(key, values);
|
||||
}
|
||||
errors.push(curr);
|
||||
values.push(curr);
|
||||
return acc;
|
||||
}, new Map<string, CoreError.ErrorLocation[]>());
|
||||
}, new Map<K, V[]>());
|
||||
}
|
||||
|
||||
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;
|
||||
private monacoEditor(uri: string): Promise<MonacoEditor | undefined>;
|
||||
private monacoEditor(
|
||||
uriOrWidget: string | EditorWidget
|
||||
uri: string | monaco.Uri
|
||||
): Promise<MonacoEditor | undefined>;
|
||||
private monacoEditor(
|
||||
uriOrWidget: string | monaco.Uri | EditorWidget
|
||||
): MaybePromise<MonacoEditor | undefined> {
|
||||
if (uriOrWidget instanceof EditorWidget) {
|
||||
const editor = uriOrWidget.editor;
|
||||
@@ -646,5 +797,8 @@ export namespace CompilerErrors {
|
||||
export const PREVIOUS_ERROR: Command = {
|
||||
id: 'arduino-editor-previous-error',
|
||||
};
|
||||
export const MARK_AS_CURRENT: Command = {
|
||||
id: 'arduino-editor-mark-as-current-error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -6,15 +6,18 @@ import {
|
||||
} from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { Saveable } from '@theia/core/lib/browser/saveable';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||
|
||||
import {
|
||||
MenuModelRegistry,
|
||||
MenuContribution,
|
||||
@@ -41,10 +44,9 @@ import { SettingsService } from '../dialogs/settings/settings';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../common/protocol/sketches-service-client-impl';
|
||||
} from '../sketches-service-client-impl';
|
||||
import {
|
||||
SketchesService,
|
||||
ConfigService,
|
||||
FileSystemExt,
|
||||
Sketch,
|
||||
CoreService,
|
||||
@@ -59,6 +61,12 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { WorkspaceService } from '../theia/workspace/workspace-service';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { ConfigServiceClient } from '../config/config-service-client';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
|
||||
export {
|
||||
Command,
|
||||
@@ -104,6 +112,9 @@ export abstract class Contribution
|
||||
@inject(FrontendApplicationStateService)
|
||||
protected readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.appStateService.reachedState('ready').then(() => this.onReady());
|
||||
@@ -136,11 +147,11 @@ export abstract class SketchContribution extends Contribution {
|
||||
@inject(FileSystemExt)
|
||||
protected readonly fileSystemExt: FileSystemExt;
|
||||
|
||||
@inject(ConfigService)
|
||||
protected readonly configService: ConfigService;
|
||||
@inject(ConfigServiceClient)
|
||||
protected readonly configService: ConfigServiceClient;
|
||||
|
||||
@inject(SketchesService)
|
||||
protected readonly sketchService: SketchesService;
|
||||
protected readonly sketchesService: SketchesService;
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
@@ -154,6 +165,9 @@ export abstract class SketchContribution extends Contribution {
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
protected async sourceOverride(): Promise<Record<string, string>> {
|
||||
const override: Record<string, string> = {};
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
@@ -167,6 +181,25 @@ export abstract class SketchContribution extends Contribution {
|
||||
}
|
||||
return override;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults to `directories.user` if defined and not CLI config errors were detected.
|
||||
* Otherwise, the URI of the user home directory.
|
||||
*/
|
||||
protected async defaultUri(): Promise<URI> {
|
||||
const errors = this.configService.tryGetMessages();
|
||||
let defaultUri = this.configService.tryGetSketchDirUri();
|
||||
if (!defaultUri || errors?.length) {
|
||||
// Fall back to user home when the `directories.user` is not available or there are known CLI config errors
|
||||
defaultUri = new URI(await this.envVariableServer.getHomeDirUri());
|
||||
}
|
||||
return defaultUri;
|
||||
}
|
||||
|
||||
protected async defaultPath(): Promise<string> {
|
||||
const defaultUri = await this.defaultUri();
|
||||
return this.fileService.fsPath(defaultUri);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
@@ -186,6 +219,25 @@ export abstract class CoreServiceContribution extends SketchContribution {
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(NotificationManager)
|
||||
private readonly notificationManager: NotificationManager;
|
||||
|
||||
@inject(ApplicationShell)
|
||||
private readonly shell: ApplicationShell;
|
||||
|
||||
/**
|
||||
* This is the internal (Theia) ID of the notification that is currently visible.
|
||||
* It's stored here as a field to be able to close it before executing any new core command (such as verify, upload, etc.)
|
||||
*/
|
||||
private visibleNotificationId: string | undefined;
|
||||
|
||||
protected clearVisibleNotification(): void {
|
||||
if (this.visibleNotificationId) {
|
||||
this.notificationManager.clear(this.visibleNotificationId);
|
||||
this.visibleNotificationId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected handleError(error: unknown): void {
|
||||
this.tryToastErrorMessage(error);
|
||||
}
|
||||
@@ -204,10 +256,17 @@ export abstract class CoreServiceContribution extends SketchContribution {
|
||||
} catch {}
|
||||
}
|
||||
if (message) {
|
||||
if (message.includes('Missing FQBN (Fully Qualified Board Name)')) {
|
||||
message = nls.localize(
|
||||
'arduino/coreContribution/noBoardSelected',
|
||||
'No board selected. Please select your Arduino board from the Tools > Board menu.'
|
||||
);
|
||||
}
|
||||
const copyAction = nls.localize(
|
||||
'arduino/coreContribution/copyError',
|
||||
'Copy error messages'
|
||||
);
|
||||
this.visibleNotificationId = this.notificationId(message, copyAction);
|
||||
this.messageService.error(message, copyAction).then(async (action) => {
|
||||
if (action === copyAction) {
|
||||
const content = await this.outputChannelManager.contentOfChannel(
|
||||
@@ -228,6 +287,9 @@ export abstract class CoreServiceContribution extends SketchContribution {
|
||||
keepOutput?: boolean;
|
||||
task: (progressId: string, coreService: CoreService) => Promise<T>;
|
||||
}): Promise<T> {
|
||||
const toDisposeOnComplete = new DisposableCollection(
|
||||
this.maybeActivateMonitorWidget()
|
||||
);
|
||||
const { progressText, keepOutput, task } = options;
|
||||
this.outputChannelManager
|
||||
.getChannel('Arduino')
|
||||
@@ -239,8 +301,31 @@ export abstract class CoreServiceContribution extends SketchContribution {
|
||||
run: ({ progressId }) => task(progressId, this.coreService),
|
||||
keepOutput,
|
||||
});
|
||||
toDisposeOnComplete.dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: cleanup!
|
||||
// this dependency does not belong here
|
||||
// support core command contribution handlers, the monitor-widget should implement it and register itself as a handler
|
||||
// the monitor widget should reveal itself after a successful core command execution
|
||||
private maybeActivateMonitorWidget(): Disposable {
|
||||
const currentWidget = this.shell.bottomPanel.currentTitle?.owner;
|
||||
if (currentWidget?.id === 'serial-monitor') {
|
||||
return Disposable.create(() =>
|
||||
this.shell.bottomPanel.activateWidget(currentWidget)
|
||||
);
|
||||
}
|
||||
return Disposable.NULL;
|
||||
}
|
||||
|
||||
private notificationId(message: string, ...actions: string[]): string {
|
||||
return this.notificationManager['getMessageId']({
|
||||
text: message,
|
||||
actions,
|
||||
type: MessageType.Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Contribution {
|
||||
|
@@ -0,0 +1,118 @@
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell';
|
||||
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Create } from '../create/typings';
|
||||
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { SketchbookTree } from '../widgets/sketchbook/sketchbook-tree';
|
||||
import { SketchbookTreeModel } from '../widgets/sketchbook/sketchbook-tree-model';
|
||||
import { CloudSketchContribution, pushingSketch } from './cloud-contribution';
|
||||
import {
|
||||
CreateNewCloudSketchCallback,
|
||||
NewCloudSketch,
|
||||
NewCloudSketchParams,
|
||||
} from './new-cloud-sketch';
|
||||
import { saveOntoCopiedSketch } from './save-as-sketch';
|
||||
|
||||
interface CreateCloudCopyParams {
|
||||
readonly model: SketchbookTreeModel;
|
||||
readonly node: SketchbookTree.SketchDirNode;
|
||||
}
|
||||
function isCreateCloudCopyParams(arg: unknown): arg is CreateCloudCopyParams {
|
||||
return (
|
||||
typeof arg === 'object' &&
|
||||
(<CreateCloudCopyParams>arg).model !== undefined &&
|
||||
(<CreateCloudCopyParams>arg).model instanceof SketchbookTreeModel &&
|
||||
(<CreateCloudCopyParams>arg).node !== undefined &&
|
||||
SketchbookTree.SketchDirNode.is((<CreateCloudCopyParams>arg).node)
|
||||
);
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class CreateCloudCopy extends CloudSketchContribution {
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
private shell: ApplicationShell;
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.shell = app.shell;
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(CreateCloudCopy.Commands.CREATE_CLOUD_COPY, {
|
||||
execute: (args: CreateCloudCopyParams) => this.createCloudCopy(args),
|
||||
isEnabled: (args: unknown) =>
|
||||
Boolean(this.createFeatures.session) && isCreateCloudCopyParams(args),
|
||||
isVisible: (args: unknown) =>
|
||||
Boolean(this.createFeatures.enabled) &&
|
||||
Boolean(this.createFeatures.session) &&
|
||||
this.connectionStatus.offlineStatus !== 'internet' &&
|
||||
isCreateCloudCopyParams(args),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* - creates new cloud sketch with the name of the params sketch,
|
||||
* - pulls the cloud sketch,
|
||||
* - copies files from params sketch to pulled cloud sketch in the cache folder,
|
||||
* - pushes the cloud sketch, and
|
||||
* - opens in new window.
|
||||
*/
|
||||
private async createCloudCopy(params: CreateCloudCopyParams): Promise<void> {
|
||||
const sketch = await this.sketchesService.loadSketch(
|
||||
params.node.fileStat.resource.toString()
|
||||
);
|
||||
const callback: CreateNewCloudSketchCallback = async (
|
||||
newSketch: Create.Sketch,
|
||||
newNode: CloudSketchbookTree.CloudSketchDirNode,
|
||||
progress: Progress
|
||||
) => {
|
||||
const treeModel = await this.treeModel();
|
||||
if (!treeModel) {
|
||||
throw new Error('Could not retrieve the cloud sketchbook tree model.');
|
||||
}
|
||||
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/createCloudCopy/copyingSketchFilesMessage',
|
||||
'Copying local sketch files...'
|
||||
),
|
||||
});
|
||||
const localCacheFolderUri = newNode.uri.toString();
|
||||
await this.sketchesService.copy(sketch, {
|
||||
destinationUri: localCacheFolderUri,
|
||||
onlySketchFiles: true,
|
||||
});
|
||||
await saveOntoCopiedSketch(
|
||||
sketch,
|
||||
localCacheFolderUri,
|
||||
this.shell,
|
||||
this.editorManager
|
||||
);
|
||||
|
||||
progress.report({ message: pushingSketch(newSketch.name) });
|
||||
await treeModel.sketchbookTree().push(newNode, true, true);
|
||||
};
|
||||
return this.commandService.executeCommand(
|
||||
NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
<NewCloudSketchParams>{
|
||||
initialValue: params.node.fileStat.name,
|
||||
callback,
|
||||
skipShowErrorMessageOnOpen: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CreateCloudCopy {
|
||||
export namespace Commands {
|
||||
export const CREATE_CLOUD_COPY: Command = {
|
||||
id: 'arduino-create-cloud-copy',
|
||||
iconClass: 'fa fa-arduino-cloud-upload',
|
||||
};
|
||||
}
|
||||
}
|
@@ -3,7 +3,12 @@ import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board, BoardsService, ExecutableService } from '../../common/protocol';
|
||||
import {
|
||||
Board,
|
||||
BoardsService,
|
||||
ExecutableService,
|
||||
Sketch,
|
||||
} from '../../common/protocol';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import {
|
||||
URI,
|
||||
@@ -13,12 +18,11 @@ import {
|
||||
TabBarToolbarRegistry,
|
||||
} from './contribution';
|
||||
import { MaybePromise, MenuModelRegistry, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
|
||||
const COMPILE_FOR_DEBUG_KEY = 'arduino-compile-for-debug';
|
||||
|
||||
@injectable()
|
||||
export class Debug extends SketchContribution {
|
||||
@inject(HostedPluginSupport)
|
||||
@@ -36,9 +40,6 @@ export class Debug extends SketchContribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
private readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
/**
|
||||
* If `undefined`, debugging is enabled. Otherwise, the reason why it's disabled.
|
||||
*/
|
||||
@@ -186,7 +187,7 @@ export class Debug extends SketchContribution {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const ideTempFolderUri = await this.sketchService.getIdeTempFolderUri(
|
||||
const ideTempFolderUri = await this.sketchesService.getIdeTempFolderUri(
|
||||
sketch
|
||||
);
|
||||
const [cliPath, sketchPath, configPath] = await Promise.all([
|
||||
@@ -203,7 +204,28 @@ export class Debug extends SketchContribution {
|
||||
sketchPath,
|
||||
configPath,
|
||||
};
|
||||
return this.commandService.executeCommand('arduino.debug.start', config);
|
||||
try {
|
||||
await this.commandService.executeCommand('arduino.debug.start', config);
|
||||
} catch (err) {
|
||||
if (await this.isSketchNotVerifiedError(err, sketch)) {
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
const answer = await this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/debug/sketchIsNotCompiled',
|
||||
"Sketch '{0}' must be verified before starting a debug session. Please verify the sketch and start debugging again. Do you want to verify the sketch now?",
|
||||
sketch.name
|
||||
),
|
||||
yes
|
||||
);
|
||||
if (answer === yes) {
|
||||
this.commandService.executeCommand('arduino-verify-sketch');
|
||||
}
|
||||
} else {
|
||||
this.messageService.error(
|
||||
err instanceof Error ? err.message : String(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get compileForDebug(): boolean {
|
||||
@@ -215,7 +237,24 @@ export class Debug extends SketchContribution {
|
||||
const oldState = this.compileForDebug;
|
||||
const newState = !oldState;
|
||||
window.localStorage.setItem(COMPILE_FOR_DEBUG_KEY, String(newState));
|
||||
this.mainMenuManager.update();
|
||||
this.menuManager.update();
|
||||
}
|
||||
|
||||
private async isSketchNotVerifiedError(
|
||||
err: unknown,
|
||||
sketch: Sketch
|
||||
): Promise<boolean> {
|
||||
if (err instanceof Error) {
|
||||
try {
|
||||
const tempBuildPaths = await this.sketchesService.tempBuildPath(sketch);
|
||||
return tempBuildPaths.some((tempBuildPath) =>
|
||||
err.message.includes(tempBuildPath)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export namespace Debug {
|
||||
|
168
arduino-ide-extension/src/browser/contributions/delete-sketch.ts
Normal file
168
arduino-ide-extension/src/browser/contributions/delete-sketch.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { ipcRenderer } from '@theia/core/electron-shared/electron';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import type { MaybeArray } from '@theia/core/lib/common/types';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import type { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
import { SCHEDULE_DELETION_SIGNAL } from '../../electron-common/electron-messages';
|
||||
import { Sketch } from '../contributions/contribution';
|
||||
import { isNotFound } from '../create/typings';
|
||||
import { Command, CommandRegistry } from './contribution';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
|
||||
export interface DeleteSketchParams {
|
||||
/**
|
||||
* Either the URI of the sketch folder or the sketch to delete.
|
||||
*/
|
||||
readonly toDelete: string | Sketch;
|
||||
/**
|
||||
* If `true`, the currently opened sketch is expected to be deleted.
|
||||
* Hence, the editors must be closed, the sketch will be scheduled
|
||||
* for deletion, and the browser window will close or navigate away.
|
||||
* If `false`, the sketch will be scheduled for deletion,
|
||||
* but the current window remains open. If `force`, the window will
|
||||
* navigate away, but IDE2 won't open any confirmation dialogs.
|
||||
*/
|
||||
readonly willNavigateAway?: boolean | 'force';
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DeleteSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(DeleteSketch.Commands.DELETE_SKETCH, {
|
||||
execute: (params: DeleteSketchParams) => this.deleteSketch(params),
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteSketch(params: DeleteSketchParams): Promise<void> {
|
||||
const { toDelete, willNavigateAway } = params;
|
||||
let sketch: Sketch;
|
||||
if (typeof toDelete === 'string') {
|
||||
const resolvedSketch = await this.loadSketch(toDelete);
|
||||
if (!resolvedSketch) {
|
||||
console.info(
|
||||
`Failed to load the sketch. It was not found at '${toDelete}'. Skipping deletion.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
sketch = resolvedSketch;
|
||||
} else {
|
||||
sketch = toDelete;
|
||||
}
|
||||
if (!willNavigateAway) {
|
||||
this.scheduleDeletion(sketch);
|
||||
return;
|
||||
}
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (willNavigateAway !== 'force') {
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localizeByDefault('Delete'),
|
||||
type: 'question',
|
||||
buttons: [Dialog.CANCEL, Dialog.OK],
|
||||
message: cloudUri
|
||||
? nls.localize(
|
||||
'theia/workspace/deleteCloudSketch',
|
||||
"The cloud sketch '{0}' will be permanently deleted from the Arduino servers and the local caches. This action is irreversible. Do you want to delete the current sketch?",
|
||||
sketch.name
|
||||
)
|
||||
: nls.localize(
|
||||
'theia/workspace/deleteCurrentSketch',
|
||||
"The sketch '{0}' will be permanently deleted. This action is irreversible. Do you want to delete the current sketch?",
|
||||
sketch.name
|
||||
),
|
||||
});
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (cloudUri) {
|
||||
const posixPath = cloudUri.path.toString();
|
||||
const cloudSketch = this.createApi.sketchCache.getSketch(posixPath);
|
||||
if (!cloudSketch) {
|
||||
throw new Error(
|
||||
`Cloud sketch with path '${posixPath}' was not cached. Cache: ${this.createApi.sketchCache.toString()}`
|
||||
);
|
||||
}
|
||||
try {
|
||||
// IDE2 cannot use DELETE directory as the server responses with HTTP 500 if it's missing.
|
||||
// https://github.com/arduino/arduino-ide/issues/1825#issuecomment-1406301406
|
||||
await this.createApi.deleteSketch(cloudSketch.path);
|
||||
} catch (err) {
|
||||
if (!isNotFound(err)) {
|
||||
throw err;
|
||||
} else {
|
||||
console.info(
|
||||
`Could not delete the cloud sketch with path '${posixPath}'. It does not exist.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all([
|
||||
...Sketch.uris(sketch).map((uri) =>
|
||||
this.closeWithoutSaving(new URI(uri))
|
||||
),
|
||||
]);
|
||||
this.windowService.setSafeToShutDown();
|
||||
this.scheduleDeletion(sketch);
|
||||
return window.close();
|
||||
}
|
||||
|
||||
private scheduleDeletion(sketch: Sketch): void {
|
||||
ipcRenderer.send(SCHEDULE_DELETION_SIGNAL, sketch);
|
||||
}
|
||||
|
||||
private async loadSketch(uri: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchesService.loadSketch(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// fix: https://github.com/eclipse-theia/theia/issues/12107
|
||||
private async closeWithoutSaving(uri: URI): Promise<void> {
|
||||
const affected = getAffected(this.shell.widgets, uri);
|
||||
const toClose = [...affected].map(([, widget]) => widget);
|
||||
await this.shell.closeMany(toClose, { save: false });
|
||||
}
|
||||
}
|
||||
export namespace DeleteSketch {
|
||||
export namespace Commands {
|
||||
export const DELETE_SKETCH: Command = {
|
||||
id: 'arduino-delete-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getAffected<T extends Widget>(
|
||||
widgets: Iterable<T>,
|
||||
context: MaybeArray<URI>
|
||||
): [URI, T & NavigatableWidget][] {
|
||||
const uris = Array.isArray(context) ? context : [context];
|
||||
const result: [URI, T & NavigatableWidget][] = [];
|
||||
for (const widget of widgets) {
|
||||
if (NavigatableWidget.is(widget)) {
|
||||
const resourceUri = widget.getResourceUri();
|
||||
if (resourceUri && uris.some((uri) => uri.isEqualOrParent(resourceUri))) {
|
||||
result.push([resourceUri, widget]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
@@ -49,30 +49,6 @@ export class EditContributions extends Contribution {
|
||||
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
|
||||
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
|
||||
execute: async () => {
|
||||
const settings = await this.settingsService.settings();
|
||||
if (settings.autoScaleInterface) {
|
||||
settings.interfaceScale = settings.interfaceScale + 1;
|
||||
} else {
|
||||
settings.editorFontSize = settings.editorFontSize + 1;
|
||||
}
|
||||
await this.settingsService.update(settings);
|
||||
await this.settingsService.save();
|
||||
},
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
|
||||
execute: async () => {
|
||||
const settings = await this.settingsService.settings();
|
||||
if (settings.autoScaleInterface) {
|
||||
settings.interfaceScale = settings.interfaceScale - 1;
|
||||
} else {
|
||||
settings.editorFontSize = settings.editorFontSize - 1;
|
||||
}
|
||||
await this.settingsService.update(settings);
|
||||
await this.settingsService.save();
|
||||
},
|
||||
});
|
||||
/* Tools */ registry.registerCommand(
|
||||
EditContributions.Commands.AUTO_FORMAT,
|
||||
{ execute: () => this.run('editor.action.formatDocument') }
|
||||
@@ -81,9 +57,11 @@ export class EditContributions extends Contribution {
|
||||
execute: async () => {
|
||||
const value = await this.currentValue();
|
||||
if (value !== undefined) {
|
||||
this.clipboardService.writeText(`\`\`\`cpp
|
||||
this.clipboardService.writeText(`
|
||||
\`\`\`cpp
|
||||
${value}
|
||||
\`\`\``);
|
||||
\`\`\`
|
||||
`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -141,22 +119,10 @@ ${value}
|
||||
label: nls.localize('arduino/editor/decreaseIndent', 'Decrease Indent'),
|
||||
order: '2',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/increaseFontSize',
|
||||
'Increase Font Size'
|
||||
),
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/decreaseFontSize',
|
||||
'Decrease Font Size'
|
||||
),
|
||||
order: '1',
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__CODE_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.AUTO_FORMAT.id,
|
||||
label: nls.localize('arduino/editor/autoFormat', 'Auto Format'),
|
||||
order: '3',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
@@ -215,15 +181,6 @@ ${value}
|
||||
when: 'editorFocus',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+=',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+-',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND.id,
|
||||
keybinding: 'CtrlCmd+F',
|
||||
@@ -248,10 +205,13 @@ ${value}
|
||||
});
|
||||
}
|
||||
|
||||
protected async current(): Promise<ICodeEditor | StandaloneCodeEditor | undefined> {
|
||||
protected async current(): Promise<
|
||||
ICodeEditor | StandaloneCodeEditor | undefined
|
||||
> {
|
||||
return (
|
||||
this.codeEditorService.getFocusedCodeEditor() ||
|
||||
this.codeEditorService.getActiveCodeEditor() || undefined
|
||||
this.codeEditorService.getActiveCodeEditor() ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -307,12 +267,6 @@ export namespace EditContributions {
|
||||
export const USE_FOR_FIND: Command = {
|
||||
id: 'arduino-for-find',
|
||||
};
|
||||
export const INCREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-increase-font-size',
|
||||
};
|
||||
export const DECREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-decrease-font-size',
|
||||
};
|
||||
export const AUTO_FORMAT: Command = {
|
||||
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
|
||||
};
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import * as PQueue from 'p-queue';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||
import { CommandHandler, CommandService } from '@theia/core/lib/common/command';
|
||||
import {
|
||||
MenuPath,
|
||||
CompositeMenuNode,
|
||||
@@ -11,8 +11,11 @@ import {
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import {
|
||||
ArduinoMenus,
|
||||
examplesLabel,
|
||||
PlaceholderMenuNode,
|
||||
} from '../menu/arduino-menus';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import {
|
||||
@@ -21,26 +24,99 @@ import {
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import {
|
||||
Board,
|
||||
SketchRef,
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
CoreService,
|
||||
SketchesService,
|
||||
Sketch,
|
||||
} from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { unregisterSubmenu } from '../menu/arduino-menus';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
|
||||
/**
|
||||
* Creates a cloned copy of the example sketch and opens it in a new window.
|
||||
*/
|
||||
export async function openClonedExample(
|
||||
uri: string,
|
||||
services: {
|
||||
sketchesService: SketchesService;
|
||||
commandService: CommandService;
|
||||
},
|
||||
onError: {
|
||||
onDidFailClone?: (
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
uri: string;
|
||||
}
|
||||
>,
|
||||
uri: string
|
||||
) => MaybePromise<unknown>;
|
||||
onDidFailOpen?: (
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
uri: string;
|
||||
}
|
||||
>,
|
||||
sketch: Sketch
|
||||
) => MaybePromise<unknown>;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { sketchesService, commandService } = services;
|
||||
const { onDidFailClone, onDidFailOpen } = onError;
|
||||
try {
|
||||
const sketch = await sketchesService.cloneExample(uri);
|
||||
try {
|
||||
await commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (openError) {
|
||||
if (SketchesError.NotFound.is(openError)) {
|
||||
if (onDidFailOpen) {
|
||||
await onDidFailOpen(openError, sketch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw openError;
|
||||
}
|
||||
} catch (cloneError) {
|
||||
if (SketchesError.NotFound.is(cloneError)) {
|
||||
if (onDidFailClone) {
|
||||
await onDidFailClone(cloneError, uri);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw cloneError;
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class Examples extends SketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
protected override init(): void {
|
||||
@@ -48,12 +124,24 @@ export abstract class Examples extends SketchContribution {
|
||||
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) =>
|
||||
this.handleBoardChanged(selectedBoard)
|
||||
);
|
||||
this.notificationCenter.onDidReinitialize(() =>
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
// No force refresh. The core client was already refreshed.
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
||||
protected handleBoardChanged(board: Board | undefined): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
protected abstract update(options?: {
|
||||
board?: Board | undefined;
|
||||
forceRefresh?: boolean;
|
||||
}): void;
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
try {
|
||||
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
||||
@@ -72,7 +160,7 @@ export abstract class Examples extends SketchContribution {
|
||||
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
|
||||
nls.localize('arduino/examples/menu', 'Examples'),
|
||||
examplesLabel,
|
||||
{
|
||||
order: '4',
|
||||
}
|
||||
@@ -108,6 +196,11 @@ export abstract class Examples extends SketchContribution {
|
||||
const { label } = sketchContainerOrPlaceholder;
|
||||
submenuPath = [...menuPath, label];
|
||||
this.menuRegistry.registerSubmenu(submenuPath, label, subMenuOptions);
|
||||
this.toDispose.push(
|
||||
Disposable.create(() =>
|
||||
unregisterSubmenu(submenuPath, this.menuRegistry)
|
||||
)
|
||||
);
|
||||
sketches.push(...sketchContainerOrPlaceholder.sketches);
|
||||
children.push(...sketchContainerOrPlaceholder.children);
|
||||
} else {
|
||||
@@ -147,12 +240,29 @@ export abstract class Examples extends SketchContribution {
|
||||
}
|
||||
|
||||
protected createHandler(uri: string): CommandHandler {
|
||||
const forceUpdate = () =>
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
return {
|
||||
execute: async () => {
|
||||
const sketch = await this.sketchService.cloneExample(uri);
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
await openClonedExample(
|
||||
uri,
|
||||
{
|
||||
sketchesService: this.sketchesService,
|
||||
commandService: this.commandRegistry,
|
||||
},
|
||||
{
|
||||
onDidFailClone: () => {
|
||||
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
||||
forceUpdate();
|
||||
},
|
||||
onDidFailOpen: (err) => {
|
||||
this.messageService.error(err.message);
|
||||
forceUpdate();
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -162,10 +272,10 @@ export abstract class Examples extends SketchContribution {
|
||||
@injectable()
|
||||
export class BuiltInExamples extends Examples {
|
||||
override async onReady(): Promise<void> {
|
||||
this.register(); // no `await`
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
protected async register(): Promise<void> {
|
||||
protected override async update(): Promise<void> {
|
||||
let sketchContainers: SketchContainer[] | undefined;
|
||||
try {
|
||||
sketchContainers = await this.examplesService.builtIns();
|
||||
@@ -196,30 +306,32 @@ export class BuiltInExamples extends Examples {
|
||||
|
||||
@injectable()
|
||||
export class LibraryExamples extends Examples {
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.register());
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.register());
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.update());
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.update());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
this.register(); // no `await`
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
protected override handleBoardChanged(board: Board | undefined): void {
|
||||
this.register(board);
|
||||
this.update({ board });
|
||||
}
|
||||
|
||||
protected async register(
|
||||
board: Board | undefined = this.boardsServiceClient.boardsConfig
|
||||
.selectedBoard
|
||||
protected override async update(
|
||||
options: { board?: Board; forceRefresh?: boolean } = {
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
}
|
||||
): Promise<void> {
|
||||
const { board, forceRefresh } = options;
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
if (forceRefresh) {
|
||||
await this.coreService.refresh();
|
||||
}
|
||||
const fqbn = board?.fqbn;
|
||||
const name = board?.name;
|
||||
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
|
||||
|
@@ -1,8 +1,14 @@
|
||||
import { LocalStorageService } from '@theia/core/lib/browser';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { BoardsService, LibraryService } from '../../common/protocol';
|
||||
import {
|
||||
BoardsService,
|
||||
LibraryLocation,
|
||||
LibraryService,
|
||||
} from '../../common/protocol';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
const Arduino_BuiltIn = 'Arduino_BuiltIn';
|
||||
|
||||
@injectable()
|
||||
export class FirstStartupInstaller extends Contribution {
|
||||
@inject(LocalStorageService)
|
||||
@@ -21,8 +27,8 @@ export class FirstStartupInstaller extends Contribution {
|
||||
id: 'arduino:avr',
|
||||
});
|
||||
const builtInLibrary = (
|
||||
await this.libraryService.search({ query: 'Arduino_BuiltIn' })
|
||||
)[0];
|
||||
await this.libraryService.search({ query: Arduino_BuiltIn })
|
||||
).find(({ name }) => name === Arduino_BuiltIn); // Filter by `name` to ensure "exact match". See: https://github.com/arduino/arduino-ide/issues/1526.
|
||||
|
||||
let avrPackageError: Error | undefined;
|
||||
let builtInLibraryError: Error | undefined;
|
||||
@@ -43,7 +49,7 @@ export class FirstStartupInstaller extends Contribution {
|
||||
// If arduino:avr installation fails because it's already installed we don't want to retry on next start-up
|
||||
console.error(e);
|
||||
} else {
|
||||
// But if there is any other error (e.g.: no interntet cconnection), we want to retry next time
|
||||
// But if there is any other error (e.g.: no Internet connection), we want to retry next time
|
||||
avrPackageError = e;
|
||||
}
|
||||
}
|
||||
@@ -57,6 +63,7 @@ export class FirstStartupInstaller extends Contribution {
|
||||
item: builtInLibrary,
|
||||
installDependencies: true,
|
||||
noOverwrite: true, // We don't want to automatically replace custom libraries the user might already have in place
|
||||
installLocation: LibraryLocation.BUILTIN,
|
||||
});
|
||||
} catch (e) {
|
||||
// There's no error code, I need to parse the error message: https://github.com/arduino/arduino-cli/commit/2ea3608453b17b1157f8a1dc892af2e13e40f4f0#diff-1de7569144d4e260f8dde0e0d00a4e2a218c57966d583da1687a70d518986649R95
|
||||
@@ -64,7 +71,7 @@ export class FirstStartupInstaller extends Contribution {
|
||||
// If Arduino_BuiltIn installation fails because it's already installed we don't want to retry on next start-up
|
||||
console.log('error installing core', e);
|
||||
} else {
|
||||
// But if there is any other error (e.g.: no interntet cconnection), we want to retry next time
|
||||
// But if there is any other error (e.g.: no Internet connection), we want to retry next time
|
||||
builtInLibraryError = e;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +86,7 @@ export class FirstStartupInstaller extends Contribution {
|
||||
}
|
||||
if (builtInLibraryError) {
|
||||
this.messageService.error(
|
||||
`Could not install ${builtInLibrary.name} library: ${builtInLibraryError}`
|
||||
`Could not install ${Arduino_BuiltIn} library: ${builtInLibraryError}`
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -2,8 +2,7 @@ import { MaybePromise } from '@theia/core';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { Formatter } from '../../common/protocol/formatter';
|
||||
import { InoSelector } from '../ino-selectors';
|
||||
import { fullRange } from '../utils/monaco';
|
||||
import { InoSelector } from '../selectors';
|
||||
import { Contribution, URI } from './contribution';
|
||||
|
||||
@injectable()
|
||||
@@ -40,7 +39,7 @@ export class Format
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_token: monaco.CancellationToken
|
||||
): Promise<monaco.languages.TextEdit[]> {
|
||||
const range = fullRange(model);
|
||||
const range = model.getFullModelRange();
|
||||
const text = await this.format(model, range, options);
|
||||
return [{ range, text }];
|
||||
}
|
||||
|
@@ -41,7 +41,9 @@ export class Help extends Contribution {
|
||||
);
|
||||
registry.registerCommand(
|
||||
Help.Commands.ENVIRONMENT,
|
||||
createOpenHandler('https://www.arduino.cc/en/Guide/Environment')
|
||||
createOpenHandler(
|
||||
'https://docs.arduino.cc/software/ide-v2/tutorials/getting-started-ide-v2'
|
||||
)
|
||||
);
|
||||
registry.registerCommand(
|
||||
Help.Commands.TROUBLESHOOTING,
|
||||
|
@@ -17,7 +17,7 @@ import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class IncludeLibrary extends SketchContribution {
|
||||
@@ -53,6 +53,7 @@ export class IncludeLibrary extends SketchContribution {
|
||||
this.notificationCenter.onLibraryDidUninstall(() =>
|
||||
this.updateMenuActions()
|
||||
);
|
||||
this.notificationCenter.onDidReinitialize(() => this.updateMenuActions());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
|
@@ -16,7 +16,7 @@ export class IndexesUpdateProgress extends Contribution {
|
||||
| undefined;
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onIndexWillUpdate((progressId) =>
|
||||
this.notificationCenter.onIndexUpdateWillStart(({ progressId }) =>
|
||||
this.getOrCreateProgress(progressId)
|
||||
);
|
||||
this.notificationCenter.onIndexUpdateDidProgress((progress) => {
|
||||
@@ -24,7 +24,7 @@ export class IndexesUpdateProgress extends Contribution {
|
||||
delegate.report(progress)
|
||||
);
|
||||
});
|
||||
this.notificationCenter.onIndexDidUpdate((progressId) => {
|
||||
this.notificationCenter.onIndexUpdateDidComplete(({ progressId }) => {
|
||||
this.cancelProgress(progressId);
|
||||
});
|
||||
this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => {
|
||||
|
@@ -1,15 +1,20 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import {
|
||||
ArduinoDaemon,
|
||||
assertSanitizedFqbn,
|
||||
BoardsService,
|
||||
ExecutableService,
|
||||
sanitizeFqbn,
|
||||
} from '../../common/protocol';
|
||||
import { HostedPluginEvents } from '../hosted-plugin-events';
|
||||
import { SketchContribution, URI } from './contribution';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { HostedPluginEvents } from '../hosted-plugin-events';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { SketchContribution, URI } from './contribution';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
|
||||
@injectable()
|
||||
export class InoLanguage extends SketchContribution {
|
||||
@@ -28,8 +33,15 @@ export class InoLanguage extends SketchContribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
@inject(BoardsDataStore)
|
||||
private readonly boardDataStore: BoardsDataStore;
|
||||
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private readonly languageServerStartMutex = new Mutex();
|
||||
private languageServerFqbn?: string;
|
||||
private languageServerStartMutex = new Mutex();
|
||||
|
||||
override onReady(): void {
|
||||
const start = (
|
||||
@@ -43,27 +55,61 @@ export class InoLanguage extends SketchContribution {
|
||||
}
|
||||
}
|
||||
};
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(start);
|
||||
this.hostedPluginEvents.onPluginsDidStart(() =>
|
||||
start(this.boardsServiceProvider.boardsConfig)
|
||||
);
|
||||
this.hostedPluginEvents.onPluginsWillUnload(
|
||||
() => (this.languageServerFqbn = undefined)
|
||||
);
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, oldValue, newValue }) => {
|
||||
if (oldValue !== newValue) {
|
||||
switch (preferenceName) {
|
||||
case 'arduino.language.log':
|
||||
case 'arduino.language.realTimeDiagnostics':
|
||||
start(this.boardsServiceProvider.boardsConfig, true);
|
||||
const forceRestart = () => {
|
||||
start(this.boardsServiceProvider.boardsConfig, true);
|
||||
};
|
||||
this.toDispose.pushAll([
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(start),
|
||||
this.hostedPluginEvents.onPluginsDidStart(() =>
|
||||
start(this.boardsServiceProvider.boardsConfig)
|
||||
),
|
||||
this.hostedPluginEvents.onPluginsWillUnload(
|
||||
() => (this.languageServerFqbn = undefined)
|
||||
),
|
||||
this.preferences.onPreferenceChanged(
|
||||
({ preferenceName, oldValue, newValue }) => {
|
||||
if (oldValue !== newValue) {
|
||||
switch (preferenceName) {
|
||||
case 'arduino.language.log':
|
||||
case 'arduino.language.realTimeDiagnostics':
|
||||
forceRestart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
),
|
||||
this.notificationCenter.onLibraryDidInstall(() => forceRestart()),
|
||||
this.notificationCenter.onLibraryDidUninstall(() => forceRestart()),
|
||||
this.notificationCenter.onPlatformDidInstall(() => forceRestart()),
|
||||
this.notificationCenter.onPlatformDidUninstall(() => forceRestart()),
|
||||
this.notificationCenter.onDidReinitialize(() => forceRestart()),
|
||||
this.boardDataStore.onChanged((dataChangePerFqbn) => {
|
||||
if (this.languageServerFqbn) {
|
||||
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
|
||||
if (!sanitizeFqbn) {
|
||||
throw new Error(
|
||||
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
|
||||
);
|
||||
}
|
||||
const matchingFqbn = dataChangePerFqbn.find(
|
||||
(fqbn) => sanitizedFqbn === fqbn
|
||||
);
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
if (
|
||||
matchingFqbn &&
|
||||
boardsConfig.selectedBoard?.fqbn === matchingFqbn
|
||||
) {
|
||||
start(boardsConfig);
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
start(this.boardsServiceProvider.boardsConfig);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private async startLanguageServer(
|
||||
fqbn: string,
|
||||
name: string | undefined,
|
||||
@@ -101,11 +147,18 @@ export class InoLanguage extends SketchContribution {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!forceStart && fqbn === this.languageServerFqbn) {
|
||||
assertSanitizedFqbn(fqbn);
|
||||
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
|
||||
if (!fqbnWithConfig) {
|
||||
throw new Error(
|
||||
`Failed to append boards config to the FQBN. Original FQBN was: ${fqbn}`
|
||||
);
|
||||
}
|
||||
if (!forceStart && fqbnWithConfig === this.languageServerFqbn) {
|
||||
// NOOP
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Starting language server: ${fqbn}`);
|
||||
this.logger.info(`Starting language server: ${fqbnWithConfig}`);
|
||||
const log = this.preferences.get('arduino.language.log');
|
||||
const realTimeDiagnostics = this.preferences.get(
|
||||
'arduino.language.realTimeDiagnostics'
|
||||
@@ -141,7 +194,7 @@ export class InoLanguage extends SketchContribution {
|
||||
log: currentSketchPath ? currentSketchPath : log,
|
||||
cliDaemonInstance: '1',
|
||||
board: {
|
||||
fqbn,
|
||||
fqbn: fqbnWithConfig,
|
||||
name: name ? `"${name}"` : undefined,
|
||||
},
|
||||
realTimeDiagnostics,
|
||||
@@ -150,7 +203,7 @@ export class InoLanguage extends SketchContribution {
|
||||
),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.log(`Failed to start language server for ${fqbn}`, e);
|
||||
console.log(`Failed to start language server. Original FQBN: ${fqbn}`, e);
|
||||
this.languageServerFqbn = undefined;
|
||||
} finally {
|
||||
release();
|
||||
|
@@ -0,0 +1,173 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
Contribution,
|
||||
Command,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CommandRegistry, MaybePromise, nls } from '@theia/core/lib/common';
|
||||
import { Settings } from '../dialogs/settings/settings';
|
||||
import debounce = require('lodash.debounce');
|
||||
|
||||
@injectable()
|
||||
export class InterfaceScale extends Contribution {
|
||||
private fontScalingEnabled: InterfaceScale.FontScalingEnabled = {
|
||||
increase: true,
|
||||
decrease: true,
|
||||
};
|
||||
|
||||
private currentSettings: Settings;
|
||||
private updateSettingsDebounced = debounce(
|
||||
async () => {
|
||||
await this.settingsService.update(this.currentSettings);
|
||||
await this.settingsService.save();
|
||||
},
|
||||
100,
|
||||
{ maxWait: 200 }
|
||||
);
|
||||
|
||||
override onStart(): MaybePromise<void> {
|
||||
const updateCurrent = (settings: Settings) => {
|
||||
this.currentSettings = settings;
|
||||
this.updateFontScalingEnabled();
|
||||
};
|
||||
this.settingsService.onDidChange((settings) => updateCurrent(settings));
|
||||
this.settingsService.settings().then((settings) => updateCurrent(settings));
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(InterfaceScale.Commands.INCREASE_FONT_SIZE, {
|
||||
execute: () => this.updateFontSize('increase'),
|
||||
isEnabled: () => this.fontScalingEnabled.increase,
|
||||
});
|
||||
registry.registerCommand(InterfaceScale.Commands.DECREASE_FONT_SIZE, {
|
||||
execute: () => this.updateFontSize('decrease'),
|
||||
isEnabled: () => this.fontScalingEnabled.decrease,
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/increaseFontSize',
|
||||
'Increase Font Size'
|
||||
),
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/decreaseFontSize',
|
||||
'Decrease Font Size'
|
||||
),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
private updateFontScalingEnabled(): void {
|
||||
let fontScalingEnabled = {
|
||||
increase: true,
|
||||
decrease: true,
|
||||
};
|
||||
|
||||
if (this.currentSettings.autoScaleInterface) {
|
||||
fontScalingEnabled = {
|
||||
increase:
|
||||
this.currentSettings.interfaceScale + InterfaceScale.ZoomLevel.STEP <=
|
||||
InterfaceScale.ZoomLevel.MAX,
|
||||
decrease:
|
||||
this.currentSettings.interfaceScale - InterfaceScale.ZoomLevel.STEP >=
|
||||
InterfaceScale.ZoomLevel.MIN,
|
||||
};
|
||||
} else {
|
||||
fontScalingEnabled = {
|
||||
increase:
|
||||
this.currentSettings.editorFontSize + InterfaceScale.FontSize.STEP <=
|
||||
InterfaceScale.FontSize.MAX,
|
||||
decrease:
|
||||
this.currentSettings.editorFontSize - InterfaceScale.FontSize.STEP >=
|
||||
InterfaceScale.FontSize.MIN,
|
||||
};
|
||||
}
|
||||
|
||||
const isChanged = Object.keys(fontScalingEnabled).some(
|
||||
(key: keyof InterfaceScale.FontScalingEnabled) =>
|
||||
fontScalingEnabled[key] !== this.fontScalingEnabled[key]
|
||||
);
|
||||
if (isChanged) {
|
||||
this.fontScalingEnabled = fontScalingEnabled;
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
private updateFontSize(mode: 'increase' | 'decrease'): void {
|
||||
if (this.currentSettings.autoScaleInterface) {
|
||||
mode === 'increase'
|
||||
? (this.currentSettings.interfaceScale += InterfaceScale.ZoomLevel.STEP)
|
||||
: (this.currentSettings.interfaceScale -=
|
||||
InterfaceScale.ZoomLevel.STEP);
|
||||
} else {
|
||||
mode === 'increase'
|
||||
? (this.currentSettings.editorFontSize += InterfaceScale.FontSize.STEP)
|
||||
: (this.currentSettings.editorFontSize -= InterfaceScale.FontSize.STEP);
|
||||
}
|
||||
this.updateFontScalingEnabled();
|
||||
this.updateSettingsDebounced();
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+=',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+-',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace InterfaceScale {
|
||||
export namespace Commands {
|
||||
export const INCREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-increase-font-size',
|
||||
};
|
||||
export const DECREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-decrease-font-size',
|
||||
};
|
||||
}
|
||||
|
||||
export namespace ZoomLevel {
|
||||
export const MIN = -8;
|
||||
export const MAX = 9;
|
||||
export const STEP = 1;
|
||||
|
||||
export function toPercentage(scale: number): number {
|
||||
return scale * 20 + 100;
|
||||
}
|
||||
export function fromPercentage(percentage: number): number {
|
||||
return (percentage - 100) / 20;
|
||||
}
|
||||
export namespace Step {
|
||||
export function toPercentage(step: number): number {
|
||||
return step * 20;
|
||||
}
|
||||
export function fromPercentage(percentage: number): number {
|
||||
return percentage / 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace FontSize {
|
||||
export const MIN = 8;
|
||||
export const MAX = 72;
|
||||
export const STEP = 2;
|
||||
}
|
||||
|
||||
export interface FontScalingEnabled {
|
||||
increase: boolean;
|
||||
decrease: boolean;
|
||||
}
|
||||
}
|
@@ -0,0 +1,204 @@
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { Create, isConflict } from '../create/typings';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
TaskFactoryImpl,
|
||||
WorkspaceInputDialogWithProgress,
|
||||
} from '../theia/workspace/workspace-input-dialog';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
|
||||
import {
|
||||
CloudSketchContribution,
|
||||
pullingSketch,
|
||||
sketchAlreadyExists,
|
||||
synchronizingSketchbook,
|
||||
} from './cloud-contribution';
|
||||
import { Command, CommandRegistry, Sketch } from './contribution';
|
||||
|
||||
export interface CreateNewCloudSketchCallback {
|
||||
(
|
||||
newSketch: Create.Sketch,
|
||||
newNode: CloudSketchbookTree.CloudSketchDirNode,
|
||||
progress: Progress
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface NewCloudSketchParams {
|
||||
/**
|
||||
* Value to populate the dialog `<input>` when it opens.
|
||||
*/
|
||||
readonly initialValue?: string | undefined;
|
||||
/**
|
||||
* Additional callback to call when the new cloud sketch has been created.
|
||||
*/
|
||||
readonly callback?: CreateNewCloudSketchCallback;
|
||||
/**
|
||||
* If `true`, the validation error message will not be visible in the input dialog, but the `OK` button will be disabled. Defaults to `true`.
|
||||
*/
|
||||
readonly skipShowErrorMessageOnOpen?: boolean;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class NewCloudSketch extends CloudSketchContribution {
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
|
||||
override onReady(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.createFeatures.onDidChangeEnabled(() => this.menuManager.update()),
|
||||
this.createFeatures.onDidChangeSession(() => this.menuManager.update()),
|
||||
]);
|
||||
if (this.createFeatures.session) {
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
|
||||
execute: (params: NewCloudSketchParams) =>
|
||||
this.createNewSketch(
|
||||
params?.skipShowErrorMessageOnOpen === false ? false : true,
|
||||
params?.initialValue,
|
||||
params?.callback
|
||||
),
|
||||
isEnabled: () => Boolean(this.createFeatures.session),
|
||||
isVisible: () => this.createFeatures.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
label: nls.localize('arduino/cloudSketch/new', 'New Cloud Sketch'),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+Alt+N',
|
||||
});
|
||||
}
|
||||
|
||||
private async createNewSketch(
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined,
|
||||
callback?: CreateNewCloudSketchCallback
|
||||
): Promise<void> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (treeModel) {
|
||||
const rootNode = treeModel.root;
|
||||
return this.openWizard(
|
||||
rootNode,
|
||||
treeModel,
|
||||
skipShowErrorMessageOnOpen,
|
||||
initialValue,
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async openWizard(
|
||||
rootNode: CompositeTreeNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined,
|
||||
callback?: CreateNewCloudSketchCallback
|
||||
): Promise<void> {
|
||||
const existingNames = rootNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
const taskFactory = new TaskFactoryImpl((value) =>
|
||||
this.createNewSketchWithProgress(treeModel, value, callback)
|
||||
);
|
||||
try {
|
||||
const dialog = new WorkspaceInputDialogWithProgress(
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/newCloudSketch/newSketchTitle',
|
||||
'Name of the new Cloud Sketch'
|
||||
),
|
||||
parentUri: CreateUri.root,
|
||||
initialValue,
|
||||
validate: (input) => {
|
||||
if (existingNames.includes(input)) {
|
||||
return sketchAlreadyExists(input);
|
||||
}
|
||||
return Sketch.validateCloudSketchFolderName(input) ?? '';
|
||||
},
|
||||
},
|
||||
this.labelProvider,
|
||||
taskFactory
|
||||
);
|
||||
await dialog.open(skipShowErrorMessageOnOpen);
|
||||
if (dialog.taskResult) {
|
||||
this.openInNewWindow(dialog.taskResult);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
await treeModel.refresh();
|
||||
return this.createNewSketch(
|
||||
false,
|
||||
taskFactory.value ?? initialValue,
|
||||
callback
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private createNewSketchWithProgress(
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
value: string,
|
||||
callback?: CreateNewCloudSketchCallback
|
||||
): (
|
||||
progress: Progress
|
||||
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
|
||||
return async (progress: Progress) => {
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/creating',
|
||||
"Creating cloud sketch '{0}'...",
|
||||
value
|
||||
),
|
||||
});
|
||||
const sketch = await this.createApi.createSketch(value);
|
||||
progress.report({ message: synchronizingSketchbook });
|
||||
await treeModel.refresh();
|
||||
progress.report({ message: pullingSketch(sketch.name) });
|
||||
const node = await this.pull(sketch);
|
||||
if (callback && node) {
|
||||
await callback(sketch, node, progress);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
}
|
||||
|
||||
private openInNewWindow(
|
||||
node: CloudSketchbookTree.CloudSketchDirNode
|
||||
): Promise<void> {
|
||||
return this.commandService.executeCommand(
|
||||
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||
{ node, treeWidgetId: 'cloud-sketchbook-composite-widget' }
|
||||
);
|
||||
}
|
||||
}
|
||||
export namespace NewCloudSketch {
|
||||
export namespace Commands {
|
||||
export const NEW_CLOUD_SKETCH: Command = {
|
||||
id: 'arduino-new-cloud-sketch',
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
SketchContribution,
|
||||
URI,
|
||||
@@ -17,17 +16,12 @@ export class NewSketch extends SketchContribution {
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
||||
execute: () => this.newSketch(),
|
||||
});
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/new', 'New'),
|
||||
label: nls.localize('arduino/sketch/new', 'New Sketch'),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
@@ -41,7 +35,7 @@ export class NewSketch extends SketchContribution {
|
||||
|
||||
async newSketch(): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
const sketch = await this.sketchesService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
} catch (e) {
|
||||
await this.messageService.error(e.toString());
|
||||
@@ -54,8 +48,5 @@ export namespace NewSketch {
|
||||
export const NEW_SKETCH: Command = {
|
||||
id: 'arduino-new-sketch',
|
||||
};
|
||||
export const NEW_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-new-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class OpenRecentSketch extends SketchContribution {
|
||||
@@ -33,7 +34,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
||||
protected toDispose = new DisposableCollection();
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||
@@ -42,8 +43,12 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
this.sketchService
|
||||
.recentlyOpenedSketches()
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update(forceUpdate?: boolean): void {
|
||||
this.sketchesService
|
||||
.recentlyOpenedSketches(forceUpdate)
|
||||
.then((sketches) => this.refreshMenu(sketches));
|
||||
}
|
||||
|
||||
@@ -62,19 +67,25 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
|
||||
protected register(sketches: Sketch[]): void {
|
||||
const order = 0;
|
||||
this.toDispose.dispose();
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const toDispose = this.toDisposeBeforeRegister.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
const command = { id: `arduino-open-recent--${uri}` };
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.commandRegistry.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
),
|
||||
execute: async () => {
|
||||
try {
|
||||
await this.commandRegistry.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.update(true);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.menuRegistry.registerMenuAction(
|
||||
@@ -85,8 +96,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
order: String(order),
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeRegister.set(
|
||||
sketch.uri,
|
||||
this.toDispose.pushAll([
|
||||
new DisposableCollection(
|
||||
Disposable.create(() =>
|
||||
this.commandRegistry.unregisterCommand(command)
|
||||
@@ -94,8 +104,8 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(command)
|
||||
)
|
||||
)
|
||||
);
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,32 +1,34 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import type { Settings } from '../dialogs/settings/settings';
|
||||
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
Command,
|
||||
MenuModelRegistry,
|
||||
CommandRegistry,
|
||||
SketchContribution,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
SketchContribution,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { Settings as Preferences } from '../dialogs/settings/settings';
|
||||
import { SettingsDialog } from '../dialogs/settings/settings-dialog';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class Settings extends SketchContribution {
|
||||
export class OpenSettings extends SketchContribution {
|
||||
@inject(SettingsDialog)
|
||||
protected readonly settingsDialog: SettingsDialog;
|
||||
private readonly settingsDialog: SettingsDialog;
|
||||
|
||||
protected settingsOpened = false;
|
||||
private settingsOpened = false;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(Settings.Commands.OPEN, {
|
||||
registry.registerCommand(OpenSettings.Commands.OPEN, {
|
||||
execute: async () => {
|
||||
let settings: Preferences | undefined = undefined;
|
||||
let settings: Settings | undefined = undefined;
|
||||
try {
|
||||
this.settingsOpened = true;
|
||||
this.menuManager.update();
|
||||
settings = await this.settingsDialog.open();
|
||||
} finally {
|
||||
this.settingsOpened = false;
|
||||
this.menuManager.update();
|
||||
}
|
||||
if (settings) {
|
||||
await this.settingsService.update(settings);
|
||||
@@ -41,7 +43,7 @@ export class Settings extends SketchContribution {
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__PREFERENCES_GROUP, {
|
||||
commandId: Settings.Commands.OPEN.id,
|
||||
commandId: OpenSettings.Commands.OPEN.id,
|
||||
label:
|
||||
nls.localize(
|
||||
'vscode/preferences.contribution/preferences',
|
||||
@@ -57,13 +59,13 @@ export class Settings extends SketchContribution {
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: Settings.Commands.OPEN.id,
|
||||
command: OpenSettings.Commands.OPEN.id,
|
||||
keybinding: 'CtrlCmd+,',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Settings {
|
||||
export namespace OpenSettings {
|
||||
export namespace Commands {
|
||||
export const OPEN: Command = {
|
||||
id: 'arduino-settings-open',
|
@@ -1,7 +1,8 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
import { Later } from '../../common/nls';
|
||||
import { Sketch, SketchesError } from '../../common/protocol';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
@@ -9,12 +10,18 @@ import {
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
import { promptMoveSketch } from './open-sketch';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
import { Deferred, wait } from '@theia/core/lib/common/promise-util';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
|
||||
@injectable()
|
||||
export class OpenSketchFiles extends SketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketchFiles.Commands.OPEN_SKETCH_FILES, {
|
||||
execute: (uri: URI) => this.openSketchFiles(uri),
|
||||
execute: (uri: URI, focusMainSketchFile) =>
|
||||
this.openSketchFiles(uri, focusMainSketchFile),
|
||||
});
|
||||
registry.registerCommand(OpenSketchFiles.Commands.ENSURE_OPENED, {
|
||||
execute: (
|
||||
@@ -27,13 +34,23 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
private async openSketchFiles(uri: URI): Promise<void> {
|
||||
private async openSketchFiles(
|
||||
uri: URI,
|
||||
focusMainSketchFile = false
|
||||
): Promise<void> {
|
||||
try {
|
||||
const sketch = await this.sketchService.loadSketch(uri.toString());
|
||||
const sketch = await this.sketchesService.loadSketch(uri.toString());
|
||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||
for (const uri of [mainFileUri, ...rootFolderFileUris]) {
|
||||
await this.ensureOpened(uri);
|
||||
}
|
||||
if (focusMainSketchFile) {
|
||||
await this.ensureOpened(mainFileUri, true, {
|
||||
mode: 'activate',
|
||||
preview: false,
|
||||
counter: 0,
|
||||
});
|
||||
}
|
||||
if (mainFileUri.endsWith('.pde')) {
|
||||
const message = nls.localize(
|
||||
'arduino/common/oldFormat',
|
||||
@@ -41,24 +58,38 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
sketch.name
|
||||
);
|
||||
const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes');
|
||||
this.messageService
|
||||
.info(message, nls.localize('arduino/common/later', 'Later'), yes)
|
||||
.then(async (answer) => {
|
||||
if (answer === yes) {
|
||||
this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
{
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
this.messageService.info(message, Later, yes).then((answer) => {
|
||||
if (answer === yes) {
|
||||
this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
{
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
const { workspaceError } = this.workspaceService;
|
||||
// This happens when the IDE2 has been started (from either a terminal or clicking on an `ino` file) with a /path/to/invalid/sketch. (#964)
|
||||
if (SketchesError.InvalidName.is(workspaceError)) {
|
||||
await this.promptMove(workspaceError);
|
||||
}
|
||||
} catch (err) {
|
||||
// This happens when the user gracefully closed IDE2, all went well
|
||||
// but the main sketch file was renamed outside of IDE2 and when the user restarts the IDE2
|
||||
// the workspace path still exists, but the sketch path is not valid anymore. (#964)
|
||||
if (SketchesError.InvalidName.is(err)) {
|
||||
const movedSketch = await this.promptMove(err);
|
||||
if (!movedSketch) {
|
||||
// If user did not accept the move, or move was not possible, force reload with a fallback.
|
||||
return this.openFallbackSketch();
|
||||
}
|
||||
}
|
||||
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.openFallbackSketch();
|
||||
return this.openFallbackSketch();
|
||||
} else {
|
||||
console.error(err);
|
||||
const message =
|
||||
@@ -72,8 +103,33 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
}
|
||||
}
|
||||
|
||||
private async promptMove(
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
invalidMainSketchUri: string;
|
||||
}
|
||||
>
|
||||
): Promise<Sketch | undefined> {
|
||||
const { invalidMainSketchUri } = err.data;
|
||||
requestAnimationFrame(() => this.messageService.error(err.message));
|
||||
await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
|
||||
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
|
||||
fileService: this.fileService,
|
||||
sketchesService: this.sketchesService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
if (movedSketch) {
|
||||
this.workspaceService.open(new URI(movedSketch.uri), {
|
||||
preserveWindow: true,
|
||||
});
|
||||
return movedSketch;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async openFallbackSketch(): Promise<void> {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
const sketch = await this.sketchesService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
||||
}
|
||||
|
||||
@@ -81,20 +137,84 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
uri: string,
|
||||
forceOpen = false,
|
||||
options?: EditorOpenerOptions
|
||||
): Promise<unknown> {
|
||||
): Promise<EditorWidget | undefined> {
|
||||
const widget = this.editorManager.all.find(
|
||||
(widget) => widget.editor.uri.toString() === uri
|
||||
);
|
||||
if (!widget || forceOpen) {
|
||||
return this.editorManager.open(
|
||||
if (widget && !forceOpen) {
|
||||
return widget;
|
||||
}
|
||||
|
||||
const disposables = new DisposableCollection();
|
||||
const deferred = new Deferred<EditorWidget>();
|
||||
// An editor can be in two primary states:
|
||||
// - The editor is not yet opened. The `widget` is `undefined`. With `editorManager#open`, Theia will create an editor and fire an `editorManager#onCreated` event.
|
||||
// - The editor is opened. Can be active, current, or open.
|
||||
// - If the editor has the focus (the cursor blinks in the editor): it's the active editor.
|
||||
// - If the editor does not have the focus (the focus is on a different widget or the context menu is opened in the editor): it's the current editor.
|
||||
// - If the editor is not the top editor in the main area, it's opened.
|
||||
if (!widget) {
|
||||
// If the widget is `undefined`, IDE2 expects one `onCreate` event. Subscribe to the `onCreated` event
|
||||
// and resolve the promise with the editor only when the new editor's visibility changes.
|
||||
disposables.push(
|
||||
this.editorManager.onCreated((editor) => {
|
||||
if (editor.editor.uri.toString() === uri) {
|
||||
if (editor.isAttached && editor.isVisible) {
|
||||
deferred.resolve(editor);
|
||||
} else {
|
||||
disposables.push(
|
||||
editor.onDidChangeVisibility((visible) => {
|
||||
if (visible) {
|
||||
// wait an animation frame. although the visible and attached props are true the editor is not there.
|
||||
// let the browser render the widget
|
||||
setTimeout(
|
||||
() =>
|
||||
requestAnimationFrame(() => deferred.resolve(editor)),
|
||||
0
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.editorManager
|
||||
.open(
|
||||
new URI(uri),
|
||||
options ?? {
|
||||
mode: 'reveal',
|
||||
preview: false,
|
||||
counter: 0,
|
||||
}
|
||||
)
|
||||
.then((editorWidget) => {
|
||||
// If the widget was defined, it was already opened.
|
||||
// The editor is expected to be attached to the shell and visible in the UI.
|
||||
// The deferred promise does not have to wait for the `editorManager#onCreated` event.
|
||||
// It can resolve earlier.
|
||||
if (widget) {
|
||||
deferred.resolve(editorWidget);
|
||||
}
|
||||
});
|
||||
|
||||
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
|
||||
const result: EditorWidget | undefined | 'timeout' = await Promise.race([
|
||||
deferred.promise,
|
||||
wait(timeout).then(() => {
|
||||
disposables.dispose();
|
||||
return 'timeout' as const;
|
||||
}),
|
||||
]);
|
||||
if (result === 'timeout') {
|
||||
console.warn(
|
||||
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
export namespace OpenSketchFiles {
|
||||
|
@@ -1,115 +1,50 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
SketchesError,
|
||||
SketchesService,
|
||||
SketchRef,
|
||||
} from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
SketchContribution,
|
||||
Sketch,
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
SketchContribution,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import { BuiltInExamples } from './examples';
|
||||
import { Sketchbook } from './sketchbook';
|
||||
import { SketchContainer } from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
export type SketchLocation = string | URI | SketchRef;
|
||||
export namespace SketchLocation {
|
||||
export function toUri(location: SketchLocation): URI {
|
||||
if (typeof location === 'string') {
|
||||
return new URI(location);
|
||||
} else if (SketchRef.is(location)) {
|
||||
return toUri(location.uri);
|
||||
} else {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
export function is(arg: unknown): arg is SketchLocation {
|
||||
return typeof arg === 'string' || arg instanceof URI || SketchRef.is(arg);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class OpenSketch extends SketchContribution {
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(BuiltInExamples)
|
||||
protected readonly builtInExamples: BuiltInExamples;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(Sketchbook)
|
||||
protected readonly sketchbook: Sketchbook;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
||||
execute: (arg) =>
|
||||
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
|
||||
});
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (_: Widget, target: EventTarget) => {
|
||||
const container = await this.sketchService.getSketches({
|
||||
exclude: ['**/hardware/**'],
|
||||
});
|
||||
if (SketchContainer.isEmpty(container)) {
|
||||
this.openSketch();
|
||||
} else {
|
||||
this.toDispose.dispose();
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
|
||||
{
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: nls.localize(
|
||||
'vscode/workspaceActions/openFileFolder',
|
||||
'Open...'
|
||||
),
|
||||
}
|
||||
);
|
||||
this.toDispose.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
OpenSketch.Commands.OPEN_SKETCH
|
||||
)
|
||||
)
|
||||
);
|
||||
this.sketchbook.registerRecursively(
|
||||
[...container.children, ...container.sketches],
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
try {
|
||||
const containers = await this.examplesService.builtIns();
|
||||
for (const container of containers) {
|
||||
this.builtInExamples.registerRecursively(
|
||||
container,
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error when collecting built-in examples.', e);
|
||||
}
|
||||
const options = {
|
||||
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
y:
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
execute: async (arg) => {
|
||||
const toOpen = !SketchLocation.is(arg)
|
||||
? await this.selectSketch()
|
||||
: arg;
|
||||
if (toOpen) {
|
||||
return this.openSketch(toOpen);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -119,7 +54,7 @@ export class OpenSketch extends SketchContribution {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
|
||||
order: '1',
|
||||
order: '2',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -130,30 +65,37 @@ export class OpenSketch extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
async openSketch(
|
||||
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
|
||||
): Promise<void> {
|
||||
const sketch = await toOpen;
|
||||
if (sketch) {
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> {
|
||||
if (!toOpen) {
|
||||
return;
|
||||
}
|
||||
const uri = SketchLocation.toUri(toOpen);
|
||||
try {
|
||||
await this.sketchesService.loadSketch(uri.toString());
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
this.workspaceService.open(uri);
|
||||
}
|
||||
|
||||
protected async selectSketch(): Promise<Sketch | undefined> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri)
|
||||
private async selectSketch(): Promise<Sketch | undefined> {
|
||||
const defaultPath = await this.defaultPath();
|
||||
const { filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
|
||||
extensions: ['ino', 'pde'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
const { filePaths } = await remote.dialog.showOpenDialog({
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
|
||||
extensions: ['ino', 'pde'],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (!filePaths.length) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -164,50 +106,16 @@ export class OpenSketch extends SketchContribution {
|
||||
}
|
||||
const sketchFilePath = filePaths[0];
|
||||
const sketchFileUri = await this.fileSystemExt.getUri(sketchFilePath);
|
||||
const sketch = await this.sketchService.getSketchFolder(sketchFileUri);
|
||||
const sketch = await this.sketchesService.getSketchFolder(sketchFileUri);
|
||||
if (sketch) {
|
||||
return sketch;
|
||||
}
|
||||
if (Sketch.isSketchFile(sketchFileUri)) {
|
||||
const name = new URI(sketchFileUri).path.name;
|
||||
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||
],
|
||||
message: nls.localize(
|
||||
'arduino/sketch/movingMsg',
|
||||
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
||||
nameWithExt,
|
||||
name
|
||||
),
|
||||
return promptMoveSketch(sketchFileUri, {
|
||||
fileService: this.fileService,
|
||||
sketchesService: this.sketchesService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
if (response === 1) {
|
||||
// OK
|
||||
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
|
||||
const exists = await this.fileService.exists(newSketchUri);
|
||||
if (exists) {
|
||||
await remote.dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
||||
message: nls.localize(
|
||||
'arduino/sketch/cantOpen',
|
||||
'A folder named "{0}" already exists. Can\'t open sketch.',
|
||||
name
|
||||
),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
await this.fileService.createFolder(newSketchUri);
|
||||
await this.fileService.move(
|
||||
new URI(sketchFileUri),
|
||||
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||
);
|
||||
return this.sketchService.getSketchFolder(newSketchUri.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,8 +125,57 @@ export namespace OpenSketch {
|
||||
export const OPEN_SKETCH: Command = {
|
||||
id: 'arduino-open-sketch',
|
||||
};
|
||||
export const OPEN_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function promptMoveSketch(
|
||||
sketchFileUri: string | URI,
|
||||
options: {
|
||||
fileService: FileService;
|
||||
sketchesService: SketchesService;
|
||||
labelProvider: LabelProvider;
|
||||
}
|
||||
): Promise<Sketch | undefined> {
|
||||
const { fileService, sketchesService, labelProvider } = options;
|
||||
const uri =
|
||||
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
|
||||
const name = uri.path.name;
|
||||
const nameWithExt = labelProvider.getName(uri);
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||
],
|
||||
message: nls.localize(
|
||||
'arduino/sketch/movingMsg',
|
||||
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
||||
nameWithExt,
|
||||
name
|
||||
),
|
||||
});
|
||||
if (response === 1) {
|
||||
// OK
|
||||
const newSketchUri = uri.parent.resolve(name);
|
||||
const exists = await fileService.exists(newSketchUri);
|
||||
if (exists) {
|
||||
await remote.dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
||||
message: nls.localize(
|
||||
'arduino/sketch/cantOpen',
|
||||
'A folder named "{0}" already exists. Can\'t open sketch.',
|
||||
name
|
||||
),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
await fileService.createFolder(newSketchUri);
|
||||
await fileService.move(
|
||||
uri,
|
||||
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||
);
|
||||
return sketchesService.getSketchFolder(newSketchUri.toString());
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,166 @@
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { isConflict } from '../create/typings';
|
||||
import {
|
||||
TaskFactoryImpl,
|
||||
WorkspaceInputDialogWithProgress,
|
||||
} from '../theia/workspace/workspace-input-dialog';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import {
|
||||
CloudSketchContribution,
|
||||
pullingSketch,
|
||||
pushingSketch,
|
||||
sketchAlreadyExists,
|
||||
synchronizingSketchbook,
|
||||
} from './cloud-contribution';
|
||||
import { Command, CommandRegistry, Sketch, URI } from './contribution';
|
||||
|
||||
export interface RenameCloudSketchParams {
|
||||
readonly cloudUri: URI;
|
||||
readonly sketch: Sketch;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class RenameCloudSketch extends CloudSketchContribution {
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH, {
|
||||
execute: (params: RenameCloudSketchParams) =>
|
||||
this.renameSketch(params, true),
|
||||
});
|
||||
}
|
||||
|
||||
private async renameSketch(
|
||||
params: RenameCloudSketchParams,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initValue: string = params.sketch.name
|
||||
): Promise<string | undefined> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (treeModel) {
|
||||
const posixPath = params.cloudUri.path.toString();
|
||||
const node = treeModel.getNode(posixPath);
|
||||
const parentNode = node?.parent;
|
||||
if (
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(node) &&
|
||||
CompositeTreeNode.is(parentNode)
|
||||
) {
|
||||
return this.openWizard(
|
||||
params,
|
||||
node,
|
||||
parentNode,
|
||||
treeModel,
|
||||
skipShowErrorMessageOnOpen,
|
||||
initValue
|
||||
);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async openWizard(
|
||||
params: RenameCloudSketchParams,
|
||||
node: CloudSketchbookTree.CloudSketchDirNode,
|
||||
parentNode: CompositeTreeNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined
|
||||
): Promise<string | undefined> {
|
||||
const parentUri = CloudSketchbookTree.CloudSketchDirNode.is(parentNode)
|
||||
? parentNode.uri
|
||||
: CreateUri.root;
|
||||
const existingNames = parentNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
const taskFactory = new TaskFactoryImpl((value) =>
|
||||
this.renameSketchWithProgress(params, node, treeModel, value)
|
||||
);
|
||||
try {
|
||||
const dialog = new WorkspaceInputDialogWithProgress(
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/renameCloudSketch/renameSketchTitle',
|
||||
'New name of the Cloud Sketch'
|
||||
),
|
||||
parentUri,
|
||||
initialValue,
|
||||
validate: (input) => {
|
||||
if (existingNames.includes(input)) {
|
||||
return sketchAlreadyExists(input);
|
||||
}
|
||||
return Sketch.validateCloudSketchFolderName(input) ?? '';
|
||||
},
|
||||
},
|
||||
this.labelProvider,
|
||||
taskFactory
|
||||
);
|
||||
await dialog.open(skipShowErrorMessageOnOpen);
|
||||
return dialog.taskResult;
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
await treeModel.refresh();
|
||||
return this.renameSketch(
|
||||
params,
|
||||
false,
|
||||
taskFactory.value ?? initialValue
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private renameSketchWithProgress(
|
||||
params: RenameCloudSketchParams,
|
||||
node: CloudSketchbookTree.CloudSketchDirNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
value: string
|
||||
): (progress: Progress) => Promise<string | undefined> {
|
||||
return async (progress: Progress) => {
|
||||
const fromName = params.cloudUri.path.name;
|
||||
const fromPosixPath = params.cloudUri.path.toString();
|
||||
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
|
||||
// push
|
||||
progress.report({ message: pushingSketch(params.sketch.name) });
|
||||
await treeModel.sketchbookTree().push(node, true);
|
||||
|
||||
// rename
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/renaming',
|
||||
"Renaming cloud sketch from '{0}' to '{1}'...",
|
||||
fromName,
|
||||
value
|
||||
),
|
||||
});
|
||||
await this.createApi.rename(fromPosixPath, toPosixPath);
|
||||
|
||||
// sync
|
||||
progress.report({
|
||||
message: synchronizingSketchbook,
|
||||
});
|
||||
this.createApi.sketchCache.init(); // invalidate the cache
|
||||
await this.createApi.sketches(); // IDE2 must pull all sketches to find the new one
|
||||
const sketch = this.createApi.sketchCache.getSketch(toPosixPath);
|
||||
if (!sketch) {
|
||||
return undefined;
|
||||
}
|
||||
await treeModel.refresh();
|
||||
|
||||
// pull
|
||||
progress.report({ message: pullingSketch(sketch.name) });
|
||||
const pulledNode = await this.pull(sketch);
|
||||
return pulledNode
|
||||
? node.uri.parent.resolve(sketch.name).toString()
|
||||
: undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
export namespace RenameCloudSketch {
|
||||
export namespace Commands {
|
||||
export const RENAME_CLOUD_SKETCH: Command = {
|
||||
id: 'arduino-rename-cloud-sketch',
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,32 +1,37 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import * as dateFormat from 'dateformat';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable';
|
||||
import { Saveable } from '@theia/core/lib/browser/saveable';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
import {
|
||||
SketchContribution,
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ApplicationShell, NavigatableWidget, Saveable } from '@theia/core/lib/browser';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { DeleteSketch } from './delete-sketch';
|
||||
import {
|
||||
RenameCloudSketch,
|
||||
RenameCloudSketchParams,
|
||||
} from './rename-cloud-sketch';
|
||||
|
||||
@injectable()
|
||||
export class SaveAsSketch extends SketchContribution {
|
||||
|
||||
export class SaveAsSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
protected readonly applicationShell: ApplicationShell;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected override readonly editorManager: EditorManager;
|
||||
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService;
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(SaveAsSketch.Commands.SAVE_AS_SKETCH, {
|
||||
@@ -37,7 +42,7 @@ export class SaveAsSketch extends SketchContribution {
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
label: nls.localize('vscode/fileCommands/saveAs', 'Save As...'),
|
||||
label: nls.localizeByDefault('Save As...'),
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
@@ -52,11 +57,12 @@ export class SaveAsSketch extends SketchContribution {
|
||||
/**
|
||||
* Resolves `true` if the sketch was successfully saved as something.
|
||||
*/
|
||||
async saveAs(
|
||||
private async saveAs(
|
||||
{
|
||||
execOnlyIfTemp,
|
||||
openAfterMove,
|
||||
wipeOriginal,
|
||||
markAsRecentlyOpened,
|
||||
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||
): Promise<boolean> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
@@ -64,99 +70,186 @@ export class SaveAsSketch extends SketchContribution {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
return false;
|
||||
let destinationUri: string | undefined;
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (cloudUri) {
|
||||
destinationUri = await this.createCloudCopy({ cloudUri, sketch });
|
||||
} else {
|
||||
destinationUri = await this.createLocalCopy(sketch, execOnlyIfTemp);
|
||||
}
|
||||
|
||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||
const sketchDirUri = new URI(
|
||||
(await this.configService.getConfiguration()).sketchDirUri
|
||||
);
|
||||
const exists = await this.fileService.exists(
|
||||
sketchDirUri.resolve(sketch.name)
|
||||
);
|
||||
const defaultUri = sketchDirUri.resolve(
|
||||
exists
|
||||
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
|
||||
: sketch.name
|
||||
);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveFolderAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
});
|
||||
if (!filePath || canceled) {
|
||||
return false;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
if (!destinationUri) {
|
||||
return false;
|
||||
}
|
||||
const workspaceUri = await this.sketchService.copy(sketch, {
|
||||
|
||||
const copiedSketch = await this.sketchesService.copy(sketch, {
|
||||
destinationUri,
|
||||
});
|
||||
if (workspaceUri) {
|
||||
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
|
||||
const newWorkspaceUri = copiedSketch.uri;
|
||||
|
||||
await saveOntoCopiedSketch(
|
||||
sketch,
|
||||
newWorkspaceUri,
|
||||
this.shell,
|
||||
this.editorManager
|
||||
);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
|
||||
}
|
||||
if (workspaceUri && openAfterMove) {
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
try {
|
||||
await this.fileService.delete(new URI(sketch.uri), {
|
||||
recursive: true,
|
||||
});
|
||||
} catch {
|
||||
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
|
||||
}
|
||||
}
|
||||
const options: WorkspaceInput & StartupTask.Owner = {
|
||||
preserveWindow: true,
|
||||
tasks: [],
|
||||
};
|
||||
if (openAfterMove) {
|
||||
this.windowService.setSafeToShutDown();
|
||||
this.workspaceService.open(new URI(workspaceUri), {
|
||||
preserveWindow: true,
|
||||
});
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
options.tasks.push({
|
||||
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
args: [{ toDelete: sketch.uri }],
|
||||
});
|
||||
}
|
||||
this.workspaceService.open(new URI(newWorkspaceUri), options);
|
||||
}
|
||||
return !!workspaceUri;
|
||||
return !!newWorkspaceUri;
|
||||
}
|
||||
|
||||
private async saveOntoCopiedSketch(mainFileUri: string, sketchUri: string, newSketchUri: string): Promise<void> {
|
||||
const widgets = this.applicationShell.widgets;
|
||||
const snapshots = new Map<string, object>();
|
||||
for (const widget of widgets) {
|
||||
const saveable = Saveable.getDirty(widget);
|
||||
const uri = NavigatableWidget.getUri(widget);
|
||||
const uriString = uri?.toString();
|
||||
let relativePath: string;
|
||||
if (uri && uriString!.includes(sketchUri) && saveable && saveable.createSnapshot) {
|
||||
// The main file will change its name during the copy process
|
||||
// We need to store the new name in the map
|
||||
if (mainFileUri === uriString) {
|
||||
const lastPart = new URI(newSketchUri).path.base + uri.path.ext;
|
||||
relativePath = '/' + lastPart;
|
||||
} else {
|
||||
relativePath = uri.toString().substring(sketchUri.length);
|
||||
private async createCloudCopy(
|
||||
params: RenameCloudSketchParams
|
||||
): Promise<string | undefined> {
|
||||
return this.commandService.executeCommand<string>(
|
||||
RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH.id,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
private async createLocalCopy(
|
||||
sketch: Sketch,
|
||||
execOnlyIfTemp?: boolean
|
||||
): Promise<string | undefined> {
|
||||
const isTemp = await this.sketchesService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const sketchbookDirUri = await this.defaultUri();
|
||||
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
|
||||
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
|
||||
// Otherwise, it proposes the parent folder of the current sketch.
|
||||
const containerDirUri = isTemp
|
||||
? sketchbookDirUri
|
||||
: !sketchbookDirUri.isEqualOrParent(sketchUri)
|
||||
? sketchbookDirUri
|
||||
: sketchUri.parent;
|
||||
const exists = await this.fileService.exists(
|
||||
containerDirUri.resolve(sketch.name)
|
||||
);
|
||||
|
||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||
// IDE2 must never prompt an invalid sketch folder name (https://github.com/arduino/arduino-ide/pull/1833#issuecomment-1412569252)
|
||||
const defaultUri = containerDirUri.resolve(
|
||||
Sketch.toValidSketchFolderName(sketch.name, exists)
|
||||
);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
return await this.promptLocalSketchFolderDestination(sketch, defaultPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts for the new sketch folder name until a valid one is give,
|
||||
* then resolves with the destination sketch folder URI string,
|
||||
* or `undefined` if the operation was canceled.
|
||||
*/
|
||||
private async promptLocalSketchFolderDestination(
|
||||
sketch: Sketch,
|
||||
defaultPath: string
|
||||
): Promise<string | undefined> {
|
||||
let sketchFolderDestinationUri: string | undefined;
|
||||
while (!sketchFolderDestinationUri) {
|
||||
const { filePath } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveFolderAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
snapshots.set(relativePath, saveable.createSnapshot());
|
||||
);
|
||||
if (!filePath) {
|
||||
return undefined;
|
||||
}
|
||||
const destinationUri = await this.fileSystemExt.getUri(filePath);
|
||||
// The new location of the sketch cannot be inside the location of current sketch.
|
||||
// https://github.com/arduino/arduino-ide/issues/1882
|
||||
let dialogContent: InvalidSketchFolderDialogContent | undefined;
|
||||
if (new URI(sketch.uri).isEqualOrParent(new URI(destinationUri))) {
|
||||
dialogContent = {
|
||||
message: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderLocationMessage',
|
||||
"Invalid sketch folder location: '{0}'",
|
||||
filePath
|
||||
),
|
||||
details: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderLocationDetails',
|
||||
'You cannot save a sketch into a folder inside itself.'
|
||||
),
|
||||
question: nls.localize(
|
||||
'arduino/sketch/editInvalidSketchFolderLocationQuestion',
|
||||
'Do you want to try saving the sketch to a different location?'
|
||||
),
|
||||
};
|
||||
}
|
||||
if (!dialogContent) {
|
||||
const sketchFolderName = new URI(destinationUri).path.base;
|
||||
const errorMessage = Sketch.validateSketchFolderName(sketchFolderName);
|
||||
if (errorMessage) {
|
||||
dialogContent = {
|
||||
message: nls.localize(
|
||||
'arduino/sketch/invalidSketchFolderNameMessage',
|
||||
"Invalid sketch folder name: '{0}'",
|
||||
sketchFolderName
|
||||
),
|
||||
details: errorMessage,
|
||||
question: nls.localize(
|
||||
'arduino/sketch/editInvalidSketchFolderQuestion',
|
||||
'Do you want to try saving the sketch with a different name?'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (dialogContent) {
|
||||
const message = `
|
||||
${dialogContent.message}
|
||||
|
||||
${dialogContent.details}
|
||||
|
||||
${dialogContent.question}`.trim();
|
||||
defaultPath = filePath;
|
||||
const { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
message,
|
||||
buttons: [Dialog.CANCEL, Dialog.YES],
|
||||
}
|
||||
);
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
sketchFolderDestinationUri = destinationUri;
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
|
||||
const widgetUri = new URI(newSketchUri + path);
|
||||
try {
|
||||
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}));
|
||||
return sketchFolderDestinationUri;
|
||||
}
|
||||
}
|
||||
|
||||
interface InvalidSketchFolderDialogContent {
|
||||
readonly message: string;
|
||||
readonly details: string;
|
||||
readonly question: string;
|
||||
}
|
||||
|
||||
export namespace SaveAsSketch {
|
||||
export namespace Commands {
|
||||
export const SAVE_AS_SKETCH: Command = {
|
||||
@@ -170,12 +263,59 @@ export namespace SaveAsSketch {
|
||||
* Ignored if `openAfterMove` is `false`.
|
||||
*/
|
||||
readonly wipeOriginal?: boolean;
|
||||
readonly markAsRecentlyOpened?: boolean;
|
||||
}
|
||||
export namespace Options {
|
||||
export const DEFAULT: Options = {
|
||||
execOnlyIfTemp: false,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: false,
|
||||
markAsRecentlyOpened: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveOntoCopiedSketch(
|
||||
sketch: Sketch,
|
||||
newSketchFolderUri: string,
|
||||
shell: ApplicationShell,
|
||||
editorManager: EditorManager
|
||||
): Promise<void> {
|
||||
const widgets = shell.widgets;
|
||||
const snapshots = new Map<string, Saveable.Snapshot>();
|
||||
for (const widget of widgets) {
|
||||
const saveable = Saveable.getDirty(widget);
|
||||
const uri = NavigatableWidget.getUri(widget);
|
||||
if (!uri) {
|
||||
continue;
|
||||
}
|
||||
const uriString = uri.toString();
|
||||
let relativePath: string;
|
||||
if (uriString.includes(sketch.uri) && saveable && saveable.createSnapshot) {
|
||||
// The main file will change its name during the copy process
|
||||
// We need to store the new name in the map
|
||||
if (sketch.mainFileUri === uriString) {
|
||||
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
|
||||
relativePath = '/' + lastPart;
|
||||
} else {
|
||||
relativePath = uri.toString().substring(sketch.uri.length);
|
||||
}
|
||||
snapshots.set(relativePath, saveable.createSnapshot());
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
|
||||
const widgetUri = new URI(newSketchFolderUri + path);
|
||||
try {
|
||||
const widget = await editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
import {
|
||||
SketchContribution,
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
KeybindingRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class SaveSketch extends SketchContribution {
|
||||
@@ -19,19 +18,13 @@ export class SaveSketch extends SketchContribution {
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
||||
execute: () => this.saveSketch(),
|
||||
});
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () =>
|
||||
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
label: nls.localize('vscode/fileCommands/save', 'Save'),
|
||||
order: '6',
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,7 +40,7 @@ export class SaveSketch extends SketchContribution {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
const isTemp = await this.sketchesService.isTemp(sketch);
|
||||
if (isTemp) {
|
||||
return this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
@@ -68,8 +61,5 @@ export namespace SaveSketch {
|
||||
export const SAVE_SKETCH: Command = {
|
||||
id: 'arduino-save-sketch',
|
||||
};
|
||||
export const SAVE_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-save-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -1,50 +1,34 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
|
||||
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import {
|
||||
URI,
|
||||
SketchContribution,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
TabBarToolbarRegistry,
|
||||
MenuModelRegistry,
|
||||
open,
|
||||
SketchContribution,
|
||||
TabBarToolbarRegistry,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../common/protocol/sketches-service-client-impl';
|
||||
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class SketchControl extends SketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(EditorManager)
|
||||
protected override readonly editorManager: EditorManager;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
@inject(LocalCacheFsProvider)
|
||||
protected readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
private readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
protected readonly toDisposeBeforeCreateNewContextMenu =
|
||||
new DisposableCollection();
|
||||
@@ -57,107 +41,57 @@ export class SketchControl extends SketchContribution {
|
||||
this.shell.getWidgets('main').indexOf(widget) !== -1,
|
||||
execute: async () => {
|
||||
this.toDisposeBeforeCreateNewContextMenu.dispose();
|
||||
|
||||
let parentElement: HTMLElement | undefined = undefined;
|
||||
const target = document.getElementById(
|
||||
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
||||
);
|
||||
if (target instanceof HTMLElement) {
|
||||
parentElement = target.parentElement ?? undefined;
|
||||
}
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = document.getElementById(
|
||||
SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_RENAME.id,
|
||||
label: nls.localize('vscode/fileActions/rename', 'Rename'),
|
||||
order: '1',
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
WorkspaceCommands.FILE_RENAME
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_DELETE.id,
|
||||
label: nls.localize('vscode/fileActions/delete', 'Delete'),
|
||||
order: '2',
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
WorkspaceCommands.FILE_DELETE
|
||||
)
|
||||
)
|
||||
);
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainFileUri, rootFolderFileUris } = sketch;
|
||||
const uris = [mainFileUri, ...rootFolderFileUris];
|
||||
|
||||
const parentSketchUri = this.editorManager.currentEditor
|
||||
?.getResourceUri()
|
||||
?.toString();
|
||||
const parentSketch = await this.sketchService.getSketchFolder(
|
||||
parentSketchUri || ''
|
||||
);
|
||||
|
||||
// if the current file is in the current opened sketch, show extra menus
|
||||
if (
|
||||
sketch &&
|
||||
parentSketch &&
|
||||
parentSketch.uri === sketch.uri &&
|
||||
this.allowRename(parentSketch.uri)
|
||||
) {
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_RENAME.id,
|
||||
label: nls.localize('vscode/fileActions/rename', 'Rename'),
|
||||
order: '1',
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
WorkspaceCommands.FILE_RENAME
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const renamePlaceholder = new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
nls.localize('vscode/fileActions/rename', 'Rename')
|
||||
);
|
||||
this.menuRegistry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
renamePlaceholder
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuNode(renamePlaceholder.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
sketch &&
|
||||
parentSketch &&
|
||||
parentSketch.uri === sketch.uri &&
|
||||
this.allowDelete(parentSketch.uri)
|
||||
) {
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
{
|
||||
commandId: WorkspaceCommands.FILE_DELETE.id, // TODO: customize delete. Wipe sketch if deleting main file. Close window.
|
||||
label: nls.localize('vscode/fileActions/delete', 'Delete'),
|
||||
order: '2',
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
WorkspaceCommands.FILE_DELETE
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const deletePlaceholder = new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
nls.localize('vscode/fileActions/delete', 'Delete')
|
||||
);
|
||||
this.menuRegistry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH_CONTROL__CONTEXT__MAIN_GROUP,
|
||||
deletePlaceholder
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuNode(deletePlaceholder.id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < uris.length; i++) {
|
||||
const uri = new URI(uris[i]);
|
||||
|
||||
@@ -176,7 +110,7 @@ export class SketchControl extends SketchContribution {
|
||||
{
|
||||
commandId: command.id,
|
||||
label: this.labelProvider.getName(uri),
|
||||
order: `${i}`,
|
||||
order: String(i).padStart(4),
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
@@ -193,6 +127,7 @@ export class SketchControl extends SketchContribution {
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
showDisabled: true,
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
},
|
||||
@@ -235,7 +170,7 @@ export class SketchControl extends SketchContribution {
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: CommonCommands.PREVIOUS_TAB.id,
|
||||
keybinding: 'CtrlCmd+Alt+Left', // TODO: check why electron does not show the keybindings in the UI.
|
||||
keybinding: 'CtrlCmd+Alt+Left',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: CommonCommands.NEXT_TAB.id,
|
||||
@@ -249,27 +184,6 @@ export class SketchControl extends SketchContribution {
|
||||
command: SketchControl.Commands.OPEN_SKETCH_CONTROL__TOOLBAR.id,
|
||||
});
|
||||
}
|
||||
|
||||
protected isCloudSketch(uri: string): boolean {
|
||||
try {
|
||||
const cloudCacheLocation = this.localCacheFsProvider.from(new URI(uri));
|
||||
|
||||
if (cloudCacheLocation) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected allowRename(uri: string): boolean {
|
||||
return !this.isCloudSketch(uri);
|
||||
}
|
||||
|
||||
protected allowDelete(uri: string): boolean {
|
||||
return !this.isCloudSketch(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SketchControl {
|
||||
|
@@ -3,8 +3,8 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
|
||||
import { FileChangeType } from '@theia/filesystem/lib/common/files';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { Sketch, SketchContribution, URI } from './contribution';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { Sketch, SketchContribution } from './contribution';
|
||||
import { OpenSketchFiles } from './open-sketch-files';
|
||||
|
||||
@injectable()
|
||||
@@ -30,11 +30,7 @@ export class SketchFilesTracker extends SketchContribution {
|
||||
|
||||
override onReady(): void {
|
||||
this.sketchServiceClient.currentSketch().then(async (sketch) => {
|
||||
if (
|
||||
CurrentSketch.isValid(sketch) &&
|
||||
!(await this.sketchService.isTemp(sketch))
|
||||
) {
|
||||
this.toDisposeOnStop.push(this.fileService.watch(new URI(sketch.uri)));
|
||||
if (CurrentSketch.isValid(sketch)) {
|
||||
this.toDisposeOnStop.push(
|
||||
this.fileService.onDidFilesChange(async (event) => {
|
||||
for (const { type, resource } of event.changes) {
|
||||
@@ -42,7 +38,7 @@ export class SketchFilesTracker extends SketchContribution {
|
||||
type === FileChangeType.ADDED &&
|
||||
resource.parent.toString() === sketch.uri
|
||||
) {
|
||||
const reloadedSketch = await this.sketchService.loadSketch(
|
||||
const reloadedSketch = await this.sketchesService.loadSketch(
|
||||
sketch.uri
|
||||
);
|
||||
if (Sketch.isInSketch(resource, reloadedSketch)) {
|
||||
|
@@ -1,44 +1,27 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||
import { CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
import { MenuModelRegistry } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Examples } from './examples';
|
||||
import {
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
SketchRef,
|
||||
} from '../../common/protocol';
|
||||
import { SketchContainer, SketchesError } from '../../common/protocol';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class Sketchbook extends Examples {
|
||||
@inject(CommandRegistry)
|
||||
protected override readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected override readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
override onStart(): void {
|
||||
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||
this.configService.onDidChangeSketchDirUri(() => this.update());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.sketchService.getSketches({}).then((container) => {
|
||||
protected override update(): void {
|
||||
this.sketchesService.getSketches({}).then((container) => {
|
||||
this.register(container);
|
||||
this.mainMenuManager.update();
|
||||
this.menuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +33,7 @@ export class Sketchbook extends Examples {
|
||||
);
|
||||
}
|
||||
|
||||
protected register(container: SketchContainer): void {
|
||||
private register(container: SketchContainer): void {
|
||||
this.toDispose.dispose();
|
||||
this.registerRecursively(
|
||||
[...container.children, ...container.sketches],
|
||||
@@ -62,23 +45,18 @@ export class Sketchbook extends Examples {
|
||||
protected override createHandler(uri: string): CommandHandler {
|
||||
return {
|
||||
execute: async () => {
|
||||
let sketch: SketchRef | undefined = undefined;
|
||||
try {
|
||||
sketch = await this.sketchService.loadSketch(uri);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
// To handle the following:
|
||||
// Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch.
|
||||
// Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items.
|
||||
this.messageService.error(err.message);
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
if (sketch) {
|
||||
await this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
uri
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
// Force update the menu items to remove the absent sketch.
|
||||
this.update();
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -0,0 +1,52 @@
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import type { IpcRendererEvent } from '@theia/core/electron-shared/electron';
|
||||
import { ipcRenderer } from '@theia/core/electron-shared/electron';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class StartupTasks extends Contribution {
|
||||
override onReady(): void {
|
||||
ipcRenderer.once(
|
||||
StartupTask.Messaging.STARTUP_TASKS_SIGNAL,
|
||||
(_: IpcRendererEvent, args: unknown) => {
|
||||
console.debug(
|
||||
`Received the startup tasks from the electron main process. Args: ${JSON.stringify(
|
||||
args
|
||||
)}`
|
||||
);
|
||||
if (!StartupTask.has(args)) {
|
||||
console.warn(`Could not detect 'tasks' from the signal. Skipping.`);
|
||||
return;
|
||||
}
|
||||
const tasks = args.tasks;
|
||||
if (tasks.length) {
|
||||
console.log(`Executing startup tasks:`);
|
||||
tasks.forEach(({ command, args = [] }) => {
|
||||
console.log(
|
||||
` - '${command}' ${
|
||||
args.length ? `, args: ${JSON.stringify(args)}` : ''
|
||||
}`
|
||||
);
|
||||
this.commandService
|
||||
.executeCommand(command, ...args)
|
||||
.catch((err) =>
|
||||
console.error(
|
||||
`Error occurred when executing the startup task '${command}'${
|
||||
args?.length ? ` with args: '${JSON.stringify(args)}` : ''
|
||||
}.`,
|
||||
err
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
const { id } = remote.getCurrentWindow();
|
||||
console.debug(
|
||||
`Signalling app ready event to the electron main process. Sender ID: ${id}.`
|
||||
);
|
||||
ipcRenderer.send(StartupTask.Messaging.APP_READY_SIGNAL(id));
|
||||
}
|
||||
}
|
@@ -0,0 +1,193 @@
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CoreService, IndexType } from '../../common/protocol';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||
import { Command, CommandRegistry, Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class UpdateIndexes extends Contribution {
|
||||
@inject(WindowServiceExt)
|
||||
private readonly windowService: WindowServiceExt;
|
||||
@inject(LocalStorageService)
|
||||
private readonly localStorage: LocalStorageService;
|
||||
@inject(CoreService)
|
||||
private readonly coreService: CoreService;
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.notificationCenter.onIndexUpdateDidComplete(({ summary }) =>
|
||||
Promise.all(
|
||||
Object.entries(summary).map(([type, updatedAt]) =>
|
||||
this.setLastUpdateDateTime(type as IndexType, updatedAt)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.checkForUpdates();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UpdateIndexes.Commands.UPDATE_INDEXES, {
|
||||
execute: () => this.updateIndexes(IndexType.All, true),
|
||||
});
|
||||
registry.registerCommand(UpdateIndexes.Commands.UPDATE_PLATFORM_INDEX, {
|
||||
execute: () => this.updateIndexes(['platform'], true),
|
||||
});
|
||||
registry.registerCommand(UpdateIndexes.Commands.UPDATE_LIBRARY_INDEX, {
|
||||
execute: () => this.updateIndexes(['library'], true),
|
||||
});
|
||||
}
|
||||
|
||||
private async checkForUpdates(): Promise<void> {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
if (!checkForUpdates) {
|
||||
console.debug(
|
||||
'[update-indexes]: `arduino.checkForUpdates` is `false`. Skipping updating the indexes.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.windowService.isFirstWindow()) {
|
||||
const summary = await this.coreService.indexUpdateSummaryBeforeInit();
|
||||
if (summary.message) {
|
||||
this.messageService.error(summary.message);
|
||||
}
|
||||
const typesToCheck = IndexType.All.filter((type) => !(type in summary));
|
||||
if (Object.keys(summary).length) {
|
||||
console.debug(
|
||||
`[update-indexes]: Detected an index update summary before the core gRPC client initialization. Updating local storage with ${JSON.stringify(
|
||||
summary
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
console.debug(
|
||||
'[update-indexes]: No index update summary was available before the core gRPC client initialization. Checking the status of the all the index types.'
|
||||
);
|
||||
}
|
||||
await Promise.allSettled([
|
||||
...Object.entries(summary).map(([type, updatedAt]) =>
|
||||
this.setLastUpdateDateTime(type as IndexType, updatedAt)
|
||||
),
|
||||
this.updateIndexes(typesToCheck),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateIndexes(
|
||||
types: IndexType[],
|
||||
force = false
|
||||
): Promise<void> {
|
||||
const updatedAt = new Date().toISOString();
|
||||
return Promise.all(
|
||||
types.map((type) => this.needsIndexUpdate(type, updatedAt, force))
|
||||
).then((needsIndexUpdateResults) => {
|
||||
const typesToUpdate = needsIndexUpdateResults.filter(IndexType.is);
|
||||
if (typesToUpdate.length) {
|
||||
console.debug(
|
||||
`[update-indexes]: Requesting the index update of type: ${JSON.stringify(
|
||||
typesToUpdate
|
||||
)} with date time: ${updatedAt}.`
|
||||
);
|
||||
return this.coreService.updateIndex({ types: typesToUpdate });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async needsIndexUpdate(
|
||||
type: IndexType,
|
||||
now: string,
|
||||
force = false
|
||||
): Promise<IndexType | false> {
|
||||
if (force) {
|
||||
console.debug(
|
||||
`[update-indexes]: Update for index type: '${type}' was forcefully requested.`
|
||||
);
|
||||
return type;
|
||||
}
|
||||
const lastUpdateIsoDateTime = await this.getLastUpdateDateTime(type);
|
||||
if (!lastUpdateIsoDateTime) {
|
||||
console.debug(
|
||||
`[update-indexes]: No last update date time was persisted for index type: '${type}'. Index update is required.`
|
||||
);
|
||||
return type;
|
||||
}
|
||||
const lastUpdateDateTime = Date.parse(lastUpdateIsoDateTime);
|
||||
if (Number.isNaN(lastUpdateDateTime)) {
|
||||
console.debug(
|
||||
`[update-indexes]: Invalid last update date time was persisted for index type: '${type}'. Last update date time was: ${lastUpdateDateTime}. Index update is required.`
|
||||
);
|
||||
return type;
|
||||
}
|
||||
const diff = new Date(now).getTime() - lastUpdateDateTime;
|
||||
const needsIndexUpdate = diff >= this.threshold;
|
||||
console.debug(
|
||||
`[update-indexes]: Update for index type '${type}' is ${
|
||||
needsIndexUpdate ? '' : 'not '
|
||||
}required. Now: ${now}, Last index update date time: ${new Date(
|
||||
lastUpdateDateTime
|
||||
).toISOString()}, diff: ${diff} ms, threshold: ${this.threshold} ms.`
|
||||
);
|
||||
return needsIndexUpdate ? type : false;
|
||||
}
|
||||
|
||||
private async getLastUpdateDateTime(
|
||||
type: IndexType
|
||||
): Promise<string | undefined> {
|
||||
const key = this.storageKeyOf(type);
|
||||
return this.localStorage.getData<string>(key);
|
||||
}
|
||||
|
||||
private async setLastUpdateDateTime(
|
||||
type: IndexType,
|
||||
updatedAt: string
|
||||
): Promise<void> {
|
||||
const key = this.storageKeyOf(type);
|
||||
return this.localStorage.setData<string>(key, updatedAt).finally(() => {
|
||||
console.debug(
|
||||
`[update-indexes]: Updated the last index update date time of '${type}' to ${updatedAt}.`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private storageKeyOf(type: IndexType): string {
|
||||
return `index-last-update-time--${type}`;
|
||||
}
|
||||
|
||||
private get threshold(): number {
|
||||
return 4 * 60 * 60 * 1_000; // four hours in millis
|
||||
}
|
||||
}
|
||||
export namespace UpdateIndexes {
|
||||
export namespace Commands {
|
||||
export const UPDATE_INDEXES: Command & { label: string } = {
|
||||
id: 'arduino-update-indexes',
|
||||
label: nls.localize(
|
||||
'arduino/updateIndexes/updateIndexes',
|
||||
'Update Indexes'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const UPDATE_PLATFORM_INDEX: Command & { label: string } = {
|
||||
id: 'arduino-update-package-index',
|
||||
label: nls.localize(
|
||||
'arduino/updateIndexes/updatePackageIndex',
|
||||
'Update Package Index'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const UPDATE_LIBRARY_INDEX: Command & { label: string } = {
|
||||
id: 'arduino-update-library-index',
|
||||
label: nls.localize(
|
||||
'arduino/updateIndexes/updateLibraryIndex',
|
||||
'Update Library Index'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
@@ -12,7 +12,6 @@ import {
|
||||
PreferenceScope,
|
||||
PreferenceService,
|
||||
} from '@theia/core/lib/browser/preferences/preference-service';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import {
|
||||
arduinoCert,
|
||||
certificateList,
|
||||
@@ -31,22 +30,29 @@ export class UploadCertificate extends Contribution {
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
@inject(ArduinoFirmwareUploader)
|
||||
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||
|
||||
protected dialogOpened = false;
|
||||
|
||||
override onStart(): void {
|
||||
this.preferences.onPreferenceChanged(({ preferenceName }) => {
|
||||
if (preferenceName === 'arduino.board.certificates') {
|
||||
this.menuManager.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UploadCertificate.Commands.OPEN, {
|
||||
execute: async () => {
|
||||
try {
|
||||
this.dialogOpened = true;
|
||||
this.menuManager.update();
|
||||
await this.dialog.open();
|
||||
} finally {
|
||||
this.dialogOpened = false;
|
||||
this.menuManager.update();
|
||||
}
|
||||
},
|
||||
isEnabled: () => !this.dialogOpened,
|
||||
@@ -54,7 +60,7 @@ export class UploadCertificate extends Contribution {
|
||||
|
||||
registry.registerCommand(UploadCertificate.Commands.REMOVE_CERT, {
|
||||
execute: async (certToRemove) => {
|
||||
const certs = this.arduinoPreferences.get('arduino.board.certificates');
|
||||
const certs = this.preferences.get('arduino.board.certificates');
|
||||
|
||||
this.preferenceService.set(
|
||||
'arduino.board.certificates',
|
||||
@@ -75,7 +81,6 @@ export class UploadCertificate extends Contribution {
|
||||
.join(' ')}`
|
||||
);
|
||||
},
|
||||
isEnabled: () => true,
|
||||
});
|
||||
|
||||
registry.registerCommand(UploadCertificate.Commands.OPEN_CERT_CONTEXT, {
|
||||
@@ -89,7 +94,6 @@ export class UploadCertificate extends Contribution {
|
||||
args: [args.cert],
|
||||
});
|
||||
},
|
||||
isEnabled: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -21,9 +21,11 @@ export class UploadFirmware extends Contribution {
|
||||
execute: async () => {
|
||||
try {
|
||||
this.dialogOpened = true;
|
||||
this.menuManager.update();
|
||||
await this.dialog.open();
|
||||
} finally {
|
||||
this.dialogOpened = false;
|
||||
this.menuManager.update();
|
||||
}
|
||||
},
|
||||
isEnabled: () => !this.dialogOpened,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { BoardUserField, CoreService } from '../../common/protocol';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { CoreService, Port, sanitizeFqbn } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
Command,
|
||||
@@ -11,95 +11,36 @@ import {
|
||||
TabBarToolbarRegistry,
|
||||
CoreServiceContribution,
|
||||
} from './contribution';
|
||||
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
|
||||
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { deepClone, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import type { VerifySketchParams } from './verify-sketch';
|
||||
import { UserFields } from './user-fields';
|
||||
|
||||
@injectable()
|
||||
export class UploadSketch extends CoreServiceContribution {
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(UserFieldsDialog)
|
||||
private readonly userFieldsDialog: UserFieldsDialog;
|
||||
|
||||
private boardRequiresUserFields = false;
|
||||
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||
private readonly menuActionsDisposables = new DisposableCollection();
|
||||
|
||||
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
private uploadInProgress = false;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
|
||||
const userFields =
|
||||
await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
this.boardRequiresUserFields = userFields.length > 0;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
});
|
||||
}
|
||||
|
||||
private selectedFqbnAddress(): string {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const fqbn = boardsConfig.selectedBoard?.fqbn;
|
||||
if (!fqbn) {
|
||||
return '';
|
||||
}
|
||||
const address =
|
||||
boardsConfig.selectedBoard?.port?.address ||
|
||||
boardsConfig.selectedPort?.address;
|
||||
if (!address) {
|
||||
return '';
|
||||
}
|
||||
return fqbn + '|' + address;
|
||||
}
|
||||
@inject(UserFields)
|
||||
private readonly userFields: UserFields;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
|
||||
execute: async () => {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (!key) {
|
||||
return;
|
||||
if (await this.userFields.checkUserFieldsDialog()) {
|
||||
this.uploadSketch();
|
||||
}
|
||||
if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) {
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = (
|
||||
await this.boardsServiceProvider.selectedBoardUserFields()
|
||||
).map((f) => ({ ...f }));
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.cachedUserFields.set(key, result);
|
||||
}
|
||||
this.uploadSketch();
|
||||
},
|
||||
isEnabled: () => !this.uploadInProgress,
|
||||
});
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
|
||||
execute: async () => {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (!key) {
|
||||
return;
|
||||
if (await this.userFields.checkUserFieldsDialog(true)) {
|
||||
this.uploadSketch();
|
||||
}
|
||||
|
||||
const cached = this.cachedUserFields.get(key);
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = (
|
||||
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
|
||||
).map((f) => ({ ...f }));
|
||||
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.cachedUserFields.set(key, result);
|
||||
this.uploadSketch();
|
||||
},
|
||||
isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
|
||||
isEnabled: () => !this.uploadInProgress && this.userFields.isRequired(),
|
||||
});
|
||||
registry.registerCommand(
|
||||
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
|
||||
@@ -119,45 +60,20 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/upload', 'Upload'),
|
||||
order: '1',
|
||||
})
|
||||
);
|
||||
if (this.boardRequiresUserFields) {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
order: '2',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
{ order: '2' }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||
label: nls.localize(
|
||||
'arduino/sketch/uploadUsingProgrammer',
|
||||
'Upload Using Programmer'
|
||||
),
|
||||
order: '3',
|
||||
})
|
||||
);
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/upload', 'Upload'),
|
||||
order: '1',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||
label: nls.localize(
|
||||
'arduino/sketch/uploadUsingProgrammer',
|
||||
'Upload Using Programmer'
|
||||
),
|
||||
order: '3',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
@@ -190,7 +106,10 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
// toggle the toolbar button and menu item state.
|
||||
// uploadInProgress will be set to false whether the upload fails or not
|
||||
this.uploadInProgress = true;
|
||||
this.menuManager.update();
|
||||
this.boardsServiceProvider.snapshotBoardDiscoveryOnUpload();
|
||||
this.onDidChangeEmitter.fire();
|
||||
this.clearVisibleNotification();
|
||||
|
||||
const verifyOptions =
|
||||
await this.commandService.executeCommand<CoreService.Options.Compile>(
|
||||
@@ -212,18 +131,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This does not belong here.
|
||||
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
|
||||
if (
|
||||
uploadOptions.userFields.length === 0 &&
|
||||
this.boardRequiresUserFields
|
||||
) {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/sketch/userFieldsNotFoundError',
|
||||
"Can't find user fields for connected board"
|
||||
)
|
||||
);
|
||||
if (!this.userFields.checkUserFieldsForUpload()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -239,9 +147,12 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
} catch (e) {
|
||||
this.userFields.notifyFailedWithError(e);
|
||||
this.handleError(e);
|
||||
} finally {
|
||||
this.uploadInProgress = false;
|
||||
this.menuManager.update();
|
||||
this.boardsServiceProvider.attemptPostUploadAutoSelect();
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
}
|
||||
@@ -254,16 +165,16 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return undefined;
|
||||
}
|
||||
const userFields = this.userFields();
|
||||
const userFields = this.userFields.getUserFields();
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
|
||||
await Promise.all([
|
||||
verifyOptions.fqbn, // already decorated FQBN
|
||||
this.boardsDataStore.getData(this.sanitizeFqbn(verifyOptions.fqbn)),
|
||||
this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)),
|
||||
this.preferences.get('arduino.upload.verify'),
|
||||
this.preferences.get('arduino.upload.verbose'),
|
||||
]);
|
||||
const port = boardsConfig.selectedPort;
|
||||
const port = this.maybeUpdatePortProperties(boardsConfig.selectedPort);
|
||||
return {
|
||||
sketch,
|
||||
fqbn,
|
||||
@@ -275,21 +186,26 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
};
|
||||
}
|
||||
|
||||
private userFields() {
|
||||
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
|
||||
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
|
||||
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
|
||||
* This is a hack to ensure that the port object has the `properties` when uploading.(https://github.com/arduino/arduino-ide/issues/740)
|
||||
* This method works around a bug when restoring a `port` persisted by an older version of IDE2. See the bug [here](https://github.com/arduino/arduino-ide/pull/1335#issuecomment-1224355236).
|
||||
*
|
||||
* Before the upload, this method checks the available ports and makes sure that the `properties` of an available port, and the port selected by the user have the same `properties`.
|
||||
* This method does not update any state (for example, the `BoardsConfig.Config`) but uses the correct `properties` for the `upload`.
|
||||
*/
|
||||
private sanitizeFqbn(fqbn: string | undefined): string | undefined {
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
private maybeUpdatePortProperties(port: Port | undefined): Port | undefined {
|
||||
if (port) {
|
||||
const key = Port.keyOf(port);
|
||||
for (const candidate of this.boardsServiceProvider.availablePorts) {
|
||||
if (key === Port.keyOf(candidate) && candidate.properties) {
|
||||
return {
|
||||
...port,
|
||||
properties: deepClone(candidate.properties),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const [vendor, arch, id] = fqbn.split(':');
|
||||
return `${vendor}:${arch}:${id}`;
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +218,7 @@ export namespace UploadSketch {
|
||||
id: 'arduino-upload-with-configuration-sketch',
|
||||
label: nls.localize(
|
||||
'arduino/sketch/configureAndUpload',
|
||||
'Configure And Upload'
|
||||
'Configure and Upload'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
|
126
arduino-ide-extension/src/browser/contributions/user-fields.ts
Normal file
126
arduino-ide-extension/src/browser/contributions/user-fields.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { BoardUserField, CoreError } from '../../common/protocol';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MenuModelRegistry, Contribution } from './contribution';
|
||||
import { UploadSketch } from './upload-sketch';
|
||||
|
||||
@injectable()
|
||||
export class UserFields extends Contribution {
|
||||
private boardRequiresUserFields = false;
|
||||
private userFieldsSet = false;
|
||||
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||
|
||||
@inject(UserFieldsDialog)
|
||||
private readonly userFieldsDialog: UserFieldsDialog;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
|
||||
const userFields =
|
||||
await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
this.boardRequiresUserFields = userFields.length > 0;
|
||||
this.menuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
order: '2',
|
||||
});
|
||||
}
|
||||
|
||||
private selectedFqbnAddress(): string | undefined {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const fqbn = boardsConfig.selectedBoard?.fqbn;
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const address =
|
||||
boardsConfig.selectedBoard?.port?.address ||
|
||||
boardsConfig.selectedPort?.address ||
|
||||
'';
|
||||
return fqbn + '|' + address;
|
||||
}
|
||||
|
||||
private async showUserFieldsDialog(
|
||||
key: string
|
||||
): Promise<BoardUserField[] | undefined> {
|
||||
const cached = this.cachedUserFields.get(key);
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = cached
|
||||
? cached.slice()
|
||||
: await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.userFieldsSet = true;
|
||||
this.cachedUserFields.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async checkUserFieldsDialog(forceOpen = false): Promise<boolean> {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
/*
|
||||
If the board requires to be configured with user fields, we want
|
||||
to show the user fields dialog, but only if they weren't already
|
||||
filled in or if they were filled in, but the previous upload failed.
|
||||
*/
|
||||
if (
|
||||
!forceOpen &&
|
||||
(!this.boardRequiresUserFields ||
|
||||
(this.cachedUserFields.has(key) && this.userFieldsSet))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const userFieldsFilledIn = Boolean(await this.showUserFieldsDialog(key));
|
||||
return userFieldsFilledIn;
|
||||
}
|
||||
|
||||
checkUserFieldsForUpload(): boolean {
|
||||
// TODO: This does not belong here.
|
||||
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
|
||||
if (!this.boardRequiresUserFields || this.getUserFields().length > 0) {
|
||||
this.userFieldsSet = true;
|
||||
return true;
|
||||
}
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/sketch/userFieldsNotFoundError',
|
||||
"Can't find user fields for connected board"
|
||||
)
|
||||
);
|
||||
this.userFieldsSet = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
getUserFields(): BoardUserField[] {
|
||||
const fqbnAddress = this.selectedFqbnAddress();
|
||||
if (!fqbnAddress) {
|
||||
return [];
|
||||
}
|
||||
return this.cachedUserFields.get(fqbnAddress) ?? [];
|
||||
}
|
||||
|
||||
isRequired(): boolean {
|
||||
return this.boardRequiresUserFields;
|
||||
}
|
||||
|
||||
notifyFailedWithError(e: Error): void {
|
||||
if (this.boardRequiresUserFields && CoreError.UploadFailed.is(e)) {
|
||||
this.userFieldsSet = false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,202 @@
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { CloudSketchContribution } from './cloud-contribution';
|
||||
import { Sketch, URI } from './contribution';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
|
||||
@injectable()
|
||||
export class ValidateSketch extends CloudSketchContribution {
|
||||
override onReady(): void {
|
||||
this.validate();
|
||||
}
|
||||
|
||||
private async validate(): Promise<void> {
|
||||
const result = await this.promptFixActions();
|
||||
if (!result) {
|
||||
const yes = await this.prompt(
|
||||
nls.localize('arduino/validateSketch/abortFixTitle', 'Invalid sketch'),
|
||||
nls.localize(
|
||||
'arduino/validateSketch/abortFixMessage',
|
||||
"The sketch is still invalid. Do you want to fix the remaining problems? By clicking '{0}', a new sketch will open.",
|
||||
Dialog.NO
|
||||
),
|
||||
[Dialog.NO, Dialog.YES]
|
||||
);
|
||||
if (yes) {
|
||||
return this.validate();
|
||||
}
|
||||
const sketch = await this.sketchesService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri), {
|
||||
preserveWindow: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns with an array of actions the user has to perform to fix the invalid sketch.
|
||||
*/
|
||||
private validateSketch(
|
||||
sketch: Sketch,
|
||||
dataDirUri: URI | undefined
|
||||
): FixAction[] {
|
||||
// sketch code file validation errors first as they do not require window reload
|
||||
const actions = Sketch.uris(sketch)
|
||||
.filter((uri) => uri !== sketch.mainFileUri)
|
||||
.map((uri) => new URI(uri))
|
||||
.filter((uri) => Sketch.Extensions.CODE_FILES.includes(uri.path.ext))
|
||||
.map((uri) => ({
|
||||
uri,
|
||||
error: this.doValidate(sketch, dataDirUri, uri.path.name),
|
||||
}))
|
||||
.filter(({ error }) => Boolean(error))
|
||||
.map((object) => <{ uri: URI; error: string }>object)
|
||||
.map(({ uri, error }) => ({
|
||||
execute: async () => {
|
||||
const unknown =
|
||||
(await this.promptRenameSketchFile(uri, error)) &&
|
||||
(await this.commandService.executeCommand(
|
||||
WorkspaceCommands.FILE_RENAME.id,
|
||||
uri
|
||||
));
|
||||
return !!unknown;
|
||||
},
|
||||
}));
|
||||
|
||||
// sketch folder + main sketch file last as it requires a `Save as...` and the window reload
|
||||
const sketchFolderName = new URI(sketch.uri).path.base;
|
||||
const sketchFolderNameError = this.doValidate(
|
||||
sketch,
|
||||
dataDirUri,
|
||||
sketchFolderName
|
||||
);
|
||||
if (sketchFolderNameError) {
|
||||
actions.push({
|
||||
execute: async () => {
|
||||
const unknown =
|
||||
(await this.promptRenameSketch(sketch, sketchFolderNameError)) &&
|
||||
(await this.commandService.executeCommand(
|
||||
SaveAsSketch.Commands.SAVE_AS_SKETCH.id,
|
||||
<SaveAsSketch.Options>{
|
||||
markAsRecentlyOpened: true,
|
||||
openAfterMove: true,
|
||||
wipeOriginal: true,
|
||||
}
|
||||
));
|
||||
return !!unknown;
|
||||
},
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
private doValidate(
|
||||
sketch: Sketch,
|
||||
dataDirUri: URI | undefined,
|
||||
toValidate: string
|
||||
): string | undefined {
|
||||
const cloudUri = this.createFeatures.isCloud(sketch, dataDirUri);
|
||||
return cloudUri
|
||||
? Sketch.validateCloudSketchFolderName(toValidate)
|
||||
: Sketch.validateSketchFolderName(toValidate);
|
||||
}
|
||||
|
||||
private async currentSketch(): Promise<Sketch> {
|
||||
const sketch = this.sketchServiceClient.tryGetCurrentSketch();
|
||||
if (CurrentSketch.isValid(sketch)) {
|
||||
return sketch;
|
||||
}
|
||||
const deferred = new Deferred<Sketch>();
|
||||
const disposable = this.sketchServiceClient.onCurrentSketchDidChange(
|
||||
(sketch) => {
|
||||
if (CurrentSketch.isValid(sketch)) {
|
||||
disposable.dispose();
|
||||
deferred.resolve(sketch);
|
||||
}
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private async promptFixActions(): Promise<boolean> {
|
||||
const maybeDataDirUri = this.configService.tryGetDataDirUri();
|
||||
const [sketch, dataDirUri] = await Promise.all([
|
||||
this.currentSketch(),
|
||||
maybeDataDirUri ??
|
||||
waitForEvent(this.configService.onDidChangeDataDirUri, 5_000),
|
||||
]);
|
||||
const fixActions = this.validateSketch(sketch, dataDirUri);
|
||||
for (const fixAction of fixActions) {
|
||||
const result = await fixAction.execute();
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async promptRenameSketch(
|
||||
sketch: Sketch,
|
||||
error: string
|
||||
): Promise<boolean> {
|
||||
return this.prompt(
|
||||
nls.localize(
|
||||
'arduino/validateSketch/renameSketchFolderTitle',
|
||||
'Invalid sketch name'
|
||||
),
|
||||
nls.localize(
|
||||
'arduino/validateSketch/renameSketchFolderMessage',
|
||||
"The sketch '{0}' cannot be used. {1} To get rid of this message, rename the sketch. Do you want to rename the sketch now?",
|
||||
sketch.name,
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async promptRenameSketchFile(
|
||||
uri: URI,
|
||||
error: string
|
||||
): Promise<boolean> {
|
||||
return this.prompt(
|
||||
nls.localize(
|
||||
'arduino/validateSketch/renameSketchFileTitle',
|
||||
'Invalid sketch filename'
|
||||
),
|
||||
nls.localize(
|
||||
'arduino/validateSketch/renameSketchFileMessage',
|
||||
"The sketch file '{0}' cannot be used. {1} Do you want to rename the sketch file now?",
|
||||
uri.path.base,
|
||||
error
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async prompt(
|
||||
title: string,
|
||||
message: string,
|
||||
buttons: string[] = [Dialog.CANCEL, Dialog.OK]
|
||||
): Promise<boolean> {
|
||||
const { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title,
|
||||
message,
|
||||
type: 'warning',
|
||||
buttons,
|
||||
}
|
||||
);
|
||||
// cancel
|
||||
if (response === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
interface FixAction {
|
||||
execute(): Promise<boolean>;
|
||||
}
|
@@ -11,7 +11,7 @@ import {
|
||||
TabBarToolbarRegistry,
|
||||
} from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { CoreService } from '../../common/protocol';
|
||||
import { CoreErrorHandler } from './core-error-handler';
|
||||
|
||||
@@ -21,11 +21,18 @@ export interface VerifySketchParams {
|
||||
*/
|
||||
readonly exportBinaries?: boolean;
|
||||
/**
|
||||
* If `true`, there won't be any UI indication of the verify command. It's `false` by default.
|
||||
* If `true`, there won't be any UI indication of the verify command in the toolbar. It's `false` by default.
|
||||
*/
|
||||
readonly silent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* - `"idle"` when neither verify, nor upload is running,
|
||||
* - `"explicit-verify"` when only verify is running triggered by the user, and
|
||||
* - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
|
||||
*/
|
||||
type VerifyProgress = 'idle' | 'explicit-verify' | 'automatic-verify';
|
||||
|
||||
@injectable()
|
||||
export class VerifySketch extends CoreServiceContribution {
|
||||
@inject(CoreErrorHandler)
|
||||
@@ -33,22 +40,24 @@ export class VerifySketch extends CoreServiceContribution {
|
||||
|
||||
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
private verifyInProgress = false;
|
||||
private verifyProgress: VerifyProgress = 'idle';
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
|
||||
execute: (params?: VerifySketchParams) => this.verifySketch(params),
|
||||
isEnabled: () => !this.verifyInProgress,
|
||||
isEnabled: () => this.verifyProgress === 'idle',
|
||||
});
|
||||
registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
|
||||
execute: () => this.verifySketch({ exportBinaries: true }),
|
||||
isEnabled: () => !this.verifyInProgress,
|
||||
isEnabled: () => this.verifyProgress === 'idle',
|
||||
});
|
||||
registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
isEnabled: () => !this.verifyInProgress,
|
||||
isToggled: () => this.verifyInProgress,
|
||||
isEnabled: () => this.verifyProgress !== 'explicit-verify',
|
||||
// toggled only when verify is running, but not toggled when automatic verify is running before the upload
|
||||
// https://github.com/arduino/arduino-ide/pull/1750#pullrequestreview-1214762975
|
||||
isToggled: () => this.verifyProgress === 'explicit-verify',
|
||||
execute: () =>
|
||||
registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
|
||||
});
|
||||
@@ -99,15 +108,17 @@ export class VerifySketch extends CoreServiceContribution {
|
||||
private async verifySketch(
|
||||
params?: VerifySketchParams
|
||||
): Promise<CoreService.Options.Compile | undefined> {
|
||||
if (this.verifyInProgress) {
|
||||
if (this.verifyProgress !== 'idle') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!params?.silent) {
|
||||
this.verifyInProgress = true;
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
this.verifyProgress = params?.silent
|
||||
? 'automatic-verify'
|
||||
: 'explicit-verify';
|
||||
this.onDidChangeEmitter.fire();
|
||||
this.menuManager.update();
|
||||
this.clearVisibleNotification();
|
||||
this.coreErrorHandler.reset();
|
||||
|
||||
const options = await this.options(params?.exportBinaries);
|
||||
@@ -138,10 +149,9 @@ export class VerifySketch extends CoreServiceContribution {
|
||||
this.handleError(e);
|
||||
return undefined;
|
||||
} finally {
|
||||
this.verifyInProgress = false;
|
||||
if (!params?.silent) {
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
this.verifyProgress = 'idle';
|
||||
this.onDidChangeEmitter.fire();
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,80 +1,37 @@
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { SketchesService } from '../../common/protocol';
|
||||
import { uint8ArrayToString } from '../../common/utils';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
import * as createPaths from './create-paths';
|
||||
import { posix } from './create-paths';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
import { Create, CreateError } from './typings';
|
||||
|
||||
export interface ResponseResultProvider {
|
||||
interface ResponseResultProvider {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(response: Response): Promise<any>;
|
||||
}
|
||||
export namespace ResponseResultProvider {
|
||||
namespace ResponseResultProvider {
|
||||
export const NOOP: ResponseResultProvider = async () => undefined;
|
||||
export const TEXT: ResponseResultProvider = (response) => response.text();
|
||||
export const JSON: ResponseResultProvider = (response) => response.json();
|
||||
}
|
||||
|
||||
export function Utf8ArrayToStr(array: Uint8Array): string {
|
||||
let out, i, c;
|
||||
let char2, char3;
|
||||
|
||||
out = '';
|
||||
const len = array.length;
|
||||
i = 0;
|
||||
while (i < len) {
|
||||
c = array[i++];
|
||||
switch (c >> 4) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
// 0xxxxxxx
|
||||
out += String.fromCharCode(c);
|
||||
break;
|
||||
case 12:
|
||||
case 13:
|
||||
// 110x xxxx 10xx xxxx
|
||||
char2 = array[i++];
|
||||
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
|
||||
break;
|
||||
case 14:
|
||||
// 1110 xxxx 10xx xxxx 10xx xxxx
|
||||
char2 = array[i++];
|
||||
char3 = array[i++];
|
||||
out += String.fromCharCode(
|
||||
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
type ResourceType = 'f' | 'd';
|
||||
|
||||
@injectable()
|
||||
export class CreateApi {
|
||||
@inject(SketchCache)
|
||||
protected sketchCache: SketchCache;
|
||||
|
||||
protected authenticationService: AuthenticationClientService;
|
||||
protected arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
public init(
|
||||
authenticationService: AuthenticationClientService,
|
||||
arduinoPreferences: ArduinoPreferences
|
||||
): CreateApi {
|
||||
this.authenticationService = authenticationService;
|
||||
this.arduinoPreferences = arduinoPreferences;
|
||||
|
||||
return this;
|
||||
}
|
||||
readonly sketchCache: SketchCache;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly arduinoPreferences: ArduinoPreferences;
|
||||
@inject(SketchesService)
|
||||
private readonly sketchesService: SketchesService;
|
||||
|
||||
getSketchSecretStat(sketch: Create.Sketch): Create.Resource {
|
||||
return {
|
||||
@@ -100,6 +57,30 @@ export class CreateApi {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
|
||||
* See [Create.Resource#path](./typings.ts). If `cache` is `true` and a sketch exists with the path,
|
||||
* the cache will be updated with the new state of the sketch.
|
||||
*/
|
||||
// TODO: no nulls in API
|
||||
async sketchByPath(
|
||||
sketchPath: string,
|
||||
cache = false
|
||||
): Promise<Create.Sketch | null> {
|
||||
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
|
||||
const headers = await this.headers();
|
||||
const sketch = await this.run<Create.Sketch>(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
if (sketch && cache) {
|
||||
this.sketchCache.addSketch(sketch);
|
||||
const posixPath = createPaths.toPosixPath(sketch.path);
|
||||
this.sketchCache.purgeByPath(posixPath);
|
||||
}
|
||||
return sketch;
|
||||
}
|
||||
|
||||
async sketches(limit = 50): Promise<Create.Sketch[]> {
|
||||
const url = new URL(`${this.domain()}/sketches`);
|
||||
url.searchParams.set('user_id', 'me');
|
||||
@@ -129,14 +110,22 @@ export class CreateApi {
|
||||
|
||||
async createSketch(
|
||||
posixPath: string,
|
||||
content: string = CreateApi.defaultInoContent
|
||||
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent(),
|
||||
payloadOverride: Record<
|
||||
string,
|
||||
string | boolean | number | Record<string, unknown>
|
||||
> = {}
|
||||
): Promise<Create.Sketch> {
|
||||
const url = new URL(`${this.domain()}/sketches`);
|
||||
const headers = await this.headers();
|
||||
const [headers, content] = await Promise.all([
|
||||
this.headers(),
|
||||
contentProvider,
|
||||
]);
|
||||
const payload = {
|
||||
ino: btoa(content),
|
||||
path: posixPath,
|
||||
user_id: 'me',
|
||||
...payloadOverride,
|
||||
};
|
||||
const init = {
|
||||
method: 'PUT',
|
||||
@@ -252,7 +241,17 @@ export class CreateApi {
|
||||
return data;
|
||||
}
|
||||
|
||||
const sketch = this.sketchCache.getSketch(createPaths.parentPosix(path));
|
||||
const posixPath = createPaths.parentPosix(path);
|
||||
let sketch = this.sketchCache.getSketch(posixPath);
|
||||
// Workaround for https://github.com/arduino/arduino-ide/issues/1999.
|
||||
if (!sketch) {
|
||||
// Convert the ordinary sketch POSIX path to the Create path.
|
||||
// For example, `/sketch_apr6a` will be transformed to `8a694e4b83878cc53472bd75ee928053:kittaakos/sketches_v2/sketch_apr6a`.
|
||||
const createPathPrefix = this.sketchCache.createPathPrefix;
|
||||
if (createPathPrefix) {
|
||||
sketch = await this.sketchByPath(createPathPrefix + posixPath, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
sketch &&
|
||||
@@ -291,7 +290,7 @@ export class CreateApi {
|
||||
this.sketchCache.addSketch(sketch);
|
||||
|
||||
let file = '';
|
||||
if (sketch && sketch.secrets) {
|
||||
if (sketch.secrets) {
|
||||
for (const item of sketch.secrets) {
|
||||
file += `#define ${item.name} "${item.value}"\r\n`;
|
||||
}
|
||||
@@ -328,10 +327,9 @@ export class CreateApi {
|
||||
if (sketch) {
|
||||
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
|
||||
const headers = await this.headers();
|
||||
|
||||
// parse the secret file
|
||||
const secrets = (
|
||||
typeof content === 'string' ? content : Utf8ArrayToStr(content)
|
||||
typeof content === 'string' ? content : uint8ArrayToString(content)
|
||||
)
|
||||
.split(/\r?\n/)
|
||||
.reduce((prev, curr) => {
|
||||
@@ -381,7 +379,7 @@ export class CreateApi {
|
||||
return;
|
||||
}
|
||||
|
||||
// do not upload "do_not_sync" files/directoris and their descendants
|
||||
// do not upload "do_not_sync" files/directories and their descendants
|
||||
const segments = posixPath.split(posix.sep) || [];
|
||||
if (
|
||||
segments.some((segment) => Create.do_not_sync_files.includes(segment))
|
||||
@@ -395,7 +393,7 @@ export class CreateApi {
|
||||
const headers = await this.headers();
|
||||
|
||||
let data: string =
|
||||
typeof content === 'string' ? content : Utf8ArrayToStr(content);
|
||||
typeof content === 'string' ? content : uint8ArrayToString(content);
|
||||
data = await this.toggleSecretsInclude(posixPath, data, 'remove');
|
||||
|
||||
const payload = { data: btoa(data) };
|
||||
@@ -415,6 +413,21 @@ export class CreateApi {
|
||||
await this.delete(posixPath, 'd');
|
||||
}
|
||||
|
||||
/**
|
||||
* `sketchPath` is not the POSIX path but the path with the user UUID, username, etc.
|
||||
* See [Create.Resource#path](./typings.ts). Unlike other endpoints, it does not support the `$HOME`
|
||||
* variable substitution. The DELETE directory endpoint is bogus and responses with HTTP 500
|
||||
* instead of 404 when deleting a non-existing resource.
|
||||
*/
|
||||
async deleteSketch(sketchPath: string): Promise<void> {
|
||||
const url = new URL(`${this.domain()}/sketches/byPath/${sketchPath}`);
|
||||
const headers = await this.headers();
|
||||
await this.run(url, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
private async delete(posixPath: string, type: ResourceType): Promise<void> {
|
||||
const url = new URL(
|
||||
`${this.domain()}/files/${type}/$HOME/sketches_v2${posixPath}`
|
||||
@@ -474,15 +487,18 @@ export class CreateApi {
|
||||
await this.run(url, init, ResponseResultProvider.NOOP);
|
||||
}
|
||||
|
||||
private fetchCounter = 0;
|
||||
private async run<T>(
|
||||
requestInfo: RequestInfo | URL,
|
||||
requestInfo: URL,
|
||||
init: RequestInit | undefined,
|
||||
resultProvider: ResponseResultProvider = ResponseResultProvider.JSON
|
||||
): Promise<T> {
|
||||
const response = await fetch(
|
||||
requestInfo instanceof URL ? requestInfo.toString() : requestInfo,
|
||||
init
|
||||
);
|
||||
const fetchCount = `[${++this.fetchCounter}]`;
|
||||
const fetchStart = performance.now();
|
||||
const method = init?.method ? `${init.method}: ` : '';
|
||||
const url = requestInfo.toString();
|
||||
const response = await fetch(requestInfo.toString(), init);
|
||||
const fetchEnd = performance.now();
|
||||
if (!response.ok) {
|
||||
let details: string | undefined = undefined;
|
||||
try {
|
||||
@@ -493,7 +509,18 @@ export class CreateApi {
|
||||
const { statusText, status } = response;
|
||||
throw new CreateError(statusText, status, details);
|
||||
}
|
||||
const parseStart = performance.now();
|
||||
const result = await resultProvider(response);
|
||||
const parseEnd = performance.now();
|
||||
console.debug(
|
||||
`HTTP ${fetchCount} ${method} ${url} [fetch: ${(
|
||||
fetchEnd - fetchStart
|
||||
).toFixed(2)} ms, parse: ${(parseEnd - parseStart).toFixed(
|
||||
2
|
||||
)} ms] body: ${
|
||||
typeof result === 'string' ? result : JSON.stringify(result)
|
||||
}`
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -516,19 +543,3 @@ export class CreateApi {
|
||||
return this.authenticationService.session?.accessToken || '';
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CreateApi {
|
||||
export const defaultInoContent = `/*
|
||||
|
||||
*/
|
||||
|
||||
void setup() {
|
||||
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
}
|
||||
|
||||
`;
|
||||
}
|
||||
|
142
arduino-ide-extension/src/browser/create/create-features.ts
Normal file
142
arduino-ide-extension/src/browser/create/create-features.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Sketch } from '../../common/protocol';
|
||||
import { AuthenticationSession } from '../../node/auth/types';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
|
||||
import { CreateUri } from './create-uri';
|
||||
|
||||
export type CloudSketchState = 'push' | 'pull';
|
||||
|
||||
@injectable()
|
||||
export class CreateFeatures implements FrontendApplicationContribution {
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly preferences: ArduinoPreferences;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(LocalCacheFsProvider)
|
||||
private readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
|
||||
/**
|
||||
* The keys are the Create URI of the sketches.
|
||||
*/
|
||||
private readonly _cloudSketchStates = new Map<string, CloudSketchState>();
|
||||
private readonly onDidChangeSessionEmitter = new Emitter<
|
||||
AuthenticationSession | undefined
|
||||
>();
|
||||
private readonly onDidChangeEnabledEmitter = new Emitter<boolean>();
|
||||
private readonly onDidChangeCloudSketchStateEmitter = new Emitter<{
|
||||
uri: URI;
|
||||
state: CloudSketchState | undefined;
|
||||
}>();
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeSessionEmitter,
|
||||
this.onDidChangeEnabledEmitter,
|
||||
this.onDidChangeCloudSketchStateEmitter
|
||||
);
|
||||
private _enabled: boolean;
|
||||
private _session: AuthenticationSession | undefined;
|
||||
|
||||
onStart(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.authenticationService.onSessionDidChange((session) => {
|
||||
const oldSession = this._session;
|
||||
this._session = session;
|
||||
if (!!oldSession !== !!this._session) {
|
||||
this.onDidChangeSessionEmitter.fire(this._session);
|
||||
}
|
||||
}),
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (preferenceName === 'arduino.cloud.enabled') {
|
||||
const oldEnabled = this._enabled;
|
||||
this._enabled = Boolean(newValue);
|
||||
if (this._enabled !== oldEnabled) {
|
||||
this.onDidChangeEnabledEmitter.fire(this._enabled);
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
this._enabled = this.preferences['arduino.cloud.enabled'];
|
||||
this._session = this.authenticationService.session;
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get onDidChangeSession(): Event<AuthenticationSession | undefined> {
|
||||
return this.onDidChangeSessionEmitter.event;
|
||||
}
|
||||
|
||||
get onDidChangeEnabled(): Event<boolean> {
|
||||
return this.onDidChangeEnabledEmitter.event;
|
||||
}
|
||||
|
||||
get onDidChangeCloudSketchState(): Event<{
|
||||
uri: URI;
|
||||
state: CloudSketchState | undefined;
|
||||
}> {
|
||||
return this.onDidChangeCloudSketchStateEmitter.event;
|
||||
}
|
||||
|
||||
get session(): AuthenticationSession | undefined {
|
||||
return this._session;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
cloudSketchState(uri: URI): CloudSketchState | undefined {
|
||||
return this._cloudSketchStates.get(uri.toString());
|
||||
}
|
||||
|
||||
setCloudSketchState(uri: URI, state: CloudSketchState | undefined): void {
|
||||
if (uri.scheme !== CreateUri.scheme) {
|
||||
throw new Error(
|
||||
`Expected a URI with '${uri.scheme}' scheme. Got: ${uri.toString()}`
|
||||
);
|
||||
}
|
||||
const key = uri.toString();
|
||||
if (!state) {
|
||||
if (!this._cloudSketchStates.delete(key)) {
|
||||
console.warn(
|
||||
`Could not reset the cloud sketch state of ${key}. No state existed for the the cloud sketch.`
|
||||
);
|
||||
} else {
|
||||
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state: undefined });
|
||||
}
|
||||
} else {
|
||||
this._cloudSketchStates.set(key, state);
|
||||
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the sketch is under `directories.data/RemoteSketchbook`. Otherwise, `false`.
|
||||
* Returns with `undefined` if `dataDirUri` is `undefined`.
|
||||
*/
|
||||
isCloud(sketch: Sketch, dataDirUri: URI | undefined): boolean | undefined {
|
||||
if (!dataDirUri) {
|
||||
console.warn(
|
||||
`Could not decide whether the sketch ${sketch.uri} is cloud or local. The 'directories.data' location was not available from the CLI config.`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return dataDirUri
|
||||
.resolve('RemoteSketchbook')
|
||||
.resolve('ArduinoCloud')
|
||||
.isEqualOrParent(new URI(sketch.uri));
|
||||
}
|
||||
|
||||
cloudUri(sketch: Sketch): URI | undefined {
|
||||
if (!this.session) {
|
||||
return undefined;
|
||||
}
|
||||
return this.localCacheFsProvider.from(new URI(sketch.uri));
|
||||
}
|
||||
}
|
@@ -29,6 +29,7 @@ import { CreateUri } from './create-uri';
|
||||
import { SketchesService } from '../../common/protocol';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { Create } from './typings';
|
||||
import { stringToUint8Array } from '../../common/utils';
|
||||
|
||||
@injectable()
|
||||
export class CreateFsProvider
|
||||
@@ -154,7 +155,7 @@ export class CreateFsProvider
|
||||
|
||||
async readFile(uri: URI): Promise<Uint8Array> {
|
||||
const content = await this.getCreateApi.readFile(uri.path.toString());
|
||||
return new TextEncoder().encode(content);
|
||||
return stringToUint8Array(content);
|
||||
}
|
||||
|
||||
async writeFile(
|
||||
@@ -189,10 +190,6 @@ export class CreateFsProvider
|
||||
FileSystemProviderErrorCode.NoPermissions
|
||||
);
|
||||
}
|
||||
|
||||
return this.createApi.init(
|
||||
this.authenticationService,
|
||||
this.arduinoPreferences
|
||||
);
|
||||
return this.createApi;
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { URI as Uri } from 'vscode-uri';
|
||||
import { URI as Uri } from '@theia/core/shared/vscode-uri';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { toPosixPath, parentPosix, posix } from './create-paths';
|
||||
import { Create } from './typings';
|
||||
@@ -7,7 +7,9 @@ export namespace CreateUri {
|
||||
export const scheme = 'arduino-create';
|
||||
export const root = toUri(posix.sep);
|
||||
|
||||
export function toUri(posixPathOrResource: string | Create.Resource): URI {
|
||||
export function toUri(
|
||||
posixPathOrResource: string | Create.Resource | Create.Sketch
|
||||
): URI {
|
||||
const posixPath =
|
||||
typeof posixPathOrResource === 'string'
|
||||
? posixPathOrResource
|
||||
|
@@ -71,3 +71,23 @@ export class CreateError extends Error {
|
||||
Object.setPrototypeOf(this, CreateError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export type ConflictError = CreateError & { status: 409 };
|
||||
export function isConflict(err: unknown): err is ConflictError {
|
||||
return isErrorWithStatusOf(err, 409);
|
||||
}
|
||||
|
||||
export type NotFoundError = CreateError & { status: 404 };
|
||||
export function isNotFound(err: unknown): err is NotFoundError {
|
||||
return isErrorWithStatusOf(err, 404);
|
||||
}
|
||||
|
||||
function isErrorWithStatusOf(
|
||||
err: unknown,
|
||||
status: number
|
||||
): err is CreateError & { status: number } {
|
||||
if (err instanceof CreateError) {
|
||||
return err.status === status;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"editor.foreground": "#dae3e3",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#00818480",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editorCursor.foreground": "#dae3e3",
|
||||
"editorWhitespace.foreground": "#bfbfbf",
|
||||
"editorWidget.background": "#171e21",
|
||||
"editorWidget.foreground": "#dae3e3",
|
||||
@@ -67,7 +67,8 @@
|
||||
"tree.indentGuidesStroke": "#374146",
|
||||
"tab.unfocusedActiveForeground": "#dae3e3",
|
||||
"tab.inactiveBackground": "#171e21",
|
||||
"textLink.foreground": "#0ca1a6"
|
||||
"textLink.foreground": "#0ca1a6",
|
||||
"errorForeground": "#df7365"
|
||||
},
|
||||
"tokenColors": [
|
||||
{
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"editor.foreground": "#4e5b61",
|
||||
"editor.lineHighlightBackground": "#434f5410",
|
||||
"editor.selectionBackground": "#7fcbcdb3",
|
||||
"editorCursor.foreground": "#434f54",
|
||||
"editorCursor.foreground": "#4e5b61",
|
||||
"editorWhitespace.foreground": "#bfbfbf",
|
||||
"editorWidget.background": "#f7f9f9",
|
||||
"editorWidget.foreground": "#4e5b61",
|
||||
@@ -67,7 +67,8 @@
|
||||
"tree.indentGuidesStroke": "#dae3e3",
|
||||
"tab.unfocusedActiveForeground": "#4e5b61",
|
||||
"tab.inactiveBackground": "#ecf1f1",
|
||||
"textLink.foreground": "#008184"
|
||||
"textLink.foreground": "#008184",
|
||||
"errorForeground": "#df7365"
|
||||
},
|
||||
"tokenColors": [
|
||||
{
|
||||
|
@@ -1,5 +1,9 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { AbstractDialog } from '../../theia/dialogs/dialogs';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
@@ -19,6 +23,7 @@ import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { certificateList, sanifyCertString } from './utils';
|
||||
import { ArduinoFirmwareUploader } from '../../../common/protocol/arduino-firmware-uploader';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
|
||||
@injectable()
|
||||
export class UploadCertificateDialogWidget extends ReactWidget {
|
||||
@@ -37,6 +42,9 @@ export class UploadCertificateDialogWidget extends ReactWidget {
|
||||
@inject(ArduinoFirmwareUploader)
|
||||
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
protected certificates: string[] = [];
|
||||
protected updatableFqbns: string[] = [];
|
||||
protected availableBoards: AvailableBoard[] = [];
|
||||
@@ -66,10 +74,12 @@ export class UploadCertificateDialogWidget extends ReactWidget {
|
||||
}
|
||||
});
|
||||
|
||||
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
|
||||
this.updatableFqbns = fqbns;
|
||||
this.update();
|
||||
});
|
||||
this.appStateService.reachedState('ready').then(() =>
|
||||
this.arduinoFirmwareUploader.updatableBoards().then((fqbns) => {
|
||||
this.updatableFqbns = fqbns;
|
||||
this.update();
|
||||
})
|
||||
);
|
||||
|
||||
this.boardsServiceClient.onAvailableBoardsChanged((availableBoards) => {
|
||||
this.availableBoards = availableBoards;
|
||||
@@ -147,6 +157,7 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
|
||||
'Upload SSL Root Certificates'
|
||||
),
|
||||
});
|
||||
this.node.id = 'certificate-uploader-dialog-container';
|
||||
this.contentNode.classList.add('certificate-uploader-dialog');
|
||||
this.acceptButton = undefined;
|
||||
}
|
||||
@@ -160,6 +171,9 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
const firstButton = this.widget.node.querySelector('button');
|
||||
firstButton?.focus();
|
||||
|
||||
this.widget.busyCallback = this.busyCallback.bind(this);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
|
@@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { clipboard } from 'electron';
|
||||
import { clipboard } from '@theia/core/electron-shared/@electron/remote';
|
||||
import { ReactWidget, DialogProps } from '@theia/core/lib/browser';
|
||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||
import { CreateApi } from '../create/create-api';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user