mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-06 01:58:32 +00:00
Compare commits
86 Commits
open-recen
...
2.0.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
23c7f5f848 | ||
![]() |
f1144efb93 | ||
![]() |
9cec643cab | ||
![]() |
1a7784a540 | ||
![]() |
d24a3911f8 | ||
![]() |
3735553003 | ||
![]() |
f6d112e1f6 | ||
![]() |
cc2d557706 | ||
![]() |
103acc4b7e | ||
![]() |
c3dc7c6307 | ||
![]() |
7d6a2d5e33 | ||
![]() |
6984c52b92 | ||
![]() |
3a70547770 | ||
![]() |
8a85b5c3d8 | ||
![]() |
b998d35524 | ||
![]() |
ddec64c4a5 | ||
![]() |
8fed08003e | ||
![]() |
8454c625f7 | ||
![]() |
60df322f09 | ||
![]() |
8bfb140e7c | ||
![]() |
260227e79a | ||
![]() |
cc310bf1a5 | ||
![]() |
dbd52e2f34 | ||
![]() |
9cd03bec46 | ||
![]() |
c29452a858 | ||
![]() |
7d91f2d8cb | ||
![]() |
f6275f9f62 | ||
![]() |
0d0550974a | ||
![]() |
4e882d25d9 | ||
![]() |
f93f78039b | ||
![]() |
2b2463b834 | ||
![]() |
0773c3915c | ||
![]() |
2f5afe0d9c | ||
![]() |
b8370686ec | ||
![]() |
3b2d12eff9 | ||
![]() |
cdaaa5584d | ||
![]() |
3476de27f7 | ||
![]() |
b55cfc2052 | ||
![]() |
44751c370b | ||
![]() |
32d904ca36 | ||
![]() |
5424dfcf70 | ||
![]() |
b8bf1eefa2 | ||
![]() |
93291b6811 | ||
![]() |
87ebcbe77e | ||
![]() |
99b10942bb | ||
![]() |
960a2d0634 | ||
![]() |
e577de4e8e | ||
![]() |
f3ef95cfe2 | ||
![]() |
bc264d1adf | ||
![]() |
5444395f34 | ||
![]() |
2d2be1f6d0 | ||
![]() |
1e269ac83d | ||
![]() |
0c49709f26 | ||
![]() |
019b2d5588 | ||
![]() |
aa0807ca3f | ||
![]() |
61a11a0857 | ||
![]() |
0c20ae0e28 | ||
![]() |
945a8f4841 | ||
![]() |
ae76432944 | ||
![]() |
40807db65e | ||
![]() |
da22f1ed11 | ||
![]() |
32b70efd5c | ||
![]() |
6f07717369 | ||
![]() |
d6cb23f782 | ||
![]() |
9ac2638335 | ||
![]() |
96cf09d594 | ||
![]() |
8380c82028 | ||
![]() |
5eb2926407 | ||
![]() |
a4ab204400 | ||
![]() |
6416c431c6 | ||
![]() |
8f88aa69bf | ||
![]() |
3c2b2a0734 | ||
![]() |
39538f163f | ||
![]() |
9ef04bb8d6 | ||
![]() |
707f3bef61 | ||
![]() |
878395221a | ||
![]() |
6a35bbfa7e | ||
![]() |
42f6f43870 | ||
![]() |
6983c5bf7f | ||
![]() |
b3ab5cbd2a | ||
![]() |
8a5995920a | ||
![]() |
8de6cf84d9 | ||
![]() |
f5c36bb691 | ||
![]() |
364f8b8e51 | ||
![]() |
671d2eabd4 | ||
![]() |
9a65ef6ea8 |
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -30,7 +30,7 @@ body:
|
||||
description: |
|
||||
Which version of the Arduino IDE are you using?
|
||||
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
|
||||
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
|
||||
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds).
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -68,7 +68,7 @@ body:
|
||||
options:
|
||||
- label: I searched for previous reports in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
|
||||
required: true
|
||||
- label: I verified the problem still occurs when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
|
||||
- label: I verified the problem still occurs when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds)
|
||||
required: true
|
||||
- label: My report contains all necessary details
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -9,7 +9,7 @@ contact_links:
|
||||
url: https://forum.arduino.cc/
|
||||
about: We can help you out on the Arduino Forum!
|
||||
- name: Issue report guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/issues.md#issue-report-guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/contributor-guide/issues.md#issue-report-guide
|
||||
about: Learn about submitting issue reports to this repository.
|
||||
- name: Contributor guide
|
||||
url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide
|
||||
|
4
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -25,7 +25,7 @@ body:
|
||||
description: |
|
||||
Which version of the Arduino IDE are you using?
|
||||
See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
|
||||
This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
|
||||
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds).
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
@@ -63,7 +63,7 @@ body:
|
||||
options:
|
||||
- label: I searched for previous requests in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
|
||||
required: true
|
||||
- label: I verified the feature was still missing when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
|
||||
- label: I verified the feature was still missing when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds)
|
||||
required: true
|
||||
- label: My request contains all necessary details
|
||||
required: true
|
||||
|
15
.github/dependabot.yml
vendored
Normal file
15
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# See: https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#about-the-dependabotyml-file
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
# Configure check for outdated GitHub Actions actions in workflows.
|
||||
# Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/dependabot/README.md
|
||||
# See: https://docs.github.com/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||
- package-ecosystem: github-actions
|
||||
directory: / # Check the repository's workflows under /.github/workflows/
|
||||
assignees:
|
||||
- per1234
|
||||
schedule:
|
||||
interval: daily
|
||||
labels:
|
||||
- "topic: infrastructure"
|
131
.github/tools/fetch_athena_stats.py
vendored
131
.github/tools/fetch_athena_stats.py
vendored
@@ -1,131 +0,0 @@
|
||||
import boto3
|
||||
import semver
|
||||
import os
|
||||
import logging
|
||||
import uuid
|
||||
import time
|
||||
|
||||
|
||||
# logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
||||
log = logging.getLogger()
|
||||
logging.getLogger("boto3").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
|
||||
|
||||
|
||||
def execute(client, statement, dest_s3_output_location):
|
||||
log.info("execute query: {} dumping in {}".format(statement, dest_s3_output_location))
|
||||
result = client.start_query_execution(
|
||||
QueryString=statement,
|
||||
ClientRequestToken=str(uuid.uuid4()),
|
||||
ResultConfiguration={
|
||||
"OutputLocation": dest_s3_output_location,
|
||||
},
|
||||
)
|
||||
execution_id = result["QueryExecutionId"]
|
||||
log.info("wait for query {} completion".format(execution_id))
|
||||
wait_for_query_execution_completion(client, execution_id)
|
||||
log.info("operation successful")
|
||||
return execution_id
|
||||
|
||||
|
||||
def wait_for_query_execution_completion(client, query_execution_id):
|
||||
query_ended = False
|
||||
while not query_ended:
|
||||
query_execution = client.get_query_execution(QueryExecutionId=query_execution_id)
|
||||
state = query_execution["QueryExecution"]["Status"]["State"]
|
||||
if state == "SUCCEEDED":
|
||||
query_ended = True
|
||||
elif state in ["FAILED", "CANCELLED"]:
|
||||
raise BaseException(
|
||||
"query failed or canceled: {}".format(query_execution["QueryExecution"]["Status"]["StateChangeReason"])
|
||||
)
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def valid(key):
|
||||
split = key.split("_")
|
||||
if len(split) < 1:
|
||||
return False
|
||||
try:
|
||||
semver.parse(split[0])
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_results(client, execution_id):
|
||||
results_paginator = client.get_paginator("get_query_results")
|
||||
results_iter = results_paginator.paginate(QueryExecutionId=execution_id, PaginationConfig={"PageSize": 1000})
|
||||
res = {}
|
||||
for results_page in results_iter:
|
||||
for row in results_page["ResultSet"]["Rows"][1:]:
|
||||
# Loop through the JSON objects
|
||||
key = row["Data"][0]["VarCharValue"]
|
||||
if valid(key):
|
||||
res[key] = row["Data"][1]["VarCharValue"]
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def convert_data(data):
|
||||
result = []
|
||||
for key, value in data.items():
|
||||
# 0.18.0_macOS_64bit.tar.gz
|
||||
split_key = key.split("_")
|
||||
if len(split_key) != 3:
|
||||
continue
|
||||
(version, os_version, arch) = split_key
|
||||
arch_split = arch.split(".")
|
||||
if len(arch_split) < 1:
|
||||
continue
|
||||
arch = arch_split[0]
|
||||
if len(arch) > 10:
|
||||
# This can't be an architecture really.
|
||||
# It's an ugly solution but works for now so deal with it.
|
||||
continue
|
||||
repo = os.environ["GITHUB_REPOSITORY"].split("/")[1]
|
||||
result.append(
|
||||
{
|
||||
"type": "gauge",
|
||||
"name": "arduino.downloads.total",
|
||||
"value": value,
|
||||
"host": os.environ["GITHUB_REPOSITORY"],
|
||||
"tags": [
|
||||
f"version:{version}",
|
||||
f"os:{os_version}",
|
||||
f"arch:{arch}",
|
||||
"cdn:downloads.arduino.cc",
|
||||
f"project:{repo}",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
DEST_S3_OUTPUT = os.environ["AWS_ATHENA_OUTPUT_LOCATION"]
|
||||
AWS_ATHENA_SOURCE_TABLE = os.environ["AWS_ATHENA_SOURCE_TABLE"]
|
||||
|
||||
session = boto3.session.Session(region_name="us-east-1")
|
||||
athena_client = session.client("athena")
|
||||
|
||||
# Load all partitions before querying downloads
|
||||
execute(athena_client, f"MSCK REPAIR TABLE {AWS_ATHENA_SOURCE_TABLE};", DEST_S3_OUTPUT)
|
||||
|
||||
query = f"""SELECT replace(json_extract_scalar(url_decode(url_decode(querystring)),
|
||||
'$.data.url'), 'https://downloads.arduino.cc/arduino-ide/arduino-ide_', '')
|
||||
AS flavor, count(json_extract(url_decode(url_decode(querystring)),'$')) AS gauge
|
||||
FROM {AWS_ATHENA_SOURCE_TABLE}
|
||||
WHERE json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
|
||||
LIKE 'https://downloads.arduino.cc/arduino-ide/arduino-ide_%'
|
||||
AND json_extract_scalar(url_decode(url_decode(querystring)),'$.data.url')
|
||||
NOT LIKE '%latest%' -- exclude latest redirect
|
||||
group by 1 ;"""
|
||||
exec_id = execute(athena_client, query, DEST_S3_OUTPUT)
|
||||
results = get_results(athena_client, exec_id)
|
||||
result_json = convert_data(results)
|
||||
|
||||
print(f"::set-output name=result::{result_json}")
|
57
.github/workflows/arduino-stats.yaml
vendored
57
.github/workflows/arduino-stats.yaml
vendored
@@ -1,57 +0,0 @@
|
||||
name: arduino-stats
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every day at 07:00 AM, 03:00 PM and 11:00 PM
|
||||
- cron: "0 7,15,23 * * *"
|
||||
workflow_dispatch:
|
||||
repository_dispatch:
|
||||
|
||||
jobs:
|
||||
push-stats:
|
||||
# This workflow is only of value to the arduino/arduino-ide repository and
|
||||
# would always fail in forks
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Fetch downloads count form Arduino CDN using AWS Athena
|
||||
id: fetch
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.STATS_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.STATS_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_ATHENA_SOURCE_TABLE: ${{ secrets.STATS_AWS_ATHENA_SOURCE_TABLE }}
|
||||
AWS_ATHENA_OUTPUT_LOCATION: ${{ secrets.STATS_AWS_ATHENA_OUTPUT_LOCATION }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
pip install boto3 semver
|
||||
python .github/tools/fetch_athena_stats.py
|
||||
|
||||
- name: Send metrics
|
||||
uses: masci/datadog@v1
|
||||
with:
|
||||
api-key: ${{ secrets.DD_API_KEY }}
|
||||
# Metrics input expects YAML but JSON will work just right.
|
||||
metrics: ${{steps.fetch.outputs.result}}
|
||||
|
||||
- name: Report failure
|
||||
if: failure()
|
||||
uses: masci/datadog@v1
|
||||
with:
|
||||
api-key: ${{ secrets.DD_API_KEY }}
|
||||
events: |
|
||||
- title: "Arduino IDE stats failing"
|
||||
text: "Stats collection failed"
|
||||
alert_type: "error"
|
||||
host: ${{ github.repository }}
|
||||
tags:
|
||||
- "project:arduino-ide"
|
||||
- "cdn:downloads.arduino.cc"
|
||||
- "workflow:${{ github.workflow }}"
|
32
.github/workflows/build.yml
vendored
32
.github/workflows/build.yml
vendored
@@ -55,16 +55,16 @@ 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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -183,12 +183,12 @@ jobs:
|
||||
OUTPUT_SAFE_BODY="${BODY//'%'/'%25'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
|
||||
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
|
||||
echo "::set-output name=BODY::$OUTPUT_SAFE_BODY"
|
||||
echo "BODY=$OUTPUT_SAFE_BODY" >> $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 +199,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 +220,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 +228,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.3.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
|
||||
@@ -263,6 +263,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 }}
|
||||
|
2
.github/workflows/check-certificates.yml
vendored
2
.github/workflows/check-certificates.yml
vendored
@@ -108,7 +108,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
|
||||
|
8
.github/workflows/check-i18n-task.yml
vendored
8
.github/workflows/check-i18n-task.yml
vendored
@@ -27,12 +27,12 @@ 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
|
||||
|
@@ -8,7 +8,7 @@ on:
|
||||
env:
|
||||
CHANGELOG_ARTIFACTS: changelog
|
||||
# See: https://github.com/actions/setup-node/#readme
|
||||
NODE_VERSION: 14.x
|
||||
NODE_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
create-changelog:
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Get Tag
|
||||
id: tag_name
|
||||
run: |
|
||||
echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
|
||||
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create full changelog
|
||||
id: full-changelog
|
96
.github/workflows/github-stats.yaml
vendored
96
.github/workflows/github-stats.yaml
vendored
@@ -1,96 +0,0 @@
|
||||
name: github-stats
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# run every 30 minutes
|
||||
- cron: "*/30 * * * *"
|
||||
workflow_dispatch:
|
||||
repository_dispatch:
|
||||
|
||||
jobs:
|
||||
push-stats:
|
||||
# This workflow is only of value to the arduino/arduino-ide repository and
|
||||
# would always fail in forks
|
||||
if: github.repository == 'arduino/arduino-ide'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Fetch downloads count
|
||||
id: fetch
|
||||
uses: actions/github-script@v4
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
let metrics = []
|
||||
|
||||
// Get a list of releases
|
||||
const opts = github.repos.listReleases.endpoint.merge({
|
||||
...context.repo
|
||||
})
|
||||
const releases = await github.paginate(opts)
|
||||
|
||||
// Get download stats for every release
|
||||
for (const rel of releases) {
|
||||
// Names for assets are like `arduino-ide_2.0.0-beta.12_Linux_64bit.zip`,
|
||||
// we'll use this later to split the asset file name more easily
|
||||
const baseName = `arduino-ide_${rel.name}_`
|
||||
|
||||
// Get a list of assets for this release
|
||||
const opts = github.repos.listReleaseAssets.endpoint.merge({
|
||||
...context.repo,
|
||||
release_id: rel.id
|
||||
})
|
||||
const assets = await github.paginate(opts)
|
||||
|
||||
for (const asset of assets) {
|
||||
// Ignore files that are not arduino-ide packages
|
||||
if (!asset.name.startsWith(baseName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Strip the base and remove file extension to get `Linux_32bit`
|
||||
systemArch = asset.name.replace(baseName, "").split(".")[0].split("_")
|
||||
|
||||
// Add a metric object to the list of gathered metrics
|
||||
metrics.push({
|
||||
"type": "gauge",
|
||||
"name": "arduino.downloads.total",
|
||||
"value": asset.download_count,
|
||||
"host": "${{ github.repository }}",
|
||||
"tags": [
|
||||
`version:${rel.name}`,
|
||||
`os:${systemArch[0]}`,
|
||||
`arch:${systemArch[1]}`,
|
||||
"cdn:github.com",
|
||||
"project:arduino-ide"
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The action will put whatever we return from this function in
|
||||
// `outputs.result`, JSON encoded. So we just return the array
|
||||
// of objects and GitHub will do the rest.
|
||||
return metrics
|
||||
|
||||
- name: Send metrics
|
||||
uses: masci/datadog@v1
|
||||
with:
|
||||
api-key: ${{ secrets.DD_API_KEY }}
|
||||
# Metrics input expects YAML but JSON will work just right.
|
||||
metrics: ${{steps.fetch.outputs.result}}
|
||||
|
||||
- name: Report failure
|
||||
if: failure()
|
||||
uses: masci/datadog@v1
|
||||
with:
|
||||
api-key: ${{ secrets.DD_API_KEY }}
|
||||
events: |
|
||||
- title: "Arduino IDE stats failing"
|
||||
text: "Stats collection failed"
|
||||
alert_type: "error"
|
||||
host: ${{ github.repository }}
|
||||
tags:
|
||||
- "project:arduino-ide"
|
||||
- "cdn:github.com"
|
||||
- "workflow:${{ github.workflow }}"
|
8
.github/workflows/i18n-nightly-push.yml
vendored
8
.github/workflows/i18n-nightly-push.yml
vendored
@@ -14,12 +14,12 @@ 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
|
||||
|
10
.github/workflows/i18n-weekly-pull.yml
vendored
10
.github/workflows/i18n-weekly-pull.yml
vendored
@@ -14,12 +14,12 @@ 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
|
||||
@@ -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
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 }}
|
||||
|
||||
|
2
.github/workflows/themes-weekly-pull.yml
vendored
2
.github/workflows/themes-weekly-pull.yml
vendored
@@ -9,7 +9,7 @@ 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
|
||||
NODE_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
pull-from-jsonbin:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -56,7 +56,7 @@
|
||||
"@types/temp": "^0.8.34",
|
||||
"@types/which": "^1.3.1",
|
||||
"ajv": "^6.5.3",
|
||||
"arduino-serial-plotter-webapp": "0.1.0",
|
||||
"arduino-serial-plotter-webapp": "0.2.0",
|
||||
"async-mutex": "^0.3.0",
|
||||
"atob": "^2.1.2",
|
||||
"auth0-js": "^9.14.0",
|
||||
@@ -158,16 +158,16 @@
|
||||
],
|
||||
"arduino": {
|
||||
"cli": {
|
||||
"version": "0.27.1"
|
||||
"version": "0.29.0"
|
||||
},
|
||||
"fwuploader": {
|
||||
"version": "2.2.0"
|
||||
"version": "2.2.2"
|
||||
},
|
||||
"clangd": {
|
||||
"version": "14.0.0"
|
||||
},
|
||||
"languageServer": {
|
||||
"version": "0.7.1"
|
||||
"version": "0.7.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
// @ts-check
|
||||
|
||||
// The version to use.
|
||||
const version = '1.9.1';
|
||||
const version = '1.10.0';
|
||||
|
||||
(async () => {
|
||||
const os = require('os');
|
||||
|
@@ -76,6 +76,12 @@
|
||||
lsSuffix = 'macOS_64bit.tar.gz';
|
||||
clangdSuffix = 'macOS_64bit';
|
||||
break;
|
||||
case 'darwin-arm64':
|
||||
clangdExecutablePath = path.join(build, 'clangd');
|
||||
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||
lsSuffix = 'macOS_ARM64.tar.gz';
|
||||
clangdSuffix = 'macOS_ARM64';
|
||||
break;
|
||||
case 'linux-x64':
|
||||
clangdExecutablePath = path.join(build, 'clangd');
|
||||
clangFormatExecutablePath = path.join(build, 'clang-format');
|
||||
|
@@ -53,8 +53,6 @@ import {
|
||||
DockPanelRenderer as TheiaDockPanelRenderer,
|
||||
TabBarRendererFactory,
|
||||
ContextMenuRenderer,
|
||||
createTreeContainer,
|
||||
TreeWidget,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||
import {
|
||||
@@ -207,12 +205,8 @@ import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } f
|
||||
import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution';
|
||||
import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager';
|
||||
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
|
||||
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
|
||||
import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget';
|
||||
import { SearchInWorkspaceFactory as TheiaSearchInWorkspaceFactory } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';
|
||||
import { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory';
|
||||
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
|
||||
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
|
||||
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
|
||||
import {
|
||||
MonacoEditorFactory,
|
||||
@@ -337,6 +331,12 @@ import { CheckForUpdates } from './contributions/check-for-updates';
|
||||
import { OutputEditorFactory } from './theia/output/output-editor-factory';
|
||||
import { StartupTaskProvider } from '../electron-common/startup-task';
|
||||
import { DeleteSketch } from './contributions/delete-sketch';
|
||||
import { UserFields } from './contributions/user-fields';
|
||||
import { UpdateIndexes } from './contributions/update-indexes';
|
||||
import { InterfaceScale } from './contributions/interface-scale';
|
||||
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
|
||||
import { NewCloudSketch } from './contributions/new-cloud-sketch';
|
||||
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
|
||||
|
||||
const registerArduinoThemes = () => {
|
||||
const themes: MonacoThemeJson[] = [
|
||||
@@ -401,6 +401,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
LibraryListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService)
|
||||
@@ -467,6 +468,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
BoardsListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
@@ -604,9 +606,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(MonacoEditorProvider).toSelf().inSingletonScope();
|
||||
rebind(TheiaMonacoEditorProvider).toService(MonacoEditorProvider);
|
||||
|
||||
bind(SearchInWorkspaceWidget).toSelf();
|
||||
rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget);
|
||||
|
||||
// Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
|
||||
bind(EditorManager).toSelf().inSingletonScope();
|
||||
rebind(TheiaEditorManager).toService(EditorManager);
|
||||
@@ -616,17 +615,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.to(SearchInWorkspaceFactory)
|
||||
.inSingletonScope();
|
||||
|
||||
rebind(TheiaSearchInWorkspaceResultTreeWidget).toDynamicValue(
|
||||
({ container }) => {
|
||||
const childContainer = createTreeContainer(container);
|
||||
childContainer.bind(SearchInWorkspaceResultTreeWidget).toSelf();
|
||||
childContainer
|
||||
.rebind(TreeWidget)
|
||||
.toService(SearchInWorkspaceResultTreeWidget);
|
||||
return childContainer.get(SearchInWorkspaceResultTreeWidget);
|
||||
}
|
||||
);
|
||||
|
||||
// Show a disconnected status bar, when the daemon is not available
|
||||
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
|
||||
rebind(TheiaApplicationConnectionStatusContribution).toService(
|
||||
@@ -761,7 +749,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, OpenBoardsConfig);
|
||||
Contribution.configure(bind, SketchFilesTracker);
|
||||
Contribution.configure(bind, CheckForUpdates);
|
||||
Contribution.configure(bind, UserFields);
|
||||
Contribution.configure(bind, DeleteSketch);
|
||||
Contribution.configure(bind, UpdateIndexes);
|
||||
Contribution.configure(bind, InterfaceScale);
|
||||
Contribution.configure(bind, NewCloudSketch);
|
||||
|
||||
bindContributionProvider(bind, StartupTaskProvider);
|
||||
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
|
||||
@@ -916,6 +908,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);
|
||||
|
@@ -249,6 +249,14 @@ export const ArduinoConfigSchema: PreferenceSchema = {
|
||||
),
|
||||
default: true,
|
||||
},
|
||||
'arduino.sketch.inoBlueprint': {
|
||||
type: 'string',
|
||||
markdownDescription: nls.localize(
|
||||
'arduino/preferences/sketch/inoBlueprint',
|
||||
'Absolute filesystem path to the default `.ino` blueprint file. If specified, the content of the blueprint file will be used for every new sketch created by the IDE. The sketches will be generated with the default Arduino content if not specified. Unaccessible blueprint files are ignored. **A restart of the IDE is needed** for this setting to take effect.'
|
||||
),
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -278,6 +286,7 @@ export interface ArduinoConfiguration {
|
||||
'arduino.auth.registerUri': string;
|
||||
'arduino.survey.notification': boolean;
|
||||
'arduino.cli.daemon.debug': boolean;
|
||||
'arduino.sketch.inoBlueprint': string;
|
||||
'arduino.checkForUpdates': boolean;
|
||||
}
|
||||
|
||||
|
@@ -34,6 +34,7 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
|
||||
) {
|
||||
super({ ...props, maxWidth: 500 });
|
||||
|
||||
this.node.id = 'select-board-dialog-container';
|
||||
this.contentNode.classList.add('select-board-dialog');
|
||||
this.contentNode.appendChild(this.createDescription());
|
||||
|
||||
|
@@ -6,7 +6,6 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
Board,
|
||||
Port,
|
||||
AttachedBoardsChangeEvent,
|
||||
BoardWithPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
@@ -113,11 +112,14 @@ export class BoardsConfig extends React.Component<
|
||||
);
|
||||
}
|
||||
}),
|
||||
this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
|
||||
this.updatePorts(
|
||||
event.newState.ports,
|
||||
AttachedBoardsChangeEvent.diff(event).detached.ports
|
||||
)
|
||||
this.props.boardsServiceProvider.onAvailablePortsChanged(
|
||||
({ newState, oldState }) => {
|
||||
const removedPorts = oldState.filter(
|
||||
(oldPort) =>
|
||||
!newState.find((newPort) => Port.sameAs(newPort, oldPort))
|
||||
);
|
||||
this.updatePorts(newState, removedPorts);
|
||||
}
|
||||
),
|
||||
this.props.boardsServiceProvider.onBoardsConfigChanged(
|
||||
({ selectedBoard, selectedPort }) => {
|
||||
@@ -132,7 +134,7 @@ export class BoardsConfig extends React.Component<
|
||||
this.props.notificationCenter.onPlatformDidUninstall(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onIndexDidUpdate(() =>
|
||||
this.props.notificationCenter.onIndexUpdateDidComplete(() =>
|
||||
this.updateBoards(this.state.query)
|
||||
),
|
||||
this.props.notificationCenter.onDaemonDidStart(() =>
|
||||
@@ -259,9 +261,12 @@ export class BoardsConfig extends React.Component<
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
{this.renderContainer('boards', this.renderBoards.bind(this))}
|
||||
{this.renderContainer(
|
||||
'ports',
|
||||
nls.localize('arduino/board/boards', 'boards'),
|
||||
this.renderBoards.bind(this)
|
||||
)}
|
||||
{this.renderContainer(
|
||||
nls.localize('arduino/board/ports', 'ports'),
|
||||
this.renderPorts.bind(this),
|
||||
this.renderPortsFooter.bind(this)
|
||||
)}
|
||||
@@ -299,6 +304,18 @@ export class BoardsConfig extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
const boardsList = Array.from(distinctBoards.values()).map((board) => (
|
||||
<Item<BoardWithPackage>
|
||||
key={toKey(board)}
|
||||
item={board}
|
||||
label={board.name}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={board.missing}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="search">
|
||||
@@ -315,19 +332,17 @@ export class BoardsConfig extends React.Component<
|
||||
/>
|
||||
<i className="fa fa-search"></i>
|
||||
</div>
|
||||
<div className="boards list">
|
||||
{Array.from(distinctBoards.values()).map((board) => (
|
||||
<Item<BoardWithPackage>
|
||||
key={toKey(board)}
|
||||
item={board}
|
||||
label={board.name}
|
||||
details={board.details}
|
||||
selected={board.selected}
|
||||
onClick={this.selectBoard}
|
||||
missing={board.missing}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{boardsList.length > 0 ? (
|
||||
<div className="boards list">{boardsList}</div>
|
||||
) : (
|
||||
<div className="no-result">
|
||||
{nls.localize(
|
||||
'arduino/board/noBoardsFound',
|
||||
'No boards found for "{0}"',
|
||||
query
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -342,7 +357,7 @@ export class BoardsConfig extends React.Component<
|
||||
);
|
||||
}
|
||||
return !ports.length ? (
|
||||
<div className="loading noselect">
|
||||
<div className="no-result">
|
||||
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
|
||||
</div>
|
||||
) : (
|
||||
@@ -374,7 +389,9 @@ export class BoardsConfig extends React.Component<
|
||||
defaultChecked={this.state.showAllPorts}
|
||||
onChange={this.toggleFilterPorts}
|
||||
/>
|
||||
<span>Show all ports</span>
|
||||
<span>
|
||||
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
@@ -111,7 +111,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
|
||||
const { label } = commands.get(commandId)!;
|
||||
this.menuRegistry.registerMenuAction(menuPath, {
|
||||
commandId,
|
||||
order: `${i}`,
|
||||
order: String(i).padStart(4),
|
||||
label,
|
||||
});
|
||||
return Disposable.create(() =>
|
||||
|
@@ -63,7 +63,10 @@ export class BoardsServiceProvider
|
||||
protected readonly onAvailableBoardsChangedEmitter = new Emitter<
|
||||
AvailableBoard[]
|
||||
>();
|
||||
protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
|
||||
protected readonly onAvailablePortsChangedEmitter = new Emitter<{
|
||||
newState: Port[];
|
||||
oldState: Port[];
|
||||
}>();
|
||||
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
|
||||
|
||||
/**
|
||||
@@ -120,8 +123,12 @@ export class BoardsServiceProvider
|
||||
const { boards: attachedBoards, ports: availablePorts } =
|
||||
AvailablePorts.split(state);
|
||||
this._attachedBoards = attachedBoards;
|
||||
const oldState = this._availablePorts.slice();
|
||||
this._availablePorts = availablePorts;
|
||||
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||
this.onAvailablePortsChangedEmitter.fire({
|
||||
newState: this._availablePorts.slice(),
|
||||
oldState,
|
||||
});
|
||||
|
||||
await this.reconcileAvailableBoards();
|
||||
|
||||
@@ -229,8 +236,12 @@ export class BoardsServiceProvider
|
||||
}
|
||||
|
||||
this._attachedBoards = event.newState.boards;
|
||||
const oldState = this._availablePorts.slice();
|
||||
this._availablePorts = event.newState.ports;
|
||||
this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
|
||||
this.onAvailablePortsChangedEmitter.fire({
|
||||
newState: this._availablePorts.slice(),
|
||||
oldState,
|
||||
});
|
||||
this.reconcileAvailableBoards().then(() => {
|
||||
const { uploadInProgress } = event;
|
||||
// avoid attempting "auto-selection" while an
|
||||
@@ -398,14 +409,16 @@ export class BoardsServiceProvider
|
||||
}
|
||||
|
||||
async selectedBoardUserFields(): Promise<BoardUserField[]> {
|
||||
if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
|
||||
if (!this._boardsConfig.selectedBoard) {
|
||||
return [];
|
||||
}
|
||||
const fqbn = this._boardsConfig.selectedBoard.fqbn;
|
||||
if (!fqbn) {
|
||||
return [];
|
||||
}
|
||||
const protocol = this._boardsConfig.selectedPort.protocol;
|
||||
// Protocol must be set to `default` when uploading without a port selected:
|
||||
// https://arduino.github.io/arduino-cli/dev/platform-specification/#sketch-upload-configuration
|
||||
const protocol = this._boardsConfig.selectedPort?.protocol || 'default';
|
||||
return await this.boardsService.getBoardUserFields({ fqbn, protocol });
|
||||
}
|
||||
|
||||
@@ -600,7 +613,7 @@ export class BoardsServiceProvider
|
||||
boardsConfig.selectedBoard &&
|
||||
availableBoards.every(({ selected }) => !selected)
|
||||
) {
|
||||
let port = boardsConfig.selectedPort
|
||||
let port = boardsConfig.selectedPort;
|
||||
// If the selected board has the same port of an unknown board
|
||||
// that is already in availableBoards we might get a duplicate port.
|
||||
// So we remove the one already in the array and add the selected one.
|
||||
@@ -611,7 +624,7 @@ export class BoardsServiceProvider
|
||||
// get the "Unknown board port" that we will substitute,
|
||||
// then we can include it in the "availableBoard object"
|
||||
// pushed below; to ensure addressLabel is included
|
||||
port = availableBoards[found].port
|
||||
port = availableBoards[found].port;
|
||||
availableBoards.splice(found, 1);
|
||||
}
|
||||
availableBoards.push({
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
import type {
|
||||
import {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
@@ -24,7 +25,16 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
});
|
||||
}
|
||||
|
||||
override async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
protected canParse(uri: URI): boolean {
|
||||
try {
|
||||
BoardSearch.UriParser.parse(uri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): BoardSearch | undefined {
|
||||
return BoardSearch.UriParser.parse(uri);
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ import { CurrentSketch } from '../../common/protocol/sketches-service-client-imp
|
||||
@injectable()
|
||||
export class AddFile extends SketchContribution {
|
||||
@inject(FileDialogService)
|
||||
protected readonly fileDialogService: FileDialogService;
|
||||
private readonly fileDialogService: FileDialogService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddFile.Commands.ADD_FILE, {
|
||||
@@ -31,7 +31,7 @@ export class AddFile extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
protected async addFile(): Promise<void> {
|
||||
private async addFile(): Promise<void> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
@@ -41,6 +41,7 @@ export class AddFile extends SketchContribution {
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
modal: true,
|
||||
});
|
||||
if (!toAddUri) {
|
||||
return;
|
||||
|
@@ -17,13 +17,13 @@ import { nls } from '@theia/core/lib/common';
|
||||
@injectable()
|
||||
export class AddZipLibrary extends SketchContribution {
|
||||
@inject(EnvVariablesServer)
|
||||
protected readonly envVariableServer: EnvVariablesServer;
|
||||
private readonly envVariableServer: EnvVariablesServer;
|
||||
|
||||
@inject(ResponseServiceClient)
|
||||
protected readonly responseService: ResponseServiceClient;
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(LibraryService)
|
||||
protected readonly libraryService: LibraryService;
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
|
||||
@@ -43,23 +43,26 @@ export class AddZipLibrary extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
async addZipLibrary(): Promise<void> {
|
||||
private async addZipLibrary(): Promise<void> {
|
||||
const homeUri = await this.envVariableServer.getHomeDirUri();
|
||||
const defaultPath = await this.fileService.fsPath(new URI(homeUri));
|
||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog({
|
||||
title: nls.localize(
|
||||
'arduino/selectZip',
|
||||
"Select a zip file containing the library you'd like to add"
|
||||
),
|
||||
defaultPath,
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/library/zipLibrary', 'Library'),
|
||||
extensions: ['zip'],
|
||||
},
|
||||
],
|
||||
});
|
||||
const { canceled, filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/selectZip',
|
||||
"Select a zip file containing the library you'd like to add"
|
||||
),
|
||||
defaultPath,
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/library/zipLibrary', 'Library'),
|
||||
extensions: ['zip'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
if (!canceled && filePaths.length) {
|
||||
const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
|
||||
try {
|
||||
|
@@ -28,7 +28,7 @@ export class ArchiveSketch extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
protected async archiveSketch(): Promise<void> {
|
||||
private async archiveSketch(): Promise<void> {
|
||||
const [sketch, config] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
@@ -43,13 +43,16 @@ export class ArchiveSketch extends SketchContribution {
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri).resolve(archiveBasename)
|
||||
);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveSketchAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
});
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveSketchAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
}
|
||||
);
|
||||
if (!filePath || canceled) {
|
||||
return;
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@ import {
|
||||
DisposableCollection,
|
||||
Disposable,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { firstToUpperCase } from '../../common/utils';
|
||||
import { BoardsConfig } from '../boards/boards-config';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { BoardsListWidget } from '../boards/boards-list-widget';
|
||||
@@ -200,14 +199,15 @@ PID: ${PID}`;
|
||||
});
|
||||
|
||||
// Installed boards
|
||||
for (const board of installedBoards) {
|
||||
installedBoards.forEach((board, index) => {
|
||||
const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
|
||||
|
||||
const packageLabel =
|
||||
packageName +
|
||||
`${manuallyInstalled
|
||||
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||
: ''
|
||||
`${
|
||||
manuallyInstalled
|
||||
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
|
||||
: ''
|
||||
}`;
|
||||
// Platform submenu
|
||||
const platformMenuPath = [...boardsPackagesGroup, packageId];
|
||||
@@ -240,14 +240,18 @@ PID: ${PID}`;
|
||||
};
|
||||
|
||||
// Board menu
|
||||
const menuAction = { commandId: id, label: name };
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label: name,
|
||||
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
Disposable.create(() => this.commandRegistry.unregisterCommand(command))
|
||||
);
|
||||
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
|
||||
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
|
||||
}
|
||||
});
|
||||
|
||||
// Installed ports
|
||||
const registerPorts = (
|
||||
@@ -267,8 +271,12 @@ PID: ${PID}`;
|
||||
];
|
||||
const placeholder = new PlaceholderMenuNode(
|
||||
menuPath,
|
||||
`${firstToUpperCase(protocol)} ports`,
|
||||
{ order: protocolOrder.toString() }
|
||||
nls.localize(
|
||||
'arduino/board/typeOfPorts',
|
||||
'{0} ports',
|
||||
Port.Protocols.protocolLabel(protocol)
|
||||
),
|
||||
{ order: protocolOrder.toString().padStart(4) }
|
||||
);
|
||||
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -279,11 +287,13 @@ PID: ${PID}`;
|
||||
|
||||
// First we show addresses with recognized boards connected,
|
||||
// then all the rest.
|
||||
const sortedIDs = Object.keys(ports).sort((left: string, right: string): number => {
|
||||
const [, leftBoards] = ports[left];
|
||||
const [, rightBoards] = ports[right];
|
||||
return rightBoards.length - leftBoards.length;
|
||||
});
|
||||
const sortedIDs = Object.keys(ports).sort(
|
||||
(left: string, right: string): number => {
|
||||
const [, leftBoards] = ports[left];
|
||||
const [, rightBoards] = ports[right];
|
||||
return rightBoards.length - leftBoards.length;
|
||||
}
|
||||
);
|
||||
|
||||
for (let i = 0; i < sortedIDs.length; i++) {
|
||||
const portID = sortedIDs[i];
|
||||
@@ -319,7 +329,7 @@ PID: ${PID}`;
|
||||
const menuAction = {
|
||||
commandId: id,
|
||||
label,
|
||||
order: `${protocolOrder + i + 1}`,
|
||||
order: String(protocolOrder + i + 1).padStart(4),
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.toDisposeBeforeMenuRebuild.push(
|
||||
@@ -351,7 +361,7 @@ PID: ${PID}`;
|
||||
}
|
||||
|
||||
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
|
||||
const allBoards = await this.boardsService.searchBoards({});
|
||||
const allBoards = await this.boardsService.getInstalledBoards();
|
||||
return allBoards.filter(InstalledBoardWithPackage.is);
|
||||
}
|
||||
}
|
||||
|
@@ -37,16 +37,17 @@ export class CheckForIDEUpdates extends Contribution {
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
if (!checkForUpdates) {
|
||||
return;
|
||||
}
|
||||
this.updater
|
||||
.init(
|
||||
this.preferences.get('arduino.ide.updateChannel'),
|
||||
this.preferences.get('arduino.ide.updateBaseUrl')
|
||||
)
|
||||
.then(() => this.updater.checkForUpdates(true))
|
||||
.then(() => {
|
||||
if (!this.preferences['arduino.checkForUpdates']) {
|
||||
return;
|
||||
}
|
||||
return this.updater.checkForUpdates(true);
|
||||
})
|
||||
.then(async (updateInfo) => {
|
||||
if (!updateInfo) return;
|
||||
const versionToSkip = await this.localStorage.getData<string>(
|
||||
|
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,6 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||
|
||||
import {
|
||||
@@ -61,6 +60,7 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { NotificationManager } from '../theia/messages/notifications-manager';
|
||||
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { WorkspaceService } from '../theia/workspace/workspace-service';
|
||||
|
||||
export {
|
||||
Command,
|
||||
|
@@ -49,30 +49,6 @@ export class EditContributions extends Contribution {
|
||||
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
|
||||
execute: () => this.run('editor.action.previousSelectionMatchFindAction'),
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.INCREASE_FONT_SIZE, {
|
||||
execute: async () => {
|
||||
const settings = await this.settingsService.settings();
|
||||
if (settings.autoScaleInterface) {
|
||||
settings.interfaceScale = settings.interfaceScale + 1;
|
||||
} else {
|
||||
settings.editorFontSize = settings.editorFontSize + 1;
|
||||
}
|
||||
await this.settingsService.update(settings);
|
||||
await this.settingsService.save();
|
||||
},
|
||||
});
|
||||
registry.registerCommand(EditContributions.Commands.DECREASE_FONT_SIZE, {
|
||||
execute: async () => {
|
||||
const settings = await this.settingsService.settings();
|
||||
if (settings.autoScaleInterface) {
|
||||
settings.interfaceScale = settings.interfaceScale - 1;
|
||||
} else {
|
||||
settings.editorFontSize = settings.editorFontSize - 1;
|
||||
}
|
||||
await this.settingsService.update(settings);
|
||||
await this.settingsService.save();
|
||||
},
|
||||
});
|
||||
/* Tools */ registry.registerCommand(
|
||||
EditContributions.Commands.AUTO_FORMAT,
|
||||
{ execute: () => this.run('editor.action.formatDocument') }
|
||||
@@ -147,23 +123,6 @@ ${value}
|
||||
order: '3',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/increaseFontSize',
|
||||
'Increase Font Size'
|
||||
),
|
||||
order: '0',
|
||||
});
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FONT_CONTROL_GROUP, {
|
||||
commandId: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/decreaseFontSize',
|
||||
'Decrease Font Size'
|
||||
),
|
||||
order: '1',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
|
||||
commandId: EditContributions.Commands.FIND.id,
|
||||
label: nls.localize('vscode/findController/startFindAction', 'Find'),
|
||||
@@ -220,15 +179,6 @@ ${value}
|
||||
when: 'editorFocus',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+=',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.DECREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+-',
|
||||
});
|
||||
|
||||
registry.registerKeybinding({
|
||||
command: EditContributions.Commands.FIND.id,
|
||||
keybinding: 'CtrlCmd+F',
|
||||
@@ -315,12 +265,6 @@ export namespace EditContributions {
|
||||
export const USE_FOR_FIND: Command = {
|
||||
id: 'arduino-for-find',
|
||||
};
|
||||
export const INCREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-increase-font-size',
|
||||
};
|
||||
export const DECREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-decrease-font-size',
|
||||
};
|
||||
export const AUTO_FORMAT: Command = {
|
||||
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
|
||||
};
|
||||
|
@@ -21,16 +21,23 @@ import {
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Board, SketchRef, SketchContainer } from '../../common/protocol';
|
||||
import {
|
||||
Board,
|
||||
SketchRef,
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
Sketch,
|
||||
CoreService,
|
||||
} from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
export abstract class Examples extends SketchContribution {
|
||||
@inject(CommandRegistry)
|
||||
protected readonly commandRegistry: CommandRegistry;
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly menuManager: MainMenuManager;
|
||||
@@ -38,6 +45,9 @@ export abstract class Examples extends SketchContribution {
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(CoreService)
|
||||
protected readonly coreService: CoreService;
|
||||
|
||||
@inject(BoardsServiceProvider)
|
||||
protected readonly boardsServiceClient: BoardsServiceProvider;
|
||||
|
||||
@@ -50,10 +60,16 @@ export abstract class Examples extends SketchContribution {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
|
||||
protected handleBoardChanged(board: Board | undefined): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
protected abstract update(options?: {
|
||||
board?: Board | undefined;
|
||||
forceRefresh?: boolean;
|
||||
}): void;
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
try {
|
||||
// This is a hack the ensures the desired menu ordering! We cannot use https://github.com/eclipse-theia/theia/pull/8377 due to ATL-222.
|
||||
@@ -149,23 +165,54 @@ export abstract class Examples extends SketchContribution {
|
||||
protected createHandler(uri: string): CommandHandler {
|
||||
return {
|
||||
execute: async () => {
|
||||
const sketch = await this.sketchService.cloneExample(uri);
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
const sketch = await this.clone(uri);
|
||||
if (sketch) {
|
||||
try {
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async clone(uri: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.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()
|
||||
export class BuiltInExamples extends Examples {
|
||||
override async onReady(): Promise<void> {
|
||||
this.register(); // no `await`
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
protected async register(): Promise<void> {
|
||||
protected override async update(): Promise<void> {
|
||||
let sketchContainers: SketchContainer[] | undefined;
|
||||
try {
|
||||
sketchContainers = await this.examplesService.builtIns();
|
||||
@@ -197,29 +244,34 @@ export class BuiltInExamples extends Examples {
|
||||
@injectable()
|
||||
export class LibraryExamples extends Examples {
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.register());
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.register());
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.update());
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.update());
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
this.register(); // no `await`
|
||||
this.update(); // no `await`
|
||||
}
|
||||
|
||||
protected override handleBoardChanged(board: Board | undefined): void {
|
||||
this.register(board);
|
||||
this.update({ board });
|
||||
}
|
||||
|
||||
protected async register(
|
||||
board: Board | undefined = this.boardsServiceClient.boardsConfig
|
||||
.selectedBoard
|
||||
protected override async update(
|
||||
options: { board?: Board; forceRefresh?: boolean } = {
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
}
|
||||
): Promise<void> {
|
||||
const { board, forceRefresh } = options;
|
||||
return this.queue.add(async () => {
|
||||
this.toDispose.dispose();
|
||||
if (forceRefresh) {
|
||||
await this.coreService.refresh();
|
||||
}
|
||||
const fqbn = board?.fqbn;
|
||||
const name = board?.name;
|
||||
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed.
|
||||
|
@@ -7,6 +7,8 @@ import {
|
||||
} from '../../common/protocol';
|
||||
import { Contribution } from './contribution';
|
||||
|
||||
const Arduino_BuiltIn = 'Arduino_BuiltIn';
|
||||
|
||||
@injectable()
|
||||
export class FirstStartupInstaller extends Contribution {
|
||||
@inject(LocalStorageService)
|
||||
@@ -25,8 +27,8 @@ export class FirstStartupInstaller extends Contribution {
|
||||
id: 'arduino:avr',
|
||||
});
|
||||
const builtInLibrary = (
|
||||
await this.libraryService.search({ query: 'Arduino_BuiltIn' })
|
||||
)[0];
|
||||
await this.libraryService.search({ query: Arduino_BuiltIn })
|
||||
).find(({ name }) => name === Arduino_BuiltIn); // Filter by `name` to ensure "exact match". See: https://github.com/arduino/arduino-ide/issues/1526.
|
||||
|
||||
let avrPackageError: Error | undefined;
|
||||
let builtInLibraryError: Error | undefined;
|
||||
@@ -84,7 +86,7 @@ export class FirstStartupInstaller extends Contribution {
|
||||
}
|
||||
if (builtInLibraryError) {
|
||||
this.messageService.error(
|
||||
`Could not install ${builtInLibrary.name} library: ${builtInLibraryError}`
|
||||
`Could not install ${Arduino_BuiltIn} library: ${builtInLibraryError}`
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,7 @@ export class IndexesUpdateProgress extends Contribution {
|
||||
| undefined;
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onIndexWillUpdate((progressId) =>
|
||||
this.notificationCenter.onIndexUpdateWillStart(({ progressId }) =>
|
||||
this.getOrCreateProgress(progressId)
|
||||
);
|
||||
this.notificationCenter.onIndexUpdateDidProgress((progress) => {
|
||||
@@ -24,7 +24,7 @@ export class IndexesUpdateProgress extends Contribution {
|
||||
delegate.report(progress)
|
||||
);
|
||||
});
|
||||
this.notificationCenter.onIndexDidUpdate((progressId) => {
|
||||
this.notificationCenter.onIndexUpdateDidComplete(({ progressId }) => {
|
||||
this.cancelProgress(progressId);
|
||||
});
|
||||
this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => {
|
||||
|
@@ -0,0 +1,228 @@
|
||||
import { inject, 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 { 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,
|
||||
};
|
||||
|
||||
private currentSettings: Settings;
|
||||
private updateSettingsDebounced = debounce(
|
||||
async () => {
|
||||
await this.settingsService.update(this.currentSettings);
|
||||
await this.settingsService.save();
|
||||
},
|
||||
100,
|
||||
{ maxWait: 200 }
|
||||
);
|
||||
|
||||
override onStart(): MaybePromise<void> {
|
||||
const updateCurrent = (settings: Settings) => {
|
||||
this.currentSettings = settings;
|
||||
this.updateFontScalingEnabled();
|
||||
};
|
||||
this.settingsService.onDidChange((settings) => updateCurrent(settings));
|
||||
this.settingsService.settings().then((settings) => updateCurrent(settings));
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(InterfaceScale.Commands.INCREASE_FONT_SIZE, {
|
||||
execute: () => this.updateFontSize('increase'),
|
||||
isEnabled: () => this.fontScalingEnabled.increase,
|
||||
});
|
||||
registry.registerCommand(InterfaceScale.Commands.DECREASE_FONT_SIZE, {
|
||||
execute: () => this.updateFontSize('decrease'),
|
||||
isEnabled: () => this.fontScalingEnabled.decrease,
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
const increaseFontSizeMenuAction = {
|
||||
commandId: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||
label: nls.localize(
|
||||
'arduino/editor/increaseFontSize',
|
||||
'Increase Font Size'
|
||||
),
|
||||
order: '0',
|
||||
};
|
||||
const decreaseFontSizeMenuAction = {
|
||||
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 {
|
||||
let fontScalingEnabled = {
|
||||
increase: true,
|
||||
decrease: true,
|
||||
};
|
||||
|
||||
if (this.currentSettings.autoScaleInterface) {
|
||||
fontScalingEnabled = {
|
||||
increase:
|
||||
this.currentSettings.interfaceScale + InterfaceScale.ZoomLevel.STEP <=
|
||||
InterfaceScale.ZoomLevel.MAX,
|
||||
decrease:
|
||||
this.currentSettings.interfaceScale - InterfaceScale.ZoomLevel.STEP >=
|
||||
InterfaceScale.ZoomLevel.MIN,
|
||||
};
|
||||
} else {
|
||||
fontScalingEnabled = {
|
||||
increase:
|
||||
this.currentSettings.editorFontSize + InterfaceScale.FontSize.STEP <=
|
||||
InterfaceScale.FontSize.MAX,
|
||||
decrease:
|
||||
this.currentSettings.editorFontSize - InterfaceScale.FontSize.STEP >=
|
||||
InterfaceScale.FontSize.MIN,
|
||||
};
|
||||
}
|
||||
|
||||
const isChanged = Object.keys(fontScalingEnabled).some(
|
||||
(key: keyof InterfaceScale.FontScalingEnabled) =>
|
||||
fontScalingEnabled[key] !== this.fontScalingEnabled[key]
|
||||
);
|
||||
if (isChanged) {
|
||||
this.fontScalingEnabled = fontScalingEnabled;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
private updateFontSize(mode: 'increase' | 'decrease'): void {
|
||||
if (this.currentSettings.autoScaleInterface) {
|
||||
mode === 'increase'
|
||||
? (this.currentSettings.interfaceScale += InterfaceScale.ZoomLevel.STEP)
|
||||
: (this.currentSettings.interfaceScale -=
|
||||
InterfaceScale.ZoomLevel.STEP);
|
||||
} else {
|
||||
mode === 'increase'
|
||||
? (this.currentSettings.editorFontSize += InterfaceScale.FontSize.STEP)
|
||||
: (this.currentSettings.editorFontSize -= InterfaceScale.FontSize.STEP);
|
||||
}
|
||||
this.updateFontScalingEnabled();
|
||||
this.updateSettingsDebounced();
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: InterfaceScale.Commands.INCREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+=',
|
||||
});
|
||||
registry.registerKeybinding({
|
||||
command: InterfaceScale.Commands.DECREASE_FONT_SIZE.id,
|
||||
keybinding: 'CtrlCmd+-',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace InterfaceScale {
|
||||
export namespace Commands {
|
||||
export const INCREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-increase-font-size',
|
||||
};
|
||||
export const DECREASE_FONT_SIZE: Command = {
|
||||
id: 'arduino-decrease-font-size',
|
||||
};
|
||||
}
|
||||
|
||||
export namespace ZoomLevel {
|
||||
export const MIN = -8;
|
||||
export const MAX = 9;
|
||||
export const STEP = 1;
|
||||
|
||||
export function toPercentage(scale: number): number {
|
||||
return scale * 20 + 100;
|
||||
}
|
||||
export function fromPercentage(percentage: number): number {
|
||||
return (percentage - 100) / 20;
|
||||
}
|
||||
export namespace Step {
|
||||
export function toPercentage(step: number): number {
|
||||
return step * 20;
|
||||
}
|
||||
export function fromPercentage(percentage: number): number {
|
||||
return percentage / 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace FontSize {
|
||||
export const MIN = 8;
|
||||
export const MAX = 72;
|
||||
export const STEP = 2;
|
||||
}
|
||||
|
||||
export interface FontScalingEnabled {
|
||||
increase: boolean;
|
||||
decrease: boolean;
|
||||
}
|
||||
}
|
@@ -0,0 +1,372 @@
|
||||
import { DialogError } from '@theia/core/lib/browser/dialogs';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { Widget } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import {
|
||||
Progress,
|
||||
ProgressUpdate,
|
||||
} 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 { WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog';
|
||||
import { v4 } from 'uuid';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import type { AuthenticationSession } from '../../node/auth/types';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { CreateApi } from '../create/create-api';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { Create } from '../create/typings';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { WorkspaceInputDialog } 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 { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget';
|
||||
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
|
||||
import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget';
|
||||
import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution';
|
||||
import { Command, CommandRegistry, Contribution, URI } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class NewCloudSketch extends Contribution {
|
||||
@inject(CreateApi)
|
||||
private readonly createApi: CreateApi;
|
||||
@inject(SketchbookWidgetContribution)
|
||||
private readonly widgetContribution: SketchbookWidgetContribution;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(MainMenuManager)
|
||||
private readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private _session: AuthenticationSession | undefined;
|
||||
private _enabled: boolean;
|
||||
|
||||
override onReady(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.authenticationService.onSessionDidChange((session) => {
|
||||
const oldSession = this._session;
|
||||
this._session = session;
|
||||
if (!!oldSession !== !!this._session) {
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}),
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (preferenceName === 'arduino.cloud.enabled') {
|
||||
const oldEnabled = this._enabled;
|
||||
this._enabled = Boolean(newValue);
|
||||
if (this._enabled !== oldEnabled) {
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
this._enabled = this.preferences['arduino.cloud.enabled'];
|
||||
this._session = this.authenticationService.session;
|
||||
if (this._session) {
|
||||
this.mainMenuManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
|
||||
execute: () => this.createNewSketch(),
|
||||
isEnabled: () => !!this._session,
|
||||
isVisible: () => this._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 Remote Sketch'),
|
||||
order: '1',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
keybinding: 'CtrlCmd+Alt+N',
|
||||
});
|
||||
}
|
||||
|
||||
private async createNewSketch(
|
||||
initialValue?: string | undefined
|
||||
): Promise<unknown> {
|
||||
const widget = await this.widgetContribution.widget;
|
||||
const treeModel = this.treeModelFrom(widget);
|
||||
if (!treeModel) {
|
||||
return undefined;
|
||||
}
|
||||
const rootNode = CompositeTreeNode.is(treeModel.root)
|
||||
? treeModel.root
|
||||
: undefined;
|
||||
if (!rootNode) {
|
||||
return undefined;
|
||||
}
|
||||
return this.openWizard(rootNode, treeModel, initialValue);
|
||||
}
|
||||
|
||||
private withProgress(
|
||||
value: string,
|
||||
treeModel: CloudSketchbookTreeModel
|
||||
): (progress: Progress) => Promise<unknown> {
|
||||
return async (progress: Progress) => {
|
||||
let result: Create.Sketch | undefined | 'conflict';
|
||||
try {
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/creating',
|
||||
"Creating remote sketch '{0}'...",
|
||||
value
|
||||
),
|
||||
});
|
||||
result = await this.createApi.createSketch(value);
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
result = 'conflict';
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (result) {
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/cloudSketch/synchronizing',
|
||||
"Synchronizing sketchbook, pulling '{0}'...",
|
||||
value
|
||||
),
|
||||
});
|
||||
await treeModel.refresh();
|
||||
}
|
||||
}
|
||||
if (result === 'conflict') {
|
||||
return this.createNewSketch(value);
|
||||
}
|
||||
if (result) {
|
||||
return this.open(treeModel, result);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
private async open(
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
newSketch: Create.Sketch
|
||||
): Promise<URI | undefined> {
|
||||
const id = CreateUri.toUri(newSketch).path.toString();
|
||||
const node = treeModel.getNode(id);
|
||||
if (!node) {
|
||||
throw new Error(
|
||||
`Could not find remote sketchbook tree node with Tree node ID: ${id}.`
|
||||
);
|
||||
}
|
||||
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||
throw new Error(
|
||||
`Remote sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
|
||||
);
|
||||
}
|
||||
try {
|
||||
await treeModel.sketchbookTree().pull({ node });
|
||||
} catch (err) {
|
||||
if (isNotFound(err)) {
|
||||
await treeModel.refresh();
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/newCloudSketch/notFound',
|
||||
"Could not pull the remote sketch '{0}'. It does not exist.",
|
||||
newSketch.name
|
||||
)
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return this.commandService.executeCommand(
|
||||
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||
{ node }
|
||||
);
|
||||
}
|
||||
|
||||
private treeModelFrom(
|
||||
widget: SketchbookWidget
|
||||
): CloudSketchbookTreeModel | undefined {
|
||||
const treeWidget = widget.getTreeWidget();
|
||||
if (treeWidget instanceof CloudSketchbookTreeWidget) {
|
||||
const model = treeWidget.model;
|
||||
if (model instanceof CloudSketchbookTreeModel) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async openWizard(
|
||||
rootNode: CompositeTreeNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
initialValue?: string | undefined
|
||||
): Promise<unknown> {
|
||||
const existingNames = rootNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
return new NewCloudSketchDialog(
|
||||
{
|
||||
title: nls.localize(
|
||||
'arduino/newCloudSketch/newSketchTitle',
|
||||
'Name of a new Remote Sketch'
|
||||
),
|
||||
parentUri: CreateUri.root,
|
||||
initialValue,
|
||||
validate: (input) => {
|
||||
if (existingNames.includes(input)) {
|
||||
return nls.localize(
|
||||
'arduino/newCloudSketch/sketchAlreadyExists',
|
||||
"Remote sketch '{0}' already exists.",
|
||||
input
|
||||
);
|
||||
}
|
||||
// This is how https://create.arduino.cc/editor/ works when renaming a sketch.
|
||||
if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) {
|
||||
return '';
|
||||
}
|
||||
return nls.localize(
|
||||
'arduino/newCloudSketch/invalidSketchName',
|
||||
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
|
||||
);
|
||||
},
|
||||
},
|
||||
this.labelProvider,
|
||||
(value) => this.withProgress(value, treeModel)
|
||||
).open();
|
||||
}
|
||||
}
|
||||
export namespace NewCloudSketch {
|
||||
export namespace Commands {
|
||||
export const NEW_CLOUD_SKETCH: Command = {
|
||||
id: 'arduino-new-cloud-sketch',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isConflict(err: unknown): boolean {
|
||||
return isErrorWithStatusOf(err, 409);
|
||||
}
|
||||
function isNotFound(err: unknown): boolean {
|
||||
return isErrorWithStatusOf(err, 404);
|
||||
}
|
||||
function isErrorWithStatusOf(
|
||||
err: unknown,
|
||||
status: number
|
||||
): err is Error & { status: number } {
|
||||
if (err instanceof Error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const object = err as any;
|
||||
return 'status' in object && object.status === status;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
class NewCloudSketchDialog extends WorkspaceInputDialog {
|
||||
constructor(
|
||||
@inject(WorkspaceInputDialogProps)
|
||||
protected override readonly props: WorkspaceInputDialogProps,
|
||||
@inject(LabelProvider)
|
||||
protected override readonly labelProvider: LabelProvider,
|
||||
private readonly withProgress: (
|
||||
value: string
|
||||
) => (progress: Progress) => Promise<unknown>
|
||||
) {
|
||||
super(props, labelProvider);
|
||||
}
|
||||
protected override async accept(): Promise<void> {
|
||||
if (!this.resolve) {
|
||||
return;
|
||||
}
|
||||
this.acceptCancellationSource.cancel();
|
||||
this.acceptCancellationSource = new CancellationTokenSource();
|
||||
const token = this.acceptCancellationSource.token;
|
||||
const value = this.value;
|
||||
const error = await this.isValid(value, 'open');
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
if (!DialogError.getResult(error)) {
|
||||
this.setErrorMessage(error);
|
||||
} else {
|
||||
const spinner = document.createElement('div');
|
||||
spinner.classList.add('spinner');
|
||||
const disposables = new DisposableCollection();
|
||||
try {
|
||||
this.toggleButtons(true);
|
||||
disposables.push(Disposable.create(() => this.toggleButtons(false)));
|
||||
|
||||
const closeParent = this.closeCrossNode.parentNode;
|
||||
closeParent?.removeChild(this.closeCrossNode);
|
||||
disposables.push(
|
||||
Disposable.create(() => {
|
||||
closeParent?.appendChild(this.closeCrossNode);
|
||||
})
|
||||
);
|
||||
|
||||
this.errorMessageNode.classList.add('progress');
|
||||
disposables.push(
|
||||
Disposable.create(() =>
|
||||
this.errorMessageNode.classList.remove('progress')
|
||||
)
|
||||
);
|
||||
|
||||
const errorParent = this.errorMessageNode.parentNode;
|
||||
errorParent?.insertBefore(spinner, this.errorMessageNode);
|
||||
disposables.push(
|
||||
Disposable.create(() => errorParent?.removeChild(spinner))
|
||||
);
|
||||
|
||||
const cancellationSource = new CancellationTokenSource();
|
||||
const progress: Progress = {
|
||||
id: v4(),
|
||||
cancel: () => cancellationSource.cancel(),
|
||||
report: (update: ProgressUpdate) => {
|
||||
this.setProgressMessage(update);
|
||||
},
|
||||
result: Promise.resolve(value),
|
||||
};
|
||||
await this.withProgress(value)(progress);
|
||||
} finally {
|
||||
disposables.dispose();
|
||||
}
|
||||
this.resolve(value);
|
||||
Widget.detach(this);
|
||||
}
|
||||
}
|
||||
|
||||
private toggleButtons(disabled: boolean): void {
|
||||
if (this.acceptButton) {
|
||||
this.acceptButton.disabled = disabled;
|
||||
}
|
||||
if (this.closeButton) {
|
||||
this.closeButton.disabled = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
private setProgressMessage(update: ProgressUpdate): void {
|
||||
if (update.work && update.work.done === update.work.total) {
|
||||
this.errorMessageNode.innerText = '';
|
||||
} else {
|
||||
if (update.message) {
|
||||
this.errorMessageNode.innerText = update.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
SketchContribution,
|
||||
URI,
|
||||
@@ -17,17 +16,12 @@ export class NewSketch extends SketchContribution {
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
|
||||
execute: () => this.newSketch(),
|
||||
});
|
||||
registry.registerCommand(NewSketch.Commands.NEW_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () => registry.executeCommand(NewSketch.Commands.NEW_SKETCH.id),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/new', 'New'),
|
||||
label: nls.localize('arduino/sketch/new', 'New Sketch'),
|
||||
order: '0',
|
||||
});
|
||||
}
|
||||
@@ -54,8 +48,5 @@ export namespace NewSketch {
|
||||
export const NEW_SKETCH: Command = {
|
||||
id: 'arduino-new-sketch',
|
||||
};
|
||||
export const NEW_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-new-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class OpenRecentSketch extends SketchContribution {
|
||||
@@ -33,7 +34,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
|
||||
protected toDispose = new DisposableCollection();
|
||||
|
||||
override onStart(): void {
|
||||
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
|
||||
@@ -42,8 +43,12 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update(forceUpdate?: boolean): void {
|
||||
this.sketchService
|
||||
.recentlyOpenedSketches()
|
||||
.recentlyOpenedSketches(forceUpdate)
|
||||
.then((sketches) => this.refreshMenu(sketches));
|
||||
}
|
||||
|
||||
@@ -62,19 +67,25 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
|
||||
protected register(sketches: Sketch[]): void {
|
||||
const order = 0;
|
||||
this.toDispose.dispose();
|
||||
for (const sketch of sketches) {
|
||||
const { uri } = sketch;
|
||||
const toDispose = this.toDisposeBeforeRegister.get(uri);
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
}
|
||||
const command = { id: `arduino-open-recent--${uri}` };
|
||||
const handler = {
|
||||
execute: () =>
|
||||
this.commandRegistry.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
),
|
||||
execute: async () => {
|
||||
try {
|
||||
await this.commandRegistry.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.update(true);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
this.commandRegistry.registerCommand(command, handler);
|
||||
this.menuRegistry.registerMenuAction(
|
||||
@@ -85,8 +96,7 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
order: String(order),
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeRegister.set(
|
||||
sketch.uri,
|
||||
this.toDispose.pushAll([
|
||||
new DisposableCollection(
|
||||
Disposable.create(() =>
|
||||
this.commandRegistry.unregisterCommand(command)
|
||||
@@ -94,8 +104,8 @@ export class OpenRecentSketch extends SketchContribution {
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(command)
|
||||
)
|
||||
)
|
||||
);
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { Later } from '../../common/nls';
|
||||
import { SketchesError } from '../../common/protocol';
|
||||
import { Sketch, SketchesError } from '../../common/protocol';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
import { promptMoveSketch } from './open-sketch';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
import { Deferred, wait } from '@theia/core/lib/common/promise-util';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
|
||||
@injectable()
|
||||
export class OpenSketchFiles extends SketchContribution {
|
||||
@@ -55,9 +60,25 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
}
|
||||
});
|
||||
}
|
||||
const { workspaceError } = this.workspaceService;
|
||||
// This happens when the IDE2 has been started (from either a terminal or clicking on an `ino` file) with a /path/to/invalid/sketch. (#964)
|
||||
if (SketchesError.InvalidName.is(workspaceError)) {
|
||||
await this.promptMove(workspaceError);
|
||||
}
|
||||
} catch (err) {
|
||||
// This happens when the user gracefully closed IDE2, all went well
|
||||
// but the main sketch file was renamed outside of IDE2 and when the user restarts the IDE2
|
||||
// the workspace path still exists, but the sketch path is not valid anymore. (#964)
|
||||
if (SketchesError.InvalidName.is(err)) {
|
||||
const movedSketch = await this.promptMove(err);
|
||||
if (!movedSketch) {
|
||||
// If user did not accept the move, or move was not possible, force reload with a fallback.
|
||||
return this.openFallbackSketch();
|
||||
}
|
||||
}
|
||||
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.openFallbackSketch();
|
||||
return this.openFallbackSketch();
|
||||
} else {
|
||||
console.error(err);
|
||||
const message =
|
||||
@@ -71,6 +92,31 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
}
|
||||
}
|
||||
|
||||
private async promptMove(
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
invalidMainSketchUri: string;
|
||||
}
|
||||
>
|
||||
): Promise<Sketch | undefined> {
|
||||
const { invalidMainSketchUri } = err.data;
|
||||
requestAnimationFrame(() => this.messageService.error(err.message));
|
||||
await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
|
||||
const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
|
||||
fileService: this.fileService,
|
||||
sketchService: this.sketchService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
if (movedSketch) {
|
||||
this.workspaceService.open(new URI(movedSketch.uri), {
|
||||
preserveWindow: true,
|
||||
});
|
||||
return movedSketch;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async openFallbackSketch(): Promise<void> {
|
||||
const sketch = await this.sketchService.createNewSketch();
|
||||
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
|
||||
@@ -84,16 +130,79 @@ export class OpenSketchFiles extends SketchContribution {
|
||||
const widget = this.editorManager.all.find(
|
||||
(widget) => widget.editor.uri.toString() === uri
|
||||
);
|
||||
if (!widget || forceOpen) {
|
||||
return this.editorManager.open(
|
||||
if (widget && !forceOpen) {
|
||||
return widget;
|
||||
}
|
||||
|
||||
const disposables = new DisposableCollection();
|
||||
const deferred = new Deferred<EditorWidget>();
|
||||
// An editor can be in two primary states:
|
||||
// - The editor is not yet opened. The `widget` is `undefined`. With `editorManager#open`, Theia will create an editor and fire an `editorManager#onCreated` event.
|
||||
// - The editor is opened. Can be active, current, or open.
|
||||
// - If the editor has the focus (the cursor blinks in the editor): it's the active editor.
|
||||
// - If the editor does not have the focus (the focus is on a different widget or the context menu is opened in the editor): it's the current editor.
|
||||
// - If the editor is not the top editor in the main area, it's opened.
|
||||
if (!widget) {
|
||||
// If the widget is `undefined`, IDE2 expects one `onCreate` event. Subscribe to the `onCreated` event
|
||||
// and resolve the promise with the editor only when the new editor's visibility changes.
|
||||
disposables.push(
|
||||
this.editorManager.onCreated((editor) => {
|
||||
if (editor.editor.uri.toString() === uri) {
|
||||
if (editor.isAttached && editor.isVisible) {
|
||||
deferred.resolve(editor);
|
||||
} else {
|
||||
disposables.push(
|
||||
editor.onDidChangeVisibility((visible) => {
|
||||
if (visible) {
|
||||
// wait an animation frame. although the visible and attached props are true the editor is not there.
|
||||
// let the browser render the widget
|
||||
setTimeout(
|
||||
() =>
|
||||
requestAnimationFrame(() => deferred.resolve(editor)),
|
||||
0
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.editorManager
|
||||
.open(
|
||||
new URI(uri),
|
||||
options ?? {
|
||||
mode: 'reveal',
|
||||
preview: false,
|
||||
counter: 0,
|
||||
}
|
||||
)
|
||||
.then((editorWidget) => {
|
||||
// If the widget was defined, it was already opened.
|
||||
// The editor is expected to be attached to the shell and visible in the UI.
|
||||
// The deferred promise does not have to wait for the `editorManager#onCreated` event.
|
||||
// It can resolve earlier.
|
||||
if (!widget) {
|
||||
deferred.resolve(editorWidget);
|
||||
}
|
||||
});
|
||||
|
||||
const timeout = 5_000; // number of ms IDE2 waits for the editor to show up in the UI
|
||||
const result = 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;
|
||||
}
|
||||
}
|
||||
export namespace OpenSketchFiles {
|
||||
|
@@ -1,115 +1,50 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
SketchesError,
|
||||
SketchesService,
|
||||
SketchRef,
|
||||
} from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
SketchContribution,
|
||||
Sketch,
|
||||
URI,
|
||||
Command,
|
||||
CommandRegistry,
|
||||
MenuModelRegistry,
|
||||
KeybindingRegistry,
|
||||
MenuModelRegistry,
|
||||
Sketch,
|
||||
SketchContribution,
|
||||
URI,
|
||||
} from './contribution';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import { BuiltInExamples } from './examples';
|
||||
import { Sketchbook } from './sketchbook';
|
||||
import { SketchContainer } from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
export type SketchLocation = string | URI | SketchRef;
|
||||
export namespace SketchLocation {
|
||||
export function toUri(location: SketchLocation): URI {
|
||||
if (typeof location === 'string') {
|
||||
return new URI(location);
|
||||
} else if (SketchRef.is(location)) {
|
||||
return toUri(location.uri);
|
||||
} else {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
export function is(arg: unknown): arg is SketchLocation {
|
||||
return typeof arg === 'string' || arg instanceof URI || SketchRef.is(arg);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class OpenSketch extends SketchContribution {
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
@inject(BuiltInExamples)
|
||||
protected readonly builtInExamples: BuiltInExamples;
|
||||
|
||||
@inject(ExamplesService)
|
||||
protected readonly examplesService: ExamplesService;
|
||||
|
||||
@inject(Sketchbook)
|
||||
protected readonly sketchbook: Sketchbook;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
|
||||
execute: (arg) =>
|
||||
Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
|
||||
});
|
||||
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: async (_: Widget, target: EventTarget) => {
|
||||
const container = await this.sketchService.getSketches({
|
||||
exclude: ['**/hardware/**'],
|
||||
});
|
||||
if (SketchContainer.isEmpty(container)) {
|
||||
this.openSketch();
|
||||
} else {
|
||||
this.toDispose.dispose();
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const { parentElement } = target;
|
||||
if (!parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuRegistry.registerMenuAction(
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__OPEN_GROUP,
|
||||
{
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: nls.localize(
|
||||
'vscode/workspaceActions/openFileFolder',
|
||||
'Open...'
|
||||
),
|
||||
}
|
||||
);
|
||||
this.toDispose.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(
|
||||
OpenSketch.Commands.OPEN_SKETCH
|
||||
)
|
||||
)
|
||||
);
|
||||
this.sketchbook.registerRecursively(
|
||||
[...container.children, ...container.sketches],
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__RECENT_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
try {
|
||||
const containers = await this.examplesService.builtIns();
|
||||
for (const container of containers) {
|
||||
this.builtInExamples.registerRecursively(
|
||||
container,
|
||||
ArduinoMenus.OPEN_SKETCH__CONTEXT__EXAMPLES_GROUP,
|
||||
this.toDispose
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error when collecting built-in examples.', e);
|
||||
}
|
||||
const options = {
|
||||
menuPath: ArduinoMenus.OPEN_SKETCH__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
y:
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
execute: async (arg) => {
|
||||
const toOpen = !SketchLocation.is(arg)
|
||||
? await this.selectSketch()
|
||||
: arg;
|
||||
if (toOpen) {
|
||||
return this.openSketch(toOpen);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -119,7 +54,7 @@ export class OpenSketch extends SketchContribution {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
|
||||
order: '1',
|
||||
order: '2',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -130,30 +65,40 @@ export class OpenSketch extends SketchContribution {
|
||||
});
|
||||
}
|
||||
|
||||
async openSketch(
|
||||
toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
|
||||
): Promise<void> {
|
||||
const sketch = await toOpen;
|
||||
if (sketch) {
|
||||
this.workspaceService.open(new URI(sketch.uri));
|
||||
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> {
|
||||
if (!toOpen) {
|
||||
return;
|
||||
}
|
||||
const uri = SketchLocation.toUri(toOpen);
|
||||
try {
|
||||
await this.sketchService.loadSketch(uri.toString());
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
this.workspaceService.open(uri);
|
||||
}
|
||||
|
||||
protected async selectSketch(): Promise<Sketch | undefined> {
|
||||
private async selectSketch(): Promise<Sketch | undefined> {
|
||||
const config = await this.configService.getConfiguration();
|
||||
const defaultPath = await this.fileService.fsPath(
|
||||
new URI(config.sketchDirUri)
|
||||
);
|
||||
const { filePaths } = await remote.dialog.showOpenDialog({
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
|
||||
extensions: ['ino', 'pde'],
|
||||
},
|
||||
],
|
||||
});
|
||||
const { filePaths } = await remote.dialog.showOpenDialog(
|
||||
remote.getCurrentWindow(),
|
||||
{
|
||||
defaultPath,
|
||||
properties: ['createDirectory', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: nls.localize('arduino/sketch/sketch', 'Sketch'),
|
||||
extensions: ['ino', 'pde'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
if (!filePaths.length) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -169,45 +114,11 @@ export class OpenSketch extends SketchContribution {
|
||||
return sketch;
|
||||
}
|
||||
if (Sketch.isSketchFile(sketchFileUri)) {
|
||||
const name = new URI(sketchFileUri).path.name;
|
||||
const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||
],
|
||||
message: nls.localize(
|
||||
'arduino/sketch/movingMsg',
|
||||
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
||||
nameWithExt,
|
||||
name
|
||||
),
|
||||
return promptMoveSketch(sketchFileUri, {
|
||||
fileService: this.fileService,
|
||||
sketchService: this.sketchService,
|
||||
labelProvider: this.labelProvider,
|
||||
});
|
||||
if (response === 1) {
|
||||
// OK
|
||||
const newSketchUri = new URI(sketchFileUri).parent.resolve(name);
|
||||
const exists = await this.fileService.exists(newSketchUri);
|
||||
if (exists) {
|
||||
await remote.dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
||||
message: nls.localize(
|
||||
'arduino/sketch/cantOpen',
|
||||
'A folder named "{0}" already exists. Can\'t open sketch.',
|
||||
name
|
||||
),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
await this.fileService.createFolder(newSketchUri);
|
||||
await this.fileService.move(
|
||||
new URI(sketchFileUri),
|
||||
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||
);
|
||||
return this.sketchService.getSketchFolder(newSketchUri.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,8 +128,57 @@ export namespace OpenSketch {
|
||||
export const OPEN_SKETCH: Command = {
|
||||
id: 'arduino-open-sketch',
|
||||
};
|
||||
export const OPEN_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-open-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function promptMoveSketch(
|
||||
sketchFileUri: string | URI,
|
||||
options: {
|
||||
fileService: FileService;
|
||||
sketchService: SketchesService;
|
||||
labelProvider: LabelProvider;
|
||||
}
|
||||
): Promise<Sketch | undefined> {
|
||||
const { fileService, sketchService, labelProvider } = options;
|
||||
const uri =
|
||||
sketchFileUri instanceof URI ? sketchFileUri : new URI(sketchFileUri);
|
||||
const name = uri.path.name;
|
||||
const nameWithExt = labelProvider.getName(uri);
|
||||
const { response } = await remote.dialog.showMessageBox({
|
||||
title: nls.localize('arduino/sketch/moving', 'Moving'),
|
||||
type: 'question',
|
||||
buttons: [
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||
nls.localize('vscode/issueMainService/ok', 'OK'),
|
||||
],
|
||||
message: nls.localize(
|
||||
'arduino/sketch/movingMsg',
|
||||
'The file "{0}" needs to be inside a sketch folder named "{1}".\nCreate this folder, move the file, and continue?',
|
||||
nameWithExt,
|
||||
name
|
||||
),
|
||||
});
|
||||
if (response === 1) {
|
||||
// OK
|
||||
const newSketchUri = uri.parent.resolve(name);
|
||||
const exists = await fileService.exists(newSketchUri);
|
||||
if (exists) {
|
||||
await remote.dialog.showMessageBox({
|
||||
type: 'error',
|
||||
title: nls.localize('vscode/dialog/dialogErrorMessage', 'Error'),
|
||||
message: nls.localize(
|
||||
'arduino/sketch/cantOpen',
|
||||
'A folder named "{0}" already exists. Can\'t open sketch.',
|
||||
name
|
||||
),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
await fileService.createFolder(newSketchUri);
|
||||
await fileService.move(
|
||||
uri,
|
||||
new URI(newSketchUri.resolve(nameWithExt).toString())
|
||||
);
|
||||
return sketchService.getSketchFolder(newSketchUri.toString());
|
||||
}
|
||||
}
|
||||
|
@@ -50,7 +50,7 @@ export class SaveAsSketch extends SketchContribution {
|
||||
/**
|
||||
* Resolves `true` if the sketch was successfully saved as something.
|
||||
*/
|
||||
async saveAs(
|
||||
private async saveAs(
|
||||
{
|
||||
execOnlyIfTemp,
|
||||
openAfterMove,
|
||||
@@ -58,7 +58,10 @@ export class SaveAsSketch extends SketchContribution {
|
||||
markAsRecentlyOpened,
|
||||
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
|
||||
): Promise<boolean> {
|
||||
const sketch = await this.sketchServiceClient.currentSketch();
|
||||
const [sketch, configuration] = await Promise.all([
|
||||
this.sketchServiceClient.currentSketch(),
|
||||
this.configService.getConfiguration(),
|
||||
]);
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return false;
|
||||
}
|
||||
@@ -68,27 +71,38 @@ export class SaveAsSketch extends SketchContribution {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sketchUri = new URI(sketch.uri);
|
||||
const sketchbookDirUri = new URI(configuration.sketchDirUri);
|
||||
// If the sketch is temp, IDE2 proposes the default sketchbook folder URI.
|
||||
// If the sketch is not temp, but not contained in the default sketchbook folder, IDE2 proposes the default location.
|
||||
// Otherwise, it proposes the parent folder of the current sketch.
|
||||
const containerDirUri = isTemp
|
||||
? sketchbookDirUri
|
||||
: !sketchbookDirUri.isEqualOrParent(sketchUri)
|
||||
? sketchbookDirUri
|
||||
: sketchUri.parent;
|
||||
const exists = await this.fileService.exists(
|
||||
containerDirUri.resolve(sketch.name)
|
||||
);
|
||||
|
||||
// If target does not exist, propose a `directories.user`/${sketch.name} path
|
||||
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
|
||||
const sketchDirUri = new URI(
|
||||
(await this.configService.getConfiguration()).sketchDirUri
|
||||
);
|
||||
const exists = await this.fileService.exists(
|
||||
sketchDirUri.resolve(sketch.name)
|
||||
);
|
||||
const defaultUri = sketchDirUri.resolve(
|
||||
const defaultUri = containerDirUri.resolve(
|
||||
exists
|
||||
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
|
||||
: sketch.name
|
||||
);
|
||||
const defaultPath = await this.fileService.fsPath(defaultUri);
|
||||
const { filePath, canceled } = await remote.dialog.showSaveDialog({
|
||||
title: nls.localize(
|
||||
'arduino/sketch/saveFolderAs',
|
||||
'Save sketch folder as...'
|
||||
),
|
||||
defaultPath,
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import { SaveAsSketch } from './save-as-sketch';
|
||||
import {
|
||||
SketchContribution,
|
||||
@@ -19,19 +18,13 @@ export class SaveSketch extends SketchContribution {
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
|
||||
execute: () => this.saveSketch(),
|
||||
});
|
||||
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH__TOOLBAR, {
|
||||
isVisible: (widget) =>
|
||||
ArduinoToolbar.is(widget) && widget.side === 'left',
|
||||
execute: () =>
|
||||
registry.executeCommand(SaveSketch.Commands.SAVE_SKETCH.id),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||
label: nls.localize('vscode/fileCommands/save', 'Save'),
|
||||
order: '6',
|
||||
order: '7',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,8 +61,5 @@ export namespace SaveSketch {
|
||||
export const SAVE_SKETCH: Command = {
|
||||
id: 'arduino-save-sketch',
|
||||
};
|
||||
export const SAVE_SKETCH__TOOLBAR: Command = {
|
||||
id: 'arduino-save-sketch--toolbar',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -176,7 +176,7 @@ export class SketchControl extends SketchContribution {
|
||||
{
|
||||
commandId: command.id,
|
||||
label: this.labelProvider.getName(uri),
|
||||
order: `${i}`,
|
||||
order: String(i).padStart(4),
|
||||
}
|
||||
);
|
||||
this.toDisposeBeforeCreateNewContextMenu.push(
|
||||
|
@@ -1,32 +1,14 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||
import { CommandRegistry, MenuModelRegistry } from './contribution';
|
||||
import { MenuModelRegistry } from './contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { Examples } from './examples';
|
||||
import {
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
SketchRef,
|
||||
} from '../../common/protocol';
|
||||
import { SketchContainer, SketchesError } from '../../common/protocol';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class Sketchbook extends Examples {
|
||||
@inject(CommandRegistry)
|
||||
protected override readonly commandRegistry: CommandRegistry;
|
||||
|
||||
@inject(MenuModelRegistry)
|
||||
protected override readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
override onStart(): void {
|
||||
this.sketchServiceClient.onSketchbookDidChange(() => this.update());
|
||||
}
|
||||
@@ -35,10 +17,10 @@ export class Sketchbook extends Examples {
|
||||
this.update();
|
||||
}
|
||||
|
||||
private update() {
|
||||
protected override update(): void {
|
||||
this.sketchService.getSketches({}).then((container) => {
|
||||
this.register(container);
|
||||
this.mainMenuManager.update();
|
||||
this.menuManager.update();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +32,7 @@ export class Sketchbook extends Examples {
|
||||
);
|
||||
}
|
||||
|
||||
protected register(container: SketchContainer): void {
|
||||
private register(container: SketchContainer): void {
|
||||
this.toDispose.dispose();
|
||||
this.registerRecursively(
|
||||
[...container.children, ...container.sketches],
|
||||
@@ -62,23 +44,18 @@ export class Sketchbook extends Examples {
|
||||
protected override createHandler(uri: string): CommandHandler {
|
||||
return {
|
||||
execute: async () => {
|
||||
let sketch: SketchRef | undefined = undefined;
|
||||
try {
|
||||
sketch = await this.sketchService.loadSketch(uri);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
// To handle the following:
|
||||
// Open IDE2, delete a sketch from sketchbook, click on File > Sketchbook > the deleted sketch.
|
||||
// Filesystem watcher misses out delete events on macOS; hence IDE2 has no chance to update the menu items.
|
||||
this.messageService.error(err.message);
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
if (sketch) {
|
||||
await this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
uri
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
// Force update the menu items to remove the absent sketch.
|
||||
this.update();
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -0,0 +1,193 @@
|
||||
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CoreService, IndexType } from '../../common/protocol';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||
import { Command, CommandRegistry, Contribution } from './contribution';
|
||||
|
||||
@injectable()
|
||||
export class UpdateIndexes extends Contribution {
|
||||
@inject(WindowServiceExt)
|
||||
private readonly windowService: WindowServiceExt;
|
||||
@inject(LocalStorageService)
|
||||
private readonly localStorage: LocalStorageService;
|
||||
@inject(CoreService)
|
||||
private readonly coreService: CoreService;
|
||||
@inject(NotificationCenter)
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.notificationCenter.onIndexUpdateDidComplete(({ summary }) =>
|
||||
Promise.all(
|
||||
Object.entries(summary).map(([type, updatedAt]) =>
|
||||
this.setLastUpdateDateTime(type as IndexType, updatedAt)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override onReady(): void {
|
||||
this.checkForUpdates();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UpdateIndexes.Commands.UPDATE_INDEXES, {
|
||||
execute: () => this.updateIndexes(IndexType.All, true),
|
||||
});
|
||||
registry.registerCommand(UpdateIndexes.Commands.UPDATE_PLATFORM_INDEX, {
|
||||
execute: () => this.updateIndexes(['platform'], true),
|
||||
});
|
||||
registry.registerCommand(UpdateIndexes.Commands.UPDATE_LIBRARY_INDEX, {
|
||||
execute: () => this.updateIndexes(['library'], true),
|
||||
});
|
||||
}
|
||||
|
||||
private async checkForUpdates(): Promise<void> {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
if (!checkForUpdates) {
|
||||
console.debug(
|
||||
'[update-indexes]: `arduino.checkForUpdates` is `false`. Skipping updating the indexes.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await this.windowService.isFirstWindow()) {
|
||||
const summary = await this.coreService.indexUpdateSummaryBeforeInit();
|
||||
if (summary.message) {
|
||||
this.messageService.error(summary.message);
|
||||
}
|
||||
const typesToCheck = IndexType.All.filter((type) => !(type in summary));
|
||||
if (Object.keys(summary).length) {
|
||||
console.debug(
|
||||
`[update-indexes]: Detected an index update summary before the core gRPC client initialization. Updating local storage with ${JSON.stringify(
|
||||
summary
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
console.debug(
|
||||
'[update-indexes]: No index update summary was available before the core gRPC client initialization. Checking the status of the all the index types.'
|
||||
);
|
||||
}
|
||||
await Promise.allSettled([
|
||||
...Object.entries(summary).map(([type, updatedAt]) =>
|
||||
this.setLastUpdateDateTime(type as IndexType, updatedAt)
|
||||
),
|
||||
this.updateIndexes(typesToCheck),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateIndexes(
|
||||
types: IndexType[],
|
||||
force = false
|
||||
): Promise<void> {
|
||||
const updatedAt = new Date().toISOString();
|
||||
return Promise.all(
|
||||
types.map((type) => this.needsIndexUpdate(type, updatedAt, force))
|
||||
).then((needsIndexUpdateResults) => {
|
||||
const typesToUpdate = needsIndexUpdateResults.filter(IndexType.is);
|
||||
if (typesToUpdate.length) {
|
||||
console.debug(
|
||||
`[update-indexes]: Requesting the index update of type: ${JSON.stringify(
|
||||
typesToUpdate
|
||||
)} with date time: ${updatedAt}.`
|
||||
);
|
||||
return this.coreService.updateIndex({ types: typesToUpdate });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async needsIndexUpdate(
|
||||
type: IndexType,
|
||||
now: string,
|
||||
force = false
|
||||
): Promise<IndexType | false> {
|
||||
if (force) {
|
||||
console.debug(
|
||||
`[update-indexes]: Update for index type: '${type}' was forcefully requested.`
|
||||
);
|
||||
return type;
|
||||
}
|
||||
const lastUpdateIsoDateTime = await this.getLastUpdateDateTime(type);
|
||||
if (!lastUpdateIsoDateTime) {
|
||||
console.debug(
|
||||
`[update-indexes]: No last update date time was persisted for index type: '${type}'. Index update is required.`
|
||||
);
|
||||
return type;
|
||||
}
|
||||
const lastUpdateDateTime = Date.parse(lastUpdateIsoDateTime);
|
||||
if (Number.isNaN(lastUpdateDateTime)) {
|
||||
console.debug(
|
||||
`[update-indexes]: Invalid last update date time was persisted for index type: '${type}'. Last update date time was: ${lastUpdateDateTime}. Index update is required.`
|
||||
);
|
||||
return type;
|
||||
}
|
||||
const diff = new Date(now).getTime() - lastUpdateDateTime;
|
||||
const needsIndexUpdate = diff >= this.threshold;
|
||||
console.debug(
|
||||
`[update-indexes]: Update for index type '${type}' is ${
|
||||
needsIndexUpdate ? '' : 'not '
|
||||
}required. Now: ${now}, Last index update date time: ${new Date(
|
||||
lastUpdateDateTime
|
||||
).toISOString()}, diff: ${diff} ms, threshold: ${this.threshold} ms.`
|
||||
);
|
||||
return needsIndexUpdate ? type : false;
|
||||
}
|
||||
|
||||
private async getLastUpdateDateTime(
|
||||
type: IndexType
|
||||
): Promise<string | undefined> {
|
||||
const key = this.storageKeyOf(type);
|
||||
return this.localStorage.getData<string>(key);
|
||||
}
|
||||
|
||||
private async setLastUpdateDateTime(
|
||||
type: IndexType,
|
||||
updatedAt: string
|
||||
): Promise<void> {
|
||||
const key = this.storageKeyOf(type);
|
||||
return this.localStorage.setData<string>(key, updatedAt).finally(() => {
|
||||
console.debug(
|
||||
`[update-indexes]: Updated the last index update date time of '${type}' to ${updatedAt}.`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private storageKeyOf(type: IndexType): string {
|
||||
return `index-last-update-time--${type}`;
|
||||
}
|
||||
|
||||
private get threshold(): number {
|
||||
return 4 * 60 * 60 * 1_000; // four hours in millis
|
||||
}
|
||||
}
|
||||
export namespace UpdateIndexes {
|
||||
export namespace Commands {
|
||||
export const UPDATE_INDEXES: Command & { label: string } = {
|
||||
id: 'arduino-update-indexes',
|
||||
label: nls.localize(
|
||||
'arduino/updateIndexes/updateIndexes',
|
||||
'Update Indexes'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const UPDATE_PLATFORM_INDEX: Command & { label: string } = {
|
||||
id: 'arduino-update-package-index',
|
||||
label: nls.localize(
|
||||
'arduino/updateIndexes/updatePackageIndex',
|
||||
'Update Package Index'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const UPDATE_LIBRARY_INDEX: Command & { label: string } = {
|
||||
id: 'arduino-update-library-index',
|
||||
label: nls.localize(
|
||||
'arduino/updateIndexes/updateLibraryIndex',
|
||||
'Update Library Index'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { BoardUserField, CoreService, Port } from '../../common/protocol';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import { CoreService, Port } from '../../common/protocol';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
|
||||
import {
|
||||
Command,
|
||||
@@ -11,96 +11,36 @@ import {
|
||||
TabBarToolbarRegistry,
|
||||
CoreServiceContribution,
|
||||
} from './contribution';
|
||||
import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
|
||||
import { deepClone, DisposableCollection, nls } from '@theia/core/lib/common';
|
||||
import { deepClone, nls } from '@theia/core/lib/common';
|
||||
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
|
||||
import type { VerifySketchParams } from './verify-sketch';
|
||||
import { UserFields } from './user-fields';
|
||||
|
||||
@injectable()
|
||||
export class UploadSketch extends CoreServiceContribution {
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(UserFieldsDialog)
|
||||
private readonly userFieldsDialog: UserFieldsDialog;
|
||||
|
||||
private boardRequiresUserFields = false;
|
||||
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||
private readonly menuActionsDisposables = new DisposableCollection();
|
||||
|
||||
private readonly onDidChangeEmitter = new Emitter<void>();
|
||||
private readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
private uploadInProgress = false;
|
||||
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.boardsServiceProvider.onBoardsConfigChanged(async () => {
|
||||
const userFields =
|
||||
await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
this.boardRequiresUserFields = userFields.length > 0;
|
||||
this.registerMenus(this.menuRegistry);
|
||||
});
|
||||
}
|
||||
|
||||
private selectedFqbnAddress(): string {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const fqbn = boardsConfig.selectedBoard?.fqbn;
|
||||
if (!fqbn) {
|
||||
return '';
|
||||
}
|
||||
const address =
|
||||
boardsConfig.selectedBoard?.port?.address ||
|
||||
boardsConfig.selectedPort?.address;
|
||||
if (!address) {
|
||||
return '';
|
||||
}
|
||||
return fqbn + '|' + address;
|
||||
}
|
||||
@inject(UserFields)
|
||||
private readonly userFields: UserFields;
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
|
||||
execute: async () => {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (
|
||||
this.boardRequiresUserFields &&
|
||||
key &&
|
||||
!this.cachedUserFields.has(key)
|
||||
) {
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = (
|
||||
await this.boardsServiceProvider.selectedBoardUserFields()
|
||||
).map((f) => ({ ...f }));
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.cachedUserFields.set(key, result);
|
||||
if (await this.userFields.checkUserFieldsDialog()) {
|
||||
this.uploadSketch();
|
||||
}
|
||||
this.uploadSketch();
|
||||
},
|
||||
isEnabled: () => !this.uploadInProgress,
|
||||
});
|
||||
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
|
||||
execute: async () => {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (!key) {
|
||||
return;
|
||||
if (await this.userFields.checkUserFieldsDialog(true)) {
|
||||
this.uploadSketch();
|
||||
}
|
||||
|
||||
const cached = this.cachedUserFields.get(key);
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = (
|
||||
cached ?? (await this.boardsServiceProvider.selectedBoardUserFields())
|
||||
).map((f) => ({ ...f }));
|
||||
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
this.cachedUserFields.set(key, result);
|
||||
this.uploadSketch();
|
||||
},
|
||||
isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
|
||||
isEnabled: () => !this.uploadInProgress && this.userFields.isRequired(),
|
||||
});
|
||||
registry.registerCommand(
|
||||
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
|
||||
@@ -120,45 +60,20 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
this.menuActionsDisposables.dispose();
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/upload', 'Upload'),
|
||||
order: '1',
|
||||
})
|
||||
);
|
||||
if (this.boardRequiresUserFields) {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
order: '2',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
new PlaceholderMenuNode(
|
||||
ArduinoMenus.SKETCH__MAIN_GROUP,
|
||||
// commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
|
||||
UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label,
|
||||
{ order: '2' }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
this.menuActionsDisposables.push(
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||
label: nls.localize(
|
||||
'arduino/sketch/uploadUsingProgrammer',
|
||||
'Upload Using Programmer'
|
||||
),
|
||||
order: '3',
|
||||
})
|
||||
);
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id,
|
||||
label: nls.localize('arduino/sketch/upload', 'Upload'),
|
||||
order: '1',
|
||||
});
|
||||
|
||||
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
|
||||
commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id,
|
||||
label: nls.localize(
|
||||
'arduino/sketch/uploadUsingProgrammer',
|
||||
'Upload Using Programmer'
|
||||
),
|
||||
order: '3',
|
||||
});
|
||||
}
|
||||
|
||||
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||
@@ -215,18 +130,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This does not belong here.
|
||||
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
|
||||
if (
|
||||
uploadOptions.userFields.length === 0 &&
|
||||
this.boardRequiresUserFields
|
||||
) {
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/sketch/userFieldsNotFoundError',
|
||||
"Can't find user fields for connected board"
|
||||
)
|
||||
);
|
||||
if (!this.userFields.checkUserFieldsForUpload()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,6 +146,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
} catch (e) {
|
||||
this.userFields.notifyFailedWithError(e);
|
||||
this.handleError(e);
|
||||
} finally {
|
||||
this.uploadInProgress = false;
|
||||
@@ -258,7 +163,7 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return undefined;
|
||||
}
|
||||
const userFields = this.userFields();
|
||||
const userFields = this.userFields.getUserFields();
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
|
||||
await Promise.all([
|
||||
@@ -301,10 +206,6 @@ export class UploadSketch extends CoreServiceContribution {
|
||||
return port;
|
||||
}
|
||||
|
||||
private userFields(): BoardUserField[] {
|
||||
return this.cachedUserFields.get(this.selectedFqbnAddress()) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
|
||||
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
|
||||
@@ -328,7 +229,7 @@ export namespace UploadSketch {
|
||||
id: 'arduino-upload-with-configuration-sketch',
|
||||
label: nls.localize(
|
||||
'arduino/sketch/configureAndUpload',
|
||||
'Configure And Upload'
|
||||
'Configure and Upload'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
|
147
arduino-ide-extension/src/browser/contributions/user-fields.ts
Normal file
147
arduino-ide-extension/src/browser/contributions/user-fields.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { DisposableCollection, 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 { MenuModelRegistry, Contribution } from './contribution';
|
||||
import { UploadSketch } from './upload-sketch';
|
||||
|
||||
@injectable()
|
||||
export class UserFields extends Contribution {
|
||||
private boardRequiresUserFields = false;
|
||||
private userFieldsSet = false;
|
||||
private readonly cachedUserFields: Map<string, BoardUserField[]> = new Map();
|
||||
private readonly menuActionsDisposables = new DisposableCollection();
|
||||
|
||||
@inject(UserFieldsDialog)
|
||||
private readonly userFieldsDialog: UserFieldsDialog;
|
||||
|
||||
@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);
|
||||
});
|
||||
}
|
||||
|
||||
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' }
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private selectedFqbnAddress(): string | undefined {
|
||||
const { boardsConfig } = this.boardsServiceProvider;
|
||||
const fqbn = boardsConfig.selectedBoard?.fqbn;
|
||||
if (!fqbn) {
|
||||
return undefined;
|
||||
}
|
||||
const address =
|
||||
boardsConfig.selectedBoard?.port?.address ||
|
||||
boardsConfig.selectedPort?.address ||
|
||||
'';
|
||||
return fqbn + '|' + address;
|
||||
}
|
||||
|
||||
private async showUserFieldsDialog(
|
||||
key: string
|
||||
): Promise<BoardUserField[] | undefined> {
|
||||
const cached = this.cachedUserFields.get(key);
|
||||
// Deep clone the array of board fields to avoid editing the cached ones
|
||||
this.userFieldsDialog.value = cached
|
||||
? cached.slice()
|
||||
: await this.boardsServiceProvider.selectedBoardUserFields();
|
||||
const result = await this.userFieldsDialog.open();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.userFieldsSet = true;
|
||||
this.cachedUserFields.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async checkUserFieldsDialog(forceOpen = false): Promise<boolean> {
|
||||
const key = this.selectedFqbnAddress();
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
/*
|
||||
If the board requires to be configured with user fields, we want
|
||||
to show the user fields dialog, but only if they weren't already
|
||||
filled in or if they were filled in, but the previous upload failed.
|
||||
*/
|
||||
if (
|
||||
!forceOpen &&
|
||||
(!this.boardRequiresUserFields ||
|
||||
(this.cachedUserFields.has(key) && this.userFieldsSet))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const userFieldsFilledIn = Boolean(await this.showUserFieldsDialog(key));
|
||||
return userFieldsFilledIn;
|
||||
}
|
||||
|
||||
checkUserFieldsForUpload(): boolean {
|
||||
// TODO: This does not belong here.
|
||||
// IDE2 should not do any preliminary checks but let the CLI fail and then toast a user consumable error message.
|
||||
if (!this.boardRequiresUserFields || this.getUserFields().length > 0) {
|
||||
this.userFieldsSet = true;
|
||||
return true;
|
||||
}
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/sketch/userFieldsNotFoundError',
|
||||
"Can't find user fields for connected board"
|
||||
)
|
||||
);
|
||||
this.userFieldsSet = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
getUserFields(): BoardUserField[] {
|
||||
const fqbnAddress = this.selectedFqbnAddress();
|
||||
if (!fqbnAddress) {
|
||||
return [];
|
||||
}
|
||||
return this.cachedUserFields.get(fqbnAddress) ?? [];
|
||||
}
|
||||
|
||||
isRequired(): boolean {
|
||||
return this.boardRequiresUserFields;
|
||||
}
|
||||
|
||||
notifyFailedWithError(e: Error): void {
|
||||
if (this.boardRequiresUserFields && CoreError.UploadFailed.is(e)) {
|
||||
this.userFieldsSet = false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -171,6 +171,9 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
const firstButton = this.widget.node.querySelector('button');
|
||||
firstButton?.focus();
|
||||
|
||||
this.widget.busyCallback = this.busyCallback.bind(this);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
|
@@ -115,6 +115,8 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
|
||||
Widget.detach(this.widget);
|
||||
}
|
||||
Widget.attach(this.widget, this.contentNode);
|
||||
const firstButton = this.widget.node.querySelector('button');
|
||||
firstButton?.focus();
|
||||
this.widget.busyCallback = this.busyCallback.bind(this);
|
||||
super.onAfterAttach(msg);
|
||||
this.update();
|
||||
|
@@ -10,6 +10,7 @@ import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/fil
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
AdditionalUrls,
|
||||
CompilerWarnings,
|
||||
CompilerWarningLiterals,
|
||||
Network,
|
||||
ProxySettings,
|
||||
@@ -22,14 +23,22 @@ import {
|
||||
LanguageInfo,
|
||||
} from '@theia/core/lib/common/i18n/localization';
|
||||
import SettingsStepInput from './settings-step-input';
|
||||
import { InterfaceScale } from '../../contributions/interface-scale';
|
||||
|
||||
const maxScale = 280;
|
||||
const minScale = -60;
|
||||
const scaleStep = 20;
|
||||
const maxScale = InterfaceScale.ZoomLevel.toPercentage(
|
||||
InterfaceScale.ZoomLevel.MAX
|
||||
);
|
||||
const minScale = InterfaceScale.ZoomLevel.toPercentage(
|
||||
InterfaceScale.ZoomLevel.MIN
|
||||
);
|
||||
const scaleStep = InterfaceScale.ZoomLevel.Step.toPercentage(
|
||||
InterfaceScale.ZoomLevel.STEP
|
||||
);
|
||||
|
||||
const maxFontSize = InterfaceScale.FontSize.MAX;
|
||||
const minFontSize = InterfaceScale.FontSize.MIN;
|
||||
const fontSizeStep = InterfaceScale.FontSize.STEP;
|
||||
|
||||
const maxFontSize = 72;
|
||||
const minFontSize = 0;
|
||||
const fontSizeStep = 2;
|
||||
export class SettingsComponent extends React.Component<
|
||||
SettingsComponent.Props,
|
||||
SettingsComponent.State
|
||||
@@ -171,7 +180,8 @@ export class SettingsComponent extends React.Component<
|
||||
<div className="column">
|
||||
<div className="flex-line">
|
||||
<SettingsStepInput
|
||||
value={this.state.editorFontSize}
|
||||
key={`font-size-stepper-${String(this.state.editorFontSize)}`}
|
||||
initialValue={this.state.editorFontSize}
|
||||
setSettingsStateValue={this.setFontSize}
|
||||
step={fontSizeStep}
|
||||
maxValue={maxFontSize}
|
||||
@@ -190,13 +200,18 @@ export class SettingsComponent extends React.Component<
|
||||
</label>
|
||||
<div>
|
||||
<SettingsStepInput
|
||||
value={scalePercentage}
|
||||
key={`scale-stepper-${String(scalePercentage)}`}
|
||||
initialValue={scalePercentage}
|
||||
setSettingsStateValue={this.setInterfaceScale}
|
||||
step={scaleStep}
|
||||
maxValue={maxScale}
|
||||
minValue={minScale}
|
||||
unitOfMeasure="%"
|
||||
classNames={{ input: 'theia-input small with-margin' }}
|
||||
classNames={{
|
||||
input: 'theia-input small with-margin',
|
||||
buttonsContainer:
|
||||
'settings-step-input-buttons-container-perc',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -260,7 +275,7 @@ export class SettingsComponent extends React.Component<
|
||||
>
|
||||
{CompilerWarningLiterals.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
{CompilerWarnings.labelOf(value)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -398,10 +413,22 @@ export class SettingsComponent extends React.Component<
|
||||
</form>
|
||||
<div className="flex-line proxy-settings">
|
||||
<div className="column">
|
||||
<div className="flex-line">Host name:</div>
|
||||
<div className="flex-line">Port number:</div>
|
||||
<div className="flex-line">Username:</div>
|
||||
<div className="flex-line">Password:</div>
|
||||
<div className="flex-line">{`${nls.localize(
|
||||
'arduino/preferences/proxySettings/hostname',
|
||||
'Host name'
|
||||
)}:`}</div>
|
||||
<div className="flex-line">{`${nls.localize(
|
||||
'arduino/preferences/proxySettings/port',
|
||||
'Port number'
|
||||
)}:`}</div>
|
||||
<div className="flex-line">{`${nls.localize(
|
||||
'arduino/preferences/proxySettings/username',
|
||||
'Username'
|
||||
)}:`}</div>
|
||||
<div className="flex-line">{`${nls.localize(
|
||||
'arduino/preferences/proxySettings/password',
|
||||
'Password'
|
||||
)}:`}</div>
|
||||
</div>
|
||||
<div className="column stretch">
|
||||
<div className="flex-line">
|
||||
@@ -502,6 +529,7 @@ export class SettingsComponent extends React.Component<
|
||||
canSelectFiles: false,
|
||||
canSelectMany: false,
|
||||
canSelectFolders: true,
|
||||
modal: true,
|
||||
});
|
||||
if (uri) {
|
||||
const sketchbookPath = await this.props.fileService.fsPath(uri);
|
||||
@@ -540,8 +568,7 @@ export class SettingsComponent extends React.Component<
|
||||
};
|
||||
|
||||
private setInterfaceScale = (percentage: number) => {
|
||||
const interfaceScale = (percentage - 100) / 20;
|
||||
|
||||
const interfaceScale = InterfaceScale.ZoomLevel.fromPercentage(percentage);
|
||||
this.setState({ interfaceScale });
|
||||
};
|
||||
|
||||
|
@@ -155,7 +155,6 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
|
||||
|
||||
this.textArea = document.createElement('textarea');
|
||||
this.textArea.className = 'theia-input';
|
||||
this.textArea.setAttribute('style', 'flex: 0;');
|
||||
this.textArea.value = urls
|
||||
.filter((url) => url.trim())
|
||||
.filter((url) => !!url)
|
||||
@@ -181,10 +180,10 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
|
||||
);
|
||||
this.contentNode.appendChild(anchor);
|
||||
|
||||
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
|
||||
this.appendCloseButton(
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel')
|
||||
);
|
||||
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
|
||||
}
|
||||
|
||||
get value(): string[] {
|
||||
|
@@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface SettingsStepInputProps {
|
||||
value: number;
|
||||
initialValue: number;
|
||||
setSettingsStateValue: (value: number) => void;
|
||||
step: number;
|
||||
maxValue: number;
|
||||
@@ -15,7 +15,7 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
|
||||
props: SettingsStepInputProps
|
||||
) => {
|
||||
const {
|
||||
value,
|
||||
initialValue,
|
||||
setSettingsStateValue,
|
||||
step,
|
||||
maxValue,
|
||||
@@ -24,18 +24,35 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
|
||||
classNames,
|
||||
} = props;
|
||||
|
||||
const [valueState, setValueState] = React.useState<{
|
||||
currentValue: number;
|
||||
isEmptyString: boolean;
|
||||
}>({
|
||||
currentValue: initialValue,
|
||||
isEmptyString: false,
|
||||
});
|
||||
const { currentValue, isEmptyString } = valueState;
|
||||
|
||||
const clamp = (value: number, min: number, max: number): number => {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
};
|
||||
|
||||
const resetToInitialState = (): void => {
|
||||
setValueState({
|
||||
currentValue: initialValue,
|
||||
isEmptyString: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onStep = (
|
||||
roundingOperation: 'ceil' | 'floor',
|
||||
stepOperation: (a: number, b: number) => number
|
||||
): void => {
|
||||
const valueRoundedToScale = Math[roundingOperation](value / step) * step;
|
||||
const valueRoundedToScale =
|
||||
Math[roundingOperation](currentValue / step) * step;
|
||||
const calculatedValue =
|
||||
valueRoundedToScale === value
|
||||
? stepOperation(value, step)
|
||||
valueRoundedToScale === currentValue
|
||||
? stepOperation(currentValue, step)
|
||||
: valueRoundedToScale;
|
||||
const newValue = clamp(calculatedValue, minValue, maxValue);
|
||||
|
||||
@@ -52,33 +69,53 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
|
||||
|
||||
const onUserInput = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const { value: eventValue } = event.target;
|
||||
|
||||
if (eventValue === '') {
|
||||
setSettingsStateValue(0);
|
||||
}
|
||||
|
||||
const number = Number(eventValue);
|
||||
|
||||
if (!isNaN(number) && number !== value) {
|
||||
const newValue = clamp(number, minValue, maxValue);
|
||||
|
||||
setSettingsStateValue(newValue);
|
||||
}
|
||||
setValueState({
|
||||
currentValue: Number(eventValue),
|
||||
isEmptyString: eventValue === '',
|
||||
});
|
||||
};
|
||||
|
||||
const upDisabled = value >= maxValue;
|
||||
const downDisabled = value <= minValue;
|
||||
/* Prevent the user from entering invalid values */
|
||||
const onBlur = (event: React.FocusEvent): void => {
|
||||
if (
|
||||
(currentValue === initialValue && !isEmptyString) ||
|
||||
event.currentTarget.contains(event.relatedTarget as Node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clampedValue = clamp(currentValue, minValue, maxValue);
|
||||
if (clampedValue === initialValue || isNaN(currentValue) || isEmptyString) {
|
||||
resetToInitialState();
|
||||
return;
|
||||
}
|
||||
|
||||
setSettingsStateValue(clampedValue);
|
||||
};
|
||||
|
||||
const valueIsNotWithinRange =
|
||||
currentValue < minValue || currentValue > maxValue;
|
||||
const isDisabledException =
|
||||
valueIsNotWithinRange || isEmptyString || isNaN(currentValue);
|
||||
|
||||
const upDisabled = isDisabledException || currentValue >= maxValue;
|
||||
const downDisabled = isDisabledException || currentValue <= minValue;
|
||||
|
||||
return (
|
||||
<div className="settings-step-input-container">
|
||||
<div className="settings-step-input-container" onBlur={onBlur}>
|
||||
<input
|
||||
className={classnames('settings-step-input-element', classNames?.input)}
|
||||
value={value.toString()}
|
||||
value={isEmptyString ? '' : String(currentValue)}
|
||||
onChange={onUserInput}
|
||||
type="number"
|
||||
pattern="[0-9]+"
|
||||
/>
|
||||
<div className="settings-step-input-buttons-container">
|
||||
<div
|
||||
className={classnames(
|
||||
'settings-step-input-buttons-container',
|
||||
classNames?.buttonsContainer
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="settings-step-input-button settings-step-input-up-button"
|
||||
disabled={upDisabled}
|
||||
|
@@ -16,9 +16,9 @@ export const UserFieldsComponent = ({
|
||||
const [boardUserFields, setBoardUserFields] = React.useState<
|
||||
BoardUserField[]
|
||||
>(initialBoardUserFields);
|
||||
|
||||
const [uploadButtonDisabled, setUploadButtonDisabled] =
|
||||
React.useState<boolean>(true);
|
||||
const firstInputElement = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setBoardUserFields(initialBoardUserFields);
|
||||
@@ -48,7 +48,10 @@ export const UserFieldsComponent = ({
|
||||
React.useEffect(() => {
|
||||
updateUserFields(boardUserFields);
|
||||
setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields));
|
||||
}, [boardUserFields]);
|
||||
if (firstInputElement.current) {
|
||||
firstInputElement.current.focus();
|
||||
}
|
||||
}, [boardUserFields, updateUserFields]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -71,6 +74,7 @@ export const UserFieldsComponent = ({
|
||||
field.label
|
||||
)}
|
||||
onChange={updateUserField(index)}
|
||||
ref={index === 0 ? firstInputElement : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -13,7 +13,7 @@ import { BoardUserField } from '../../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export class UserFieldsDialogWidget extends ReactWidget {
|
||||
protected _currentUserFields: BoardUserField[] = [];
|
||||
private _currentUserFields: BoardUserField[] = [];
|
||||
|
||||
constructor(private cancel: () => void, private accept: () => Promise<void>) {
|
||||
super();
|
||||
@@ -34,7 +34,7 @@ export class UserFieldsDialogWidget extends ReactWidget {
|
||||
});
|
||||
}
|
||||
|
||||
protected setUserFields(userFields: BoardUserField[]): void {
|
||||
private setUserFields(userFields: BoardUserField[]): void {
|
||||
this._currentUserFields = userFields;
|
||||
}
|
||||
|
||||
|
@@ -119,20 +119,16 @@ export class LibraryListWidget extends ListWidget<
|
||||
message.appendChild(question);
|
||||
const result = await new MessageBoxDialog({
|
||||
title: nls.localize(
|
||||
'arduino/library/dependenciesForLibrary',
|
||||
'Dependencies for library {0}:{1}',
|
||||
item.name,
|
||||
version
|
||||
'arduino/library/installLibraryDependencies',
|
||||
'Install library dependencies'
|
||||
),
|
||||
message,
|
||||
buttons: [
|
||||
nls.localize('arduino/library/installAll', 'Install all'),
|
||||
nls.localize(
|
||||
'arduino/library/installOnly',
|
||||
'Install {0} only',
|
||||
item.name
|
||||
'arduino/library/installWithoutDependencies',
|
||||
'Install without dependencies'
|
||||
),
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel'),
|
||||
nls.localize('arduino/library/installAll', 'Install All'),
|
||||
],
|
||||
maxWidth: 740, // Aligned with `settings-dialog.css`.
|
||||
}).open();
|
||||
@@ -140,11 +136,11 @@ export class LibraryListWidget extends ListWidget<
|
||||
if (result) {
|
||||
const { response } = result;
|
||||
if (response === 0) {
|
||||
// All
|
||||
installDependencies = true;
|
||||
} else if (response === 1) {
|
||||
// Current only
|
||||
installDependencies = false;
|
||||
} else if (response === 1) {
|
||||
// All
|
||||
installDependencies = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -201,7 +197,13 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
|
||||
options.buttons || [nls.localize('vscode/issueMainService/ok', 'OK')]
|
||||
).forEach((text, index) => {
|
||||
const button = this.createButton(text);
|
||||
button.classList.add(index === 0 ? 'main' : 'secondary');
|
||||
const isPrimaryButton =
|
||||
index === (options.buttons ? options.buttons.length - 1 : 0);
|
||||
button.title = text;
|
||||
button.classList.add(
|
||||
isPrimaryButton ? 'main' : 'secondary',
|
||||
'message-box-dialog-button'
|
||||
);
|
||||
this.controlPanel.appendChild(button);
|
||||
this.toDisposeOnDetach.push(
|
||||
addEventListener(button, 'click', () => {
|
||||
|
@@ -1,16 +1,17 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { MenuModelRegistry } from '@theia/core';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
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 { URI } from '../contributions/contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetFrontendContribution
|
||||
extends AbstractViewContribution<LibraryListWidget>
|
||||
implements FrontendApplicationContribution
|
||||
{
|
||||
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
LibraryPackage,
|
||||
LibrarySearch
|
||||
> {
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: LibraryListWidget.WIDGET_ID,
|
||||
@@ -24,10 +25,6 @@ export class LibraryListWidgetFrontendContribution
|
||||
});
|
||||
}
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
}
|
||||
|
||||
override registerMenus(menus: MenuModelRegistry): void {
|
||||
if (this.toggleCommand) {
|
||||
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
|
||||
@@ -40,4 +37,17 @@ export class LibraryListWidgetFrontendContribution
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected canParse(uri: URI): boolean {
|
||||
try {
|
||||
LibrarySearch.UriParser.parse(uri);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): LibrarySearch | undefined {
|
||||
return LibrarySearch.UriParser.parse(uri);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
@@ -153,7 +152,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]);
|
||||
}
|
||||
|
@@ -8,6 +8,9 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import {
|
||||
IndexUpdateDidCompleteParams,
|
||||
IndexUpdateDidFailParams,
|
||||
IndexUpdateWillStartParams,
|
||||
NotificationServiceClient,
|
||||
NotificationServiceServer,
|
||||
} from '../common/protocol/notification-service';
|
||||
@@ -29,48 +32,48 @@ export class NotificationCenter
|
||||
implements NotificationServiceClient, FrontendApplicationContribution
|
||||
{
|
||||
@inject(NotificationServiceServer)
|
||||
protected readonly server: JsonRpcProxy<NotificationServiceServer>;
|
||||
private readonly server: JsonRpcProxy<NotificationServiceServer>;
|
||||
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
protected readonly indexDidUpdateEmitter = new Emitter<string>();
|
||||
protected readonly indexWillUpdateEmitter = new Emitter<string>();
|
||||
protected readonly indexUpdateDidProgressEmitter =
|
||||
private readonly indexUpdateDidCompleteEmitter =
|
||||
new Emitter<IndexUpdateDidCompleteParams>();
|
||||
private readonly indexUpdateWillStartEmitter =
|
||||
new Emitter<IndexUpdateWillStartParams>();
|
||||
private readonly indexUpdateDidProgressEmitter =
|
||||
new Emitter<ProgressMessage>();
|
||||
protected readonly indexUpdateDidFailEmitter = new Emitter<{
|
||||
progressId: string;
|
||||
message: string;
|
||||
}>();
|
||||
protected readonly daemonDidStartEmitter = new Emitter<string>();
|
||||
protected readonly daemonDidStopEmitter = new Emitter<void>();
|
||||
protected readonly configDidChangeEmitter = new Emitter<{
|
||||
private readonly indexUpdateDidFailEmitter =
|
||||
new Emitter<IndexUpdateDidFailParams>();
|
||||
private readonly daemonDidStartEmitter = new Emitter<string>();
|
||||
private readonly daemonDidStopEmitter = new Emitter<void>();
|
||||
private readonly configDidChangeEmitter = new Emitter<{
|
||||
config: Config | undefined;
|
||||
}>();
|
||||
protected readonly platformDidInstallEmitter = new Emitter<{
|
||||
private readonly platformDidInstallEmitter = new Emitter<{
|
||||
item: BoardsPackage;
|
||||
}>();
|
||||
protected readonly platformDidUninstallEmitter = new Emitter<{
|
||||
private readonly platformDidUninstallEmitter = new Emitter<{
|
||||
item: BoardsPackage;
|
||||
}>();
|
||||
protected readonly libraryDidInstallEmitter = new Emitter<{
|
||||
private readonly libraryDidInstallEmitter = new Emitter<{
|
||||
item: LibraryPackage;
|
||||
}>();
|
||||
protected readonly libraryDidUninstallEmitter = new Emitter<{
|
||||
private readonly libraryDidUninstallEmitter = new Emitter<{
|
||||
item: LibraryPackage;
|
||||
}>();
|
||||
protected readonly attachedBoardsDidChangeEmitter =
|
||||
private readonly attachedBoardsDidChangeEmitter =
|
||||
new Emitter<AttachedBoardsChangeEvent>();
|
||||
protected readonly recentSketchesChangedEmitter = new Emitter<{
|
||||
private readonly recentSketchesChangedEmitter = new Emitter<{
|
||||
sketches: Sketch[];
|
||||
}>();
|
||||
private readonly onAppStateDidChangeEmitter =
|
||||
new Emitter<FrontendApplicationState>();
|
||||
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.indexWillUpdateEmitter,
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.indexUpdateWillStartEmitter,
|
||||
this.indexUpdateDidProgressEmitter,
|
||||
this.indexDidUpdateEmitter,
|
||||
this.indexUpdateDidCompleteEmitter,
|
||||
this.indexUpdateDidFailEmitter,
|
||||
this.daemonDidStartEmitter,
|
||||
this.daemonDidStopEmitter,
|
||||
@@ -82,8 +85,8 @@ export class NotificationCenter
|
||||
this.attachedBoardsDidChangeEmitter
|
||||
);
|
||||
|
||||
readonly onIndexDidUpdate = this.indexDidUpdateEmitter.event;
|
||||
readonly onIndexWillUpdate = this.indexDidUpdateEmitter.event;
|
||||
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event;
|
||||
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event;
|
||||
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
|
||||
readonly onIndexUpdateDidFail = this.indexUpdateDidFailEmitter.event;
|
||||
readonly onDaemonDidStart = this.daemonDidStartEmitter.event;
|
||||
@@ -112,26 +115,20 @@ export class NotificationCenter
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
notifyIndexWillUpdate(progressId: string): void {
|
||||
this.indexWillUpdateEmitter.fire(progressId);
|
||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void {
|
||||
this.indexUpdateWillStartEmitter.fire(params);
|
||||
}
|
||||
|
||||
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
|
||||
this.indexUpdateDidProgressEmitter.fire(progressMessage);
|
||||
}
|
||||
|
||||
notifyIndexDidUpdate(progressId: string): void {
|
||||
this.indexDidUpdateEmitter.fire(progressId);
|
||||
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void {
|
||||
this.indexUpdateDidCompleteEmitter.fire(params);
|
||||
}
|
||||
|
||||
notifyIndexUpdateDidFail({
|
||||
progressId,
|
||||
message,
|
||||
}: {
|
||||
progressId: string;
|
||||
message: string;
|
||||
}): void {
|
||||
this.indexUpdateDidFailEmitter.fire({ progressId, message });
|
||||
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void {
|
||||
this.indexUpdateDidFailEmitter.fire(params);
|
||||
}
|
||||
|
||||
notifyDaemonDidStart(port: string): void {
|
||||
|
@@ -6,6 +6,53 @@ import { BoardsServiceProvider } from '../../boards/boards-service-provider';
|
||||
import { MonitorModel } from '../../monitor-model';
|
||||
import { Unknown } from '../../../common/nls';
|
||||
|
||||
class HistoryList {
|
||||
private readonly items: string[] = [];
|
||||
private index = -1;
|
||||
|
||||
constructor(private readonly size = 100) {}
|
||||
|
||||
push(val: string): void {
|
||||
if (val !== this.items[this.items.length - 1]) {
|
||||
this.items.push(val);
|
||||
}
|
||||
while (this.items.length > this.size) {
|
||||
this.items.shift();
|
||||
}
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
previous(): string {
|
||||
if (this.index === -1) {
|
||||
this.index = this.items.length - 1;
|
||||
return this.items[this.index];
|
||||
}
|
||||
if (this.hasPrevious) {
|
||||
return this.items[--this.index];
|
||||
}
|
||||
return this.items[this.index];
|
||||
}
|
||||
|
||||
private get hasPrevious(): boolean {
|
||||
return this.index >= 1;
|
||||
}
|
||||
|
||||
next(): string {
|
||||
if (this.index === this.items.length - 1) {
|
||||
this.index = -1;
|
||||
return '';
|
||||
}
|
||||
if (this.hasNext) {
|
||||
return this.items[++this.index];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private get hasNext(): boolean {
|
||||
return this.index >= 0 && this.index !== this.items.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SerialMonitorSendInput {
|
||||
export interface Props {
|
||||
readonly boardsServiceProvider: BoardsServiceProvider;
|
||||
@@ -16,6 +63,7 @@ export namespace SerialMonitorSendInput {
|
||||
export interface State {
|
||||
text: string;
|
||||
connected: boolean;
|
||||
history: HistoryList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +75,7 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
|
||||
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
|
||||
super(props);
|
||||
this.state = { text: '', connected: true };
|
||||
this.state = { text: '', connected: true, history: new HistoryList() };
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onSend = this.onSend.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
@@ -90,7 +138,7 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
protected setRef = (element: HTMLElement | null) => {
|
||||
protected setRef = (element: HTMLElement | null): void => {
|
||||
if (this.props.resolveFocus) {
|
||||
this.props.resolveFocus(element || undefined);
|
||||
}
|
||||
@@ -110,7 +158,17 @@ export class SerialMonitorSendInput extends React.Component<
|
||||
if (keyCode) {
|
||||
const { key } = keyCode;
|
||||
if (key === Key.ENTER) {
|
||||
const { text } = this.state;
|
||||
this.onSend();
|
||||
if (text) {
|
||||
this.state.history.push(text);
|
||||
}
|
||||
} else if (key === Key.ARROW_UP) {
|
||||
this.setState({ text: this.state.history.previous() });
|
||||
} else if (key === Key.ARROW_DOWN) {
|
||||
this.setState({ text: this.state.history.next() });
|
||||
} else if (key === Key.ESCAPE) {
|
||||
this.setState({ text: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ import {
|
||||
CLOSE_PLOTTER_WINDOW,
|
||||
SHOW_PLOTTER_WINDOW,
|
||||
} from '../../../common/ipc-communication';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
const queryString = require('query-string');
|
||||
|
||||
@@ -107,7 +108,12 @@ export class PlotterFrontendContribution extends Contribution {
|
||||
if (wsPort) {
|
||||
this.open(wsPort);
|
||||
} else {
|
||||
this.messageService.error(`Couldn't open serial plotter`);
|
||||
this.messageService.error(
|
||||
nls.localize(
|
||||
'arduino/contributions/plotter/couldNotOpen',
|
||||
"Couldn't open serial plotter"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,11 @@
|
||||
#select-board-dialog-container > .dialogBlock {
|
||||
width: 640px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
div#select-board-dialog {
|
||||
margin: 5px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div#select-board-dialog .selectBoardContainer {
|
||||
@@ -7,12 +13,17 @@ div#select-board-dialog .selectBoardContainer {
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.select-board-dialog .head {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.dialogContent.select-board-dialog {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
div.dialogContent.select-board-dialog > div.head .title {
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.02em;
|
||||
@@ -63,6 +74,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .left.container .content {
|
||||
@@ -131,6 +143,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
|
||||
#select-board-dialog .selectBoardContainer .list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#select-board-dialog .selectBoardContainer .ports.list {
|
||||
@@ -282,3 +295,11 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#select-board-dialog .no-result {
|
||||
text-transform: uppercase;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
padding: 10px 5px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
@@ -9,7 +9,8 @@
|
||||
total = padding + margin = 96px
|
||||
*/
|
||||
max-width: calc(100% - 96px) !important;
|
||||
min-width: unset;
|
||||
|
||||
min-width: 424px;
|
||||
max-height: 560px;
|
||||
padding: 0 28px;
|
||||
}
|
||||
@@ -54,6 +55,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogControl .spinner,
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner {
|
||||
background: var(--theia-icon-loading) center center no-repeat;
|
||||
animation: theia-spin 1.25s linear infinite;
|
||||
@@ -62,11 +64,11 @@
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow:first-child {
|
||||
margin-top: 0px;
|
||||
margin-top: 0px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.fl1{
|
||||
.fl1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -79,9 +81,13 @@
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-height: 560px) {
|
||||
.p-Widget.dialogOverlay .dialogBlock {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .error.progress {
|
||||
color: var(--theia-button-background);
|
||||
align-self: center;
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
}
|
||||
.firmware-uploader-dialog .arduino-select__control {
|
||||
height: 31px;
|
||||
background: var(--theia-menubar-selectionBackground) !important;
|
||||
background: var(--theia-input-background) !important;
|
||||
}
|
||||
|
||||
.firmware-uploader-dialog .dialogRow > button{
|
||||
@@ -28,4 +28,4 @@
|
||||
|
||||
.firmware-uploader-dialog .status-icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@@ -55,7 +55,8 @@
|
||||
/* Makes the sidepanel a bit wider when opening the widget */
|
||||
.p-DockPanel-widget {
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
min-height: 20px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
/* Overrule the default Theia CSS button styles. */
|
||||
@@ -108,6 +109,13 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
|
||||
background-color: var(--theia-secondaryButton-background);
|
||||
}
|
||||
|
||||
button.theia-button.message-box-dialog-button {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* To make the progress-bar slightly thicker, and use the color from the status bar */
|
||||
.theia-progress-bar-container {
|
||||
width: 100%;
|
||||
@@ -136,6 +144,9 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* High Contrast Theme rules */
|
||||
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
|
||||
|
@@ -111,13 +111,13 @@
|
||||
font-weight: bold;
|
||||
max-height: calc(1em + 4px);
|
||||
color: var(--theia-button-foreground);
|
||||
content: 'INSTALLED';
|
||||
content: attr(install);
|
||||
}
|
||||
|
||||
.component-list-item .header .installed:hover:before {
|
||||
background-color: var(--theia-button-foreground);
|
||||
color: var(--theia-button-background);
|
||||
content: 'UNINSTALL';
|
||||
content: attr(uninstall);
|
||||
}
|
||||
|
||||
.component-list-item[min-width~="170px"] .footer {
|
||||
@@ -131,7 +131,7 @@
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.component-list-item:hover .footer > * {
|
||||
.component-list-item .footer > * {
|
||||
display: inline-block;
|
||||
margin: 5px 0px 0px 10px;
|
||||
}
|
||||
@@ -160,4 +160,4 @@
|
||||
|
||||
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before {
|
||||
border: 1px solid var(--theia-button-border);
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,16 @@
|
||||
background: var(--theia-editorGroupHeader-tabsBackground);
|
||||
}
|
||||
|
||||
/* Avoid the Intellisense widget may be cover by the bottom panel partially.
|
||||
TODO: This issue may be resolved after monaco-editor upgrade */
|
||||
#theia-main-content-panel {
|
||||
z-index: auto
|
||||
}
|
||||
|
||||
#theia-main-content-panel div[id^="code-editor-opener"] {
|
||||
z-index: auto;
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item.arduino-tool-item {
|
||||
margin-left: 0;
|
||||
}
|
||||
@@ -87,8 +97,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--theia-titleBar-activeBackground);
|
||||
|
||||
background-color: var(--theia-titleBar-activeBackground);
|
||||
}
|
||||
|
||||
#arduino-toolbar-container {
|
||||
@@ -243,3 +252,10 @@
|
||||
outline: 1px solid var(--theia-contrastBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.markdown-hover:first-child p {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.monaco-hover .hover-row.markdown-hover:first-child .monaco-tokenized-source {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
@@ -14,6 +14,10 @@
|
||||
font-family: monospace
|
||||
}
|
||||
|
||||
.serial-monitor-messages pre {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.serial-monitor .head {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
|
@@ -88,9 +88,13 @@
|
||||
}
|
||||
|
||||
.additional-urls-dialog textarea {
|
||||
width: 100%;
|
||||
resize: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent.additional-urls-dialog {
|
||||
display: block;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: 0 1px;
|
||||
margin: 0 -1px;
|
||||
}
|
||||
|
@@ -2,17 +2,17 @@
|
||||
position: relative
|
||||
}
|
||||
|
||||
.settings-step-input-element::-webkit-inner-spin-button,
|
||||
.settings-step-input-element::-webkit-inner-spin-button,
|
||||
.settings-step-input-element::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-step-input-buttons-container {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
right: 0px;
|
||||
top: 50%;
|
||||
transform: translate(0px, -50%);
|
||||
height: calc(100% - 4px);
|
||||
@@ -21,7 +21,11 @@
|
||||
background: var(--theia-input-background);
|
||||
}
|
||||
|
||||
.settings-step-input-container:hover > .settings-step-input-buttons-container {
|
||||
.settings-step-input-buttons-container-perc {
|
||||
right: 14px;
|
||||
}
|
||||
|
||||
.settings-step-input-container:hover>.settings-step-input-buttons-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -43,4 +47,4 @@
|
||||
|
||||
.settings-step-input-button:hover {
|
||||
background: rgba(128, 128, 128, 0.8);
|
||||
}
|
||||
}
|
@@ -33,6 +33,22 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sketchbook-trees-container .create-new {
|
||||
min-height: 58px;
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
/*
|
||||
By default, theia-button has a left-margin. IDE2 does not need the left margin
|
||||
for the _New Remote? Sketch_. Otherwise, the button does not fit the default
|
||||
widget width.
|
||||
*/
|
||||
.sketchbook-trees-container .create-new .theia-button {
|
||||
margin-left: unset;
|
||||
}
|
||||
|
||||
.sketchbook-tree__opts {
|
||||
background-color: var(--theia-foreground);
|
||||
-webkit-mask: url(./sketchbook-opts-icon.svg);
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
|
||||
import { duration } from '../../../common/decorators';
|
||||
|
||||
export class AboutDialog extends TheiaAboutDialog {
|
||||
@duration({ name: 'theia-about#init' })
|
||||
protected override async init(): Promise<void> {
|
||||
// NOOP
|
||||
// IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time.
|
||||
|
@@ -1,44 +0,0 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import {
|
||||
SearchInWorkspaceFileNode,
|
||||
SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget,
|
||||
} from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
|
||||
import { MEMORY_TEXT } from '@theia/core/lib/common/resource';
|
||||
|
||||
/**
|
||||
* Workaround for https://github.com/eclipse-theia/theia/pull/9192/.
|
||||
*/
|
||||
@injectable()
|
||||
export class SearchInWorkspaceResultTreeWidget extends TheiaSearchInWorkspaceResultTreeWidget {
|
||||
protected override async createReplacePreview(
|
||||
node: SearchInWorkspaceFileNode
|
||||
): Promise<URI> {
|
||||
const fileUri = new URI(node.fileUri).withScheme('file');
|
||||
const openedEditor = this.editorManager.all.find(
|
||||
({ editor }) => editor.uri.toString() === fileUri.toString()
|
||||
);
|
||||
let content: string;
|
||||
if (openedEditor) {
|
||||
content = openedEditor.editor.document.getText();
|
||||
} else {
|
||||
const resource = await this.fileResourceResolver.resolve(fileUri);
|
||||
content = await resource.readContents();
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
node.children.map((l) => {
|
||||
const leftPositionedNodes = node.children.filter(
|
||||
(rl) => rl.line === l.line && rl.character < l.character
|
||||
);
|
||||
const diff =
|
||||
(this._replaceTerm.length - this.searchTerm.length) *
|
||||
leftPositionedNodes.length;
|
||||
const start = lines[l.line - 1].substr(0, l.character - 1 + diff);
|
||||
const end = lines[l.line - 1].substr(l.character - 1 + diff + l.length);
|
||||
lines[l.line - 1] = start + this._replaceTerm + end;
|
||||
});
|
||||
|
||||
return fileUri.withScheme(MEMORY_TEXT).withQuery(lines.join('\n'));
|
||||
}
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
import { injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { Key, KeyCode } from '@theia/core/lib/browser';
|
||||
import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-widget';
|
||||
|
||||
/**
|
||||
* Workaround for https://github.com/eclipse-theia/theia/pull/9183.
|
||||
*/
|
||||
@injectable()
|
||||
export class SearchInWorkspaceWidget extends TheiaSearchInWorkspaceWidget {
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.title.iconClass = 'fa fa-arduino-search';
|
||||
}
|
||||
|
||||
protected override renderGlobField(kind: 'include' | 'exclude'): React.ReactNode {
|
||||
const currentValue = this.searchInWorkspaceOptions[kind];
|
||||
const value = (currentValue && currentValue.join(', ')) || '';
|
||||
return (
|
||||
<div className="glob-field">
|
||||
<div className="label">{'files to ' + kind}</div>
|
||||
<input
|
||||
className="theia-input"
|
||||
type="text"
|
||||
size={1}
|
||||
defaultValue={value}
|
||||
id={kind + '-glob-field'}
|
||||
onKeyUp={(e) => {
|
||||
if (e.target) {
|
||||
const targetValue = (e.target as HTMLInputElement).value || '';
|
||||
let shouldSearch =
|
||||
Key.ENTER.keyCode ===
|
||||
KeyCode.createKeyCode(e.nativeEvent).key?.keyCode;
|
||||
const currentOptions = (this.searchInWorkspaceOptions[kind] || [])
|
||||
.slice()
|
||||
.map((s) => s.trim())
|
||||
.sort();
|
||||
const candidateOptions = this.splitOnComma(targetValue)
|
||||
.map((s) => s.trim())
|
||||
.sort();
|
||||
const sameAs = (left: string[], right: string[]) => {
|
||||
if (left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < left.length; i++) {
|
||||
if (left[i] !== right[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!sameAs(currentOptions, candidateOptions)) {
|
||||
this.searchInWorkspaceOptions[kind] =
|
||||
this.splitOnComma(targetValue);
|
||||
shouldSearch = true;
|
||||
}
|
||||
if (shouldSearch) {
|
||||
this.resultTreeWidget.search(
|
||||
this.searchTerm,
|
||||
this.searchInWorkspaceOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onFocus={
|
||||
kind === 'include'
|
||||
? this.handleFocusIncludesInputBox
|
||||
: this.handleFocusExcludesInputBox
|
||||
}
|
||||
onBlur={
|
||||
kind === 'include'
|
||||
? this.handleBlurIncludesInputBox
|
||||
: this.handleBlurExcludesInputBox
|
||||
}
|
||||
></input>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -17,7 +17,6 @@ import {
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../../common/protocol/sketches-service-client-impl';
|
||||
import { SaveAsSketch } from '../../contributions/save-as-sketch';
|
||||
import { SingleTextInputDialog } from '@theia/core/lib/browser';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
@injectable()
|
||||
@@ -161,20 +160,26 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
|
||||
return;
|
||||
}
|
||||
const initialValue = uri.path.base;
|
||||
const dialog = new SingleTextInputDialog({
|
||||
title: nls.localize('theia/workspace/newFileName', 'New name for file'),
|
||||
initialValue,
|
||||
initialSelectionRange: {
|
||||
start: 0,
|
||||
end: uri.path.name.length,
|
||||
const parentUri = parent.resource;
|
||||
|
||||
const dialog = new WorkspaceInputDialog(
|
||||
{
|
||||
title: nls.localize('theia/workspace/newFileName', 'New name for file'),
|
||||
initialValue,
|
||||
parentUri,
|
||||
initialSelectionRange: {
|
||||
start: 0,
|
||||
end: uri.path.name.length,
|
||||
},
|
||||
validate: (name, mode) => {
|
||||
if (initialValue === name && mode === 'preview') {
|
||||
return false;
|
||||
}
|
||||
return this.validateFileName(name, parent, false);
|
||||
},
|
||||
},
|
||||
validate: (name, mode) => {
|
||||
if (initialValue === name && mode === 'preview') {
|
||||
return false;
|
||||
}
|
||||
return this.validateFileName(name, parent, false);
|
||||
},
|
||||
});
|
||||
this.labelProvider
|
||||
);
|
||||
const newName = await dialog.open();
|
||||
const newNameWithExt = this.maybeAppendInoExt(newName);
|
||||
if (newNameWithExt) {
|
||||
|
@@ -14,9 +14,11 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
|
||||
constructor(
|
||||
@inject(WorkspaceInputDialogProps)
|
||||
protected override readonly props: WorkspaceInputDialogProps,
|
||||
@inject(LabelProvider) protected override readonly labelProvider: LabelProvider
|
||||
@inject(LabelProvider)
|
||||
protected override readonly labelProvider: LabelProvider
|
||||
) {
|
||||
super(props, labelProvider);
|
||||
this.node.classList.add('workspace-input-dialog');
|
||||
this.appendCloseButton(
|
||||
nls.localize('vscode/issueMainService/cancel', 'Cancel')
|
||||
);
|
||||
@@ -41,4 +43,14 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
|
||||
this.errorMessageNode.innerText = DialogError.getMessage(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected override appendCloseButton(text: string): HTMLButtonElement {
|
||||
this.closeButton = this.createButton(text);
|
||||
this.controlPanel.insertBefore(
|
||||
this.closeButton,
|
||||
this.controlPanel.lastChild
|
||||
);
|
||||
this.closeButton.classList.add('secondary');
|
||||
return this.closeButton;
|
||||
}
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import {
|
||||
import {
|
||||
SketchesService,
|
||||
Sketch,
|
||||
SketchesError,
|
||||
} from '../../../common/protocol/sketches-service';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import {
|
||||
@@ -38,6 +39,7 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
private readonly providers: ContributionProvider<StartupTaskProvider>;
|
||||
|
||||
private version?: string;
|
||||
private _workspaceError: Error | undefined;
|
||||
|
||||
async onStart(application: FrontendApplication): Promise<void> {
|
||||
const info = await this.applicationServer.getApplicationInfo();
|
||||
@@ -51,6 +53,10 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
this.onCurrentWidgetChange({ newValue, oldValue: null });
|
||||
}
|
||||
|
||||
get workspaceError(): Error | undefined {
|
||||
return this._workspaceError;
|
||||
}
|
||||
|
||||
protected override async toFileStat(
|
||||
uri: string | URI | undefined
|
||||
): Promise<FileStat | undefined> {
|
||||
@@ -59,6 +65,31 @@ export class WorkspaceService extends TheiaWorkspaceService {
|
||||
const newSketchUri = await this.sketchService.createNewSketch();
|
||||
return this.toFileStat(newSketchUri.uri);
|
||||
}
|
||||
// When opening a file instead of a directory, IDE2 (and Theia) expects a workspace JSON file.
|
||||
// Nothing will work if the workspace file is invalid. Users tend to start (see #964) IDE2 from the `.ino` files,
|
||||
// so here, IDE2 tries to load the sketch via the CLI from the main sketch file URI.
|
||||
// If loading the sketch is OK, IDE2 starts and uses the sketch folder as the workspace root instead of the sketch file.
|
||||
// If loading fails due to invalid name error, IDE2 loads a temp sketch and preserves the startup error, and offers the sketch move to the user later.
|
||||
// If loading the sketch fails, create a fallback sketch and open the new temp sketch folder as the workspace root.
|
||||
if (stat.isFile && stat.resource.path.ext === '.ino') {
|
||||
try {
|
||||
const sketch = await this.sketchService.loadSketch(
|
||||
stat.resource.toString()
|
||||
);
|
||||
return this.toFileStat(sketch.uri);
|
||||
} catch (err) {
|
||||
if (SketchesError.InvalidName.is(err)) {
|
||||
this._workspaceError = err;
|
||||
const newSketchUri = await this.sketchService.createNewSketch();
|
||||
return this.toFileStat(newSketchUri.uri);
|
||||
} else if (SketchesError.NotFound.is(err)) {
|
||||
this._workspaceError = err;
|
||||
const newSketchUri = await this.sketchService.createNewSketch();
|
||||
return this.toFileStat(newSketchUri.uri);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return stat;
|
||||
}
|
||||
|
||||
|
@@ -1,78 +1,78 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import * as ReactDOM from '@theia/core/shared/react-dom';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { Message, MessageLoop } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { UserStatus } from './cloud-user-status';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
|
||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget';
|
||||
import { CreateNew } from '../sketchbook/create-new';
|
||||
import { AuthenticationSession } from '../../../node/auth/types';
|
||||
|
||||
@injectable()
|
||||
export class CloudSketchbookCompositeWidget extends BaseWidget {
|
||||
export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidget<CloudSketchbookTreeWidget> {
|
||||
@inject(AuthenticationClientService)
|
||||
protected readonly authenticationService: AuthenticationClientService;
|
||||
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(CloudSketchbookTreeWidget)
|
||||
protected readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget;
|
||||
|
||||
private compositeNode: HTMLElement;
|
||||
private cloudUserStatusNode: HTMLElement;
|
||||
private readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget;
|
||||
private _session: AuthenticationSession | undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.compositeNode = document.createElement('div');
|
||||
this.compositeNode.classList.add('composite-node');
|
||||
this.cloudUserStatusNode = document.createElement('div');
|
||||
this.cloudUserStatusNode.classList.add('cloud-status-node');
|
||||
this.compositeNode.appendChild(this.cloudUserStatusNode);
|
||||
this.node.appendChild(this.compositeNode);
|
||||
this.id = 'cloud-sketchbook-composite-widget';
|
||||
this.title.caption = nls.localize(
|
||||
'arduino/cloud/remoteSketchbook',
|
||||
'Remote Sketchbook'
|
||||
);
|
||||
this.title.iconClass = 'cloud-sketchbook-tree-icon';
|
||||
this.title.closable = false;
|
||||
this.id = 'cloud-sketchbook-composite-widget';
|
||||
}
|
||||
|
||||
public getTreeWidget(): CloudSketchbookTreeWidget {
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.push(
|
||||
this.authenticationService.onSessionDidChange((session) => {
|
||||
const oldSession = this._session;
|
||||
this._session = session;
|
||||
if (!!oldSession !== !!this._session) {
|
||||
this.updateFooter();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get treeWidget(): CloudSketchbookTreeWidget {
|
||||
return this.cloudSketchbookTreeWidget;
|
||||
}
|
||||
|
||||
protected override onAfterAttach(message: Message): void {
|
||||
super.onAfterAttach(message);
|
||||
Widget.attach(this.cloudSketchbookTreeWidget, this.compositeNode);
|
||||
protected renderFooter(footerNode: HTMLElement): void {
|
||||
ReactDOM.render(
|
||||
<UserStatus
|
||||
model={this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel}
|
||||
authenticationService={this.authenticationService}
|
||||
/>,
|
||||
this.cloudUserStatusNode
|
||||
);
|
||||
this.toDisposeOnDetach.push(
|
||||
Disposable.create(() => Widget.detach(this.cloudSketchbookTreeWidget))
|
||||
<>
|
||||
{this._session && (
|
||||
<CreateNew
|
||||
label={nls.localize(
|
||||
'arduino/sketchbook/newRemoteSketch',
|
||||
'New Remote Sketch'
|
||||
)}
|
||||
onClick={this.onDidClickCreateNew}
|
||||
/>
|
||||
)}
|
||||
<UserStatus
|
||||
model={
|
||||
this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel
|
||||
}
|
||||
authenticationService={this.authenticationService}
|
||||
/>
|
||||
</>,
|
||||
footerNode
|
||||
);
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
|
||||
/*
|
||||
Sending a resize message is needed because otherwise the cloudSketchbookTreeWidget
|
||||
would render empty
|
||||
*/
|
||||
this.onResize(Widget.ResizeMessage.UnknownSize);
|
||||
}
|
||||
|
||||
protected override onResize(message: Widget.ResizeMessage): void {
|
||||
super.onResize(message);
|
||||
MessageLoop.sendMessage(
|
||||
this.cloudSketchbookTreeWidget,
|
||||
Widget.ResizeMessage.UnknownSize
|
||||
);
|
||||
}
|
||||
private onDidClickCreateNew: () => void = () => {
|
||||
this.commandService.executeCommand('arduino-new-cloud-sketch');
|
||||
};
|
||||
}
|
||||
|
@@ -1,20 +1,26 @@
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { TreeNode } from '@theia/core/lib/browser/tree';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { CompositeTreeNode, TreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { posixSegments, splitSketchPath } from '../../create/create-paths';
|
||||
import { CreateApi } from '../../create/create-api';
|
||||
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
|
||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||
import { SketchbookTreeModel } from '../sketchbook/sketchbook-tree-model';
|
||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
|
||||
import { CreateUri } from '../../create/create-uri';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { LocalCacheFsProvider } from '../../local-cache/local-cache-fs-provider';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { FileChangesEvent, FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import {
|
||||
LocalCacheFsProvider,
|
||||
LocalCacheUri,
|
||||
} from '../../local-cache/local-cache-fs-provider';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { SketchCache } from './cloud-sketch-cache';
|
||||
import { Create } from '../../create/typings';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
|
||||
export function sketchBaseDir(sketch: Create.Sketch): FileStat {
|
||||
// extract the sketch path
|
||||
@@ -52,26 +58,16 @@ export function sketchesToFileStats(sketches: Create.Sketch[]): FileStat[] {
|
||||
|
||||
@injectable()
|
||||
export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
||||
@inject(FileService)
|
||||
protected override readonly fileService: FileService;
|
||||
|
||||
@inject(AuthenticationClientService)
|
||||
protected readonly authenticationService: AuthenticationClientService;
|
||||
|
||||
@inject(CreateApi)
|
||||
protected readonly createApi: CreateApi;
|
||||
|
||||
@inject(CloudSketchbookTree)
|
||||
protected readonly cloudSketchbookTree: CloudSketchbookTree;
|
||||
|
||||
@inject(ArduinoPreferences)
|
||||
protected override readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
private readonly createApi: CreateApi;
|
||||
@inject(AuthenticationClientService)
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(LocalCacheFsProvider)
|
||||
protected readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
|
||||
private readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
@inject(SketchCache)
|
||||
protected readonly sketchCache: SketchCache;
|
||||
private readonly sketchCache: SketchCache;
|
||||
|
||||
private _localCacheFsProviderReady: Deferred<void> | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
@@ -81,6 +77,50 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
||||
);
|
||||
}
|
||||
|
||||
override *getNodesByUri(uri: URI): IterableIterator<TreeNode> {
|
||||
if (uri.scheme === LocalCacheUri.scheme) {
|
||||
const workspace = this.root;
|
||||
const { session } = this.authenticationService;
|
||||
if (session && WorkspaceNode.is(workspace)) {
|
||||
const currentUri = this.localCacheFsProvider.to(uri);
|
||||
if (currentUri) {
|
||||
const rootPath = this.localCacheFsProvider
|
||||
.toUri(session)
|
||||
.path.toString();
|
||||
const currentPath = currentUri.path.toString();
|
||||
if (rootPath === currentPath) {
|
||||
return workspace;
|
||||
}
|
||||
if (currentPath.startsWith(rootPath)) {
|
||||
const id = currentPath.substring(rootPath.length);
|
||||
const node = this.getNode(id);
|
||||
if (node) {
|
||||
yield node;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override isRootAffected(changes: FileChangesEvent): boolean {
|
||||
return changes.changes
|
||||
.map(({ resource }) => resource)
|
||||
.some(
|
||||
(uri) => uri.parent.toString().startsWith(LocalCacheUri.root.toString()) // all files under the root might affect the tree
|
||||
);
|
||||
}
|
||||
|
||||
override async refresh(
|
||||
parent?: Readonly<CompositeTreeNode>
|
||||
): Promise<CompositeTreeNode | undefined> {
|
||||
if (parent) {
|
||||
return super.refresh(parent);
|
||||
}
|
||||
await this.updateRoot();
|
||||
return super.refresh();
|
||||
}
|
||||
|
||||
override async createRoot(): Promise<TreeNode | undefined> {
|
||||
const { session } = this.authenticationService;
|
||||
if (!session) {
|
||||
@@ -89,7 +129,10 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
||||
}
|
||||
this.createApi.init(this.authenticationService, this.arduinoPreferences);
|
||||
this.sketchCache.init();
|
||||
const sketches = await this.createApi.sketches();
|
||||
const [sketches] = await Promise.all([
|
||||
this.createApi.sketches(),
|
||||
this.ensureLocalFsProviderReady(),
|
||||
]);
|
||||
const rootFileStats = sketchesToFileStats(sketches);
|
||||
if (this.workspaceService.opened) {
|
||||
const workspaceNode = WorkspaceNode.createRoot(
|
||||
@@ -108,7 +151,9 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
||||
return this.tree as CloudSketchbookTree;
|
||||
}
|
||||
|
||||
protected override recursivelyFindSketchRoot(node: TreeNode): any {
|
||||
protected override recursivelyFindSketchRoot(
|
||||
node: TreeNode
|
||||
): TreeNode | false {
|
||||
if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||
return node;
|
||||
}
|
||||
@@ -122,13 +167,25 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
||||
}
|
||||
|
||||
override async revealFile(uri: URI): Promise<TreeNode | undefined> {
|
||||
await this.localCacheFsProvider.ready.promise;
|
||||
// we use remote uris as keys for the tree
|
||||
// convert local URIs
|
||||
const remoteuri = this.localCacheFsProvider.from(uri);
|
||||
if (remoteuri) {
|
||||
return super.revealFile(remoteuri);
|
||||
const remoteUri = this.localCacheFsProvider.from(uri);
|
||||
if (remoteUri) {
|
||||
return super.revealFile(remoteUri);
|
||||
} else {
|
||||
return super.revealFile(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureLocalFsProviderReady(): Promise<void> {
|
||||
if (this._localCacheFsProviderReady) {
|
||||
return this._localCacheFsProviderReady.promise;
|
||||
}
|
||||
this._localCacheFsProviderReady = new Deferred();
|
||||
this.fileService
|
||||
.access(LocalCacheUri.root)
|
||||
.then(() => this._localCacheFsProviderReady?.resolve());
|
||||
return this._localCacheFsProviderReady.promise;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { TreeModel } from '@theia/core/lib/browser/tree/tree-model';
|
||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||
@@ -27,12 +27,6 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
|
||||
@inject(CloudSketchbookTree)
|
||||
protected readonly cloudSketchbookTree: CloudSketchbookTree;
|
||||
|
||||
@postConstruct()
|
||||
protected override async init(): Promise<void> {
|
||||
await super.init();
|
||||
this.addClass('tree-container'); // Adds `height: 100%` to the tree. Otherwise you cannot see it.
|
||||
}
|
||||
|
||||
protected override renderTree(model: TreeModel): React.ReactNode {
|
||||
if (this.shouldShowWelcomeView()) return this.renderViewWelcome();
|
||||
if (this.shouldShowEmptyView()) return this.renderEmptyView();
|
||||
@@ -61,10 +55,10 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="theia-button"
|
||||
className="theia-button uppercase"
|
||||
onClick={() => shell.openExternal('https://create.arduino.cc/editor')}
|
||||
>
|
||||
{nls.localize('cloud/GoToCloud', 'GO TO CLOUD')}
|
||||
{nls.localize('arduino/cloud/goToCloud', 'Go to Cloud')}
|
||||
</button>
|
||||
<div className="center item"></div>
|
||||
</div>
|
||||
|
@@ -136,7 +136,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.runWithState(node, 'pulling', async (node) => {
|
||||
return this.runWithState(node, 'pulling', async (node) => {
|
||||
const commandsCopy = node.commands;
|
||||
node.commands = [];
|
||||
|
||||
@@ -196,7 +196,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.runWithState(node, 'pushing', async (node) => {
|
||||
return this.runWithState(node, 'pushing', async (node) => {
|
||||
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
|
||||
throw new Error(
|
||||
nls.localize(
|
||||
@@ -269,7 +269,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// do not map "do_not_sync" files/directoris and their descendants
|
||||
// do not map "do_not_sync" files/directories and their descendants
|
||||
const segments = path[1].split(posix.sep) || [];
|
||||
if (
|
||||
segments.some((segment) => Create.do_not_sync_files.includes(segment))
|
||||
|
@@ -2,6 +2,7 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'
|
||||
import { CloudSketchbookCompositeWidget } from './cloud-sketchbook-composite-widget';
|
||||
import { SketchbookWidget } from '../sketchbook/sketchbook-widget';
|
||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget';
|
||||
|
||||
@injectable()
|
||||
export class CloudSketchbookWidget extends SketchbookWidget {
|
||||
@@ -19,8 +20,8 @@ export class CloudSketchbookWidget extends SketchbookWidget {
|
||||
override getTreeWidget(): any {
|
||||
const widget: any = this.sketchbookTreesContainer.selectedWidgets().next();
|
||||
|
||||
if (widget && typeof widget.getTreeWidget !== 'undefined') {
|
||||
return (widget as CloudSketchbookCompositeWidget).getTreeWidget();
|
||||
if (widget instanceof BaseSketchbookCompositeWidget) {
|
||||
return widget.treeWidget;
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
@@ -30,7 +31,7 @@ export class CloudSketchbookWidget extends SketchbookWidget {
|
||||
this.sketchbookTreesContainer.activateWidget(this.widget);
|
||||
} else {
|
||||
this.sketchbookTreesContainer.activateWidget(
|
||||
this.localSketchbookTreeWidget
|
||||
this.sketchbookCompositeWidget
|
||||
);
|
||||
}
|
||||
this.setDocumentMode();
|
||||
|
@@ -14,34 +14,21 @@ export class ComponentListItem<
|
||||
)[0];
|
||||
this.state = {
|
||||
selectedVersion: version,
|
||||
focus: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
override componentDidUpdate(
|
||||
prevProps: ComponentListItem.Props<T>,
|
||||
prevState: ComponentListItem.State
|
||||
): void {
|
||||
if (this.state.focus !== prevState.focus) {
|
||||
this.props.onFocusDidChange();
|
||||
}
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { item, itemRenderer } = this.props;
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => this.setState({ focus: true })}
|
||||
onMouseLeave={() => this.setState({ focus: false })}
|
||||
>
|
||||
<>
|
||||
{itemRenderer.renderItem(
|
||||
Object.assign(this.state, { item }),
|
||||
this.install.bind(this),
|
||||
this.uninstall.bind(this),
|
||||
this.onVersionChange.bind(this)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,11 +64,9 @@ export namespace ComponentListItem {
|
||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly onFocusDidChange: () => void;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
selectedVersion?: Installable.Version;
|
||||
focus: boolean;
|
||||
}
|
||||
}
|
||||
|
@@ -125,7 +125,7 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
|
||||
rowIndex={index}
|
||||
parent={parent}
|
||||
>
|
||||
{({ measure, registerChild }) => (
|
||||
{({ registerChild }) => (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<div ref={registerChild} style={style}>
|
||||
@@ -135,7 +135,6 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
|
||||
itemRenderer={this.props.itemRenderer}
|
||||
install={this.props.install}
|
||||
uninstall={this.props.uninstall}
|
||||
onFocusDidChange={() => measure()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@@ -111,19 +111,7 @@ export class FilterableListContainer<
|
||||
const { searchable } = this.props;
|
||||
searchable
|
||||
.search(searchOptions)
|
||||
.then((items) => this.setState({ items: this.sort(items) }));
|
||||
}
|
||||
|
||||
protected sort(items: T[]): T[] {
|
||||
const { itemLabel, itemDeprecated } = this.props;
|
||||
return items.sort((left, right) => {
|
||||
// always put deprecated items at the bottom of the list
|
||||
if (itemDeprecated(left)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return itemLabel(left).localeCompare(itemLabel(right));
|
||||
});
|
||||
.then((items) => this.setState({ items: this.props.sort(items) }));
|
||||
}
|
||||
|
||||
protected async install(
|
||||
@@ -139,7 +127,7 @@ export class FilterableListContainer<
|
||||
run: ({ progressId }) => install({ item, progressId, version }),
|
||||
});
|
||||
const items = await searchable.search(this.state.searchOptions);
|
||||
this.setState({ items: this.sort(items) });
|
||||
this.setState({ items: this.props.sort(items) });
|
||||
}
|
||||
|
||||
protected async uninstall(item: T): Promise<void> {
|
||||
@@ -167,7 +155,7 @@ export class FilterableListContainer<
|
||||
run: ({ progressId }) => uninstall({ item, progressId }),
|
||||
});
|
||||
const items = await searchable.search(this.state.searchOptions);
|
||||
this.setState({ items: this.sort(items) });
|
||||
this.setState({ items: this.props.sort(items) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +192,7 @@ export namespace FilterableListContainer {
|
||||
progressId: string;
|
||||
}) => Promise<void>;
|
||||
readonly commandService: CommandService;
|
||||
readonly sort: (items: T[]) => T[];
|
||||
}
|
||||
|
||||
export interface State<T, S extends Searchable.Options> {
|
||||
|
@@ -28,7 +28,7 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
uninstall: (item: T) => Promise<void>,
|
||||
onVersionChange: (version: Installable.Version) => void
|
||||
): React.ReactNode {
|
||||
const { item, focus } = input;
|
||||
const { item } = input;
|
||||
let nameAndAuthor: JSX.Element;
|
||||
if (item.name && item.author) {
|
||||
const name = <span className="name">{item.name}</span>;
|
||||
@@ -55,7 +55,14 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
item.installedVersion
|
||||
)}
|
||||
</span>
|
||||
<span className="installed" onClick={onClickUninstall} />
|
||||
<span
|
||||
className="installed uppercase"
|
||||
onClick={onClickUninstall}
|
||||
{...{
|
||||
install: nls.localize('arduino/component/installed', 'Installed'),
|
||||
uninstall: nls.localize('arduino/component/uninstall', 'Uninstall'),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -70,10 +77,10 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
const onClickInstall = () => install(item);
|
||||
const installButton = item.installable && (
|
||||
<button
|
||||
className="theia-button secondary install"
|
||||
className="theia-button secondary install uppercase"
|
||||
onClick={onClickInstall}
|
||||
>
|
||||
{nls.localize('arduino/component/install', 'INSTALL')}
|
||||
{nls.localize('arduino/component/install', 'Install')}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -120,12 +127,10 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
{description}
|
||||
</div>
|
||||
<div className="info">{moreInfo}</div>
|
||||
{focus && (
|
||||
<div className="footer">
|
||||
{versions}
|
||||
{installButton}
|
||||
</div>
|
||||
)}
|
||||
<div className="footer">
|
||||
{versions}
|
||||
{installButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,9 +1,15 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import {
|
||||
OpenerOptions,
|
||||
OpenHandler,
|
||||
} from '@theia/core/lib/browser/opener-service';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { URI } from '@theia/core/lib/common/uri';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { Searchable } from '../../../common/protocol';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ListWidget } from './list-widget';
|
||||
import { Searchable } from '../../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidgetFrontendContribution<
|
||||
@@ -11,14 +17,49 @@ export abstract class ListWidgetFrontendContribution<
|
||||
S extends Searchable.Options
|
||||
>
|
||||
extends AbstractViewContribution<ListWidget<T, S>>
|
||||
implements FrontendApplicationContribution
|
||||
implements FrontendApplicationContribution, OpenHandler
|
||||
{
|
||||
readonly id: string = `http-opener-${this.viewId}`;
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
// TS requires at least one method from `FrontendApplicationContribution`.
|
||||
// Expected to be empty.
|
||||
this.openView();
|
||||
}
|
||||
|
||||
override registerMenus(): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
override registerMenus(_: MenuModelRegistry): void {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
canHandle(uri: URI, _?: OpenerOptions): number {
|
||||
// `500` is the default HTTP opener in Theia. IDE2 has higher priority.
|
||||
// https://github.com/eclipse-theia/theia/blob/b75b6144b0ffea06a549294903c374fa642135e4/packages/core/src/browser/http-open-handler.ts#L39
|
||||
return this.canParse(uri) ? 501 : 0;
|
||||
}
|
||||
|
||||
async open(
|
||||
uri: URI,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_?: OpenerOptions | undefined
|
||||
): Promise<void> {
|
||||
const searchOptions = this.parse(uri);
|
||||
if (!searchOptions) {
|
||||
console.warn(
|
||||
`Failed to parse URI into a search options. URI: ${uri.toString()}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const widget = await this.openView({
|
||||
activate: true,
|
||||
reveal: true,
|
||||
});
|
||||
if (!widget) {
|
||||
console.warn(`Failed to open view for URI: ${uri.toString()}`);
|
||||
return;
|
||||
}
|
||||
widget.refresh(searchOptions);
|
||||
}
|
||||
|
||||
protected abstract canParse(uri: URI): boolean;
|
||||
protected abstract parse(uri: URI): S | undefined;
|
||||
}
|
||||
|
@@ -51,9 +51,11 @@ export abstract class ListWidget<
|
||||
*/
|
||||
protected firstActivate = true;
|
||||
|
||||
protected readonly defaultSortComparator: (left: T, right: T) => number;
|
||||
|
||||
constructor(protected options: ListWidget.Options<T, S>) {
|
||||
super();
|
||||
const { id, label, iconClass } = options;
|
||||
const { id, label, iconClass, itemDeprecated, itemLabel } = options;
|
||||
this.id = id;
|
||||
this.title.label = label;
|
||||
this.title.caption = label;
|
||||
@@ -63,12 +65,23 @@ export abstract class ListWidget<
|
||||
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
||||
this.scrollOptions = undefined;
|
||||
this.toDispose.push(this.searchOptionsChangeEmitter);
|
||||
|
||||
this.defaultSortComparator = (left, right): number => {
|
||||
// always put deprecated items at the bottom of the list
|
||||
if (itemDeprecated(left)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return itemLabel(left).localeCompare(itemLabel(right));
|
||||
};
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onIndexDidUpdate(() => this.refresh(undefined)),
|
||||
this.notificationCenter.onIndexUpdateDidComplete(() =>
|
||||
this.refresh(undefined)
|
||||
),
|
||||
this.notificationCenter.onDaemonDidStart(() => this.refresh(undefined)),
|
||||
this.notificationCenter.onDaemonDidStop(() => this.refresh(undefined)),
|
||||
]);
|
||||
@@ -128,6 +141,30 @@ export abstract class ListWidget<
|
||||
return this.options.installable.uninstall({ item, progressId });
|
||||
}
|
||||
|
||||
protected filterableListSort = (items: T[]): T[] => {
|
||||
const isArduinoTypeComparator = (left: T, right: T) => {
|
||||
const aIsArduinoType = left.types.includes('Arduino');
|
||||
const bIsArduinoType = right.types.includes('Arduino');
|
||||
|
||||
if (aIsArduinoType && !bIsArduinoType && !left.deprecated) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!aIsArduinoType && bIsArduinoType && !right.deprecated) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
return items.sort((left, right) => {
|
||||
return (
|
||||
isArduinoTypeComparator(left, right) ||
|
||||
this.defaultSortComparator(left, right)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<FilterableListContainer<T, S>
|
||||
@@ -145,6 +182,7 @@ export abstract class ListWidget<
|
||||
messageService={this.messageService}
|
||||
commandService={this.commandService}
|
||||
responseService={this.responseService}
|
||||
sort={this.filterableListSort}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -0,0 +1,20 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
|
||||
export class CreateNew extends React.Component<CreateNew.Props> {
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<div className="create-new">
|
||||
<button className="theia-button secondary" onClick={this.props.onClick}>
|
||||
{this.props.label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CreateNew {
|
||||
export interface Props {
|
||||
readonly label: string;
|
||||
readonly onClick: () => void;
|
||||
}
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import * as ReactDOM from '@theia/core/shared/react-dom';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { Message, MessageLoop } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
|
||||
import { CreateNew } from '../sketchbook/create-new';
|
||||
|
||||
@injectable()
|
||||
export abstract class BaseSketchbookCompositeWidget<
|
||||
TW extends SketchbookTreeWidget
|
||||
> extends BaseWidget {
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
private readonly compositeNode: HTMLElement;
|
||||
private readonly footerNode: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.compositeNode = document.createElement('div');
|
||||
this.compositeNode.classList.add('composite-node');
|
||||
this.footerNode = document.createElement('div');
|
||||
this.footerNode.classList.add('footer-node');
|
||||
this.compositeNode.appendChild(this.footerNode);
|
||||
this.node.appendChild(this.compositeNode);
|
||||
this.title.closable = false;
|
||||
}
|
||||
|
||||
abstract get treeWidget(): TW;
|
||||
protected abstract renderFooter(footerNode: HTMLElement): void;
|
||||
protected updateFooter(): void {
|
||||
this.renderFooter(this.footerNode);
|
||||
}
|
||||
|
||||
protected override onAfterAttach(message: Message): void {
|
||||
super.onAfterAttach(message);
|
||||
Widget.attach(this.treeWidget, this.compositeNode);
|
||||
this.renderFooter(this.footerNode);
|
||||
this.toDisposeOnDetach.push(
|
||||
Disposable.create(() => Widget.detach(this.treeWidget))
|
||||
);
|
||||
}
|
||||
|
||||
protected override onActivateRequest(message: Message): void {
|
||||
super.onActivateRequest(message);
|
||||
// Sending a resize message is needed because otherwise the tree widget would render empty
|
||||
this.onResize(Widget.ResizeMessage.UnknownSize);
|
||||
}
|
||||
|
||||
protected override onResize(message: Widget.ResizeMessage): void {
|
||||
super.onResize(message);
|
||||
MessageLoop.sendMessage(this.treeWidget, Widget.ResizeMessage.UnknownSize);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class SketchbookCompositeWidget extends BaseSketchbookCompositeWidget<SketchbookTreeWidget> {
|
||||
@inject(SketchbookTreeWidget)
|
||||
private readonly sketchbookTreeWidget: SketchbookTreeWidget;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = 'sketchbook-composite-widget';
|
||||
this.title.caption = nls.localize(
|
||||
'arduino/sketch/titleLocalSketchbook',
|
||||
'Local Sketchbook'
|
||||
);
|
||||
this.title.iconClass = 'sketchbook-tree-icon';
|
||||
}
|
||||
|
||||
get treeWidget(): SketchbookTreeWidget {
|
||||
return this.sketchbookTreeWidget;
|
||||
}
|
||||
|
||||
protected renderFooter(footerNode: HTMLElement): void {
|
||||
ReactDOM.render(
|
||||
<CreateNew
|
||||
label={nls.localize('arduino/sketchbook/newSketch', 'New Sketch')}
|
||||
onClick={this.onDidClickCreateNew}
|
||||
/>,
|
||||
footerNode
|
||||
);
|
||||
}
|
||||
|
||||
private onDidClickCreateNew: () => void = () => {
|
||||
this.commandService.executeCommand('arduino-new-sketch');
|
||||
};
|
||||
}
|
@@ -59,6 +59,7 @@ export class SketchbookTreeWidget extends FileTreeWidget {
|
||||
'Local Sketchbook'
|
||||
);
|
||||
this.title.closable = false;
|
||||
this.addClass('tree-container'); // Adds `height: 100%` to the tree. Otherwise you cannot see it.
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
|
@@ -11,15 +11,21 @@ import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { CloudSketchbookCompositeWidget } from '../cloud-sketchbook/cloud-sketchbook-composite-widget';
|
||||
import { URI } from '../../contributions/contribution';
|
||||
import {
|
||||
BaseSketchbookCompositeWidget,
|
||||
SketchbookCompositeWidget,
|
||||
} from './sketchbook-composite-widget';
|
||||
|
||||
@injectable()
|
||||
export class SketchbookWidget extends BaseWidget {
|
||||
static LABEL = nls.localize('arduino/sketch/titleSketchbook', 'Sketchbook');
|
||||
static readonly LABEL = nls.localize(
|
||||
'arduino/sketch/titleSketchbook',
|
||||
'Sketchbook'
|
||||
);
|
||||
|
||||
@inject(SketchbookTreeWidget)
|
||||
protected readonly localSketchbookTreeWidget: SketchbookTreeWidget;
|
||||
@inject(SketchbookCompositeWidget)
|
||||
protected readonly sketchbookCompositeWidget: SketchbookCompositeWidget;
|
||||
|
||||
protected readonly sketchbookTreesContainer: DockPanel;
|
||||
|
||||
@@ -36,7 +42,7 @@ export class SketchbookWidget extends BaseWidget {
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.sketchbookTreesContainer.addWidget(this.localSketchbookTreeWidget);
|
||||
this.sketchbookTreesContainer.addWidget(this.sketchbookCompositeWidget);
|
||||
}
|
||||
|
||||
protected override onAfterAttach(message: Message): void {
|
||||
@@ -48,7 +54,7 @@ export class SketchbookWidget extends BaseWidget {
|
||||
}
|
||||
|
||||
getTreeWidget(): SketchbookTreeWidget {
|
||||
return this.localSketchbookTreeWidget;
|
||||
return this.sketchbookCompositeWidget.treeWidget;
|
||||
}
|
||||
|
||||
activeTreeWidgetId(): string | undefined {
|
||||
@@ -80,8 +86,8 @@ export class SketchbookWidget extends BaseWidget {
|
||||
if (widget instanceof SketchbookTreeWidget) {
|
||||
return widget;
|
||||
}
|
||||
if (widget instanceof CloudSketchbookCompositeWidget) {
|
||||
return widget.getTreeWidget();
|
||||
if (widget instanceof BaseSketchbookCompositeWidget) {
|
||||
return widget.treeWidget;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
@@ -3,7 +3,14 @@ import { Searchable } from './searchable';
|
||||
import { Installable } from './installable';
|
||||
import { ArduinoComponent } from './arduino-component';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { All, Contributed, Partner, Type, Updatable } from '../nls';
|
||||
import {
|
||||
All,
|
||||
Contributed,
|
||||
Partner,
|
||||
Type as TypeLabel,
|
||||
Updatable,
|
||||
} from '../nls';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export type AvailablePorts = Record<string, [Port, Array<Board>]>;
|
||||
export namespace AvailablePorts {
|
||||
@@ -141,6 +148,7 @@ export interface BoardsService
|
||||
fqbn: string;
|
||||
}): Promise<BoardsPackage | undefined>;
|
||||
searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]>;
|
||||
getInstalledBoards(): Promise<BoardWithPackage[]>;
|
||||
getBoardUserFields(options: {
|
||||
fqbn: string;
|
||||
protocol: string;
|
||||
@@ -151,6 +159,7 @@ export interface BoardSearch extends Searchable.Options {
|
||||
readonly type?: BoardSearch.Type;
|
||||
}
|
||||
export namespace BoardSearch {
|
||||
export const Default: BoardSearch = { type: 'All' };
|
||||
export const TypeLiterals = [
|
||||
'All',
|
||||
'Updatable',
|
||||
@@ -161,6 +170,11 @@ export namespace BoardSearch {
|
||||
'Arduino@Heart',
|
||||
] as const;
|
||||
export type Type = typeof TypeLiterals[number];
|
||||
export namespace Type {
|
||||
export function is(arg: unknown): arg is Type {
|
||||
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
|
||||
}
|
||||
}
|
||||
export const TypeLabels: Record<Type, string> = {
|
||||
All: All,
|
||||
Updatable: Updatable,
|
||||
@@ -177,8 +191,41 @@ export namespace BoardSearch {
|
||||
keyof Omit<BoardSearch, 'query'>,
|
||||
string
|
||||
> = {
|
||||
type: Type,
|
||||
type: TypeLabel,
|
||||
};
|
||||
export namespace UriParser {
|
||||
export const authority = 'boardsmanager';
|
||||
export function parse(uri: URI): BoardSearch | undefined {
|
||||
if (uri.scheme !== 'http') {
|
||||
throw new Error(
|
||||
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
if (uri.authority !== authority) {
|
||||
throw new Error(
|
||||
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
|
||||
if (segments.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
let searchOptions: BoardSearch | undefined = undefined;
|
||||
const [type] = segments;
|
||||
if (!type) {
|
||||
searchOptions = BoardSearch.Default;
|
||||
} else if (BoardSearch.Type.is(type)) {
|
||||
searchOptions = { type };
|
||||
}
|
||||
if (searchOptions) {
|
||||
return {
|
||||
...searchOptions,
|
||||
...Searchable.UriParser.parseQuery(uri),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Port {
|
||||
@@ -285,6 +332,29 @@ export namespace Port {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace Protocols {
|
||||
export const KnownProtocolLiterals = ['serial', 'network'] as const;
|
||||
export type KnownProtocol = typeof KnownProtocolLiterals[number];
|
||||
export namespace KnownProtocol {
|
||||
export function is(protocol: unknown): protocol is KnownProtocol {
|
||||
return (
|
||||
typeof protocol === 'string' &&
|
||||
KnownProtocolLiterals.indexOf(protocol as KnownProtocol) >= 0
|
||||
);
|
||||
}
|
||||
}
|
||||
export const ProtocolLabels: Record<KnownProtocol, string> = {
|
||||
serial: nls.localize('arduino/portProtocol/serial', 'Serial'),
|
||||
network: nls.localize('arduino/portProtocol/network', 'Network'),
|
||||
};
|
||||
export function protocolLabel(protocol: string): string {
|
||||
if (KnownProtocol.is(protocol)) {
|
||||
return ProtocolLabels[protocol];
|
||||
}
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface BoardsPackage extends ArduinoComponent {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
import type {
|
||||
Location,
|
||||
@@ -10,6 +11,7 @@ import type {
|
||||
} from '../../common/protocol/boards-service';
|
||||
import type { Programmer } from './boards-service';
|
||||
import type { Sketch } from './sketches-service';
|
||||
import { IndexUpdateSummary } from './notification-service';
|
||||
|
||||
export const CompilerWarningLiterals = [
|
||||
'None',
|
||||
@@ -18,6 +20,17 @@ export const CompilerWarningLiterals = [
|
||||
'All',
|
||||
] as const;
|
||||
export type CompilerWarnings = typeof CompilerWarningLiterals[number];
|
||||
export namespace CompilerWarnings {
|
||||
export function labelOf(warning: CompilerWarnings): string {
|
||||
return CompilerWarningLabels[warning];
|
||||
}
|
||||
const CompilerWarningLabels: Record<CompilerWarnings, string> = {
|
||||
None: nls.localize('arduino/core/compilerWarnings/none', 'None'),
|
||||
Default: nls.localize('arduino/core/compilerWarnings/default', 'Default'),
|
||||
More: nls.localize('arduino/core/compilerWarnings/more', 'More'),
|
||||
All: nls.localize('arduino/core/compilerWarnings/all', 'All'),
|
||||
};
|
||||
}
|
||||
export namespace CoreError {
|
||||
export interface ErrorLocationRef {
|
||||
readonly message: string;
|
||||
@@ -96,6 +109,37 @@ export interface CoreService {
|
||||
compile(options: CoreService.Options.Compile): Promise<void>;
|
||||
upload(options: CoreService.Options.Upload): Promise<void>;
|
||||
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
|
||||
/**
|
||||
* Refreshes the underling core gRPC client for the Arduino CLI.
|
||||
*/
|
||||
refresh(): Promise<void>;
|
||||
/**
|
||||
* Updates the index of the given index types and refreshes (`init`) the underlying core gRPC client.
|
||||
* If `types` is empty, only the refresh part will be executed.
|
||||
*/
|
||||
updateIndex({ types }: { types: IndexType[] }): Promise<void>;
|
||||
/**
|
||||
* If the IDE2 detects invalid or missing indexes on core client init,
|
||||
* IDE2 tries to update the indexes before the first frontend connects.
|
||||
* Use this method to determine whether the backend has already updated
|
||||
* the indexes before updating them.
|
||||
*
|
||||
* If yes, the connected frontend can update the local storage with the most
|
||||
* recent index update date-time for a particular index type,
|
||||
* and IDE2 can avoid the double indexes update.
|
||||
*/
|
||||
indexUpdateSummaryBeforeInit(): Promise<Readonly<IndexUpdateSummary>>;
|
||||
}
|
||||
|
||||
export const IndexTypeLiterals = ['platform', 'library'] as const;
|
||||
export type IndexType = typeof IndexTypeLiterals[number];
|
||||
export namespace IndexType {
|
||||
export function is(arg: unknown): arg is IndexType {
|
||||
return (
|
||||
typeof arg === 'string' && IndexTypeLiterals.includes(arg as IndexType)
|
||||
);
|
||||
}
|
||||
export const All: IndexType[] = IndexTypeLiterals.filter(is);
|
||||
}
|
||||
|
||||
export namespace CoreService {
|
||||
|
@@ -8,9 +8,10 @@ import {
|
||||
Partner,
|
||||
Recommended,
|
||||
Retired,
|
||||
Type,
|
||||
Type as TypeLabel,
|
||||
Updatable,
|
||||
} from '../nls';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export const LibraryServicePath = '/services/library-service';
|
||||
export const LibraryService = Symbol('LibraryService');
|
||||
@@ -55,6 +56,7 @@ export interface LibrarySearch extends Searchable.Options {
|
||||
readonly topic?: LibrarySearch.Topic;
|
||||
}
|
||||
export namespace LibrarySearch {
|
||||
export const Default: LibrarySearch = { type: 'All', topic: 'All' };
|
||||
export const TypeLiterals = [
|
||||
'All',
|
||||
'Updatable',
|
||||
@@ -66,6 +68,11 @@ export namespace LibrarySearch {
|
||||
'Retired',
|
||||
] as const;
|
||||
export type Type = typeof TypeLiterals[number];
|
||||
export namespace Type {
|
||||
export function is(arg: unknown): arg is Type {
|
||||
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
|
||||
}
|
||||
}
|
||||
export const TypeLabels: Record<Type, string> = {
|
||||
All: All,
|
||||
Updatable: Updatable,
|
||||
@@ -90,6 +97,11 @@ export namespace LibrarySearch {
|
||||
'Uncategorized',
|
||||
] as const;
|
||||
export type Topic = typeof TopicLiterals[number];
|
||||
export namespace Topic {
|
||||
export function is(arg: unknown): arg is Topic {
|
||||
return typeof arg === 'string' && TopicLiterals.includes(arg as Topic);
|
||||
}
|
||||
}
|
||||
export const TopicLabels: Record<Topic, string> = {
|
||||
All: All,
|
||||
Communication: nls.localize(
|
||||
@@ -126,8 +138,60 @@ export namespace LibrarySearch {
|
||||
string
|
||||
> = {
|
||||
topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
|
||||
type: Type,
|
||||
type: TypeLabel,
|
||||
};
|
||||
export namespace UriParser {
|
||||
export const authority = 'librarymanager';
|
||||
export function parse(uri: URI): LibrarySearch | undefined {
|
||||
if (uri.scheme !== 'http') {
|
||||
throw new Error(
|
||||
`Invalid 'scheme'. Expected 'http'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
if (uri.authority !== authority) {
|
||||
throw new Error(
|
||||
`Invalid 'authority'. Expected: '${authority}'. URI was: ${uri.toString()}.`
|
||||
);
|
||||
}
|
||||
const segments = Searchable.UriParser.normalizedSegmentsOf(uri);
|
||||
// Special magic handling for `Signal Input/Output`.
|
||||
// TODO: IDE2 deserves a better lib/boards URL spec.
|
||||
// https://github.com/arduino/arduino-ide/issues/1442#issuecomment-1252136377
|
||||
if (segments.length === 3) {
|
||||
const [type, topicHead, topicTail] = segments;
|
||||
const maybeTopic = `${topicHead}/${topicTail}`;
|
||||
if (
|
||||
LibrarySearch.Topic.is(maybeTopic) &&
|
||||
maybeTopic === 'Signal Input/Output' &&
|
||||
LibrarySearch.Type.is(type)
|
||||
) {
|
||||
return {
|
||||
type,
|
||||
topic: maybeTopic,
|
||||
...Searchable.UriParser.parseQuery(uri),
|
||||
};
|
||||
}
|
||||
}
|
||||
let searchOptions: LibrarySearch | undefined = undefined;
|
||||
const [type, topic] = segments;
|
||||
if (!type && !topic) {
|
||||
searchOptions = LibrarySearch.Default;
|
||||
} else if (LibrarySearch.Type.is(type)) {
|
||||
if (!topic) {
|
||||
searchOptions = { ...LibrarySearch.Default, type };
|
||||
} else if (LibrarySearch.Topic.is(topic)) {
|
||||
searchOptions = { type, topic };
|
||||
}
|
||||
}
|
||||
if (searchOptions) {
|
||||
return {
|
||||
...searchOptions,
|
||||
...Searchable.UriParser.parseQuery(uri),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace LibraryService {
|
||||
@@ -162,12 +226,6 @@ export enum LibraryLocation {
|
||||
}
|
||||
|
||||
export interface LibraryPackage extends ArduinoComponent {
|
||||
/**
|
||||
* Same as [`Library#real_name`](https://arduino.github.io/arduino-cli/latest/rpc/commands/#library).
|
||||
* Should be used for the UI, and `name` is used to uniquely identify a library. It does not have an ID.
|
||||
*/
|
||||
readonly label: string;
|
||||
|
||||
/**
|
||||
* An array of string that should be included into the `ino` file if this library is used.
|
||||
* For example, including `SD` will prepend `#include <SD.h>` to the `ino` file. While including `Bridge`
|
||||
|
@@ -5,27 +5,62 @@ import type {
|
||||
Config,
|
||||
ProgressMessage,
|
||||
Sketch,
|
||||
IndexType,
|
||||
} from '../protocol';
|
||||
import type { LibraryPackage } from './library-service';
|
||||
|
||||
/**
|
||||
* Values are [ISO 8601](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
|
||||
* strings representing the date-time when the update of the index has been completed.
|
||||
*/
|
||||
export type IndexUpdateSummary = {
|
||||
[T in IndexType]: string;
|
||||
} & { message?: string };
|
||||
export interface IndexUpdateParams {
|
||||
/**
|
||||
* Application unique ID of the progress.
|
||||
*/
|
||||
readonly progressId: string;
|
||||
/**
|
||||
* The type of the index is which is being updated.
|
||||
*/
|
||||
readonly types: IndexType[];
|
||||
}
|
||||
export type IndexUpdateWillStartParams = IndexUpdateParams;
|
||||
export interface IndexUpdateDidCompleteParams
|
||||
extends Omit<IndexUpdateParams, 'types'> {
|
||||
readonly summary: IndexUpdateSummary;
|
||||
}
|
||||
export interface IndexUpdateDidFailParams extends IndexUpdateParams {
|
||||
/**
|
||||
* Describes the reason of the index update failure.
|
||||
*/
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export interface NotificationServiceClient {
|
||||
notifyIndexWillUpdate(progressId: string): void;
|
||||
// Index
|
||||
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void;
|
||||
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void;
|
||||
notifyIndexDidUpdate(progressId: string): void;
|
||||
notifyIndexUpdateDidFail({
|
||||
progressId,
|
||||
message,
|
||||
}: {
|
||||
progressId: string;
|
||||
message: string;
|
||||
}): void;
|
||||
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void;
|
||||
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void;
|
||||
|
||||
// Daemon
|
||||
notifyDaemonDidStart(port: string): void;
|
||||
notifyDaemonDidStop(): void;
|
||||
|
||||
// CLI config
|
||||
notifyConfigDidChange(event: { config: Config | undefined }): void;
|
||||
|
||||
// Platforms
|
||||
notifyPlatformDidInstall(event: { item: BoardsPackage }): void;
|
||||
notifyPlatformDidUninstall(event: { item: BoardsPackage }): void;
|
||||
|
||||
// Libraries
|
||||
notifyLibraryDidInstall(event: { item: LibraryPackage }): void;
|
||||
notifyLibraryDidUninstall(event: { item: LibraryPackage }): void;
|
||||
|
||||
// Boards discovery
|
||||
notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): void;
|
||||
notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void;
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export interface Searchable<T, O extends Searchable.Options> {
|
||||
search(options: O): Promise<T[]>;
|
||||
}
|
||||
@@ -8,4 +10,24 @@ export namespace Searchable {
|
||||
*/
|
||||
readonly query?: string;
|
||||
}
|
||||
export namespace UriParser {
|
||||
/**
|
||||
* Parses the `URI#fragment` into a query term.
|
||||
*/
|
||||
export function parseQuery(uri: URI): { query: string } {
|
||||
return { query: uri.fragment };
|
||||
}
|
||||
/**
|
||||
* Splits the `URI#path#toString` on the `/` POSIX separator into decoded segments. The first, empty segment representing the root is omitted.
|
||||
* Examples:
|
||||
* - `/` -> `['']`
|
||||
* - `/All` -> `['All']`
|
||||
* - `/All/Device%20Control` -> `['All', 'Device Control']`
|
||||
* - `/All/Display` -> `['All', 'Display']`
|
||||
* - `/Updatable/Signal%20Input%2FOutput` -> `['Updatable', 'Signal Input', 'Output']` (**caveat**!)
|
||||
*/
|
||||
export function normalizedSegmentsOf(uri: URI): string[] {
|
||||
return uri.path.toString().split('/').slice(1).map(decodeURIComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user