Compare commits
101 Commits
2.0.1
...
show-updat
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5c31c93636 | ||
![]() |
03355903b9 | ||
![]() |
fa4626bf14 | ||
![]() |
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 |
@@ -15,7 +15,6 @@ module.exports = {
|
||||
'.browser_modules/*',
|
||||
'docs/*',
|
||||
'scripts/*',
|
||||
'electron/*',
|
||||
'electron-app/*',
|
||||
'plugins/*',
|
||||
'arduino-ide-extension/src/node/cli-protocol',
|
||||
|
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
|
||||
|
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
@@ -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"
|
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
@@ -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 }}"
|
46
.github/workflows/build.yml
vendored
@@ -29,7 +29,7 @@ on:
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
GO_VERSION: "1.19"
|
||||
JOB_TRANSFER_ARTIFACT: build-artifacts
|
||||
CHANGELOG_ARTIFACTS: changelog
|
||||
|
||||
@@ -55,21 +55,21 @@ 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@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
@@ -109,7 +109,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/
|
||||
@@ -140,13 +140,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 }}
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
BODY: ${{ steps.changelog.outputs.BODY }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # To fetch all history for all branches and tags.
|
||||
|
||||
@@ -180,15 +180,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
|
||||
@@ -199,7 +203,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 }}
|
||||
@@ -220,7 +224,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 }}
|
||||
@@ -228,10 +232,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 }}
|
||||
@@ -263,6 +267,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
@@ -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
|
||||
|
14
.github/workflows/check-i18n-task.yml
vendored
@@ -2,7 +2,7 @@ name: Check Internationalization
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on:
|
||||
@@ -27,16 +27,16 @@ 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@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
@@ -48,6 +48,8 @@ jobs:
|
||||
|
||||
- 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
@@ -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 }}"
|
12
.github/workflows/i18n-nightly-push.yml
vendored
@@ -2,7 +2,7 @@ name: i18n-nightly-push
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -14,16 +14,16 @@ 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@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
14
.github/workflows/i18n-weekly-pull.yml
vendored
@@ -2,7 +2,7 @@ name: i18n-weekly-pull
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -14,16 +14,16 @@ 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@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
@@ -45,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@v4
|
||||
with:
|
||||
commit-message: Updated translation files
|
||||
title: Update translation files
|
||||
|
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 }}
|
||||
|
||||
|
6
.github/workflows/themes-weekly-pull.yml
vendored
@@ -8,8 +8,8 @@ on:
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
NODE_VERSION: 14.x
|
||||
GO_VERSION: "1.19"
|
||||
NODE_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
pull-from-jsonbin:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
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
|
||||
|
8
.vscode/launch.json
vendored
@@ -14,14 +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",
|
||||
"--content-trace",
|
||||
"--open-devtools"
|
||||
"--open-devtools",
|
||||
"--no-ping-timeout",
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
@@ -51,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"
|
||||
|
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
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.5",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -17,74 +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/is-valid-path": "^0.1.0",
|
||||
"@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",
|
||||
"@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",
|
||||
@@ -92,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": {
|
||||
@@ -102,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",
|
||||
@@ -114,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"
|
||||
},
|
||||
@@ -159,7 +163,11 @@
|
||||
],
|
||||
"arduino": {
|
||||
"cli": {
|
||||
"version": "0.28.0"
|
||||
"version": {
|
||||
"owner": "arduino",
|
||||
"repo": "arduino-cli",
|
||||
"commitish": "71a8576"
|
||||
}
|
||||
},
|
||||
"fwuploader": {
|
||||
"version": "2.2.2"
|
||||
@@ -168,7 +176,7 @@
|
||||
"version": "14.0.0"
|
||||
},
|
||||
"languageServer": {
|
||||
"version": "0.7.1"
|
||||
"version": "0.7.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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');
|
||||
|
@@ -10,10 +10,7 @@ import {
|
||||
MenuContribution,
|
||||
MenuModelRegistry,
|
||||
} from '@theia/core';
|
||||
import {
|
||||
FrontendApplication,
|
||||
FrontendApplicationContribution,
|
||||
} 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';
|
||||
@@ -31,7 +28,7 @@ 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 { 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 { ArduinoMenus } from './menu/arduino-menus';
|
||||
@@ -58,8 +55,8 @@ export class ArduinoFrontendContribution
|
||||
@inject(CommandRegistry)
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly arduinoPreferences: ArduinoPreferences;
|
||||
@inject(ElectronWindowPreferences)
|
||||
private readonly electronWindowPreferences: ElectronWindowPreferences;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
@@ -77,11 +74,11 @@ export class ArduinoFrontendContribution
|
||||
}
|
||||
}
|
||||
|
||||
onStart(app: FrontendApplication): 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);
|
||||
@@ -91,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 {
|
||||
|
@@ -1,18 +1,18 @@
|
||||
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,
|
||||
FrontendApplication as TheiaFrontendApplication,
|
||||
} from '@theia/core/lib/browser/frontend-application';
|
||||
import { LibraryListWidget } from './library/library-list-widget';
|
||||
import {
|
||||
LibraryListWidget,
|
||||
LibraryListWidgetSearchOptions,
|
||||
} from './library/library-list-widget';
|
||||
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
|
||||
import {
|
||||
LibraryService,
|
||||
@@ -26,9 +26,12 @@ 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 {
|
||||
BoardsListWidget,
|
||||
BoardsListWidgetSearchOptions,
|
||||
} from './boards/boards-list-widget';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
@@ -76,18 +79,21 @@ import {
|
||||
} from '../common/protocol/config-service';
|
||||
import { MonitorWidget } from './serial/monitor/monitor-widget';
|
||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import {
|
||||
TabBarDecorator,
|
||||
TabBarDecoratorService as TheiaTabBarDecoratorService,
|
||||
} from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
||||
import { ProblemManager } from './theia/markers/problem-manager';
|
||||
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
||||
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
|
||||
import { 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,
|
||||
@@ -96,6 +102,8 @@ import { EditorCommandContribution as TheiaEditorCommandContribution } from '@th
|
||||
import {
|
||||
FrontendConnectionStatusService,
|
||||
ApplicationConnectionStatusContribution,
|
||||
DaemonPort,
|
||||
IsOnline,
|
||||
} from './theia/core/connection-status-service';
|
||||
import {
|
||||
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
||||
@@ -133,11 +141,10 @@ 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 { BurnBootloader } from './contributions/burn-bootloader';
|
||||
@@ -181,8 +188,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';
|
||||
@@ -241,7 +246,6 @@ import { UploadFirmware } from './contributions/upload-firmware';
|
||||
import {
|
||||
UploadFirmwareDialog,
|
||||
UploadFirmwareDialogProps,
|
||||
UploadFirmwareDialogWidget,
|
||||
} from './dialogs/firmware-uploader/firmware-uploader-dialog';
|
||||
|
||||
import { UploadCertificate } from './contributions/upload-certificate';
|
||||
@@ -258,7 +262,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';
|
||||
@@ -271,7 +274,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';
|
||||
@@ -313,10 +315,6 @@ import { SelectedBoard } from './contributions/selected-board';
|
||||
import { CheckForIDEUpdates } from './contributions/check-for-ide-updates';
|
||||
import { OpenBoardsConfig } from './contributions/open-boards-config';
|
||||
import { SketchFilesTracker } from './contributions/sketch-files-tracker';
|
||||
import { 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';
|
||||
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';
|
||||
@@ -324,10 +322,10 @@ import { PreferencesEditorWidget } from './theia/preferences/preference-editor-w
|
||||
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';
|
||||
CheckForUpdates,
|
||||
BoardsUpdates,
|
||||
LibraryUpdates,
|
||||
} 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';
|
||||
@@ -335,32 +333,43 @@ 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';
|
||||
|
||||
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 { 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 { ThemeServiceWithDB } from './theia/core/theming';
|
||||
import { ThemeServiceWithDB as TheiaThemeServiceWithDB } from '@theia/monaco/lib/browser/monaco-indexed-db';
|
||||
import { MonacoThemingService } from './theia/monaco/monaco-theming-service';
|
||||
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 {
|
||||
BoardsListWidgetTabBarDecorator,
|
||||
LibraryListWidgetTabBarDecorator,
|
||||
} from './widgets/component-list/list-widget-tabbar-decorator';
|
||||
import { HoverService } from './theia/core/hover-service';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Commands and toolbar items
|
||||
@@ -376,8 +385,6 @@ 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)
|
||||
@@ -400,6 +407,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
LibraryListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
|
||||
bind(TabBarToolbarContribution).toService(
|
||||
LibraryListWidgetFrontendContribution
|
||||
);
|
||||
bind(CommandContribution).toService(LibraryListWidgetFrontendContribution);
|
||||
bind(LibraryListWidgetSearchOptions).toSelf().inSingletonScope();
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService)
|
||||
@@ -422,6 +434,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
)
|
||||
)
|
||||
.inSingletonScope();
|
||||
bind(ConfigServiceClient).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(ConfigServiceClient);
|
||||
|
||||
// Boards service
|
||||
bind(BoardsService)
|
||||
@@ -467,6 +481,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
BoardsListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
|
||||
bind(TabBarToolbarContribution).toService(
|
||||
BoardsListWidgetFrontendContribution
|
||||
);
|
||||
bind(CommandContribution).toService(BoardsListWidgetFrontendContribution);
|
||||
bind(BoardsListWidgetSearchOptions).toSelf().inSingletonScope();
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
@@ -585,14 +604,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(OutputChannelManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaOutputChannelManager).toService(OutputChannelManager);
|
||||
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
|
||||
@@ -717,7 +728,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);
|
||||
@@ -751,6 +762,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
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
|
||||
@@ -835,9 +852,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(
|
||||
@@ -851,10 +865,6 @@ 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);
|
||||
@@ -905,6 +915,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);
|
||||
@@ -913,6 +928,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();
|
||||
@@ -929,17 +946,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',
|
||||
@@ -950,13 +964,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',
|
||||
@@ -983,4 +995,81 @@ 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(ThemeServiceWithDB).toSelf().inSingletonScope();
|
||||
rebind(TheiaThemeServiceWithDB).toService(ThemeServiceWithDB);
|
||||
bind(MonacoThemingService).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoThemingService).toService(MonacoThemingService);
|
||||
|
||||
// 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);
|
||||
|
||||
bind(HoverService).toSelf().inSingletonScope();
|
||||
bind(LibraryUpdates).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(LibraryUpdates);
|
||||
bind(LibraryListWidgetTabBarDecorator).toSelf().inSingletonScope();
|
||||
bind(TabBarDecorator).toService(LibraryListWidgetTabBarDecorator);
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
LibraryListWidgetTabBarDecorator
|
||||
);
|
||||
bind(BoardsUpdates).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsUpdates);
|
||||
bind(BoardsListWidgetTabBarDecorator).toSelf().inSingletonScope();
|
||||
bind(TabBarDecorator).toService(BoardsListWidgetTabBarDecorator);
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
BoardsListWidgetTabBarDecorator
|
||||
);
|
||||
});
|
||||
|
@@ -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',
|
||||
@@ -270,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;
|
||||
|
@@ -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',
|
||||
};
|
||||
}
|
||||
|
@@ -174,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];
|
||||
|
@@ -6,6 +6,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
Board,
|
||||
Port,
|
||||
BoardConfig as ProtocolBoardConfig,
|
||||
BoardWithPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
@@ -18,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;
|
||||
|
@@ -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(() =>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
@@ -8,10 +9,18 @@ import {
|
||||
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';
|
||||
import {
|
||||
ListWidget,
|
||||
ListWidgetSearchOptions,
|
||||
} from '../widgets/component-list/list-widget';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetSearchOptions extends ListWidgetSearchOptions<BoardSearch> {
|
||||
get defaultOptions(): Required<BoardSearch> {
|
||||
return { query: '', type: 'All' };
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
@@ -21,7 +30,8 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
constructor(
|
||||
@inject(BoardsService) service: BoardsService,
|
||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
|
||||
@inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
|
||||
@inject(BoardsListWidgetSearchOptions)
|
||||
searchOptions: BoardsListWidgetSearchOptions
|
||||
) {
|
||||
super({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
@@ -30,10 +40,8 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemDeprecated: (item: BoardsPackage) => item.deprecated,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All' },
|
||||
searchOptions,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -158,15 +158,9 @@ export class BoardsServiceProvider
|
||||
this.lastAvailablePortsOnUpload = undefined;
|
||||
}
|
||||
|
||||
private portToAutoSelectCanBeDerived(): boolean {
|
||||
return Boolean(
|
||||
this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload
|
||||
);
|
||||
}
|
||||
|
||||
attemptPostUploadAutoSelect(): void {
|
||||
setTimeout(() => {
|
||||
if (this.portToAutoSelectCanBeDerived()) {
|
||||
if (this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload) {
|
||||
this.attemptAutoSelect({
|
||||
ports: this._availablePorts,
|
||||
boards: this._availableBoards,
|
||||
@@ -185,12 +179,12 @@ export class BoardsServiceProvider
|
||||
private deriveBoardConfigToAutoSelect(
|
||||
newState: AttachedBoardsChangeEvent['newState']
|
||||
): void {
|
||||
if (!this.portToAutoSelectCanBeDerived()) {
|
||||
if (!this.lastBoardsConfigOnUpload || !this.lastAvailablePortsOnUpload) {
|
||||
this.boardConfigToAutoSelect = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPorts = this.lastAvailablePortsOnUpload!;
|
||||
const oldPorts = this.lastAvailablePortsOnUpload;
|
||||
const { ports: newPorts, boards: newBoards } = newState;
|
||||
|
||||
const appearedPorts =
|
||||
@@ -205,20 +199,39 @@ export class BoardsServiceProvider
|
||||
Port.sameAs(board.port, port)
|
||||
);
|
||||
|
||||
const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload!;
|
||||
const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload;
|
||||
|
||||
if (
|
||||
boardOnAppearedPort &&
|
||||
lastBoardsConfigOnUpload.selectedBoard &&
|
||||
Board.sameAs(
|
||||
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: boardOnAppearedPort,
|
||||
selectedBoard: boardToAutoSelect,
|
||||
selectedPort: port,
|
||||
};
|
||||
return;
|
||||
@@ -326,8 +339,10 @@ export class BoardsServiceProvider
|
||||
// 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 &&
|
||||
@@ -353,9 +368,28 @@ export class BoardsServiceProvider
|
||||
|
||||
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 &&
|
||||
@@ -365,12 +399,15 @@ export class BoardsServiceProvider
|
||||
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;
|
||||
}
|
||||
|
@@ -1,17 +1,28 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { MenuPath } from '@theia/core';
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Type as TypeLabel } from '../../common/nls';
|
||||
import {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { MenuActionTemplate, SubmenuTemplate } from '../menu/register-menu';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
import {
|
||||
BoardsListWidget,
|
||||
BoardsListWidgetSearchOptions,
|
||||
} from './boards-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
BoardsPackage,
|
||||
BoardSearch
|
||||
> {
|
||||
@inject(BoardsListWidgetSearchOptions)
|
||||
protected readonly searchOptions: BoardsListWidgetSearchOptions;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: BoardsListWidget.WIDGET_ID,
|
||||
@@ -37,4 +48,51 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
protected parse(uri: URI): BoardSearch | undefined {
|
||||
return BoardSearch.UriParser.parse(uri);
|
||||
}
|
||||
|
||||
protected buildFilterMenuGroup(
|
||||
menuPath: MenuPath
|
||||
): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||
const typeSubmenuPath = [...menuPath, TypeLabel];
|
||||
return [
|
||||
{
|
||||
submenuPath: typeSubmenuPath,
|
||||
menuLabel: `${TypeLabel}: "${
|
||||
BoardSearch.TypeLabels[this.searchOptions.options.type]
|
||||
}"`,
|
||||
options: { order: String(0) },
|
||||
},
|
||||
...this.buildMenuActions<BoardSearch.Type>(
|
||||
typeSubmenuPath,
|
||||
BoardSearch.TypeLiterals.slice(),
|
||||
(type) => this.searchOptions.options.type === type,
|
||||
(type) => this.searchOptions.update({ type }),
|
||||
(type) => BoardSearch.TypeLabels[type]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected get showViewFilterContextMenuCommand(): Command & {
|
||||
label: string;
|
||||
} {
|
||||
return BoardsListWidgetFrontendContribution.Commands
|
||||
.SHOW_BOARDS_LIST_WIDGET_FILTER_CONTEXT_MENU;
|
||||
}
|
||||
|
||||
protected get showInstalledCommandId(): string {
|
||||
return 'arduino-show-installed-boards';
|
||||
}
|
||||
|
||||
protected get showUpdatesCommandId(): string {
|
||||
return 'arduino-show-boards-updates';
|
||||
}
|
||||
}
|
||||
export namespace BoardsListWidgetFrontendContribution {
|
||||
export namespace Commands {
|
||||
export const SHOW_BOARDS_LIST_WIDGET_FILTER_CONTEXT_MENU: Command & {
|
||||
label: string;
|
||||
} = {
|
||||
id: 'arduino-boards-list-widget-show-filter-context-menu',
|
||||
label: nls.localize('arduino/boards/filterBoards', 'Filter Boards...'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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
@@ -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,10 +7,11 @@ 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 {
|
||||
@@ -46,9 +47,7 @@ export class AddFile extends SketchContribution {
|
||||
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({
|
||||
@@ -80,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,9 +15,6 @@ import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export class AddZipLibrary extends SketchContribution {
|
||||
@inject(EnvVariablesServer)
|
||||
private readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
|
||||
|
@@ -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 {
|
||||
@@ -29,10 +28,7 @@ export class ArchiveSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
private async archiveSketch(): Promise<void> {
|
||||
const [sketch, config] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
@@ -40,9 +36,9 @@ 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(),
|
||||
{
|
||||
@@ -60,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',
|
||||
|
@@ -20,6 +20,7 @@ import {
|
||||
InstalledBoardWithPackage,
|
||||
AvailablePorts,
|
||||
Port,
|
||||
getBoardInfo,
|
||||
} from '../../common/protocol';
|
||||
import { SketchContribution, Command, CommandRegistry } from './contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
@@ -49,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')],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -155,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,
|
||||
|
@@ -1,45 +1,55 @@
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import type { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { InstallManually, Later } from '../../common/nls';
|
||||
import {
|
||||
ArduinoComponent,
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
BoardsService,
|
||||
LibraryPackage,
|
||||
LibrarySearch,
|
||||
LibraryService,
|
||||
ResponseServiceClient,
|
||||
Searchable,
|
||||
Updatable,
|
||||
} 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 { NotificationCenter } from '../notification-center';
|
||||
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||
import type { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { Command, CommandRegistry, Contribution } from './contribution';
|
||||
import { Emitter } from '@theia/core';
|
||||
import debounce = require('lodash.debounce');
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
|
||||
const NoUpdates = nls.localize(
|
||||
const noUpdates = nls.localize(
|
||||
'arduino/checkForUpdates/noUpdates',
|
||||
'There are no recent updates available.'
|
||||
);
|
||||
const PromptUpdateBoards = nls.localize(
|
||||
const promptUpdateBoards = nls.localize(
|
||||
'arduino/checkForUpdates/promptUpdateBoards',
|
||||
'Updates are available for some of your boards.'
|
||||
);
|
||||
const PromptUpdateLibraries = nls.localize(
|
||||
const promptUpdateLibraries = nls.localize(
|
||||
'arduino/checkForUpdates/promptUpdateLibraries',
|
||||
'Updates are available for some of your libraries.'
|
||||
);
|
||||
const UpdatingBoards = nls.localize(
|
||||
const updatingBoards = nls.localize(
|
||||
'arduino/checkForUpdates/updatingBoards',
|
||||
'Updating boards...'
|
||||
);
|
||||
const UpdatingLibraries = nls.localize(
|
||||
const updatingLibraries = nls.localize(
|
||||
'arduino/checkForUpdates/updatingLibraries',
|
||||
'Updating libraries...'
|
||||
);
|
||||
const InstallAll = nls.localize(
|
||||
const installAll = nls.localize(
|
||||
'arduino/checkForUpdates/installAll',
|
||||
'Install All'
|
||||
);
|
||||
@@ -49,7 +59,24 @@ interface Task<T extends ArduinoComponent> {
|
||||
readonly item: T;
|
||||
}
|
||||
|
||||
const Updatable = { type: 'Updatable' } as const;
|
||||
const updatableLibrariesSearchOption: LibrarySearch = {
|
||||
query: '',
|
||||
topic: 'All',
|
||||
...Updatable,
|
||||
};
|
||||
const updatableBoardsSearchOption: BoardSearch = {
|
||||
query: '',
|
||||
...Updatable,
|
||||
};
|
||||
const installedLibrariesSearchOptions: LibrarySearch = {
|
||||
query: '',
|
||||
topic: 'All',
|
||||
type: 'Installed',
|
||||
};
|
||||
const installedBoardsSearchOptions: BoardSearch = {
|
||||
query: '',
|
||||
type: 'Installed',
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class CheckForUpdates extends Contribution {
|
||||
@@ -70,6 +97,37 @@ export class CheckForUpdates extends Contribution {
|
||||
register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
|
||||
execute: () => this.checkForUpdates(false),
|
||||
});
|
||||
register.registerCommand(CheckForUpdates.Commands.SHOW_BOARDS_UPDATES, {
|
||||
execute: () =>
|
||||
this.showUpdatableItems(
|
||||
this.boardsContribution,
|
||||
updatableBoardsSearchOption
|
||||
),
|
||||
});
|
||||
register.registerCommand(CheckForUpdates.Commands.SHOW_LIBRARY_UPDATES, {
|
||||
execute: () =>
|
||||
this.showUpdatableItems(
|
||||
this.librariesContribution,
|
||||
updatableLibrariesSearchOption
|
||||
),
|
||||
});
|
||||
register.registerCommand(CheckForUpdates.Commands.SHOW_INSTALLED_BOARDS, {
|
||||
execute: () =>
|
||||
this.showUpdatableItems(
|
||||
this.boardsContribution,
|
||||
installedBoardsSearchOptions
|
||||
),
|
||||
});
|
||||
register.registerCommand(
|
||||
CheckForUpdates.Commands.SHOW_INSTALLED_LIBRARIES,
|
||||
{
|
||||
execute: () =>
|
||||
this.showUpdatableItems(
|
||||
this.librariesContribution,
|
||||
installedLibrariesSearchOptions
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
@@ -85,13 +143,13 @@ export class CheckForUpdates extends Contribution {
|
||||
|
||||
private async checkForUpdates(silent = true) {
|
||||
const [boardsPackages, libraryPackages] = await Promise.all([
|
||||
this.boardsService.search(Updatable),
|
||||
this.libraryService.search(Updatable),
|
||||
this.boardsService.search(updatableBoardsSearchOption),
|
||||
this.libraryService.search(updatableLibrariesSearchOption),
|
||||
]);
|
||||
this.promptUpdateBoards(boardsPackages);
|
||||
this.promptUpdateLibraries(libraryPackages);
|
||||
if (!libraryPackages.length && !boardsPackages.length && !silent) {
|
||||
this.messageService.info(NoUpdates);
|
||||
this.messageService.info(noUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +158,9 @@ export class CheckForUpdates extends Contribution {
|
||||
items,
|
||||
installable: this.boardsService,
|
||||
viewContribution: this.boardsContribution,
|
||||
viewSearchOptions: { query: '', ...Updatable },
|
||||
promptMessage: PromptUpdateBoards,
|
||||
updatingMessage: UpdatingBoards,
|
||||
viewSearchOptions: updatableBoardsSearchOption,
|
||||
promptMessage: promptUpdateBoards,
|
||||
updatingMessage: updatingBoards,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,9 +169,9 @@ export class CheckForUpdates extends Contribution {
|
||||
items,
|
||||
installable: this.libraryService,
|
||||
viewContribution: this.librariesContribution,
|
||||
viewSearchOptions: { query: '', topic: 'All', ...Updatable },
|
||||
promptMessage: PromptUpdateLibraries,
|
||||
updatingMessage: UpdatingLibraries,
|
||||
viewSearchOptions: updatableLibrariesSearchOption,
|
||||
promptMessage: promptUpdateLibraries,
|
||||
updatingMessage: updatingLibraries,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,21 +199,30 @@ export class CheckForUpdates extends Contribution {
|
||||
return;
|
||||
}
|
||||
this.messageService
|
||||
.info(message, Later, InstallManually, InstallAll)
|
||||
.info(message, Later, InstallManually, installAll)
|
||||
.then((answer) => {
|
||||
if (answer === InstallAll) {
|
||||
if (answer === installAll) {
|
||||
const tasks = items.map((item) =>
|
||||
this.createInstallTask(item, installable)
|
||||
);
|
||||
this.executeTasks(updatingMessage, tasks);
|
||||
return this.executeTasks(updatingMessage, tasks);
|
||||
} else if (answer === InstallManually) {
|
||||
viewContribution
|
||||
.openView({ reveal: true })
|
||||
.then((widget) => widget.refresh(viewSearchOptions));
|
||||
return this.showUpdatableItems(viewContribution, viewSearchOptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async showUpdatableItems<
|
||||
T extends ArduinoComponent,
|
||||
S extends Searchable.Options
|
||||
>(
|
||||
viewContribution: AbstractViewContribution<ListWidget<T, S>>,
|
||||
viewSearchOptions: S
|
||||
): Promise<void> {
|
||||
const widget = await viewContribution.openView({ reveal: true });
|
||||
widget.refresh(viewSearchOptions);
|
||||
}
|
||||
|
||||
private async executeTasks(
|
||||
message: string,
|
||||
tasks: Task<ArduinoComponent>[]
|
||||
@@ -217,5 +284,127 @@ export namespace CheckForUpdates {
|
||||
},
|
||||
'arduino/checkForUpdates/checkForUpdates'
|
||||
);
|
||||
export const SHOW_BOARDS_UPDATES: Command & { label: string } = {
|
||||
id: 'arduino-show-boards-updates',
|
||||
label: nls.localize(
|
||||
'arduino/checkForUpdates/showBoardsUpdates',
|
||||
'Boards Updates'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const SHOW_LIBRARY_UPDATES: Command & { label: string } = {
|
||||
id: 'arduino-show-library-updates',
|
||||
label: nls.localize(
|
||||
'arduino/checkForUpdates/showLibraryUpdates',
|
||||
'Library Updates'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const SHOW_INSTALLED_BOARDS: Command & { label: string } = {
|
||||
id: 'arduino-show-installed-boards',
|
||||
label: nls.localize(
|
||||
'arduino/checkForUpdates/showInstalledBoards',
|
||||
'Installed Boards'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const SHOW_INSTALLED_LIBRARIES: Command & { label: string } = {
|
||||
id: 'arduino-show-installed-libraries',
|
||||
label: nls.localize(
|
||||
'arduino/checkForUpdates/showInstalledLibraries',
|
||||
'Installed Libraries'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
abstract class ComponentUpdates<T extends ArduinoComponent>
|
||||
implements FrontendApplicationContribution
|
||||
{
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly preferences: ArduinoPreferences;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
private _updates: T[] | undefined;
|
||||
private readonly onDidChangeEmitter = new Emitter<T[]>();
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeEmitter
|
||||
);
|
||||
|
||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
readonly refresh = debounce(() => this.refreshDebounced(), 200);
|
||||
|
||||
onStart(): void {
|
||||
this.appStateService.reachedState('ready').then(() => this.refresh());
|
||||
this.toDispose.push(
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (
|
||||
preferenceName === 'arduino.checkForUpdates' &&
|
||||
typeof newValue === 'boolean'
|
||||
) {
|
||||
this.refresh();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get updates(): T[] | undefined {
|
||||
return this._updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search updatable components (libraries and platforms) via the CLI.
|
||||
*/
|
||||
abstract searchUpdates(): Promise<T[]>;
|
||||
|
||||
private async refreshDebounced(): Promise<void> {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
this._updates = checkForUpdates ? await this.searchUpdates() : [];
|
||||
this.onDidChangeEmitter.fire(this._updates.slice());
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LibraryUpdates extends ComponentUpdates<LibraryPackage> {
|
||||
@inject(LibraryService)
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
override onStart(): void {
|
||||
super.onStart();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.refresh()),
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.refresh()),
|
||||
]);
|
||||
}
|
||||
|
||||
override searchUpdates(): Promise<LibraryPackage[]> {
|
||||
return this.libraryService.search(updatableLibrariesSearchOption);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BoardsUpdates extends ComponentUpdates<BoardsPackage> {
|
||||
@inject(BoardsService)
|
||||
private readonly boardsService: BoardsService;
|
||||
|
||||
override onStart(): void {
|
||||
super.onStart();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onPlatformDidInstall(() => this.refresh()),
|
||||
this.notificationCenter.onPlatformDidUninstall(() => this.refresh()),
|
||||
this.notificationCenter.onIndexUpdateDidComplete(() => this.refresh()),
|
||||
]);
|
||||
}
|
||||
|
||||
override searchUpdates(): Promise<BoardsPackage[]> {
|
||||
return this.boardsService.search(updatableBoardsSearchOption);
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ import {
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { Dialog } from '@theia/core/lib/browser/dialogs';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
|
||||
/**
|
||||
@@ -65,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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ export class Close extends SketchContribution {
|
||||
private async isCurrentSketchTemp(): Promise<false | Sketch> {
|
||||
const currentSketch = await this.sketchServiceClient.currentSketch();
|
||||
if (CurrentSketch.isValid(currentSketch)) {
|
||||
const isTemp = await this.sketchService.isTemp(currentSketch);
|
||||
const isTemp = await this.sketchesService.isTemp(currentSketch);
|
||||
if (isTemp) {
|
||||
return currentSketch;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -12,8 +12,8 @@ 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 { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||
|
||||
import {
|
||||
MenuModelRegistry,
|
||||
MenuContribution,
|
||||
@@ -40,10 +40,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,
|
||||
@@ -58,9 +57,11 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { NotificationManager } from '../theia/messages/notifications-manager';
|
||||
import { 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';
|
||||
|
||||
export {
|
||||
Command,
|
||||
@@ -106,6 +107,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());
|
||||
@@ -138,11 +142,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;
|
||||
@@ -156,6 +160,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();
|
||||
@@ -169,6 +176,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()
|
||||
@@ -268,7 +294,7 @@ export abstract class CoreServiceContribution extends SketchContribution {
|
||||
}
|
||||
|
||||
private notificationId(message: string, ...actions: string[]): string {
|
||||
return this.notificationManager.getMessageId({
|
||||
return this.notificationManager['getMessageId']({
|
||||
text: message,
|
||||
actions,
|
||||
type: MessageType.Error,
|
||||
|
@@ -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 {
|
||||
|
@@ -1,32 +1,131 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
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 {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
SketchContribution,
|
||||
Sketch,
|
||||
} from './contribution';
|
||||
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 SketchContribution {
|
||||
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: (uri: string) => this.deleteSketch(uri),
|
||||
execute: (params: DeleteSketchParams) => this.deleteSketch(params),
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteSketch(uri: string): Promise<void> {
|
||||
const sketch = await this.loadSketch(uri);
|
||||
if (!sketch) {
|
||||
console.info(`Sketch not found at ${uri}. Skipping deletion.`);
|
||||
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;
|
||||
}
|
||||
return this.sketchService.deleteSketch(sketch);
|
||||
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.sketchService.loadSketch(uri);
|
||||
const sketch = await this.sketchesService.loadSketch(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
@@ -35,6 +134,13 @@ export class DeleteSketch extends SketchContribution {
|
||||
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 {
|
||||
@@ -43,3 +149,20 @@ export namespace DeleteSketch {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@@ -57,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}
|
||||
\`\`\``);
|
||||
\`\`\`
|
||||
`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@@ -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 {
|
||||
@@ -26,10 +29,73 @@ import {
|
||||
SketchRef,
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
Sketch,
|
||||
CoreService,
|
||||
SketchesService,
|
||||
Sketch,
|
||||
} from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
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 {
|
||||
@@ -37,10 +103,7 @@ export abstract class Examples extends SketchContribution {
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
@@ -51,6 +114,9 @@ export abstract class Examples extends SketchContribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
protected override init(): void {
|
||||
@@ -58,6 +124,12 @@ 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
|
||||
@@ -88,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',
|
||||
}
|
||||
@@ -124,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 {
|
||||
@@ -163,47 +240,33 @@ 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.clone(uri);
|
||||
if (sketch) {
|
||||
try {
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
await openClonedExample(
|
||||
uri,
|
||||
{
|
||||
sketchesService: this.sketchesService,
|
||||
commandService: this.commandRegistry,
|
||||
},
|
||||
{
|
||||
onDidFailClone: () => {
|
||||
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
forceUpdate();
|
||||
},
|
||||
onDidFailOpen: (err) => {
|
||||
this.messageService.error(err.message);
|
||||
forceUpdate();
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async clone(uri: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchService.cloneExample(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
@@ -243,9 +306,6 @@ export class BuiltInExamples extends Examples {
|
||||
|
||||
@injectable()
|
||||
export class LibraryExamples extends Examples {
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
override onStart(): void {
|
||||
|
@@ -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> {
|
||||
|
@@ -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();
|
||||
|
@@ -1,31 +1,17 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
Contribution,
|
||||
Command,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
} from './contribution';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import {
|
||||
CommandRegistry,
|
||||
DisposableCollection,
|
||||
MaybePromise,
|
||||
nls,
|
||||
} from '@theia/core/lib/common';
|
||||
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { CommandRegistry, MaybePromise, nls } from '@theia/core/lib/common';
|
||||
import { Settings } from '../dialogs/settings/settings';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import debounce = require('lodash.debounce');
|
||||
|
||||
@injectable()
|
||||
export class InterfaceScale extends Contribution {
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
private readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
private readonly menuActionsDisposables = new DisposableCollection();
|
||||
private fontScalingEnabled: InterfaceScale.FontScalingEnabled = {
|
||||
increase: true,
|
||||
decrease: true,
|
||||
@@ -62,63 +48,22 @@ export class InterfaceScale extends Contribution {
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
const increaseFontSizeMenuAction = {
|
||||
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',
|
||||
};
|
||||
const decreaseFontSizeMenuAction = {
|
||||
});
|
||||
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',
|
||||
};
|
||||
|
||||
if (this.fontScalingEnabled.increase) {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
increaseFontSizeMenuAction
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuNode(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
increaseFontSizeMenuAction.label,
|
||||
{ order: increaseFontSizeMenuAction.order }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (this.fontScalingEnabled.decrease) {
|
||||
this.menuActionsDisposables.push(
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
decreaseFontSizeMenuAction
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.menuActionsDisposables.push(
|
||||
this.menuRegistry.registerMenuNode(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.EDIT__FONT_CONTROL_GROUP,
|
||||
decreaseFontSizeMenuAction.label,
|
||||
{ order: decreaseFontSizeMenuAction.order }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
this.mainMenuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
private updateFontScalingEnabled(): void {
|
||||
@@ -153,7 +98,7 @@ export class InterfaceScale extends Contribution {
|
||||
);
|
||||
if (isChanged) {
|
||||
this.fontScalingEnabled = fontScalingEnabled;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
this.menuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
@@ -21,7 +21,7 @@ export class NewSketch extends SketchContribution {
|
||||
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',
|
||||
});
|
||||
}
|
||||
@@ -35,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());
|
||||
|
@@ -47,7 +47,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
private update(forceUpdate?: boolean): void {
|
||||
this.sketchService
|
||||
this.sketchesService
|
||||
.recentlyOpenedSketches(forceUpdate)
|
||||
.then((sketches) => this.refreshMenu(sketches));
|
||||
}
|
||||
|
@@ -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,5 +1,5 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { Later } from '../../common/nls';
|
||||
import { Sketch, SketchesError } from '../../common/protocol';
|
||||
@@ -15,17 +15,13 @@ 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';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { ContextKeyService as VSCodeContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/browser/contextKeyService';
|
||||
|
||||
@injectable()
|
||||
export class OpenSketchFiles extends SketchContribution {
|
||||
@inject(VSCodeContextKeyService)
|
||||
private readonly contextKeyService: VSCodeContextKeyService;
|
||||
|
||||
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: (
|
||||
@@ -38,13 +34,19 @@ 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' });
|
||||
}
|
||||
if (mainFileUri.endsWith('.pde')) {
|
||||
const message = nls.localize(
|
||||
'arduino/common/oldFormat',
|
||||
@@ -107,10 +109,10 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
): Promise<Sketch | undefined> {
|
||||
const { invalidMainSketchUri } = err.data;
|
||||
requestAnimationFrame(() => this.messageService.error(err.message));
|
||||
await wait(10); // let IDE2 toast the error message.
|
||||
await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
|
||||
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
|
||||
fileService: this.fileService,
|
||||
sketchService: this.sketchService,
|
||||
sketchesService: this.sketchesService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
if (movedSketch) {
|
||||
@@ -123,7 +125,7 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -131,43 +133,40 @@ 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 widget;
|
||||
}
|
||||
|
||||
const disposables = new DisposableCollection();
|
||||
if (!widget || forceOpen) {
|
||||
const deferred = new Deferred<EditorWidget>();
|
||||
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.isVisible) {
|
||||
disposables.dispose();
|
||||
if (editor.isAttached && editor.isVisible) {
|
||||
deferred.resolve(editor);
|
||||
} else {
|
||||
// In Theia, the promise resolves after opening the editor, but the editor is neither attached to the DOM, nor visible.
|
||||
// This is a hack to first get an event from monaco after the widget update request, then IDE2 waits for the next monaco context key event.
|
||||
// Here, the monaco context key event is not used, but this is the first event after the editor is visible in the UI.
|
||||
disposables.push(
|
||||
(editor.editor as MonacoEditor).onDidResize((dimension) => {
|
||||
if (dimension) {
|
||||
const isKeyOwner = (
|
||||
arg: unknown
|
||||
): arg is { key: string } => {
|
||||
if (typeof arg === 'object') {
|
||||
const object = arg as Record<string, unknown>;
|
||||
return typeof object['key'] === 'string';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
disposables.push(
|
||||
this.contextKeyService.onDidChangeContext((e) => {
|
||||
// `commentIsEmpty` is the first context key change event received from monaco after the editor is for real visible in the UI.
|
||||
if (isKeyOwner(e) && e.key === 'commentIsEmpty') {
|
||||
deferred.resolve(editor);
|
||||
disposables.dispose();
|
||||
}
|
||||
})
|
||||
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
|
||||
);
|
||||
}
|
||||
})
|
||||
@@ -176,29 +175,42 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
}
|
||||
})
|
||||
);
|
||||
this.editorManager.open(
|
||||
}
|
||||
|
||||
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}`
|
||||
);
|
||||
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
|
||||
const result = await Promise.race([
|
||||
deferred.promise,
|
||||
wait(timeout).then(() => {
|
||||
disposables.dispose();
|
||||
return 'timeout';
|
||||
}),
|
||||
]);
|
||||
if (result === 'timeout') {
|
||||
console.warn(
|
||||
`Timeout after ${timeout} millis. The editor has not shown up in time. URI: ${uri}`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
return undefined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
export namespace OpenSketchFiles {
|
||||
|
@@ -54,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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export class OpenSketch extends SketchContribution {
|
||||
}
|
||||
const uri = SketchLocation.toUri(toOpen);
|
||||
try {
|
||||
await this.sketchService.loadSketch(uri.toString());
|
||||
await this.sketchesService.loadSketch(uri.toString());
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
@@ -82,10 +82,7 @@ export class OpenSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
private async selectSketch(): Promise<Sketch | undefined> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri)
|
||||
);
|
||||
const defaultPath = await this.defaultPath();
|
||||
const { filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
@@ -109,14 +106,14 @@ 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)) {
|
||||
return promptMoveSketch(sketchFileUri, {
|
||||
fileService: this.fileService,
|
||||
sketchService: this.sketchService,
|
||||
sketchesService: this.sketchesService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
}
|
||||
@@ -135,11 +132,11 @@ export async function promptMoveSketch(
|
||||
sketchFileUri: string | URI,
|
||||
options: {
|
||||
fileService: FileService;
|
||||
sketchService: SketchesService;
|
||||
sketchesService: SketchesService;
|
||||
labelProvider: LabelProvider;
|
||||
}
|
||||
): Promise<Sketch | undefined> {
|
||||
const { fileService, sketchService, labelProvider } = options;
|
||||
const { fileService, sketchesService, labelProvider } = options;
|
||||
const uri =
|
||||
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
|
||||
const name = uri.path.name;
|
||||
@@ -179,6 +176,6 @@ export async function promptMoveSketch(
|
||||
uri,
|
||||
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||
);
|
||||
return sketchService.getSketchFolder(newSketchUri.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,28 +1,35 @@
|
||||
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 { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { DeleteSketch } from './delete-sketch';
|
||||
import {
|
||||
RenameCloudSketch,
|
||||
RenameCloudSketchParams,
|
||||
} from './rename-cloud-sketch';
|
||||
|
||||
@injectable()
|
||||
export class SaveAsSketch extends SketchContribution {
|
||||
export class SaveAsSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly applicationShell: ApplicationShell;
|
||||
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
@@ -35,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',
|
||||
});
|
||||
}
|
||||
@@ -58,21 +65,73 @@ export class SaveAsSketch extends SketchContribution {
|
||||
markAsRecentlyOpened,
|
||||
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||
): Promise<boolean> {
|
||||
const [sketch, configuration] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTemp = await this.sketchService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
let destinationUri: string | undefined;
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (cloudUri) {
|
||||
destinationUri = await this.createCloudCopy({ cloudUri, sketch });
|
||||
} else {
|
||||
destinationUri = await this.createLocalCopy(sketch, execOnlyIfTemp);
|
||||
}
|
||||
if (!destinationUri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const copiedSketch = await this.sketchesService.copy(sketch, {
|
||||
destinationUri,
|
||||
});
|
||||
const newWorkspaceUri = copiedSketch.uri;
|
||||
|
||||
await saveOntoCopiedSketch(
|
||||
sketch,
|
||||
newWorkspaceUri,
|
||||
this.shell,
|
||||
this.editorManager
|
||||
);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
|
||||
}
|
||||
const options: WorkspaceInput & StartupTask.Owner = {
|
||||
preserveWindow: true,
|
||||
tasks: [],
|
||||
};
|
||||
if (openAfterMove) {
|
||||
this.windowService.setSafeToShutDown();
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
options.tasks.push({
|
||||
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
args: [{ toDelete: sketch.uri }],
|
||||
});
|
||||
}
|
||||
this.workspaceService.open(new URI(newWorkspaceUri), options);
|
||||
}
|
||||
return !!newWorkspaceUri;
|
||||
}
|
||||
|
||||
private async createCloudCopy(
|
||||
params: RenameCloudSketchParams
|
||||
): Promise<string | undefined> {
|
||||
return this.commandService.executeCommand<string>(
|
||||
RenameCloudSketch.Commands.RENAME_CLOUD_SKETCH.id,
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
private async createLocalCopy(
|
||||
sketch: Sketch,
|
||||
execOnlyIfTemp?: boolean
|
||||
): Promise<string | undefined> {
|
||||
const isTemp = await this.sketchesService.isTemp(sketch);
|
||||
if (!isTemp && !!execOnlyIfTemp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const sketchbookDirUri = new URI(configuration.sketchDirUri);
|
||||
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.
|
||||
@@ -87,91 +146,110 @@ export class SaveAsSketch extends SketchContribution {
|
||||
|
||||
// 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(
|
||||
exists
|
||||
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
|
||||
: sketch.name
|
||||
Sketch.toValidSketchFolderName(sketch.name, exists)
|
||||
);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
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, {
|
||||
destinationUri,
|
||||
});
|
||||
if (workspaceUri) {
|
||||
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchService.markAsRecentlyOpened(workspaceUri);
|
||||
}
|
||||
}
|
||||
const options: WorkspaceInput & StartupTask.Owner = {
|
||||
preserveWindow: true,
|
||||
tasks: [],
|
||||
};
|
||||
if (workspaceUri && openAfterMove) {
|
||||
this.windowService.setSafeToShutDown();
|
||||
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
|
||||
options.tasks.push({
|
||||
command: DeleteSketch.Commands.DELETE_SKETCH.id,
|
||||
args: [sketch.uri],
|
||||
});
|
||||
}
|
||||
this.workspaceService.open(new URI(workspaceUri), options);
|
||||
}
|
||||
return !!workspaceUri;
|
||||
return await this.promptLocalSketchFolderDestination(sketch, defaultPath);
|
||||
}
|
||||
|
||||
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);
|
||||
/**
|
||||
* 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 = {
|
||||
@@ -196,3 +274,48 @@ export namespace SaveAsSketch {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -10,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 {
|
||||
@@ -24,7 +24,7 @@ export class SaveSketch extends SketchContribution {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
label: nls.localize('vscode/fileCommands/save', 'Save'),
|
||||
order: '6',
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,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,
|
||||
|
@@ -1,50 +1,37 @@
|
||||
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,
|
||||
showDisabledContextMenuOptions,
|
||||
} 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 +44,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]);
|
||||
|
||||
@@ -185,7 +122,7 @@ export class SketchControl extends SketchContribution {
|
||||
)
|
||||
);
|
||||
}
|
||||
const options = {
|
||||
const options = showDisabledContextMenuOptions({
|
||||
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
@@ -193,7 +130,7 @@ export class SketchControl extends SketchContribution {
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
};
|
||||
});
|
||||
this.contextMenuRenderer.render(options);
|
||||
},
|
||||
}
|
||||
@@ -235,7 +172,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 +186,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,7 +3,7 @@ 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 { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import { Sketch, SketchContribution } from './contribution';
|
||||
import { OpenSketchFiles } from './open-sketch-files';
|
||||
|
||||
@@ -38,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)) {
|
||||
|
@@ -11,6 +11,7 @@ import { nls } from '@theia/core/lib/common/nls';
|
||||
export class Sketchbook extends Examples {
|
||||
override onStart(): void {
|
||||
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||
this.configService.onDidChangeSketchDirUri(() => this.update());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
@@ -18,7 +19,7 @@ export class Sketchbook extends Examples {
|
||||
}
|
||||
|
||||
protected override update(): void {
|
||||
this.sketchService.getSketches({}).then((container) => {
|
||||
this.sketchesService.getSketches({}).then((container) => {
|
||||
this.register(container);
|
||||
this.menuManager.update();
|
||||
});
|
||||
|
@@ -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,6 +1,6 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { CoreService, Port } from '../../common/protocol';
|
||||
import { CoreService, Port, sanitizeFqbn } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CoreServiceContribution,
|
||||
} from './contribution';
|
||||
import { deepClone, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import type { VerifySketchParams } from './verify-sketch';
|
||||
import { UserFields } from './user-fields';
|
||||
|
||||
@@ -106,6 +106,7 @@ 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();
|
||||
@@ -150,6 +151,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
this.handleError(e);
|
||||
} finally {
|
||||
this.uploadInProgress = false;
|
||||
this.menuManager.update();
|
||||
this.boardsServiceProvider.attemptPostUploadAutoSelect();
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
@@ -168,7 +170,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
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'),
|
||||
]);
|
||||
@@ -205,19 +207,6 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
|
||||
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
|
||||
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
|
||||
*/
|
||||
private sanitizeFqbn(fqbn: string | undefined): string | undefined {
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const [vendor, arch, id] = fqbn.split(':');
|
||||
return `${vendor}:${arch}:${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UploadSketch {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { DisposableCollection, nls } from '@theia/core/lib/common';
|
||||
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, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MenuModelRegistry, Contribution } from './contribution';
|
||||
import { UploadSketch } from './upload-sketch';
|
||||
|
||||
@@ -12,7 +12,6 @@ export class UserFields extends Contribution {
|
||||
private boardRequiresUserFields = false;
|
||||
private userFieldsSet = false;
|
||||
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||
private readonly menuActionsDisposables = new DisposableCollection();
|
||||
|
||||
@inject(UserFieldsDialog)
|
||||
private readonly userFieldsDialog: UserFieldsDialog;
|
||||
@@ -20,42 +19,22 @@ export class UserFields extends Contribution {
|
||||
@inject(BoardsServiceProvider)
|
||||
private readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
|
||||
const userFields =
|
||||
await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
this.boardRequiresUserFields = userFields.length > 0;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
this.menuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
if (this.boardRequiresUserFields) {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
order: '2',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
{ order: '2' }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
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 {
|
||||
|
@@ -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,16 @@ 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();
|
||||
|
||||
@@ -139,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 {
|
||||
@@ -129,10 +86,13 @@ export class CreateApi {
|
||||
|
||||
async createSketch(
|
||||
posixPath: string,
|
||||
content: string = CreateApi.defaultInoContent
|
||||
contentProvider: MaybePromise<string> = this.sketchesService.defaultInoContent()
|
||||
): 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,
|
||||
@@ -291,7 +251,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 +288,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 +340,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 +354,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 +374,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}`
|
||||
@@ -475,14 +449,12 @@ export class CreateApi {
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
console.debug(`HTTP ${init?.method}: ${requestInfo.toString()}`);
|
||||
const response = await fetch(requestInfo.toString(), init);
|
||||
if (!response.ok) {
|
||||
let details: string | undefined = undefined;
|
||||
try {
|
||||
@@ -516,19 +488,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
@@ -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",
|
||||
@@ -38,7 +38,8 @@
|
||||
"activityBar.foreground": "#dae3e3",
|
||||
"activityBar.inactiveForeground": "#4e5b61",
|
||||
"activityBar.activeBorder": "#0ca1a6",
|
||||
"statusBar.background": "#171e21",
|
||||
"activityBarBadge.background": "#008184",
|
||||
"statusBar.background": "#0ca1a6",
|
||||
"secondaryButton.background": "#ff000000",
|
||||
"secondaryButton.foreground": "#dae3e3",
|
||||
"secondaryButton.hoverBackground": "#ffffff1a",
|
||||
@@ -67,7 +68,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",
|
||||
@@ -38,6 +38,7 @@
|
||||
"activityBar.foreground": "#4e5b61",
|
||||
"activityBar.inactiveForeground": "#bdc7c7",
|
||||
"activityBar.activeBorder": "#008184",
|
||||
"activityBarBadge.background": "#008184",
|
||||
"statusBar.background": "#006d70",
|
||||
"secondaryButton.background": "#ff000000",
|
||||
"secondaryButton.foreground": "#008184",
|
||||
@@ -67,7 +68,8 @@
|
||||
"tree.indentGuidesStroke": "#dae3e3",
|
||||
"tab.unfocusedActiveForeground": "#4e5b61",
|
||||
"tab.inactiveBackground": "#ecf1f1",
|
||||
"textLink.foreground": "#008184"
|
||||
"textLink.foreground": "#008184",
|
||||
"errorForeground": "#df7365"
|
||||
},
|
||||
"tokenColors": [
|
||||
{
|
||||
|
@@ -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';
|
||||
|
@@ -5,10 +5,8 @@ import {
|
||||
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';
|
||||
import { ReactDialog } from '../../theia/dialogs/dialogs';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import {
|
||||
AvailableBoard,
|
||||
BoardsServiceProvider,
|
||||
@@ -23,26 +21,30 @@ import { Port } from '../../../common/protocol';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
|
||||
@injectable()
|
||||
export class UploadFirmwareDialogWidget extends ReactWidget {
|
||||
export class UploadFirmwareDialogProps extends DialogProps {}
|
||||
|
||||
@injectable()
|
||||
export class UploadFirmwareDialog extends ReactDialog<void> {
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
private readonly boardsServiceClient: BoardsServiceProvider;
|
||||
@inject(ArduinoFirmwareUploader)
|
||||
protected readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||
|
||||
private readonly arduinoFirmwareUploader: ArduinoFirmwareUploader;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStatusService: FrontendApplicationStateService;
|
||||
|
||||
protected updatableFqbns: string[] = [];
|
||||
protected availableBoards: AvailableBoard[] = [];
|
||||
protected isOpen = new Object();
|
||||
private updatableFqbns: string[] = [];
|
||||
private availableBoards: AvailableBoard[] = [];
|
||||
private isOpen = new Object();
|
||||
private busy = false;
|
||||
|
||||
public busyCallback = (busy: boolean) => {
|
||||
return;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(
|
||||
@inject(UploadFirmwareDialogProps)
|
||||
protected override readonly props: UploadFirmwareDialogProps
|
||||
) {
|
||||
super({ title: UploadFirmware.Commands.OPEN.label || '' });
|
||||
this.node.id = 'firmware-uploader-dialog-container';
|
||||
this.contentNode.classList.add('firmware-uploader-dialog');
|
||||
this.acceptButton = undefined;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
@@ -59,79 +61,34 @@ export class UploadFirmwareDialogWidget extends ReactWidget {
|
||||
});
|
||||
}
|
||||
|
||||
protected flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
|
||||
this.busyCallback(true);
|
||||
return this.arduinoFirmwareUploader
|
||||
.flash(firmware, port)
|
||||
.finally(() => this.busyCallback(false));
|
||||
}
|
||||
|
||||
protected override onCloseRequest(msg: Message): void {
|
||||
super.onCloseRequest(msg);
|
||||
this.isOpen = new Object();
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return (
|
||||
<form>
|
||||
<FirmwareUploaderComponent
|
||||
availableBoards={this.availableBoards}
|
||||
firmwareUploader={this.arduinoFirmwareUploader}
|
||||
flashFirmware={this.flashFirmware.bind(this)}
|
||||
updatableFqbns={this.updatableFqbns}
|
||||
isOpen={this.isOpen}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class UploadFirmwareDialogProps extends DialogProps {}
|
||||
|
||||
@injectable()
|
||||
export class UploadFirmwareDialog extends AbstractDialog<void> {
|
||||
@inject(UploadFirmwareDialogWidget)
|
||||
protected readonly widget: UploadFirmwareDialogWidget;
|
||||
|
||||
private busy = false;
|
||||
|
||||
constructor(
|
||||
@inject(UploadFirmwareDialogProps)
|
||||
protected override readonly props: UploadFirmwareDialogProps
|
||||
) {
|
||||
super({ title: UploadFirmware.Commands.OPEN.label || '' });
|
||||
this.node.id = 'firmware-uploader-dialog-container';
|
||||
this.contentNode.classList.add('firmware-uploader-dialog');
|
||||
this.acceptButton = undefined;
|
||||
}
|
||||
|
||||
get value(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
protected override render(): React.ReactNode {
|
||||
return (
|
||||
<div>
|
||||
<form>
|
||||
<FirmwareUploaderComponent
|
||||
availableBoards={this.availableBoards}
|
||||
firmwareUploader={this.arduinoFirmwareUploader}
|
||||
flashFirmware={this.flashFirmware.bind(this)}
|
||||
updatableFqbns={this.updatableFqbns}
|
||||
isOpen={this.isOpen}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected override onAfterAttach(msg: Message): void {
|
||||
if (this.widget.isAttached) {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
const firstButton = this.widget.node.querySelector('button');
|
||||
const firstButton = this.node.querySelector('button');
|
||||
firstButton?.focus();
|
||||
this.widget.busyCallback = this.busyCallback.bind(this);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected override onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.widget.update();
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.widget.activate();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
protected override handleEnter(event: KeyboardEvent): boolean | void {
|
||||
return false;
|
||||
}
|
||||
@@ -140,11 +97,11 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
this.widget.close();
|
||||
super.close();
|
||||
this.isOpen = new Object();
|
||||
}
|
||||
|
||||
busyCallback(busy: boolean): void {
|
||||
private busyCallback(busy: boolean): void {
|
||||
this.busy = busy;
|
||||
if (busy) {
|
||||
this.closeCrossNode.classList.add('disabled');
|
||||
@@ -152,4 +109,11 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
|
||||
this.closeCrossNode.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
private flashFirmware(firmware: FirmwareInfo, port: Port): Promise<any> {
|
||||
this.busyCallback(true);
|
||||
return this.arduinoFirmwareUploader
|
||||
.flash(firmware, port)
|
||||
.finally(() => this.busyCallback(false));
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { shell } from 'electron';
|
||||
import { shell } from '@theia/core/electron-shared/@electron/remote';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import * as ReactDOM from '@theia/core/shared/react-dom';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { ProgressInfo, UpdateInfo } from '../../../common/protocol/ide-updater';
|
||||
import ProgressBar from '../../components/ProgressBar';
|
||||
@@ -28,32 +27,19 @@ export const IDEUpdaterComponent = ({
|
||||
},
|
||||
}: IDEUpdaterComponentProps): React.ReactElement => {
|
||||
const { version, releaseNotes } = updateInfo;
|
||||
const changelogDivRef =
|
||||
React.useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||
const [changelog, setChangelog] = React.useState<string>('');
|
||||
React.useEffect(() => {
|
||||
if (!!releaseNotes && changelogDivRef.current) {
|
||||
let changelog: string;
|
||||
if (typeof releaseNotes === 'string') changelog = releaseNotes;
|
||||
else
|
||||
changelog = releaseNotes.reduce((acc, item) => {
|
||||
return item.note ? (acc += `${item.note}\n\n`) : acc;
|
||||
}, '');
|
||||
ReactDOM.render(
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ href, children, ...props }) => (
|
||||
<a onClick={() => href && shell.openExternal(href)} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{changelog}
|
||||
</ReactMarkdown>,
|
||||
changelogDivRef.current
|
||||
if (releaseNotes) {
|
||||
setChangelog(
|
||||
typeof releaseNotes === 'string'
|
||||
? releaseNotes
|
||||
: releaseNotes.reduce(
|
||||
(acc, item) => (item.note ? (acc += `${item.note}\n\n`) : acc),
|
||||
''
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [updateInfo]);
|
||||
}, [releaseNotes, changelog]);
|
||||
|
||||
const DownloadCompleted: () => React.ReactElement = () => (
|
||||
<div className="ide-updater-dialog--downloaded">
|
||||
@@ -106,9 +92,24 @@ export const IDEUpdaterComponent = ({
|
||||
version
|
||||
)}
|
||||
</div>
|
||||
{releaseNotes && (
|
||||
{changelog && (
|
||||
<div className="dialogRow changelog-container">
|
||||
<div className="changelog" ref={changelogDivRef} />
|
||||
<div className="changelog">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ href, children, ...props }) => (
|
||||
<a
|
||||
onClick={() => href && shell.openExternal(href)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{changelog}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -5,10 +5,8 @@ import {
|
||||
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';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import { ReactDialog } from '../../theia/dialogs/dialogs';
|
||||
import { nls } from '@theia/core';
|
||||
import { IDEUpdaterComponent, UpdateProgress } from './ide-updater-component';
|
||||
import {
|
||||
@@ -22,47 +20,11 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
|
||||
const DOWNLOAD_PAGE_URL = 'https://www.arduino.cc/en/software';
|
||||
|
||||
@injectable()
|
||||
export class IDEUpdaterDialogWidget extends ReactWidget {
|
||||
private _updateInfo: UpdateInfo;
|
||||
private _updateProgress: UpdateProgress = {};
|
||||
|
||||
setUpdateInfo(updateInfo: UpdateInfo): void {
|
||||
this._updateInfo = updateInfo;
|
||||
this.update();
|
||||
}
|
||||
|
||||
mergeUpdateProgress(updateProgress: UpdateProgress): void {
|
||||
this._updateProgress = { ...this._updateProgress, ...updateProgress };
|
||||
this.update();
|
||||
}
|
||||
|
||||
get updateInfo(): UpdateInfo {
|
||||
return this._updateInfo;
|
||||
}
|
||||
|
||||
get updateProgress(): UpdateProgress {
|
||||
return this._updateProgress;
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return !!this._updateInfo ? (
|
||||
<IDEUpdaterComponent
|
||||
updateInfo={this._updateInfo}
|
||||
updateProgress={this._updateProgress}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class IDEUpdaterDialogProps extends DialogProps {}
|
||||
|
||||
@injectable()
|
||||
export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
|
||||
@inject(IDEUpdaterDialogWidget)
|
||||
private readonly widget: IDEUpdaterDialogWidget;
|
||||
|
||||
export class IDEUpdaterDialog extends ReactDialog<UpdateInfo | undefined> {
|
||||
@inject(IDEUpdater)
|
||||
private readonly updater: IDEUpdater;
|
||||
|
||||
@@ -75,6 +37,9 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
private _updateInfo: UpdateInfo | undefined;
|
||||
private _updateProgress: UpdateProgress = {};
|
||||
|
||||
constructor(
|
||||
@inject(IDEUpdaterDialogProps)
|
||||
protected override readonly props: IDEUpdaterDialogProps
|
||||
@@ -94,26 +59,34 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
|
||||
protected init(): void {
|
||||
this.updaterClient.onUpdaterDidFail((error) => {
|
||||
this.appendErrorButtons();
|
||||
this.widget.mergeUpdateProgress({ error });
|
||||
this.mergeUpdateProgress({ error });
|
||||
});
|
||||
this.updaterClient.onDownloadProgressDidChange((progressInfo) => {
|
||||
this.widget.mergeUpdateProgress({ progressInfo });
|
||||
this.mergeUpdateProgress({ progressInfo });
|
||||
});
|
||||
this.updaterClient.onDownloadDidFinish(() => {
|
||||
this.appendInstallButtons();
|
||||
this.widget.mergeUpdateProgress({ downloadFinished: true });
|
||||
this.mergeUpdateProgress({ downloadFinished: true });
|
||||
});
|
||||
}
|
||||
|
||||
get value(): UpdateInfo {
|
||||
return this.widget.updateInfo;
|
||||
protected render(): React.ReactNode {
|
||||
return (
|
||||
this.updateInfo && (
|
||||
<IDEUpdaterComponent
|
||||
updateInfo={this.updateInfo}
|
||||
updateProgress={this.updateProgress}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
get value(): UpdateInfo | undefined {
|
||||
return this.updateInfo;
|
||||
}
|
||||
|
||||
protected override onAfterAttach(msg: Message): void {
|
||||
if (this.widget.isAttached) {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
this.update();
|
||||
this.appendInitialButtons();
|
||||
super.onAfterAttach(msg);
|
||||
}
|
||||
@@ -196,15 +169,19 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
|
||||
}
|
||||
|
||||
private skipVersion(): void {
|
||||
if (!this.updateInfo) {
|
||||
console.warn(`Nothing to skip. No update info is available`);
|
||||
return;
|
||||
}
|
||||
this.localStorageService.setData<string>(
|
||||
SKIP_IDE_VERSION,
|
||||
this.widget.updateInfo.version
|
||||
this.updateInfo.version
|
||||
);
|
||||
this.close();
|
||||
}
|
||||
|
||||
private startDownload(): void {
|
||||
this.widget.mergeUpdateProgress({
|
||||
this.mergeUpdateProgress({
|
||||
downloadStarted: true,
|
||||
});
|
||||
this.clearButtons();
|
||||
@@ -216,31 +193,48 @@ export class IDEUpdaterDialog extends AbstractDialog<UpdateInfo> {
|
||||
this.close();
|
||||
}
|
||||
|
||||
private set updateInfo(updateInfo: UpdateInfo | undefined) {
|
||||
this._updateInfo = updateInfo;
|
||||
this.update();
|
||||
}
|
||||
|
||||
private get updateInfo(): UpdateInfo | undefined {
|
||||
return this._updateInfo;
|
||||
}
|
||||
|
||||
private get updateProgress(): UpdateProgress {
|
||||
return this._updateProgress;
|
||||
}
|
||||
|
||||
private mergeUpdateProgress(updateProgress: UpdateProgress): void {
|
||||
this._updateProgress = { ...this._updateProgress, ...updateProgress };
|
||||
this.update();
|
||||
}
|
||||
|
||||
override async open(
|
||||
data: UpdateInfo | undefined = undefined
|
||||
): Promise<UpdateInfo | undefined> {
|
||||
if (data && data.version) {
|
||||
this.widget.mergeUpdateProgress({
|
||||
this.mergeUpdateProgress({
|
||||
progressInfo: undefined,
|
||||
downloadStarted: false,
|
||||
downloadFinished: false,
|
||||
error: undefined,
|
||||
});
|
||||
this.widget.setUpdateInfo(data);
|
||||
this.updateInfo = data;
|
||||
return super.open();
|
||||
}
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.widget.activate();
|
||||
this.update();
|
||||
}
|
||||
|
||||
override close(): void {
|
||||
this.widget.dispose();
|
||||
if (
|
||||
this.widget.updateProgress?.downloadStarted &&
|
||||
!this.widget.updateProgress?.downloadFinished
|
||||
this.updateProgress?.downloadStarted &&
|
||||
!this.updateProgress?.downloadFinished
|
||||
) {
|
||||
this.updater.stopDownload();
|
||||
}
|
||||
|
@@ -218,16 +218,14 @@ export class SettingsComponent extends React.Component<
|
||||
<div className="flex-line">
|
||||
<select
|
||||
className="theia-select"
|
||||
value={ThemeService.get().getCurrentTheme().label}
|
||||
value={this.props.themeService.getCurrentTheme().label}
|
||||
onChange={this.themeDidChange}
|
||||
>
|
||||
{ThemeService.get()
|
||||
.getThemes()
|
||||
.map(({ id, label }) => (
|
||||
<option key={id} value={label}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
{this.props.themeService.getThemes().map(({ id, label }) => (
|
||||
<option key={id} value={label}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-line">
|
||||
@@ -408,7 +406,7 @@ export class SettingsComponent extends React.Component<
|
||||
}
|
||||
onChange={this.socksProtocolDidChange}
|
||||
/>
|
||||
SOCKS
|
||||
SOCKS5
|
||||
</label>
|
||||
</form>
|
||||
<div className="flex-line proxy-settings">
|
||||
@@ -612,11 +610,11 @@ export class SettingsComponent extends React.Component<
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
): void => {
|
||||
const { selectedIndex } = event.target.options;
|
||||
const theme = ThemeService.get().getThemes()[selectedIndex];
|
||||
const theme = this.props.themeService.getThemes()[selectedIndex];
|
||||
if (theme) {
|
||||
this.setState({ themeId: theme.id });
|
||||
if (ThemeService.get().getCurrentTheme().id !== theme.id) {
|
||||
ThemeService.get().setCurrentTheme(theme.id);
|
||||
if (this.props.themeService.getCurrentTheme().id !== theme.id) {
|
||||
this.props.themeService.setCurrentTheme(theme.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -684,7 +682,7 @@ export class SettingsComponent extends React.Component<
|
||||
): void => {
|
||||
if (this.state.network !== 'none') {
|
||||
const network = this.cloneProxySettings;
|
||||
network.protocol = event.target.checked ? 'http' : 'socks';
|
||||
network.protocol = event.target.checked ? 'http' : 'socks5';
|
||||
this.setState({ network });
|
||||
}
|
||||
};
|
||||
@@ -694,7 +692,7 @@ export class SettingsComponent extends React.Component<
|
||||
): void => {
|
||||
if (this.state.network !== 'none') {
|
||||
const network = this.cloneProxySettings;
|
||||
network.protocol = event.target.checked ? 'socks' : 'http';
|
||||
network.protocol = event.target.checked ? 'socks5' : 'http';
|
||||
this.setState({ network });
|
||||
}
|
||||
};
|
||||
@@ -755,6 +753,7 @@ export namespace SettingsComponent {
|
||||
readonly fileDialogService: FileDialogService;
|
||||
readonly windowService: WindowService;
|
||||
readonly localizationProvider: AsyncLocalizationProvider;
|
||||
readonly themeService: ThemeService;
|
||||
}
|
||||
export type State = Settings & {
|
||||
rawAdditionalUrlsValue: string;
|
||||
|
@@ -35,6 +35,9 @@ export class SettingsWidget extends ReactWidget {
|
||||
@inject(AsyncLocalizationProvider)
|
||||
protected readonly localizationProvider: AsyncLocalizationProvider;
|
||||
|
||||
@inject(ThemeService)
|
||||
private readonly themeService: ThemeService;
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return (
|
||||
<SettingsComponent
|
||||
@@ -43,6 +46,7 @@ export class SettingsWidget extends ReactWidget {
|
||||
fileDialogService={this.fileDialogService}
|
||||
windowService={this.windowService}
|
||||
localizationProvider={this.localizationProvider}
|
||||
themeService={this.themeService}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -59,6 +63,9 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
|
||||
@inject(SettingsWidget)
|
||||
protected readonly widget: SettingsWidget;
|
||||
|
||||
@inject(ThemeService)
|
||||
private readonly themeService: ThemeService;
|
||||
|
||||
constructor(
|
||||
@inject(SettingsDialogProps)
|
||||
protected override readonly props: SettingsDialogProps
|
||||
@@ -121,11 +128,11 @@ export class SettingsDialog extends AbstractDialog<Promise<Settings>> {
|
||||
}
|
||||
|
||||
override async open(): Promise<Promise<Settings> | undefined> {
|
||||
const themeIdBeforeOpen = ThemeService.get().getCurrentTheme().id;
|
||||
const themeIdBeforeOpen = this.themeService.getCurrentTheme().id;
|
||||
const result = await super.open();
|
||||
if (!result) {
|
||||
if (ThemeService.get().getCurrentTheme().id !== themeIdBeforeOpen) {
|
||||
ThemeService.get().setCurrentTheme(themeIdBeforeOpen);
|
||||
if (this.themeService.getCurrentTheme().id !== themeIdBeforeOpen) {
|
||||
this.themeService.setCurrentTheme(themeIdBeforeOpen);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
} from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { Deferred, timeout } from '@theia/core/lib/common/promise-util';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { deepClone } from '@theia/core/lib/common/objects';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { ThemeService } from '@theia/core/lib/browser/theming';
|
||||
@@ -25,17 +25,21 @@ import {
|
||||
LanguageInfo,
|
||||
} from '@theia/core/lib/common/i18n/localization';
|
||||
import { ElectronCommands } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
|
||||
import { DefaultTheme } from '@theia/application-package/lib/application-props';
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import type { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
|
||||
export const WINDOW_SETTING = 'window';
|
||||
export const EDITOR_SETTING = 'editor';
|
||||
export const FONT_SIZE_SETTING = `${EDITOR_SETTING}.fontSize`;
|
||||
export const AUTO_SAVE_SETTING = `files.autoSave`;
|
||||
export const QUICK_SUGGESTIONS_SETTING = `${EDITOR_SETTING}.quickSuggestions`;
|
||||
export const ARDUINO_SETTING = 'arduino';
|
||||
export const WINDOW_SETTING = `${ARDUINO_SETTING}.window`;
|
||||
export const ARDUINO_WINDOW_SETTING = `${ARDUINO_SETTING}.window`;
|
||||
export const COMPILE_SETTING = `${ARDUINO_SETTING}.compile`;
|
||||
export const UPLOAD_SETTING = `${ARDUINO_SETTING}.upload`;
|
||||
export const SKETCHBOOK_SETTING = `${ARDUINO_SETTING}.sketchbook`;
|
||||
export const AUTO_SCALE_SETTING = `${WINDOW_SETTING}.autoScale`;
|
||||
export const AUTO_SCALE_SETTING = `${ARDUINO_WINDOW_SETTING}.autoScale`;
|
||||
export const ZOOM_LEVEL_SETTING = `${WINDOW_SETTING}.zoomLevel`;
|
||||
export const COMPILE_VERBOSE_SETTING = `${COMPILE_SETTING}.verbose`;
|
||||
export const COMPILE_WARNINGS_SETTING = `${COMPILE_SETTING}.warnings`;
|
||||
@@ -53,7 +57,7 @@ export interface Settings {
|
||||
currentLanguage: string;
|
||||
|
||||
autoScaleInterface: boolean; // `arduino.window.autoScale`
|
||||
interfaceScale: number; // `arduino.window.zoomLevel` https://github.com/eclipse-theia/theia/issues/8751
|
||||
interfaceScale: number; // `window.zoomLevel`
|
||||
verboseOnCompile: boolean; // `arduino.compile.verbose`
|
||||
compilerWarnings: CompilerWarnings; // `arduino.compile.warnings`
|
||||
verboseOnUpload: boolean; // `arduino.upload.verbose`
|
||||
@@ -101,6 +105,9 @@ export class SettingsService {
|
||||
@inject(CommandService)
|
||||
protected commandService: CommandService;
|
||||
|
||||
@inject(ThemeService)
|
||||
private readonly themeService: ThemeService;
|
||||
|
||||
protected readonly onDidChangeEmitter = new Emitter<Readonly<Settings>>();
|
||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
protected readonly onDidResetEmitter = new Emitter<Readonly<Settings>>();
|
||||
@@ -141,10 +148,9 @@ export class SettingsService {
|
||||
this.preferenceService.get<number>(FONT_SIZE_SETTING, 12),
|
||||
this.preferenceService.get<string>(
|
||||
'workbench.colorTheme',
|
||||
window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'arduino-theme-dark'
|
||||
: 'arduino-theme'
|
||||
DefaultTheme.defaultForOSTheme(
|
||||
FrontendApplicationConfigProvider.get().defaultTheme
|
||||
)
|
||||
),
|
||||
this.preferenceService.get<Settings.AutoSave>(
|
||||
AUTO_SAVE_SETTING,
|
||||
@@ -166,7 +172,15 @@ export class SettingsService {
|
||||
this.preferenceService.get<boolean>(SHOW_ALL_FILES_SETTING, false),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
const { additionalUrls, sketchDirUri, network } = cliConfig;
|
||||
const {
|
||||
config = {
|
||||
additionalUrls: [],
|
||||
sketchDirUri: '',
|
||||
network: Network.Default(),
|
||||
},
|
||||
} = cliConfig;
|
||||
const { additionalUrls, sketchDirUri, network } = config;
|
||||
|
||||
const sketchbookPath = await this.fileService.fsPath(new URI(sketchDirUri));
|
||||
return {
|
||||
editorFontSize,
|
||||
@@ -218,7 +232,11 @@ export class SettingsService {
|
||||
try {
|
||||
const { sketchbookPath, editorFontSize, themeId } = await settings;
|
||||
const sketchbookDir = await this.fileSystemExt.getUri(sketchbookPath);
|
||||
if (!(await this.fileService.exists(new URI(sketchbookDir)))) {
|
||||
let sketchbookStat: FileStat | undefined = undefined;
|
||||
try {
|
||||
sketchbookStat = await this.fileService.resolve(new URI(sketchbookDir));
|
||||
} catch {}
|
||||
if (!sketchbookStat || !sketchbookStat.isDirectory) {
|
||||
return nls.localize(
|
||||
'arduino/preferences/invalid.sketchbook.location',
|
||||
'Invalid sketchbook location: {0}',
|
||||
@@ -231,11 +249,7 @@ export class SettingsService {
|
||||
'Invalid editor font size. It must be a positive integer.'
|
||||
);
|
||||
}
|
||||
if (
|
||||
!ThemeService.get()
|
||||
.getThemes()
|
||||
.find(({ id }) => id === themeId)
|
||||
) {
|
||||
if (!this.themeService.getThemes().find(({ id }) => id === themeId)) {
|
||||
return nls.localize(
|
||||
'arduino/preferences/invalid.theme',
|
||||
'Invalid theme.'
|
||||
@@ -252,7 +266,6 @@ export class SettingsService {
|
||||
|
||||
private async savePreference(name: string, value: unknown): Promise<void> {
|
||||
await this.preferenceService.set(name, value, PreferenceScope.User);
|
||||
await timeout(5);
|
||||
}
|
||||
|
||||
async save(): Promise<string | true> {
|
||||
@@ -274,28 +287,38 @@ export class SettingsService {
|
||||
network,
|
||||
sketchbookShowAllFiles,
|
||||
} = this._settings;
|
||||
const [config, sketchDirUri] = await Promise.all([
|
||||
const [cliConfig, sketchDirUri] = await Promise.all([
|
||||
this.configService.getConfiguration(),
|
||||
this.fileSystemExt.getUri(sketchbookPath),
|
||||
]);
|
||||
const { config } = cliConfig;
|
||||
if (!config) {
|
||||
// Do not check for any error messages. The config might has errors (such as invalid directories.user) right before saving the new values.
|
||||
return nls.localize(
|
||||
'arduino/preferences/noCliConfig',
|
||||
'Could not load the CLI configuration'
|
||||
);
|
||||
}
|
||||
|
||||
(config as any).additionalUrls = additionalUrls;
|
||||
(config as any).sketchDirUri = sketchDirUri;
|
||||
(config as any).network = network;
|
||||
(config as any).locale = currentLanguage;
|
||||
|
||||
await this.savePreference('editor.fontSize', editorFontSize);
|
||||
await this.savePreference('workbench.colorTheme', themeId);
|
||||
await this.savePreference(AUTO_SAVE_SETTING, autoSave);
|
||||
await this.savePreference('editor.quickSuggestions', quickSuggestions);
|
||||
await this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface);
|
||||
await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale);
|
||||
await this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale);
|
||||
await this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile);
|
||||
await this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings);
|
||||
await this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload);
|
||||
await this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload);
|
||||
await this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles);
|
||||
await this.configService.setConfiguration(config);
|
||||
await Promise.all([
|
||||
this.savePreference('editor.fontSize', editorFontSize),
|
||||
this.savePreference('workbench.colorTheme', themeId),
|
||||
this.savePreference(AUTO_SAVE_SETTING, autoSave),
|
||||
this.savePreference('editor.quickSuggestions', quickSuggestions),
|
||||
this.savePreference(AUTO_SCALE_SETTING, autoScaleInterface),
|
||||
this.savePreference(ZOOM_LEVEL_SETTING, interfaceScale),
|
||||
this.savePreference(COMPILE_VERBOSE_SETTING, verboseOnCompile),
|
||||
this.savePreference(COMPILE_WARNINGS_SETTING, compilerWarnings),
|
||||
this.savePreference(UPLOAD_VERBOSE_SETTING, verboseOnUpload),
|
||||
this.savePreference(UPLOAD_VERIFY_SETTING, verifyAfterUpload),
|
||||
this.savePreference(SHOW_ALL_FILES_SETTING, sketchbookShowAllFiles),
|
||||
this.configService.setConfiguration(config),
|
||||
]);
|
||||
this.onDidChangeEmitter.fire(this._settings);
|
||||
|
||||
// after saving all the settings, if we need to change the language we need to perform a reload
|
||||
|
@@ -1,63 +1,18 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
AbstractDialog,
|
||||
DialogProps,
|
||||
ReactWidget,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { UploadSketch } from '../../contributions/upload-sketch';
|
||||
import { UserFieldsComponent } from './user-fields-component';
|
||||
import { BoardUserField } from '../../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class UserFieldsDialogWidget extends ReactWidget {
|
||||
private _currentUserFields: BoardUserField[] = [];
|
||||
|
||||
constructor(private cancel: () => void, private accept: () => Promise<void>) {
|
||||
super();
|
||||
}
|
||||
|
||||
set currentUserFields(userFields: BoardUserField[]) {
|
||||
this.setUserFields(userFields);
|
||||
}
|
||||
|
||||
get currentUserFields(): BoardUserField[] {
|
||||
return this._currentUserFields;
|
||||
}
|
||||
|
||||
resetUserFieldsValue(): void {
|
||||
this._currentUserFields = this._currentUserFields.map((field) => {
|
||||
field.value = '';
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
private setUserFields(userFields: BoardUserField[]): void {
|
||||
this._currentUserFields = userFields;
|
||||
}
|
||||
|
||||
protected render(): React.ReactNode {
|
||||
return (
|
||||
<form>
|
||||
<UserFieldsComponent
|
||||
initialBoardUserFields={this._currentUserFields}
|
||||
updateUserFields={this.setUserFields.bind(this)}
|
||||
cancel={this.cancel}
|
||||
accept={this.accept}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
import { ReactDialog } from '../../theia/dialogs/dialogs';
|
||||
|
||||
@injectable()
|
||||
export class UserFieldsDialogProps extends DialogProps {}
|
||||
|
||||
@injectable()
|
||||
export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
|
||||
protected readonly widget: UserFieldsDialogWidget;
|
||||
export class UserFieldsDialog extends ReactDialog<BoardUserField[]> {
|
||||
private _currentUserFields: BoardUserField[] = [];
|
||||
|
||||
constructor(
|
||||
@inject(UserFieldsDialogProps)
|
||||
@@ -69,39 +24,36 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
|
||||
this.titleNode.classList.add('user-fields-dialog-title');
|
||||
this.contentNode.classList.add('user-fields-dialog-content');
|
||||
this.acceptButton = undefined;
|
||||
this.widget = new UserFieldsDialogWidget(
|
||||
this.close.bind(this),
|
||||
this.accept.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
set value(userFields: BoardUserField[]) {
|
||||
this.widget.currentUserFields = userFields;
|
||||
}
|
||||
|
||||
get value(): BoardUserField[] {
|
||||
return this.widget.currentUserFields;
|
||||
return this._currentUserFields;
|
||||
}
|
||||
|
||||
set value(userFields: BoardUserField[]) {
|
||||
this._currentUserFields = userFields;
|
||||
}
|
||||
|
||||
protected override render(): React.ReactNode {
|
||||
return (
|
||||
<div>
|
||||
<form>
|
||||
<UserFieldsComponent
|
||||
initialBoardUserFields={this.value}
|
||||
updateUserFields={this.doUpdateUserFields}
|
||||
cancel={this.doCancel}
|
||||
accept={this.doAccept}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
protected override onAfterAttach(msg: Message): void {
|
||||
if (this.widget.isAttached) {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected override onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.widget.update();
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.widget.activate();
|
||||
}
|
||||
|
||||
protected override async accept(): Promise<void> {
|
||||
// If the user presses enter and at least
|
||||
// a field is empty don't accept the input
|
||||
@@ -114,8 +66,21 @@ export class UserFieldsDialog extends AbstractDialog<BoardUserField[]> {
|
||||
}
|
||||
|
||||
override close(): void {
|
||||
this.widget.resetUserFieldsValue();
|
||||
this.widget.close();
|
||||
this.resetUserFieldsValue();
|
||||
super.close();
|
||||
}
|
||||
|
||||
private resetUserFieldsValue(): void {
|
||||
this.value = this.value.map((field) => {
|
||||
field.value = '';
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
private readonly doCancel: () => void = () => this.close();
|
||||
private readonly doAccept: () => Promise<void> = () => this.accept();
|
||||
private readonly doUpdateUserFields: (userFields: BoardUserField[]) => void =
|
||||
(userFields: BoardUserField[]) => {
|
||||
this.value = userFields;
|
||||
};
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.355 3.85509L2.85504 14.3551C2.76026 14.448 2.63281 14.5001 2.50006 14.5001C2.36731 14.5001 2.23986 14.448 2.14508 14.3551C2.0514 14.2607 1.99882 14.1331 1.99882 14.0001C1.99882 13.8671 2.0514 13.7395 2.14508 13.6451L3.82508 11.9651C3.24351 11.8742 2.70645 11.5991 2.29291 11.1802C1.87936 10.7613 1.61116 10.2208 1.52775 9.63811C1.44434 9.05543 1.55012 8.46136 1.82955 7.94328C2.10897 7.4252 2.54728 7.01047 3.08 6.76009C3.20492 6.18251 3.47405 5.64596 3.86232 5.20047C4.25058 4.75498 4.74532 4.41505 5.30042 4.21239C5.85552 4.00972 6.45289 3.9509 7.03686 4.04143C7.62082 4.13196 8.17236 4.36887 8.64004 4.73009C9.01346 4.56809 9.41786 4.48995 9.82475 4.50117C10.2316 4.51239 10.6311 4.6127 10.995 4.79503L12.645 3.14509C12.7392 3.05094 12.8669 2.99805 13 2.99805C13.1332 2.99805 13.2609 3.05094 13.355 3.14509C13.4492 3.23924 13.5021 3.36694 13.5021 3.50009C13.5021 3.63324 13.4492 3.76094 13.355 3.85509V3.85509Z" fill="#7F8C8D"/>
|
||||
<path d="M14.5 9.25047C14.4987 9.97942 14.2086 10.6782 13.6931 11.1936C13.1777 11.709 12.479 11.9992 11.75 12.0005H6.70996L12.355 6.35547C12.38 6.43042 12.4 6.50547 12.4201 6.58044C13.0153 6.72902 13.5436 7.07272 13.9206 7.55669C14.2976 8.04066 14.5016 8.63699 14.5 9.25047V9.25047Z" fill="#7F8C8D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.5 9.24997C14.4987 9.97893 14.2086 10.6777 13.6932 11.1931C13.1777 11.7086 12.479 11.9987 11.75 12H4.25003C3.62476 11.9998 3.01822 11.7866 2.53034 11.3955C2.04247 11.0045 1.70238 10.4589 1.56612 9.84864C1.42986 9.2384 1.50556 8.59995 1.78074 8.0385C2.05593 7.47705 2.51418 7.0261 3.07998 6.75997C3.2049 6.18239 3.47404 5.64584 3.8623 5.20035C4.25056 4.75486 4.74531 4.41494 5.3004 4.21227C5.8555 4.0096 6.45288 3.95078 7.03684 4.04131C7.62081 4.13184 8.17234 4.36875 8.64003 4.72997C8.99025 4.57772 9.36814 4.49942 9.75003 4.49997C10.3635 4.49838 10.9598 4.70238 11.4438 5.07939C11.9278 5.45641 12.2715 5.9847 12.4201 6.57993C13.0153 6.7285 13.5436 7.07221 13.9206 7.55618C14.2976 8.04015 14.5016 8.63649 14.5 9.24997Z" fill="#7F8C8D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 852 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.42 6.58044C12.4 6.50549 12.38 6.43042 12.355 6.35547L11.525 7.18555C11.5575 7.27223 11.6136 7.34811 11.6869 7.40464C11.7603 7.46117 11.8479 7.4961 11.94 7.50549C12.3852 7.55476 12.7947 7.77259 13.0843 8.11428C13.374 8.45597 13.5218 8.89557 13.4975 9.34284C13.4732 9.7901 13.2785 10.2111 12.9536 10.5194C12.6286 10.8276 12.1979 10.9998 11.75 11.0005H7.70996L6.70996 12.0005H11.75C12.421 12.0001 13.0688 11.7545 13.5714 11.3099C14.074 10.8653 14.3969 10.2524 14.4792 9.58644C14.5615 8.92048 14.3977 8.24739 14.0184 7.69379C13.6392 7.14019 13.0708 6.74425 12.42 6.58044V6.58044Z" fill="#7F8C8D"/>
|
||||
<path d="M13.355 3.14532C13.2606 3.05161 13.133 2.99902 13 2.99902C12.867 2.99902 12.7394 3.05161 12.6451 3.14532L10.995 4.79524C10.6311 4.61291 10.2316 4.5126 9.82472 4.50139C9.41783 4.49017 9.01343 4.56832 8.64002 4.73032C8.17233 4.3691 7.6208 4.13219 7.03684 4.04166C6.45287 3.95114 5.85549 4.00995 5.3004 4.21262C4.7453 4.41529 4.25056 4.75521 3.86229 5.2007C3.47403 5.64619 3.2049 6.18274 3.07997 6.76033C2.54726 7.01071 2.10896 7.42543 1.82954 7.9435C1.55013 8.46157 1.44434 9.05564 1.52775 9.63832C1.61115 10.221 1.87935 10.7615 2.29288 11.1804C2.70641 11.5993 3.24346 11.8744 3.82502 11.9653L2.14502 13.6453C2.05133 13.7397 1.99876 13.8673 1.99876 14.0003C1.99876 14.1333 2.05133 14.2609 2.14502 14.3553C2.23979 14.4482 2.36725 14.5003 2.5 14.5003C2.63275 14.5003 2.7602 14.4482 2.85498 14.3553L13.355 3.85528C13.4487 3.7609 13.5012 3.6333 13.5013 3.50031C13.5013 3.36732 13.4487 3.23972 13.355 3.14532V3.14532ZM4.79006 11.0003H4.25002C3.8356 11.0005 3.43458 10.8535 3.11841 10.5856C2.80224 10.3177 2.59145 9.94623 2.52362 9.5374C2.45578 9.12857 2.53529 8.70893 2.74799 8.35326C2.96069 7.99758 3.29275 7.72898 3.68502 7.59529C3.77434 7.56478 3.85319 7.50962 3.91248 7.43617C3.97176 7.36272 4.00904 7.274 4.02002 7.18025C4.09848 6.57783 4.39334 6.0245 4.84963 5.62341C5.30592 5.22233 5.89251 5.00087 6.50002 5.00032C7.1425 4.99652 7.76054 5.24628 8.21999 5.69539C8.30086 5.77275 8.40511 5.8211 8.5164 5.83285C8.6277 5.8446 8.73974 5.8191 8.83499 5.76033C9.10926 5.58886 9.42655 5.4987 9.75002 5.50032C9.9105 5.50127 10.0702 5.5231 10.225 5.56526L4.79006 11.0003Z" fill="#7F8C8D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.75 12H4.25C3.62484 11.9999 3.01838 11.7868 2.53053 11.3959C2.04269 11.0049 1.70257 10.4595 1.56622 9.84934C1.42987 9.23923 1.50542 8.60087 1.78043 8.03945C2.05543 7.47802 2.51348 7.02702 3.0791 6.76076C3.24864 5.97929 3.68041 5.27932 4.3027 4.77712C4.92499 4.27492 5.70035 4.00071 6.5 4.00002C7.27505 3.99715 8.02853 4.25513 8.63916 4.73244C9.00591 4.57154 9.40329 4.49243 9.8037 4.5006C10.2041 4.50877 10.5979 4.60403 10.9578 4.77976C11.3177 4.9555 11.635 5.20748 11.8876 5.51822C12.1403 5.82895 12.3223 6.19097 12.4209 6.57912C13.0715 6.74324 13.6398 7.13939 14.0188 7.69309C14.3979 8.24679 14.5616 8.91989 14.4792 9.58582C14.3967 10.2518 14.0739 10.8646 13.5713 11.3092C13.0687 11.7538 12.421 11.9995 11.75 12ZM6.5 5.00002C5.89213 5.00017 5.30514 5.22179 4.84885 5.62344C4.39257 6.02508 4.09826 6.57921 4.021 7.18215C4.0093 7.27546 3.97153 7.36357 3.91202 7.43638C3.85252 7.50918 3.77369 7.56374 3.68458 7.59377C3.29236 7.72769 2.9604 7.99647 2.7478 8.35224C2.5352 8.70801 2.45576 9.12768 2.52363 9.53654C2.5915 9.9454 2.80227 10.3169 3.11841 10.5849C3.43455 10.8529 3.83555 11 4.25 11H11.75C12.198 10.9996 12.6289 10.8275 12.9539 10.5191C13.279 10.2108 13.4735 9.7896 13.4975 9.34221C13.5215 8.89481 13.3732 8.45522 13.083 8.11384C12.7929 7.77246 12.3829 7.55524 11.9375 7.50686C11.8238 7.4948 11.7176 7.44411 11.6368 7.36325C11.5559 7.28238 11.5052 7.17624 11.4932 7.06252C11.4474 6.63255 11.2439 6.2348 10.9219 5.94619C10.6 5.65758 10.1824 5.49861 9.75 5.50002C9.42739 5.49791 9.11079 5.58731 8.83692 5.75783C8.74185 5.81746 8.62955 5.84352 8.51794 5.83184C8.40633 5.82015 8.30185 5.77141 8.22119 5.69338C7.76046 5.24569 7.14241 4.99672 6.5 5.00002V5.00002Z" fill="#7F8C8D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
6
arduino-ide-extension/src/browser/icons/loading-dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
|
||||
<!--Copyright (C) 2020 TypeFox and others.-->
|
||||
<!--Copied from https://raw.githubusercontent.com/microsoft/vscode-icons/9c90ce81b1f3c309000b80da0763aa09975a85f0/icons/dark/loading.svg -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0.75C8.17188 0.75 8.33333 0.783854 8.48438 0.851562C8.63542 0.914062 8.76823 1.0026 8.88281 1.11719C8.9974 1.23177 9.08594 1.36458 9.14844 1.51562C9.21615 1.66667 9.25 1.82812 9.25 2C9.25 2.17188 9.21615 2.33333 9.14844 2.48438C9.08594 2.63542 8.9974 2.76823 8.88281 2.88281C8.76823 2.9974 8.63542 3.08854 8.48438 3.15625C8.33333 3.21875 8.17188 3.25 8 3.25C7.82812 3.25 7.66667 3.21875 7.51562 3.15625C7.36458 3.08854 7.23177 2.9974 7.11719 2.88281C7.0026 2.76823 6.91146 2.63542 6.84375 2.48438C6.78125 2.33333 6.75 2.17188 6.75 2C6.75 1.82812 6.78125 1.66667 6.84375 1.51562C6.91146 1.36458 7.0026 1.23177 7.11719 1.11719C7.23177 1.0026 7.36458 0.914062 7.51562 0.851562C7.66667 0.783854 7.82812 0.75 8 0.75ZM2.63281 3.75781C2.63281 3.60156 2.66146 3.45573 2.71875 3.32031C2.77604 3.1849 2.85417 3.06771 2.95312 2.96875C3.05729 2.86458 3.17708 2.78385 3.3125 2.72656C3.45312 2.66406 3.60156 2.63281 3.75781 2.63281C3.91406 2.63281 4.0599 2.66406 4.19531 2.72656C4.33073 2.78385 4.44792 2.86458 4.54688 2.96875C4.65104 3.06771 4.73177 3.1849 4.78906 3.32031C4.85156 3.45573 4.88281 3.60156 4.88281 3.75781C4.88281 3.91406 4.85156 4.0625 4.78906 4.20312C4.73177 4.33854 4.65104 4.45833 4.54688 4.5625C4.44792 4.66146 4.33073 4.73958 4.19531 4.79688C4.0599 4.85417 3.91406 4.88281 3.75781 4.88281C3.60156 4.88281 3.45312 4.85417 3.3125 4.79688C3.17708 4.73958 3.05729 4.66146 2.95312 4.5625C2.85417 4.45833 2.77604 4.33854 2.71875 4.20312C2.66146 4.0625 2.63281 3.91406 2.63281 3.75781ZM2 7C2.14062 7 2.27083 7.02604 2.39062 7.07812C2.51042 7.13021 2.61458 7.20312 2.70312 7.29688C2.79688 7.38542 2.86979 7.48958 2.92188 7.60938C2.97396 7.72917 3 7.85938 3 8C3 8.14062 2.97396 8.27083 2.92188 8.39062C2.86979 8.51042 2.79688 8.61719 2.70312 8.71094C2.61458 8.79948 2.51042 8.86979 2.39062 8.92188C2.27083 8.97396 2.14062 9 2 9C1.85938 9 1.72917 8.97396 1.60938 8.92188C1.48958 8.86979 1.38281 8.79948 1.28906 8.71094C1.20052 8.61719 1.13021 8.51042 1.07812 8.39062C1.02604 8.27083 1 8.14062 1 8C1 7.85938 1.02604 7.72917 1.07812 7.60938C1.13021 7.48958 1.20052 7.38542 1.28906 7.29688C1.38281 7.20312 1.48958 7.13021 1.60938 7.07812C1.72917 7.02604 1.85938 7 2 7ZM2.88281 12.2422C2.88281 12.1224 2.90625 12.0104 2.95312 11.9062C3 11.7969 3.0625 11.7031 3.14062 11.625C3.21875 11.5469 3.3099 11.4844 3.41406 11.4375C3.52344 11.3906 3.63802 11.3672 3.75781 11.3672C3.8776 11.3672 3.98958 11.3906 4.09375 11.4375C4.20312 11.4844 4.29688 11.5469 4.375 11.625C4.45312 11.7031 4.51562 11.7969 4.5625 11.9062C4.60938 12.0104 4.63281 12.1224 4.63281 12.2422C4.63281 12.362 4.60938 12.4766 4.5625 12.5859C4.51562 12.6901 4.45312 12.7812 4.375 12.8594C4.29688 12.9375 4.20312 13 4.09375 13.0469C3.98958 13.0938 3.8776 13.1172 3.75781 13.1172C3.63802 13.1172 3.52344 13.0938 3.41406 13.0469C3.3099 13 3.21875 12.9375 3.14062 12.8594C3.0625 12.7812 3 12.6901 2.95312 12.5859C2.90625 12.4766 2.88281 12.362 2.88281 12.2422ZM8 13.25C8.20833 13.25 8.38542 13.3229 8.53125 13.4688C8.67708 13.6146 8.75 13.7917 8.75 14C8.75 14.2083 8.67708 14.3854 8.53125 14.5312C8.38542 14.6771 8.20833 14.75 8 14.75C7.79167 14.75 7.61458 14.6771 7.46875 14.5312C7.32292 14.3854 7.25 14.2083 7.25 14C7.25 13.7917 7.32292 13.6146 7.46875 13.4688C7.61458 13.3229 7.79167 13.25 8 13.25ZM11.6172 12.2422C11.6172 12.0651 11.6771 11.9167 11.7969 11.7969C11.9167 11.6771 12.0651 11.6172 12.2422 11.6172C12.4193 11.6172 12.5677 11.6771 12.6875 11.7969C12.8073 11.9167 12.8672 12.0651 12.8672 12.2422C12.8672 12.4193 12.8073 12.5677 12.6875 12.6875C12.5677 12.8073 12.4193 12.8672 12.2422 12.8672C12.0651 12.8672 11.9167 12.8073 11.7969 12.6875C11.6771 12.5677 11.6172 12.4193 11.6172 12.2422ZM14 7.5C14.1354 7.5 14.2526 7.54948 14.3516 7.64844C14.4505 7.7474 14.5 7.86458 14.5 8C14.5 8.13542 14.4505 8.2526 14.3516 8.35156C14.2526 8.45052 14.1354 8.5 14 8.5C13.8646 8.5 13.7474 8.45052 13.6484 8.35156C13.5495 8.2526 13.5 8.13542 13.5 8C13.5 7.86458 13.5495 7.7474 13.6484 7.64844C13.7474 7.54948 13.8646 7.5 14 7.5ZM12.2422 2.38281C12.4297 2.38281 12.6068 2.41927 12.7734 2.49219C12.9401 2.5651 13.0859 2.66406 13.2109 2.78906C13.3359 2.91406 13.4349 3.0599 13.5078 3.22656C13.5807 3.39323 13.6172 3.57031 13.6172 3.75781C13.6172 3.94531 13.5807 4.1224 13.5078 4.28906C13.4349 4.45573 13.3359 4.60156 13.2109 4.72656C13.0859 4.85156 12.9401 4.95052 12.7734 5.02344C12.6068 5.09635 12.4297 5.13281 12.2422 5.13281C12.0547 5.13281 11.8776 5.09635 11.7109 5.02344C11.5443 4.95052 11.3984 4.85156 11.2734 4.72656C11.1484 4.60156 11.0495 4.45573 10.9766 4.28906C10.9036 4.1224 10.8672 3.94531 10.8672 3.75781C10.8672 3.57031 10.9036 3.39323 10.9766 3.22656C11.0495 3.0599 11.1484 2.91406 11.2734 2.78906C11.3984 2.66406 11.5443 2.5651 11.7109 2.49219C11.8776 2.41927 12.0547 2.38281 12.2422 2.38281Z" fill="#C5C5C5" />
|
||||
</svg>
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,6 @@
|
||||
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
|
||||
<!--Copyright (C) 2020 TypeFox and others.-->
|
||||
<!--Copied from https://raw.githubusercontent.com/microsoft/vscode-icons/9c90ce81b1f3c309000b80da0763aa09975a85f0/icons/light/loading.svg -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 0.75C8.17188 0.75 8.33333 0.783854 8.48438 0.851562C8.63542 0.914062 8.76823 1.0026 8.88281 1.11719C8.9974 1.23177 9.08594 1.36458 9.14844 1.51562C9.21615 1.66667 9.25 1.82812 9.25 2C9.25 2.17188 9.21615 2.33333 9.14844 2.48438C9.08594 2.63542 8.9974 2.76823 8.88281 2.88281C8.76823 2.9974 8.63542 3.08854 8.48438 3.15625C8.33333 3.21875 8.17188 3.25 8 3.25C7.82812 3.25 7.66667 3.21875 7.51562 3.15625C7.36458 3.08854 7.23177 2.9974 7.11719 2.88281C7.0026 2.76823 6.91146 2.63542 6.84375 2.48438C6.78125 2.33333 6.75 2.17188 6.75 2C6.75 1.82812 6.78125 1.66667 6.84375 1.51562C6.91146 1.36458 7.0026 1.23177 7.11719 1.11719C7.23177 1.0026 7.36458 0.914062 7.51562 0.851562C7.66667 0.783854 7.82812 0.75 8 0.75ZM2.63281 3.75781C2.63281 3.60156 2.66146 3.45573 2.71875 3.32031C2.77604 3.1849 2.85417 3.06771 2.95312 2.96875C3.05729 2.86458 3.17708 2.78385 3.3125 2.72656C3.45312 2.66406 3.60156 2.63281 3.75781 2.63281C3.91406 2.63281 4.0599 2.66406 4.19531 2.72656C4.33073 2.78385 4.44792 2.86458 4.54688 2.96875C4.65104 3.06771 4.73177 3.1849 4.78906 3.32031C4.85156 3.45573 4.88281 3.60156 4.88281 3.75781C4.88281 3.91406 4.85156 4.0625 4.78906 4.20312C4.73177 4.33854 4.65104 4.45833 4.54688 4.5625C4.44792 4.66146 4.33073 4.73958 4.19531 4.79688C4.0599 4.85417 3.91406 4.88281 3.75781 4.88281C3.60156 4.88281 3.45312 4.85417 3.3125 4.79688C3.17708 4.73958 3.05729 4.66146 2.95312 4.5625C2.85417 4.45833 2.77604 4.33854 2.71875 4.20312C2.66146 4.0625 2.63281 3.91406 2.63281 3.75781ZM2 7C2.14062 7 2.27083 7.02604 2.39062 7.07812C2.51042 7.13021 2.61458 7.20312 2.70312 7.29688C2.79688 7.38542 2.86979 7.48958 2.92188 7.60938C2.97396 7.72917 3 7.85938 3 8C3 8.14062 2.97396 8.27083 2.92188 8.39062C2.86979 8.51042 2.79688 8.61719 2.70312 8.71094C2.61458 8.79948 2.51042 8.86979 2.39062 8.92188C2.27083 8.97396 2.14062 9 2 9C1.85938 9 1.72917 8.97396 1.60938 8.92188C1.48958 8.86979 1.38281 8.79948 1.28906 8.71094C1.20052 8.61719 1.13021 8.51042 1.07812 8.39062C1.02604 8.27083 1 8.14062 1 8C1 7.85938 1.02604 7.72917 1.07812 7.60938C1.13021 7.48958 1.20052 7.38542 1.28906 7.29688C1.38281 7.20312 1.48958 7.13021 1.60938 7.07812C1.72917 7.02604 1.85938 7 2 7ZM2.88281 12.2422C2.88281 12.1224 2.90625 12.0104 2.95312 11.9062C3 11.7969 3.0625 11.7031 3.14062 11.625C3.21875 11.5469 3.3099 11.4844 3.41406 11.4375C3.52344 11.3906 3.63802 11.3672 3.75781 11.3672C3.8776 11.3672 3.98958 11.3906 4.09375 11.4375C4.20312 11.4844 4.29688 11.5469 4.375 11.625C4.45312 11.7031 4.51562 11.7969 4.5625 11.9062C4.60938 12.0104 4.63281 12.1224 4.63281 12.2422C4.63281 12.362 4.60938 12.4766 4.5625 12.5859C4.51562 12.6901 4.45312 12.7812 4.375 12.8594C4.29688 12.9375 4.20312 13 4.09375 13.0469C3.98958 13.0938 3.8776 13.1172 3.75781 13.1172C3.63802 13.1172 3.52344 13.0938 3.41406 13.0469C3.3099 13 3.21875 12.9375 3.14062 12.8594C3.0625 12.7812 3 12.6901 2.95312 12.5859C2.90625 12.4766 2.88281 12.362 2.88281 12.2422ZM8 13.25C8.20833 13.25 8.38542 13.3229 8.53125 13.4688C8.67708 13.6146 8.75 13.7917 8.75 14C8.75 14.2083 8.67708 14.3854 8.53125 14.5312C8.38542 14.6771 8.20833 14.75 8 14.75C7.79167 14.75 7.61458 14.6771 7.46875 14.5312C7.32292 14.3854 7.25 14.2083 7.25 14C7.25 13.7917 7.32292 13.6146 7.46875 13.4688C7.61458 13.3229 7.79167 13.25 8 13.25ZM11.6172 12.2422C11.6172 12.0651 11.6771 11.9167 11.7969 11.7969C11.9167 11.6771 12.0651 11.6172 12.2422 11.6172C12.4193 11.6172 12.5677 11.6771 12.6875 11.7969C12.8073 11.9167 12.8672 12.0651 12.8672 12.2422C12.8672 12.4193 12.8073 12.5677 12.6875 12.6875C12.5677 12.8073 12.4193 12.8672 12.2422 12.8672C12.0651 12.8672 11.9167 12.8073 11.7969 12.6875C11.6771 12.5677 11.6172 12.4193 11.6172 12.2422ZM14 7.5C14.1354 7.5 14.2526 7.54948 14.3516 7.64844C14.4505 7.7474 14.5 7.86458 14.5 8C14.5 8.13542 14.4505 8.2526 14.3516 8.35156C14.2526 8.45052 14.1354 8.5 14 8.5C13.8646 8.5 13.7474 8.45052 13.6484 8.35156C13.5495 8.2526 13.5 8.13542 13.5 8C13.5 7.86458 13.5495 7.7474 13.6484 7.64844C13.7474 7.54948 13.8646 7.5 14 7.5ZM12.2422 2.38281C12.4297 2.38281 12.6068 2.41927 12.7734 2.49219C12.9401 2.5651 13.0859 2.66406 13.2109 2.78906C13.3359 2.91406 13.4349 3.0599 13.5078 3.22656C13.5807 3.39323 13.6172 3.57031 13.6172 3.75781C13.6172 3.94531 13.5807 4.1224 13.5078 4.28906C13.4349 4.45573 13.3359 4.60156 13.2109 4.72656C13.0859 4.85156 12.9401 4.95052 12.7734 5.02344C12.6068 5.09635 12.4297 5.13281 12.2422 5.13281C12.0547 5.13281 11.8776 5.09635 11.7109 5.02344C11.5443 4.95052 11.3984 4.85156 11.2734 4.72656C11.1484 4.60156 11.0495 4.45573 10.9766 4.28906C10.9036 4.1224 10.8672 3.94531 10.8672 3.75781C10.8672 3.57031 10.9036 3.39323 10.9766 3.22656C11.0495 3.0599 11.1484 2.91406 11.2734 2.78906C11.3984 2.66406 11.5443 2.5651 11.7109 2.49219C11.8776 2.41927 12.0547 2.38281 12.2422 2.38281Z" fill="#424242" />
|
||||
</svg>
|
After Width: | Height: | Size: 5.0 KiB |
@@ -1,22 +1,32 @@
|
||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
inject,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||
import { Installable } from '../../common/protocol';
|
||||
import {
|
||||
LibraryPackage,
|
||||
LibrarySearch,
|
||||
LibraryService,
|
||||
} from '../../common/protocol/library-service';
|
||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { Installable } from '../../common/protocol';
|
||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
|
||||
import {
|
||||
ListWidget,
|
||||
ListWidgetSearchOptions,
|
||||
UserAbortError,
|
||||
} from '../widgets/component-list/list-widget';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetSearchOptions extends ListWidgetSearchOptions<LibrarySearch> {
|
||||
get defaultOptions(): Required<LibrarySearch> {
|
||||
return { query: '', type: 'All', topic: 'All' };
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidget extends ListWidget<
|
||||
@@ -32,7 +42,8 @@ export class LibraryListWidget extends ListWidget<
|
||||
constructor(
|
||||
@inject(LibraryService) private service: LibraryService,
|
||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<LibraryPackage>,
|
||||
@inject(LibraryFilterRenderer) filterRenderer: LibraryFilterRenderer
|
||||
@inject(LibraryListWidgetSearchOptions)
|
||||
searchOptions: LibraryListWidgetSearchOptions
|
||||
) {
|
||||
super({
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
@@ -41,10 +52,8 @@ export class LibraryListWidget extends ListWidget<
|
||||
searchable: service,
|
||||
installable: service,
|
||||
itemLabel: (item: LibraryPackage) => item.name,
|
||||
itemDeprecated: (item: LibraryPackage) => item.deprecated,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All', topic: 'All' },
|
||||
searchOptions,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -142,6 +151,8 @@ export class LibraryListWidget extends ListWidget<
|
||||
// All
|
||||
installDependencies = true;
|
||||
}
|
||||
} else {
|
||||
throw new UserAbortError();
|
||||
}
|
||||
} else {
|
||||
// The lib does not have any dependencies.
|
||||
@@ -236,6 +247,21 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
|
||||
this.response = 0;
|
||||
super.handleEnter(event);
|
||||
}
|
||||
|
||||
protected override onAfterAttach(message: Message): void {
|
||||
super.onAfterAttach(message);
|
||||
let buttonToFocus: HTMLButtonElement | undefined = undefined;
|
||||
for (const child of Array.from(this.controlPanel.children)) {
|
||||
if (child instanceof HTMLButtonElement) {
|
||||
if (child.classList.contains('main')) {
|
||||
buttonToFocus = child;
|
||||
break;
|
||||
}
|
||||
buttonToFocus = child;
|
||||
}
|
||||
}
|
||||
buttonToFocus?.focus();
|
||||
}
|
||||
}
|
||||
export namespace MessageBoxDialog {
|
||||
export interface Options extends DialogProps {
|
||||
|
@@ -1,17 +1,30 @@
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { LibraryPackage, LibrarySearch } from '../../common/protocol';
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Type as TypeLabel } from '../../common/nls';
|
||||
import {
|
||||
LibraryPackage,
|
||||
LibrarySearch,
|
||||
TopicLabel,
|
||||
} from '../../common/protocol';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MenuActionTemplate, SubmenuTemplate } from '../menu/register-menu';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
import {
|
||||
LibraryListWidget,
|
||||
LibraryListWidgetSearchOptions,
|
||||
} from './library-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
LibraryPackage,
|
||||
LibrarySearch
|
||||
> {
|
||||
@inject(LibraryListWidgetSearchOptions)
|
||||
protected readonly searchOptions: LibraryListWidgetSearchOptions;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: LibraryListWidget.WIDGET_ID,
|
||||
@@ -38,7 +51,7 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
|
||||
}
|
||||
}
|
||||
|
||||
protected canParse(uri: URI): boolean {
|
||||
protected override canParse(uri: URI): boolean {
|
||||
try {
|
||||
LibrarySearch.UriParser.parse(uri);
|
||||
return true;
|
||||
@@ -47,7 +60,72 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): LibrarySearch | undefined {
|
||||
protected override parse(uri: URI): LibrarySearch | undefined {
|
||||
return LibrarySearch.UriParser.parse(uri);
|
||||
}
|
||||
|
||||
protected override buildFilterMenuGroup(
|
||||
menuPath: MenuPath
|
||||
): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||
const typeSubmenuPath = [...menuPath, TypeLabel];
|
||||
const topicSubmenuPath = [...menuPath, TopicLabel];
|
||||
return [
|
||||
{
|
||||
submenuPath: typeSubmenuPath,
|
||||
menuLabel: `${TypeLabel}: "${
|
||||
LibrarySearch.TypeLabels[this.searchOptions.options.type]
|
||||
}"`,
|
||||
options: { order: String(0) },
|
||||
},
|
||||
...this.buildMenuActions<LibrarySearch.Type>(
|
||||
typeSubmenuPath,
|
||||
LibrarySearch.TypeLiterals.slice(),
|
||||
(type) => this.searchOptions.options.type === type,
|
||||
(type) => this.searchOptions.update({ type }),
|
||||
(type) => LibrarySearch.TypeLabels[type]
|
||||
),
|
||||
{
|
||||
submenuPath: topicSubmenuPath,
|
||||
menuLabel: `${TopicLabel}: "${
|
||||
LibrarySearch.TopicLabels[this.searchOptions.options.topic]
|
||||
}"`,
|
||||
options: { order: String(1) },
|
||||
},
|
||||
...this.buildMenuActions<LibrarySearch.Topic>(
|
||||
topicSubmenuPath,
|
||||
LibrarySearch.TopicLiterals.slice(),
|
||||
(topic) => this.searchOptions.options.topic === topic,
|
||||
(topic) => this.searchOptions.update({ topic }),
|
||||
(topic) => LibrarySearch.TopicLabels[topic]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected override get showViewFilterContextMenuCommand(): Command & {
|
||||
label: string;
|
||||
} {
|
||||
return LibraryListWidgetFrontendContribution.Commands
|
||||
.SHOW_LIBRARY_LIST_WIDGET_FILTER_CONTEXT_MENU;
|
||||
}
|
||||
|
||||
protected get showInstalledCommandId(): string {
|
||||
return 'arduino-show-installed-libraries';
|
||||
}
|
||||
|
||||
protected get showUpdatesCommandId(): string {
|
||||
return 'arduino-show-library-updates';
|
||||
}
|
||||
}
|
||||
export namespace LibraryListWidgetFrontendContribution {
|
||||
export namespace Commands {
|
||||
export const SHOW_LIBRARY_LIST_WIDGET_FILTER_CONTEXT_MENU: Command & {
|
||||
label: string;
|
||||
} = {
|
||||
id: 'arduino-library-list-widget-show-filter-context-menu',
|
||||
label: nls.localize(
|
||||
'arduino/libraries/filterLibraries',
|
||||
'Filter Libraries...'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
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 { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import {
|
||||
@@ -34,7 +34,6 @@ export class LocalCacheFsProvider
|
||||
@inject(AuthenticationClientService)
|
||||
protected readonly authenticationService: AuthenticationClientService;
|
||||
|
||||
// TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
|
||||
readonly ready = new Deferred<void>();
|
||||
|
||||
private _localCacheRoot: URI;
|
||||
@@ -89,8 +88,25 @@ export class LocalCacheFsProvider
|
||||
}
|
||||
|
||||
protected async init(fileService: FileService): Promise<void> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
this._localCacheRoot = new URI(config.dataDirUri);
|
||||
const { config } = await this.configService.getConfiguration();
|
||||
// Any possible CLI config errors are ignored here. IDE2 does not verify the `directories.data` folder.
|
||||
// If the data dir is accessible, IDE2 creates the cache folder for the cloud sketches. Otherwise, it does not.
|
||||
// The data folder can be configured outside of the IDE2, and the new data folder will be picked up with a
|
||||
// subsequent IDE2 start.
|
||||
if (!config?.dataDirUri) {
|
||||
return; // the deferred promise will never resolve
|
||||
}
|
||||
const localCacheUri = new URI(config.dataDirUri);
|
||||
try {
|
||||
await fileService.access(localCacheUri);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`'directories.data' location is inaccessible at ${config.dataDirUri}`,
|
||||
err
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._localCacheRoot = localCacheUri;
|
||||
for (const segment of ['RemoteSketchbook', 'ArduinoCloud']) {
|
||||
this._localCacheRoot = this._localCacheRoot.resolve(segment);
|
||||
await fileService.createFolder(this._localCacheRoot);
|
||||
@@ -153,7 +169,7 @@ export class LocalCacheFsProvider
|
||||
return uri;
|
||||
}
|
||||
|
||||
private toUri(session: AuthenticationSession): URI {
|
||||
toUri(session: AuthenticationSession): URI {
|
||||
// Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
|
||||
return this._localCacheRoot.resolve(session.id.split('|')[1]);
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { RenderContextMenuOptions } from '@theia/core/lib/browser';
|
||||
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import {
|
||||
MAIN_MENU_BAR,
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
MenuPath,
|
||||
SubMenuOptions,
|
||||
} from '@theia/core/lib/common/menu';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
|
||||
export namespace ArduinoMenus {
|
||||
// Main menu
|
||||
@@ -97,6 +99,11 @@ export namespace ArduinoMenus {
|
||||
export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection'];
|
||||
// Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader`
|
||||
export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings'];
|
||||
// `Tool` > `Ports` (always visible https://github.com/arduino/arduino-ide/issues/655)
|
||||
export const TOOLS__PORTS_SUBMENU = [
|
||||
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
|
||||
'2_ports',
|
||||
];
|
||||
|
||||
// -- Help
|
||||
// `Getting Started`, `Environment`, `Troubleshooting`, etc.
|
||||
@@ -149,6 +156,36 @@ export namespace ArduinoMenus {
|
||||
'2_resources',
|
||||
];
|
||||
|
||||
// -- Account
|
||||
export const ARDUINO_ACCOUNT__CONTEXT = ['arduino-account--context'];
|
||||
export const ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP = [
|
||||
...ARDUINO_ACCOUNT__CONTEXT,
|
||||
'0_sign_in',
|
||||
];
|
||||
export const ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP = [
|
||||
...ARDUINO_ACCOUNT__CONTEXT,
|
||||
'1_learn_more',
|
||||
];
|
||||
export const ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP = [
|
||||
...ARDUINO_ACCOUNT__CONTEXT,
|
||||
'2_go_to',
|
||||
];
|
||||
export const ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP = [
|
||||
...ARDUINO_ACCOUNT__CONTEXT,
|
||||
'3_sign_out',
|
||||
];
|
||||
|
||||
// Context menu from the library and boards manager widget
|
||||
export const ARDUINO_COMPONENT__CONTEXT = ['arduino-component--context'];
|
||||
export const ARDUINO_COMPONENT__CONTEXT__INFO_GROUP = [
|
||||
...ARDUINO_COMPONENT__CONTEXT,
|
||||
'0_info',
|
||||
];
|
||||
export const ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP = [
|
||||
...ARDUINO_COMPONENT__CONTEXT,
|
||||
'1_action',
|
||||
];
|
||||
|
||||
// -- ROOT SSL CERTIFICATES
|
||||
export const ROOT_CERTIFICATES__CONTEXT = [
|
||||
'arduino-root-certificates--context',
|
||||
@@ -206,3 +243,15 @@ export class PlaceholderMenuNode implements MenuNode {
|
||||
return [...this.menuPath, 'placeholder'].join('-');
|
||||
}
|
||||
}
|
||||
|
||||
export const examplesLabel = nls.localize('arduino/examples/menu', 'Examples');
|
||||
|
||||
/**
|
||||
* Helper function to optionally show disabled context menu items in IDE2. They're invisible in Theia.
|
||||
* See `ElectronContextMenuRenderer#showDisabled` for more details.
|
||||
*/
|
||||
export function showDisabledContextMenuOptions(
|
||||
options: RenderContextMenuOptions
|
||||
): RenderContextMenuOptions {
|
||||
return Object.assign(options, { showDisabled: true });
|
||||
}
|
||||
|
151
arduino-ide-extension/src/browser/menu/register-menu.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
CommandHandler,
|
||||
CommandRegistry,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
MenuModelRegistry,
|
||||
MenuPath,
|
||||
SubMenuOptions,
|
||||
} from '@theia/core/lib/common/menu';
|
||||
import { unregisterSubmenu } from './arduino-menus';
|
||||
|
||||
export interface MenuTemplate {
|
||||
readonly menuLabel: string;
|
||||
}
|
||||
|
||||
export function isMenuTemplate(arg: unknown): arg is MenuTemplate {
|
||||
return (
|
||||
typeof arg === 'object' &&
|
||||
(arg as MenuTemplate).menuLabel !== undefined &&
|
||||
typeof (arg as MenuTemplate).menuLabel === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export interface MenuActionTemplate extends MenuTemplate {
|
||||
readonly menuPath: MenuPath;
|
||||
readonly handler: CommandHandler;
|
||||
/**
|
||||
* If not defined the insertion oder will be the order string.
|
||||
*/
|
||||
readonly order?: string;
|
||||
}
|
||||
|
||||
export function isMenuActionTemplate(
|
||||
arg: MenuTemplate
|
||||
): arg is MenuActionTemplate {
|
||||
return (
|
||||
isMenuTemplate(arg) &&
|
||||
(arg as MenuActionTemplate).handler !== undefined &&
|
||||
typeof (arg as MenuActionTemplate).handler === 'object' &&
|
||||
(arg as MenuActionTemplate).menuPath !== undefined &&
|
||||
Array.isArray((arg as MenuActionTemplate).menuPath)
|
||||
);
|
||||
}
|
||||
|
||||
export function menuActionWithCommandDelegate(
|
||||
template: Omit<MenuActionTemplate, 'handler' | 'menuLabel'> & {
|
||||
command: string;
|
||||
},
|
||||
commandRegistry: CommandRegistry
|
||||
): MenuActionTemplate {
|
||||
const id = template.command;
|
||||
const command = commandRegistry.getCommand(id);
|
||||
if (!command) {
|
||||
throw new Error(`Could not find the registered command with ID: ${id}`);
|
||||
}
|
||||
return {
|
||||
...template,
|
||||
menuLabel: command.label ?? id,
|
||||
handler: {
|
||||
execute: (args) => commandRegistry.executeCommand(id, args),
|
||||
isEnabled: (args) => commandRegistry.isEnabled(id, args),
|
||||
isVisible: (args) => commandRegistry.isVisible(id, args),
|
||||
isToggled: (args) => commandRegistry.isToggled(id, args),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface SubmenuTemplate extends MenuTemplate {
|
||||
readonly menuLabel: string;
|
||||
readonly submenuPath: MenuPath;
|
||||
readonly options?: SubMenuOptions;
|
||||
}
|
||||
|
||||
interface Services {
|
||||
readonly commandRegistry: CommandRegistry;
|
||||
readonly menuRegistry: MenuModelRegistry;
|
||||
}
|
||||
|
||||
class MenuIndexCounter {
|
||||
private _counter: number;
|
||||
constructor(counter = 0) {
|
||||
this._counter = counter;
|
||||
}
|
||||
getAndIncrement(): number {
|
||||
const counter = this._counter;
|
||||
this._counter++;
|
||||
return counter;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerMenus(
|
||||
options: {
|
||||
contextId: string;
|
||||
templates: Array<MenuActionTemplate | SubmenuTemplate>;
|
||||
} & Services
|
||||
): Disposable {
|
||||
const { templates } = options;
|
||||
const menuIndexCounter = new MenuIndexCounter();
|
||||
return new DisposableCollection(
|
||||
...templates.map((template) =>
|
||||
registerMenu({ template, menuIndexCounter, ...options })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function registerMenu(
|
||||
options: {
|
||||
contextId: string;
|
||||
menuIndexCounter: MenuIndexCounter;
|
||||
template: MenuActionTemplate | SubmenuTemplate;
|
||||
} & Services
|
||||
): Disposable {
|
||||
const {
|
||||
template,
|
||||
commandRegistry,
|
||||
menuRegistry,
|
||||
contextId,
|
||||
menuIndexCounter,
|
||||
} = options;
|
||||
if (isMenuActionTemplate(template)) {
|
||||
const { menuLabel, menuPath, handler, order } = template;
|
||||
const id = generateCommandId(contextId, menuLabel, menuPath);
|
||||
const index = menuIndexCounter.getAndIncrement();
|
||||
return new DisposableCollection(
|
||||
commandRegistry.registerCommand({ id }, handler),
|
||||
menuRegistry.registerMenuAction(menuPath, {
|
||||
commandId: id,
|
||||
label: menuLabel,
|
||||
order: typeof order === 'string' ? order : String(index).padStart(4),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const { menuLabel, submenuPath, options } = template;
|
||||
return new DisposableCollection(
|
||||
menuRegistry.registerSubmenu(submenuPath, menuLabel, options),
|
||||
Disposable.create(() => unregisterSubmenu(submenuPath, menuRegistry))
|
||||
);
|
||||
}
|
||||
|
||||
function generateCommandId(
|
||||
contextId: string,
|
||||
menuLabel: string,
|
||||
menuPath: MenuPath
|
||||
): string {
|
||||
return `arduino-${contextId}-context-${menuPath.join('-')}-${menuLabel}`;
|
||||
}
|
||||
}
|