Compare commits

..

3 Commits

Author SHA1 Message Date
Akos Kitta
750486a8f0 another approach
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
2022-09-15 19:12:31 +02:00
Francesco Spissu
a04527d3b8 different prefix for temp example sketch 2022-09-15 19:12:31 +02:00
Francesco Spissu
33ec67109b mark as recently opened only sketches that not includes sketch_ in the name 2022-09-15 19:12:31 +02:00
182 changed files with 5012 additions and 8927 deletions

View File

@@ -30,7 +30,7 @@ body:
description: | description: |
Which version of the Arduino IDE are you using? 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). See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds). This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@@ -68,7 +68,7 @@ body:
options: options:
- label: I searched for previous reports in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=) - label: I searched for previous reports in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
required: true required: true
- label: I verified the problem still occurs when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds) - label: I verified the problem still occurs when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
required: true required: true
- label: My report contains all necessary details - label: My report contains all necessary details
required: true required: true

View File

@@ -9,7 +9,7 @@ contact_links:
url: https://forum.arduino.cc/ url: https://forum.arduino.cc/
about: We can help you out on the Arduino Forum! about: We can help you out on the Arduino Forum!
- name: Issue report guide - name: Issue report guide
url: https://github.com/arduino/arduino-ide/blob/main/docs/contributor-guide/issues.md#issue-report-guide url: https://github.com/arduino/arduino-ide/blob/main/docs/issues.md#issue-report-guide
about: Learn about submitting issue reports to this repository. about: Learn about submitting issue reports to this repository.
- name: Contributor guide - name: Contributor guide
url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide url: https://github.com/arduino/arduino-ide/blob/main/docs/CONTRIBUTING.md#contributor-guide

View File

@@ -25,7 +25,7 @@ body:
description: | description: |
Which version of the Arduino IDE are you using? 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). See **Help > About Arduino IDE** in the Arduino IDE menus (**Arduino IDE > About Arduino IDE** on macOS).
This should be the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds). This should be the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds).
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@@ -63,7 +63,7 @@ body:
options: options:
- label: I searched for previous requests in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=) - label: I searched for previous requests in [the issue tracker](https://github.com/arduino/arduino-ide/issues?q=)
required: true required: true
- label: I verified the feature was still missing when using the latest [nightly build](https://www.arduino.cc/en/software#nightly-builds) - label: I verified the feature was still missing when using the latest [nightly build](https://github.com/arduino/arduino-ide#nightly-builds)
required: true required: true
- label: My request contains all necessary details - label: My request contains all necessary details
required: true required: true

View File

@@ -1,15 +0,0 @@
# 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 Normal file
View File

@@ -0,0 +1,131 @@
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 Normal file
View File

@@ -0,0 +1,57 @@
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 }}"

View File

@@ -55,16 +55,16 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Install Node.js 16.x - name: Install Node.js 14.x
uses: actions/setup-node@v3 uses: actions/setup-node@v1
with: with:
node-version: '16.x' node-version: '14.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Python 3.x - name: Install Python 3.x
uses: actions/setup-python@v4 uses: actions/setup-python@v2
with: with:
python-version: '3.x' python-version: '3.x'
@@ -109,7 +109,7 @@ jobs:
yarn --cwd ./electron/packager/ package yarn --cwd ./electron/packager/ package
- name: Upload [GitHub Actions] - name: Upload [GitHub Actions]
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v2
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }} name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: electron/build/dist/build-artifacts/ path: electron/build/dist/build-artifacts/
@@ -140,13 +140,13 @@ jobs:
steps: steps:
- name: Download job transfer artifact - name: Download job transfer artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v2
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }} name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }} path: ${{ env.JOB_TRANSFER_ARTIFACT }}
- name: Upload tester build artifact - name: Upload tester build artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v2
with: with:
name: ${{ matrix.artifact.name }} name: ${{ matrix.artifact.name }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }} path: ${{ env.JOB_TRANSFER_ARTIFACT }}/${{ matrix.artifact.path }}
@@ -158,7 +158,7 @@ jobs:
BODY: ${{ steps.changelog.outputs.BODY }} BODY: ${{ steps.changelog.outputs.BODY }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 # To fetch all history for all branches and tags. 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="${BODY//'%'/'%25'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}" OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\n'/'%0A'}"
OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}" OUTPUT_SAFE_BODY="${OUTPUT_SAFE_BODY//$'\r'/'%0D'}"
echo "BODY=$OUTPUT_SAFE_BODY" >> $GITHUB_OUTPUT echo "::set-output name=BODY::$OUTPUT_SAFE_BODY"
echo "$BODY" > CHANGELOG.txt echo "$BODY" > CHANGELOG.txt
- name: Upload Changelog [GitHub Actions] - name: Upload Changelog [GitHub Actions]
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v2
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }} name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: CHANGELOG.txt path: CHANGELOG.txt
@@ -199,7 +199,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download [GitHub Actions] - name: Download [GitHub Actions]
uses: actions/download-artifact@v3 uses: actions/download-artifact@v2
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }} name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }} path: ${{ env.JOB_TRANSFER_ARTIFACT }}
@@ -220,7 +220,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download [GitHub Actions] - name: Download [GitHub Actions]
uses: actions/download-artifact@v3 uses: actions/download-artifact@v2
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }} name: ${{ env.JOB_TRANSFER_ARTIFACT }}
path: ${{ env.JOB_TRANSFER_ARTIFACT }} path: ${{ env.JOB_TRANSFER_ARTIFACT }}
@@ -228,10 +228,10 @@ jobs:
- name: Get Tag - name: Get Tag
id: tag_name id: tag_name
run: | run: |
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
- name: Publish Release [GitHub] - name: Publish Release [GitHub]
uses: svenstaro/upload-release-action@2.3.0 uses: svenstaro/upload-release-action@2.2.0
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
release_name: ${{ steps.tag_name.outputs.TAG_NAME }} release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
@@ -263,6 +263,6 @@ jobs:
steps: steps:
- name: Remove unneeded job transfer artifact - name: Remove unneeded job transfer artifact
uses: geekyeggo/delete-artifact@v2 uses: geekyeggo/delete-artifact@v1
with: with:
name: ${{ env.JOB_TRANSFER_ARTIFACT }} name: ${{ env.JOB_TRANSFER_ARTIFACT }}

View File

@@ -108,7 +108,7 @@ jobs:
echo "Certificate expiration date: $EXPIRATION_DATE" echo "Certificate expiration date: $EXPIRATION_DATE"
echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION" echo "Days remaining before expiration: $DAYS_BEFORE_EXPIRATION"
echo "days=$DAYS_BEFORE_EXPIRATION" >> $GITHUB_OUTPUT echo "::set-output name=days::$DAYS_BEFORE_EXPIRATION"
- name: Check if expiration notification period has been reached - name: Check if expiration notification period has been reached
id: check-expiration id: check-expiration

View File

@@ -27,12 +27,12 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Install Node.js 16.x - name: Install Node.js 14.x
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: '16.x' node-version: '14.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Go - name: Install Go

View File

@@ -8,7 +8,7 @@ on:
env: env:
CHANGELOG_ARTIFACTS: changelog CHANGELOG_ARTIFACTS: changelog
# See: https://github.com/actions/setup-node/#readme # See: https://github.com/actions/setup-node/#readme
NODE_VERSION: 16.x NODE_VERSION: 14.x
jobs: jobs:
create-changelog: create-changelog:
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
@@ -27,7 +27,7 @@ jobs:
- name: Get Tag - name: Get Tag
id: tag_name id: tag_name
run: | run: |
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT echo ::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}
- name: Create full changelog - name: Create full changelog
id: full-changelog id: full-changelog

96
.github/workflows/github-stats.yaml vendored Normal file
View File

@@ -0,0 +1,96 @@
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 }}"

View File

@@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Install Node.js 16.x - name: Install Node.js 14.x
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: '16.x' node-version: '14.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Go - name: Install Go

View File

@@ -14,12 +14,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Install Node.js 16.x - name: Install Node.js 14.x
uses: actions/setup-node@v3 uses: actions/setup-node@v2
with: with:
node-version: '16.x' node-version: '14.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install Go - name: Install Go
@@ -45,7 +45,7 @@ jobs:
TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }} TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }}
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v3
with: with:
commit-message: Updated translation files commit-message: Updated translation files
title: Update translation files title: Update translation files

View File

@@ -27,11 +27,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Download JSON schema for labels configuration file - name: Download JSON schema for labels configuration file
id: download-schema id: download-schema
uses: carlosperate/download-file-action@v2 uses: carlosperate/download-file-action@v1
with: with:
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/arduino-tooling-gh-label-configuration-schema.json 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 location: ${{ runner.temp }}/label-configuration-schema
@@ -66,12 +66,12 @@ jobs:
steps: steps:
- name: Download - name: Download
uses: carlosperate/download-file-action@v2 uses: carlosperate/download-file-action@v1
with: with:
file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }} 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 - name: Pass configuration files to next job via workflow artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v2
with: with:
path: | path: |
*.yaml *.yaml
@@ -103,19 +103,19 @@ jobs:
run: | run: |
# Use of this flag in the github-label-sync command will cause it to only check the validity of the # Use of this flag in the github-label-sync command will cause it to only check the validity of the
# configuration. # configuration.
echo "flag=--dry-run" >> $GITHUB_OUTPUT echo "::set-output name=flag::--dry-run"
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Download configuration files artifact - name: Download configuration files artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v2
with: with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }} name: ${{ env.CONFIGURATIONS_ARTIFACT }}
path: ${{ env.CONFIGURATIONS_FOLDER }} path: ${{ env.CONFIGURATIONS_FOLDER }}
- name: Remove unneeded artifact - name: Remove unneeded artifact
uses: geekyeggo/delete-artifact@v2 uses: geekyeggo/delete-artifact@v1
with: with:
name: ${{ env.CONFIGURATIONS_ARTIFACT }} name: ${{ env.CONFIGURATIONS_ARTIFACT }}

View File

@@ -9,7 +9,7 @@ on:
env: env:
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml # See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
GO_VERSION: "1.17" GO_VERSION: "1.17"
NODE_VERSION: 16.x NODE_VERSION: 14.x
jobs: jobs:
pull-from-jsonbin: pull-from-jsonbin:

View File

@@ -1,6 +1,6 @@
{ {
"name": "arduino-ide-extension", "name": "arduino-ide-extension",
"version": "2.0.3", "version": "2.0.0",
"description": "An extension for Theia building the Arduino IDE", "description": "An extension for Theia building the Arduino IDE",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"scripts": { "scripts": {
@@ -56,7 +56,7 @@
"@types/temp": "^0.8.34", "@types/temp": "^0.8.34",
"@types/which": "^1.3.1", "@types/which": "^1.3.1",
"ajv": "^6.5.3", "ajv": "^6.5.3",
"arduino-serial-plotter-webapp": "0.2.0", "arduino-serial-plotter-webapp": "0.1.0",
"async-mutex": "^0.3.0", "async-mutex": "^0.3.0",
"atob": "^2.1.2", "atob": "^2.1.2",
"auth0-js": "^9.14.0", "auth0-js": "^9.14.0",
@@ -158,16 +158,16 @@
], ],
"arduino": { "arduino": {
"cli": { "cli": {
"version": "0.29.0" "version": "0.27.1"
}, },
"fwuploader": { "fwuploader": {
"version": "2.2.2" "version": "2.2.0"
}, },
"clangd": { "clangd": {
"version": "14.0.0" "version": "14.0.0"
}, },
"languageServer": { "languageServer": {
"version": "0.7.2" "version": "0.7.1"
} }
} }
} }

View File

@@ -42,9 +42,6 @@
const suffix = (() => { const suffix = (() => {
switch (platform) { switch (platform) {
case 'darwin': case 'darwin':
if (arch === 'arm64') {
return 'macOS_ARM64.tar.gz';
}
return 'macOS_64bit.tar.gz'; return 'macOS_64bit.tar.gz';
case 'win32': case 'win32':
return 'Windows_64bit.zip'; return 'Windows_64bit.zip';

View File

@@ -1,7 +1,7 @@
// @ts-check // @ts-check
// The version to use. // The version to use.
const version = '1.10.0'; const version = '1.9.1';
(async () => { (async () => {
const os = require('os'); const os = require('os');

View File

@@ -76,12 +76,6 @@
lsSuffix = 'macOS_64bit.tar.gz'; lsSuffix = 'macOS_64bit.tar.gz';
clangdSuffix = 'macOS_64bit'; clangdSuffix = 'macOS_64bit';
break; 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': case 'linux-x64':
clangdExecutablePath = path.join(build, 'clangd'); clangdExecutablePath = path.join(build, 'clangd');
clangFormatExecutablePath = path.join(build, 'clang-format'); clangFormatExecutablePath = path.join(build, 'clang-format');

View File

@@ -53,6 +53,8 @@ import {
DockPanelRenderer as TheiaDockPanelRenderer, DockPanelRenderer as TheiaDockPanelRenderer,
TabBarRendererFactory, TabBarRendererFactory,
ContextMenuRenderer, ContextMenuRenderer,
createTreeContainer,
TreeWidget,
} from '@theia/core/lib/browser'; } from '@theia/core/lib/browser';
import { MenuContribution } from '@theia/core/lib/common/menu'; import { MenuContribution } from '@theia/core/lib/common/menu';
import { import {
@@ -205,8 +207,12 @@ import { WorkspaceVariableContribution as TheiaWorkspaceVariableContribution } f
import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution'; import { WorkspaceVariableContribution } from './theia/workspace/workspace-variable-contribution';
import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager'; import { DebugConfigurationManager } from './theia/debug/debug-configuration-manager';
import { DebugConfigurationManager as TheiaDebugConfigurationManager } from '@theia/debug/lib/browser/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 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 { SearchInWorkspaceFactory } from './theia/search-in-workspace/search-in-workspace-factory';
import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget';
import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget';
import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider'; import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider';
import { import {
MonacoEditorFactory, MonacoEditorFactory,
@@ -331,12 +337,6 @@ import { CheckForUpdates } from './contributions/check-for-updates';
import { OutputEditorFactory } from './theia/output/output-editor-factory'; import { OutputEditorFactory } from './theia/output/output-editor-factory';
import { StartupTaskProvider } from '../electron-common/startup-task'; import { StartupTaskProvider } from '../electron-common/startup-task';
import { DeleteSketch } from './contributions/delete-sketch'; 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 registerArduinoThemes = () => {
const themes: MonacoThemeJson[] = [ const themes: MonacoThemeJson[] = [
@@ -401,7 +401,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService( bind(FrontendApplicationContribution).toService(
LibraryListWidgetFrontendContribution LibraryListWidgetFrontendContribution
); );
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
// Sketch list service // Sketch list service
bind(SketchesService) bind(SketchesService)
@@ -468,7 +467,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(FrontendApplicationContribution).toService( bind(FrontendApplicationContribution).toService(
BoardsListWidgetFrontendContribution BoardsListWidgetFrontendContribution
); );
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
// Board select dialog // Board select dialog
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope(); bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
@@ -606,6 +604,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MonacoEditorProvider).toSelf().inSingletonScope(); bind(MonacoEditorProvider).toSelf().inSingletonScope();
rebind(TheiaMonacoEditorProvider).toService(MonacoEditorProvider); 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. // Disabled reference counter in the editor manager to avoid opening the same editor (with different opener options) multiple times.
bind(EditorManager).toSelf().inSingletonScope(); bind(EditorManager).toSelf().inSingletonScope();
rebind(TheiaEditorManager).toService(EditorManager); rebind(TheiaEditorManager).toService(EditorManager);
@@ -615,6 +616,17 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.to(SearchInWorkspaceFactory) .to(SearchInWorkspaceFactory)
.inSingletonScope(); .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 // Show a disconnected status bar, when the daemon is not available
bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope(); bind(ApplicationConnectionStatusContribution).toSelf().inSingletonScope();
rebind(TheiaApplicationConnectionStatusContribution).toService( rebind(TheiaApplicationConnectionStatusContribution).toService(
@@ -749,11 +761,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
Contribution.configure(bind, OpenBoardsConfig); Contribution.configure(bind, OpenBoardsConfig);
Contribution.configure(bind, SketchFilesTracker); Contribution.configure(bind, SketchFilesTracker);
Contribution.configure(bind, CheckForUpdates); Contribution.configure(bind, CheckForUpdates);
Contribution.configure(bind, UserFields);
Contribution.configure(bind, DeleteSketch); Contribution.configure(bind, DeleteSketch);
Contribution.configure(bind, UpdateIndexes);
Contribution.configure(bind, InterfaceScale);
Contribution.configure(bind, NewCloudSketch);
bindContributionProvider(bind, StartupTaskProvider); bindContributionProvider(bind, StartupTaskProvider);
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
@@ -908,11 +916,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
id: 'arduino-sketchbook-widget', id: 'arduino-sketchbook-widget',
createWidget: () => container.get(SketchbookWidget), createWidget: () => container.get(SketchbookWidget),
})); }));
bind(SketchbookCompositeWidget).toSelf();
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
id: 'sketchbook-composite-widget',
createWidget: () => ctx.container.get(SketchbookCompositeWidget),
}));
bind(CloudSketchbookWidget).toSelf(); bind(CloudSketchbookWidget).toSelf();
rebind(SketchbookWidget).toService(CloudSketchbookWidget); rebind(SketchbookWidget).toService(CloudSketchbookWidget);

View File

@@ -249,14 +249,6 @@ export const ArduinoConfigSchema: PreferenceSchema = {
), ),
default: true, 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,
},
}, },
}; };
@@ -286,7 +278,6 @@ export interface ArduinoConfiguration {
'arduino.auth.registerUri': string; 'arduino.auth.registerUri': string;
'arduino.survey.notification': boolean; 'arduino.survey.notification': boolean;
'arduino.cli.daemon.debug': boolean; 'arduino.cli.daemon.debug': boolean;
'arduino.sketch.inoBlueprint': string;
'arduino.checkForUpdates': boolean; 'arduino.checkForUpdates': boolean;
} }

View File

@@ -34,7 +34,6 @@ export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
) { ) {
super({ ...props, maxWidth: 500 }); super({ ...props, maxWidth: 500 });
this.node.id = 'select-board-dialog-container';
this.contentNode.classList.add('select-board-dialog'); this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription()); this.contentNode.appendChild(this.createDescription());

View File

@@ -6,6 +6,7 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { import {
Board, Board,
Port, Port,
AttachedBoardsChangeEvent,
BoardWithPackage, BoardWithPackage,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
@@ -112,14 +113,11 @@ export class BoardsConfig extends React.Component<
); );
} }
}), }),
this.props.boardsServiceProvider.onAvailablePortsChanged( this.props.notificationCenter.onAttachedBoardsDidChange((event) =>
({ newState, oldState }) => { this.updatePorts(
const removedPorts = oldState.filter( event.newState.ports,
(oldPort) => AttachedBoardsChangeEvent.diff(event).detached.ports
!newState.find((newPort) => Port.sameAs(newPort, oldPort)) )
);
this.updatePorts(newState, removedPorts);
}
), ),
this.props.boardsServiceProvider.onBoardsConfigChanged( this.props.boardsServiceProvider.onBoardsConfigChanged(
({ selectedBoard, selectedPort }) => { ({ selectedBoard, selectedPort }) => {
@@ -134,7 +132,7 @@ export class BoardsConfig extends React.Component<
this.props.notificationCenter.onPlatformDidUninstall(() => this.props.notificationCenter.onPlatformDidUninstall(() =>
this.updateBoards(this.state.query) this.updateBoards(this.state.query)
), ),
this.props.notificationCenter.onIndexUpdateDidComplete(() => this.props.notificationCenter.onIndexDidUpdate(() =>
this.updateBoards(this.state.query) this.updateBoards(this.state.query)
), ),
this.props.notificationCenter.onDaemonDidStart(() => this.props.notificationCenter.onDaemonDidStart(() =>
@@ -261,12 +259,9 @@ export class BoardsConfig extends React.Component<
override render(): React.ReactNode { override render(): React.ReactNode {
return ( return (
<> <>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer( {this.renderContainer(
nls.localize('arduino/board/boards', 'boards'), 'ports',
this.renderBoards.bind(this)
)}
{this.renderContainer(
nls.localize('arduino/board/ports', 'ports'),
this.renderPorts.bind(this), this.renderPorts.bind(this),
this.renderPortsFooter.bind(this) this.renderPortsFooter.bind(this)
)} )}
@@ -304,18 +299,6 @@ 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 ( return (
<React.Fragment> <React.Fragment>
<div className="search"> <div className="search">
@@ -332,17 +315,19 @@ export class BoardsConfig extends React.Component<
/> />
<i className="fa fa-search"></i> <i className="fa fa-search"></i>
</div> </div>
{boardsList.length > 0 ? ( <div className="boards list">
<div className="boards list">{boardsList}</div> {Array.from(distinctBoards.values()).map((board) => (
) : ( <Item<BoardWithPackage>
<div className="no-result"> key={toKey(board)}
{nls.localize( item={board}
'arduino/board/noBoardsFound', label={board.name}
'No boards found for "{0}"', details={board.details}
query selected={board.selected}
)} onClick={this.selectBoard}
</div> missing={board.missing}
)} />
))}
</div>
</React.Fragment> </React.Fragment>
); );
} }
@@ -357,7 +342,7 @@ export class BoardsConfig extends React.Component<
); );
} }
return !ports.length ? ( return !ports.length ? (
<div className="no-result"> <div className="loading noselect">
{nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')} {nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')}
</div> </div>
) : ( ) : (
@@ -389,9 +374,7 @@ export class BoardsConfig extends React.Component<
defaultChecked={this.state.showAllPorts} defaultChecked={this.state.showAllPorts}
onChange={this.toggleFilterPorts} onChange={this.toggleFilterPorts}
/> />
<span> <span>Show all ports</span>
{nls.localize('arduino/board/showAllPorts', 'Show all ports')}
</span>
</label> </label>
</div> </div>
); );

View File

@@ -111,7 +111,7 @@ export class BoardsDataMenuUpdater implements FrontendApplicationContribution {
const { label } = commands.get(commandId)!; const { label } = commands.get(commandId)!;
this.menuRegistry.registerMenuAction(menuPath, { this.menuRegistry.registerMenuAction(menuPath, {
commandId, commandId,
order: String(i).padStart(4), order: `${i}`,
label, label,
}); });
return Disposable.create(() => return Disposable.create(() =>

View File

@@ -63,10 +63,7 @@ export class BoardsServiceProvider
protected readonly onAvailableBoardsChangedEmitter = new Emitter< protected readonly onAvailableBoardsChangedEmitter = new Emitter<
AvailableBoard[] AvailableBoard[]
>(); >();
protected readonly onAvailablePortsChangedEmitter = new Emitter<{ protected readonly onAvailablePortsChangedEmitter = new Emitter<Port[]>();
newState: Port[];
oldState: Port[];
}>();
private readonly inheritedConfig = new Deferred<BoardsConfig.Config>(); private readonly inheritedConfig = new Deferred<BoardsConfig.Config>();
/** /**
@@ -123,12 +120,8 @@ export class BoardsServiceProvider
const { boards: attachedBoards, ports: availablePorts } = const { boards: attachedBoards, ports: availablePorts } =
AvailablePorts.split(state); AvailablePorts.split(state);
this._attachedBoards = attachedBoards; this._attachedBoards = attachedBoards;
const oldState = this._availablePorts.slice();
this._availablePorts = availablePorts; this._availablePorts = availablePorts;
this.onAvailablePortsChangedEmitter.fire({ this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
newState: this._availablePorts.slice(),
oldState,
});
await this.reconcileAvailableBoards(); await this.reconcileAvailableBoards();
@@ -236,12 +229,8 @@ export class BoardsServiceProvider
} }
this._attachedBoards = event.newState.boards; this._attachedBoards = event.newState.boards;
const oldState = this._availablePorts.slice();
this._availablePorts = event.newState.ports; this._availablePorts = event.newState.ports;
this.onAvailablePortsChangedEmitter.fire({ this.onAvailablePortsChangedEmitter.fire(this._availablePorts);
newState: this._availablePorts.slice(),
oldState,
});
this.reconcileAvailableBoards().then(() => { this.reconcileAvailableBoards().then(() => {
const { uploadInProgress } = event; const { uploadInProgress } = event;
// avoid attempting "auto-selection" while an // avoid attempting "auto-selection" while an
@@ -409,16 +398,14 @@ export class BoardsServiceProvider
} }
async selectedBoardUserFields(): Promise<BoardUserField[]> { async selectedBoardUserFields(): Promise<BoardUserField[]> {
if (!this._boardsConfig.selectedBoard) { if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) {
return []; return [];
} }
const fqbn = this._boardsConfig.selectedBoard.fqbn; const fqbn = this._boardsConfig.selectedBoard.fqbn;
if (!fqbn) { if (!fqbn) {
return []; return [];
} }
// Protocol must be set to `default` when uploading without a port selected: const protocol = this._boardsConfig.selectedPort.protocol;
// 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 }); return await this.boardsService.getBoardUserFields({ fqbn, protocol });
} }
@@ -613,7 +600,7 @@ export class BoardsServiceProvider
boardsConfig.selectedBoard && boardsConfig.selectedBoard &&
availableBoards.every(({ selected }) => !selected) availableBoards.every(({ selected }) => !selected)
) { ) {
let port = boardsConfig.selectedPort; let port = boardsConfig.selectedPort
// If the selected board has the same port of an unknown board // If the selected board has the same port of an unknown board
// that is already in availableBoards we might get a duplicate port. // 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. // So we remove the one already in the array and add the selected one.
@@ -624,7 +611,7 @@ export class BoardsServiceProvider
// get the "Unknown board port" that we will substitute, // get the "Unknown board port" that we will substitute,
// then we can include it in the "availableBoard object" // then we can include it in the "availableBoard object"
// pushed below; to ensure addressLabel is included // pushed below; to ensure addressLabel is included
port = availableBoards[found].port; port = availableBoards[found].port
availableBoards.splice(found, 1); availableBoards.splice(found, 1);
} }
availableBoards.push({ availableBoards.push({

View File

@@ -1,11 +1,10 @@
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { import { BoardsListWidget } from './boards-list-widget';
import type {
BoardSearch, BoardSearch,
BoardsPackage, BoardsPackage,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import { URI } from '../contributions/contribution';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution'; import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
import { BoardsListWidget } from './boards-list-widget';
@injectable() @injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution< export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
@@ -25,16 +24,7 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
}); });
} }
protected canParse(uri: URI): boolean { override async initializeLayout(): Promise<void> {
try { this.openView();
BoardSearch.UriParser.parse(uri);
return true;
} catch {
return false;
}
}
protected parse(uri: URI): BoardSearch | undefined {
return BoardSearch.UriParser.parse(uri);
} }
} }

View File

@@ -15,7 +15,7 @@ import { CurrentSketch } from '../../common/protocol/sketches-service-client-imp
@injectable() @injectable()
export class AddFile extends SketchContribution { export class AddFile extends SketchContribution {
@inject(FileDialogService) @inject(FileDialogService)
private readonly fileDialogService: FileDialogService; protected readonly fileDialogService: FileDialogService;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddFile.Commands.ADD_FILE, { registry.registerCommand(AddFile.Commands.ADD_FILE, {
@@ -31,7 +31,7 @@ export class AddFile extends SketchContribution {
}); });
} }
private async addFile(): Promise<void> { protected async addFile(): Promise<void> {
const sketch = await this.sketchServiceClient.currentSketch(); const sketch = await this.sketchServiceClient.currentSketch();
if (!CurrentSketch.isValid(sketch)) { if (!CurrentSketch.isValid(sketch)) {
return; return;
@@ -41,7 +41,6 @@ export class AddFile extends SketchContribution {
canSelectFiles: true, canSelectFiles: true,
canSelectFolders: false, canSelectFolders: false,
canSelectMany: false, canSelectMany: false,
modal: true,
}); });
if (!toAddUri) { if (!toAddUri) {
return; return;

View File

@@ -17,13 +17,13 @@ import { nls } from '@theia/core/lib/common';
@injectable() @injectable()
export class AddZipLibrary extends SketchContribution { export class AddZipLibrary extends SketchContribution {
@inject(EnvVariablesServer) @inject(EnvVariablesServer)
private readonly envVariableServer: EnvVariablesServer; protected readonly envVariableServer: EnvVariablesServer;
@inject(ResponseServiceClient) @inject(ResponseServiceClient)
private readonly responseService: ResponseServiceClient; protected readonly responseService: ResponseServiceClient;
@inject(LibraryService) @inject(LibraryService)
private readonly libraryService: LibraryService; protected readonly libraryService: LibraryService;
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, { registry.registerCommand(AddZipLibrary.Commands.ADD_ZIP_LIBRARY, {
@@ -43,26 +43,23 @@ export class AddZipLibrary extends SketchContribution {
}); });
} }
private async addZipLibrary(): Promise<void> { async addZipLibrary(): Promise<void> {
const homeUri = await this.envVariableServer.getHomeDirUri(); const homeUri = await this.envVariableServer.getHomeDirUri();
const defaultPath = await this.fileService.fsPath(new URI(homeUri)); const defaultPath = await this.fileService.fsPath(new URI(homeUri));
const { canceled, filePaths } = await remote.dialog.showOpenDialog( const { canceled, filePaths } = await remote.dialog.showOpenDialog({
remote.getCurrentWindow(), title: nls.localize(
{ 'arduino/selectZip',
title: nls.localize( "Select a zip file containing the library you'd like to add"
'arduino/selectZip', ),
"Select a zip file containing the library you'd like to add" defaultPath,
), properties: ['openFile'],
defaultPath, filters: [
properties: ['openFile'], {
filters: [ name: nls.localize('arduino/library/zipLibrary', 'Library'),
{ extensions: ['zip'],
name: nls.localize('arduino/library/zipLibrary', 'Library'), },
extensions: ['zip'], ],
}, });
],
}
);
if (!canceled && filePaths.length) { if (!canceled && filePaths.length) {
const zipUri = await this.fileSystemExt.getUri(filePaths[0]); const zipUri = await this.fileSystemExt.getUri(filePaths[0]);
try { try {

View File

@@ -28,7 +28,7 @@ export class ArchiveSketch extends SketchContribution {
}); });
} }
private async archiveSketch(): Promise<void> { protected async archiveSketch(): Promise<void> {
const [sketch, config] = await Promise.all([ const [sketch, config] = await Promise.all([
this.sketchServiceClient.currentSketch(), this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(), this.configService.getConfiguration(),
@@ -43,16 +43,13 @@ export class ArchiveSketch extends SketchContribution {
const defaultPath = await this.fileService.fsPath( const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri).resolve(archiveBasename) new URI(config.sketchDirUri).resolve(archiveBasename)
); );
const { filePath, canceled } = await remote.dialog.showSaveDialog( const { filePath, canceled } = await remote.dialog.showSaveDialog({
remote.getCurrentWindow(), title: nls.localize(
{ 'arduino/sketch/saveSketchAs',
title: nls.localize( 'Save sketch folder as...'
'arduino/sketch/saveSketchAs', ),
'Save sketch folder as...' defaultPath,
), });
defaultPath,
}
);
if (!filePath || canceled) { if (!filePath || canceled) {
return; return;
} }

View File

@@ -5,6 +5,7 @@ import {
DisposableCollection, DisposableCollection,
Disposable, Disposable,
} from '@theia/core/lib/common/disposable'; } from '@theia/core/lib/common/disposable';
import { firstToUpperCase } from '../../common/utils';
import { BoardsConfig } from '../boards/boards-config'; import { BoardsConfig } from '../boards/boards-config';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsListWidget } from '../boards/boards-list-widget'; import { BoardsListWidget } from '../boards/boards-list-widget';
@@ -199,15 +200,14 @@ PID: ${PID}`;
}); });
// Installed boards // Installed boards
installedBoards.forEach((board, index) => { for (const board of installedBoards) {
const { packageId, packageName, fqbn, name, manuallyInstalled } = board; const { packageId, packageName, fqbn, name, manuallyInstalled } = board;
const packageLabel = const packageLabel =
packageName + packageName +
`${ `${manuallyInstalled
manuallyInstalled ? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)')
? nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)') : ''
: ''
}`; }`;
// Platform submenu // Platform submenu
const platformMenuPath = [...boardsPackagesGroup, packageId]; const platformMenuPath = [...boardsPackagesGroup, packageId];
@@ -240,18 +240,14 @@ PID: ${PID}`;
}; };
// Board menu // Board menu
const menuAction = { const menuAction = { commandId: id, label: name };
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.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push( this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() => this.commandRegistry.unregisterCommand(command)) Disposable.create(() => this.commandRegistry.unregisterCommand(command))
); );
this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction); this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction);
// Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively. // Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively.
}); }
// Installed ports // Installed ports
const registerPorts = ( const registerPorts = (
@@ -271,12 +267,8 @@ PID: ${PID}`;
]; ];
const placeholder = new PlaceholderMenuNode( const placeholder = new PlaceholderMenuNode(
menuPath, menuPath,
nls.localize( `${firstToUpperCase(protocol)} ports`,
'arduino/board/typeOfPorts', { order: protocolOrder.toString() }
'{0} ports',
Port.Protocols.protocolLabel(protocol)
),
{ order: protocolOrder.toString().padStart(4) }
); );
this.menuModelRegistry.registerMenuNode(menuPath, placeholder); this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push( this.toDisposeBeforeMenuRebuild.push(
@@ -287,13 +279,11 @@ PID: ${PID}`;
// First we show addresses with recognized boards connected, // First we show addresses with recognized boards connected,
// then all the rest. // then all the rest.
const sortedIDs = Object.keys(ports).sort( const sortedIDs = Object.keys(ports).sort((left: string, right: string): number => {
(left: string, right: string): number => { const [, leftBoards] = ports[left];
const [, leftBoards] = ports[left]; const [, rightBoards] = ports[right];
const [, rightBoards] = ports[right]; return rightBoards.length - leftBoards.length;
return rightBoards.length - leftBoards.length; });
}
);
for (let i = 0; i < sortedIDs.length; i++) { for (let i = 0; i < sortedIDs.length; i++) {
const portID = sortedIDs[i]; const portID = sortedIDs[i];
@@ -329,7 +319,7 @@ PID: ${PID}`;
const menuAction = { const menuAction = {
commandId: id, commandId: id,
label, label,
order: String(protocolOrder + i + 1).padStart(4), order: `${protocolOrder + i + 1}`,
}; };
this.commandRegistry.registerCommand(command, handler); this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push( this.toDisposeBeforeMenuRebuild.push(
@@ -361,7 +351,7 @@ PID: ${PID}`;
} }
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> { protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
const allBoards = await this.boardsService.getInstalledBoards(); const allBoards = await this.boardsService.searchBoards({});
return allBoards.filter(InstalledBoardWithPackage.is); return allBoards.filter(InstalledBoardWithPackage.is);
} }
} }

View File

@@ -37,17 +37,16 @@ export class CheckForIDEUpdates extends Contribution {
} }
override onReady(): void { override onReady(): void {
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
if (!checkForUpdates) {
return;
}
this.updater this.updater
.init( .init(
this.preferences.get('arduino.ide.updateChannel'), this.preferences.get('arduino.ide.updateChannel'),
this.preferences.get('arduino.ide.updateBaseUrl') this.preferences.get('arduino.ide.updateBaseUrl')
) )
.then(() => { .then(() => this.updater.checkForUpdates(true))
if (!this.preferences['arduino.checkForUpdates']) {
return;
}
return this.updater.checkForUpdates(true);
})
.then(async (updateInfo) => { .then(async (updateInfo) => {
if (!updateInfo) return; if (!updateInfo) return;
const versionToSkip = await this.localStorage.getData<string>( const versionToSkip = await this.localStorage.getData<string>(

View File

@@ -65,7 +65,7 @@ export class Close extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: Close.Commands.CLOSE.id, commandId: Close.Commands.CLOSE.id,
label: nls.localize('vscode/editor.contribution/close', 'Close'), label: nls.localize('vscode/editor.contribution/close', 'Close'),
order: '6', order: '5',
}); });
} }

View File

@@ -12,6 +12,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager'; import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { MessageService } from '@theia/core/lib/common/message-service'; 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 { open, OpenerService } from '@theia/core/lib/browser/opener-service';
import { import {
@@ -60,7 +61,6 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { BoardsDataStore } from '../boards/boards-data-store'; import { BoardsDataStore } from '../boards/boards-data-store';
import { NotificationManager } from '../theia/messages/notifications-manager'; import { NotificationManager } from '../theia/messages/notifications-manager';
import { MessageType } from '@theia/core/lib/common/message-service-protocol'; import { MessageType } from '@theia/core/lib/common/message-service-protocol';
import { WorkspaceService } from '../theia/workspace/workspace-service';
export { export {
Command, Command,

View File

@@ -49,6 +49,30 @@ export class EditContributions extends Contribution {
registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, { registry.registerCommand(EditContributions.Commands.USE_FOR_FIND, {
execute: () => this.run('editor.action.previousSelectionMatchFindAction'), 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( /* Tools */ registry.registerCommand(
EditContributions.Commands.AUTO_FORMAT, EditContributions.Commands.AUTO_FORMAT,
{ execute: () => this.run('editor.action.formatDocument') } { execute: () => this.run('editor.action.formatDocument') }
@@ -123,6 +147,23 @@ ${value}
order: '3', 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, { registry.registerMenuAction(ArduinoMenus.EDIT__FIND_GROUP, {
commandId: EditContributions.Commands.FIND.id, commandId: EditContributions.Commands.FIND.id,
label: nls.localize('vscode/findController/startFindAction', 'Find'), label: nls.localize('vscode/findController/startFindAction', 'Find'),
@@ -179,6 +220,15 @@ ${value}
when: 'editorFocus', 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({ registry.registerKeybinding({
command: EditContributions.Commands.FIND.id, command: EditContributions.Commands.FIND.id,
keybinding: 'CtrlCmd+F', keybinding: 'CtrlCmd+F',
@@ -265,6 +315,12 @@ export namespace EditContributions {
export const USE_FOR_FIND: Command = { export const USE_FOR_FIND: Command = {
id: 'arduino-for-find', 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 = { export const AUTO_FORMAT: Command = {
id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`. id: 'arduino-auto-format', // `Auto Format` should belong to `Tool`.
}; };

View File

@@ -19,25 +19,19 @@ import {
SketchContribution, SketchContribution,
CommandRegistry, CommandRegistry,
MenuModelRegistry, MenuModelRegistry,
URI,
} from './contribution'; } from './contribution';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { import { Board, SketchRef, SketchContainer } from '../../common/protocol';
Board, import { nls } from '@theia/core/lib/common/nls';
SketchRef,
SketchContainer,
SketchesError,
Sketch,
CoreService,
} from '../../common/protocol';
import { nls } from '@theia/core/lib/common';
@injectable() @injectable()
export abstract class Examples extends SketchContribution { export abstract class Examples extends SketchContribution {
@inject(CommandRegistry) @inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry; protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry) @inject(MenuModelRegistry)
private readonly menuRegistry: MenuModelRegistry; protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager) @inject(MainMenuManager)
protected readonly menuManager: MainMenuManager; protected readonly menuManager: MainMenuManager;
@@ -45,9 +39,6 @@ export abstract class Examples extends SketchContribution {
@inject(ExamplesService) @inject(ExamplesService)
protected readonly examplesService: ExamplesService; protected readonly examplesService: ExamplesService;
@inject(CoreService)
protected readonly coreService: CoreService;
@inject(BoardsServiceProvider) @inject(BoardsServiceProvider)
protected readonly boardsServiceClient: BoardsServiceProvider; protected readonly boardsServiceClient: BoardsServiceProvider;
@@ -60,16 +51,10 @@ 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 { protected handleBoardChanged(board: Board | undefined): void {
// NOOP // NOOP
} }
protected abstract update(options?: {
board?: Board | undefined;
forceRefresh?: boolean;
}): void;
override registerMenus(registry: MenuModelRegistry): void { override registerMenus(registry: MenuModelRegistry): void {
try { 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. // 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.
@@ -165,54 +150,26 @@ export abstract class Examples extends SketchContribution {
protected createHandler(uri: string): CommandHandler { protected createHandler(uri: string): CommandHandler {
return { return {
execute: async () => { execute: async () => {
const sketch = await this.clone(uri); const sketch = await this.sketchService.cloneExample(uri);
if (sketch) { return this.commandService
try { .executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch)
return this.commandService.executeCommand( .then((result) => {
OpenSketch.Commands.OPEN_SKETCH.id, const name = new URI(uri).path.base;
sketch this.sketchService.markAsRecentlyOpened({ name, sourceUri: uri }); // no await
); return result;
} 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() @injectable()
export class BuiltInExamples extends Examples { export class BuiltInExamples extends Examples {
override async onReady(): Promise<void> { override async onReady(): Promise<void> {
this.update(); // no `await` this.register(); // no `await`
} }
protected override async update(): Promise<void> { protected async register(): Promise<void> {
let sketchContainers: SketchContainer[] | undefined; let sketchContainers: SketchContainer[] | undefined;
try { try {
sketchContainers = await this.examplesService.builtIns(); sketchContainers = await this.examplesService.builtIns();
@@ -244,34 +201,29 @@ export class BuiltInExamples extends Examples {
@injectable() @injectable()
export class LibraryExamples extends Examples { export class LibraryExamples extends Examples {
@inject(NotificationCenter) @inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
private readonly queue = new PQueue({ autoStart: true, concurrency: 1 }); protected readonly queue = new PQueue({ autoStart: true, concurrency: 1 });
override onStart(): void { override onStart(): void {
this.notificationCenter.onLibraryDidInstall(() => this.update()); this.notificationCenter.onLibraryDidInstall(() => this.register());
this.notificationCenter.onLibraryDidUninstall(() => this.update()); this.notificationCenter.onLibraryDidUninstall(() => this.register());
} }
override async onReady(): Promise<void> { override async onReady(): Promise<void> {
this.update(); // no `await` this.register(); // no `await`
} }
protected override handleBoardChanged(board: Board | undefined): void { protected override handleBoardChanged(board: Board | undefined): void {
this.update({ board }); this.register(board);
} }
protected override async update( protected async register(
options: { board?: Board; forceRefresh?: boolean } = { board: Board | undefined = this.boardsServiceClient.boardsConfig
board: this.boardsServiceClient.boardsConfig.selectedBoard, .selectedBoard
}
): Promise<void> { ): Promise<void> {
const { board, forceRefresh } = options;
return this.queue.add(async () => { return this.queue.add(async () => {
this.toDispose.dispose(); this.toDispose.dispose();
if (forceRefresh) {
await this.coreService.refresh();
}
const fqbn = board?.fqbn; const fqbn = board?.fqbn;
const name = board?.name; const name = board?.name;
// Shows all examples when no board is selected, or the platform of the currently selected board is not installed. // Shows all examples when no board is selected, or the platform of the currently selected board is not installed.

View File

@@ -7,8 +7,6 @@ import {
} from '../../common/protocol'; } from '../../common/protocol';
import { Contribution } from './contribution'; import { Contribution } from './contribution';
const Arduino_BuiltIn = 'Arduino_BuiltIn';
@injectable() @injectable()
export class FirstStartupInstaller extends Contribution { export class FirstStartupInstaller extends Contribution {
@inject(LocalStorageService) @inject(LocalStorageService)
@@ -27,8 +25,8 @@ export class FirstStartupInstaller extends Contribution {
id: 'arduino:avr', id: 'arduino:avr',
}); });
const builtInLibrary = ( const builtInLibrary = (
await this.libraryService.search({ query: Arduino_BuiltIn }) 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. )[0];
let avrPackageError: Error | undefined; let avrPackageError: Error | undefined;
let builtInLibraryError: Error | undefined; let builtInLibraryError: Error | undefined;
@@ -86,7 +84,7 @@ export class FirstStartupInstaller extends Contribution {
} }
if (builtInLibraryError) { if (builtInLibraryError) {
this.messageService.error( this.messageService.error(
`Could not install ${Arduino_BuiltIn} library: ${builtInLibraryError}` `Could not install ${builtInLibrary.name} library: ${builtInLibraryError}`
); );
} }

View File

@@ -16,7 +16,7 @@ export class IndexesUpdateProgress extends Contribution {
| undefined; | undefined;
override onStart(): void { override onStart(): void {
this.notificationCenter.onIndexUpdateWillStart(({ progressId }) => this.notificationCenter.onIndexWillUpdate((progressId) =>
this.getOrCreateProgress(progressId) this.getOrCreateProgress(progressId)
); );
this.notificationCenter.onIndexUpdateDidProgress((progress) => { this.notificationCenter.onIndexUpdateDidProgress((progress) => {
@@ -24,7 +24,7 @@ export class IndexesUpdateProgress extends Contribution {
delegate.report(progress) delegate.report(progress)
); );
}); });
this.notificationCenter.onIndexUpdateDidComplete(({ progressId }) => { this.notificationCenter.onIndexDidUpdate((progressId) => {
this.cancelProgress(progressId); this.cancelProgress(progressId);
}); });
this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => { this.notificationCenter.onIndexUpdateDidFail(({ progressId, message }) => {

View File

@@ -1,228 +0,0 @@
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;
}
}

View File

@@ -1,372 +0,0 @@
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;
}
}
}
}

View File

@@ -1,6 +1,7 @@
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
SketchContribution, SketchContribution,
URI, URI,
@@ -16,12 +17,17 @@ export class NewSketch extends SketchContribution {
registry.registerCommand(NewSketch.Commands.NEW_SKETCH, { registry.registerCommand(NewSketch.Commands.NEW_SKETCH, {
execute: () => this.newSketch(), 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 { override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: NewSketch.Commands.NEW_SKETCH.id, commandId: NewSketch.Commands.NEW_SKETCH.id,
label: nls.localize('arduino/sketch/new', 'New Sketch'), label: nls.localize('arduino/sketch/new', 'New'),
order: '0', order: '0',
}); });
} }
@@ -48,5 +54,8 @@ export namespace NewSketch {
export const NEW_SKETCH: Command = { export const NEW_SKETCH: Command = {
id: 'arduino-new-sketch', id: 'arduino-new-sketch',
}; };
export const NEW_SKETCH__TOOLBAR: Command = {
id: 'arduino-new-sketch--toolbar',
};
} }
} }

View File

@@ -15,7 +15,7 @@ import { MainMenuManager } from '../../common/main-menu-manager';
import { OpenSketch } from './open-sketch'; import { OpenSketch } from './open-sketch';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { SketchesError } from '../../common/protocol'; import { ExampleRef } from '../../common/protocol';
@injectable() @injectable()
export class OpenRecentSketch extends SketchContribution { export class OpenRecentSketch extends SketchContribution {
@@ -34,7 +34,7 @@ export class OpenRecentSketch extends SketchContribution {
@inject(NotificationCenter) @inject(NotificationCenter)
protected readonly notificationCenter: NotificationCenter; protected readonly notificationCenter: NotificationCenter;
protected toDispose = new DisposableCollection(); protected toDisposeBeforeRegister = new Map<string, DisposableCollection>();
override onStart(): void { override onStart(): void {
this.notificationCenter.onRecentSketchesDidChange(({ sketches }) => this.notificationCenter.onRecentSketchesDidChange(({ sketches }) =>
@@ -43,12 +43,8 @@ export class OpenRecentSketch extends SketchContribution {
} }
override async onReady(): Promise<void> { override async onReady(): Promise<void> {
this.update();
}
private update(forceUpdate?: boolean): void {
this.sketchService this.sketchService
.recentlyOpenedSketches(forceUpdate) .recentlyOpenedSketches()
.then((sketches) => this.refreshMenu(sketches)); .then((sketches) => this.refreshMenu(sketches));
} }
@@ -60,31 +56,29 @@ export class OpenRecentSketch extends SketchContribution {
); );
} }
private refreshMenu(sketches: Sketch[]): void { private refreshMenu(sketches: (Sketch | ExampleRef)[]): void {
this.register(sketches); this.register(sketches);
this.mainMenuManager.update(); this.mainMenuManager.update();
} }
protected register(sketches: Sketch[]): void { protected register(sketches: (Sketch | ExampleRef)[]): void {
const order = 0; const order = 0;
this.toDispose.dispose();
for (const sketch of sketches) { for (const sketch of sketches) {
const { uri } = sketch; const uri = Sketch.is(sketch) ? sketch.uri : sketch.sourceUri;
const toDispose = this.toDisposeBeforeRegister.get(uri);
if (toDispose) {
toDispose.dispose();
}
const command = { id: `arduino-open-recent--${uri}` }; const command = { id: `arduino-open-recent--${uri}` };
const handler = { const handler = {
execute: async () => { execute: async () => {
try { const toOpen = Sketch.is(sketch)
await this.commandRegistry.executeCommand( ? sketch
OpenSketch.Commands.OPEN_SKETCH.id, : await this.sketchService.cloneExample(sketch.sourceUri);
sketch this.commandRegistry.executeCommand(
); OpenSketch.Commands.OPEN_SKETCH.id,
} catch (err) { toOpen
if (SketchesError.NotFound.is(err)) { );
this.update(true);
} else {
throw err;
}
}
}, },
}; };
this.commandRegistry.registerCommand(command, handler); this.commandRegistry.registerCommand(command, handler);
@@ -96,7 +90,8 @@ export class OpenRecentSketch extends SketchContribution {
order: String(order), order: String(order),
} }
); );
this.toDispose.pushAll([ this.toDisposeBeforeRegister.set(
uri,
new DisposableCollection( new DisposableCollection(
Disposable.create(() => Disposable.create(() =>
this.commandRegistry.unregisterCommand(command) this.commandRegistry.unregisterCommand(command)
@@ -104,8 +99,8 @@ export class OpenRecentSketch extends SketchContribution {
Disposable.create(() => Disposable.create(() =>
this.menuRegistry.unregisterMenuAction(command) this.menuRegistry.unregisterMenuAction(command)
) )
), )
]); );
} }
} }
} }

View File

@@ -2,7 +2,7 @@ import { nls } from '@theia/core/lib/common/nls';
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager'; import type { EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager';
import { Later } from '../../common/nls'; import { Later } from '../../common/nls';
import { Sketch, SketchesError } from '../../common/protocol'; import { SketchesError } from '../../common/protocol';
import { import {
Command, Command,
CommandRegistry, CommandRegistry,
@@ -10,11 +10,6 @@ import {
URI, URI,
} from './contribution'; } from './contribution';
import { SaveAsSketch } from './save-as-sketch'; 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() @injectable()
export class OpenSketchFiles extends SketchContribution { export class OpenSketchFiles extends SketchContribution {
@@ -60,25 +55,9 @@ 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) { } 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)) { if (SketchesError.NotFound.is(err)) {
return this.openFallbackSketch(); this.openFallbackSketch();
} else { } else {
console.error(err); console.error(err);
const message = const message =
@@ -92,31 +71,6 @@ 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> { private async openFallbackSketch(): Promise<void> {
const sketch = await this.sketchService.createNewSketch(); const sketch = await this.sketchService.createNewSketch();
this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true }); this.workspaceService.open(new URI(sketch.uri), { preserveWindow: true });
@@ -130,79 +84,16 @@ export class OpenSketchFiles extends SketchContribution {
const widget = this.editorManager.all.find( const widget = this.editorManager.all.find(
(widget) => widget.editor.uri.toString() === uri (widget) => widget.editor.uri.toString() === uri
); );
if (widget && !forceOpen) { if (!widget || forceOpen) {
return widget; return this.editorManager.open(
}
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), new URI(uri),
options ?? { options ?? {
mode: 'reveal', mode: 'reveal',
preview: false, preview: false,
counter: 0, 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 { export namespace OpenSketchFiles {

View File

@@ -1,50 +1,115 @@
import { inject, injectable } from '@theia/core/shared/inversify';
import * as remote from '@theia/core/electron-shared/@electron/remote'; import * as remote from '@theia/core/electron-shared/@electron/remote';
import { nls } from '@theia/core/lib/common/nls'; import { MaybePromise } from '@theia/core/lib/common/types';
import { injectable } from '@theia/core/shared/inversify'; import { Widget, ContextMenuRenderer } from '@theia/core/lib/browser';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { import {
SketchesError, Disposable,
SketchesService, DisposableCollection,
SketchRef, } from '@theia/core/lib/common/disposable';
} from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
SketchContribution,
Sketch,
URI,
Command, Command,
CommandRegistry, CommandRegistry,
KeybindingRegistry,
MenuModelRegistry, MenuModelRegistry,
Sketch, KeybindingRegistry,
SketchContribution,
URI,
} from './contribution'; } from './contribution';
import { ExamplesService } from '../../common/protocol/examples-service';
export type SketchLocation = string | URI | SketchRef; import { BuiltInExamples } from './examples';
export namespace SketchLocation { import { Sketchbook } from './sketchbook';
export function toUri(location: SketchLocation): URI { import { SketchContainer } from '../../common/protocol';
if (typeof location === 'string') { import { nls } from '@theia/core/lib/common';
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() @injectable()
export class OpenSketch extends SketchContribution { 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 { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, { registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH, {
execute: async (arg) => { execute: (arg) =>
const toOpen = !SketchLocation.is(arg) Sketch.is(arg) ? this.openSketch(arg) : this.openSketch(),
? await this.selectSketch() });
: arg; registry.registerCommand(OpenSketch.Commands.OPEN_SKETCH__TOOLBAR, {
if (toOpen) { isVisible: (widget) =>
return this.openSketch(toOpen); 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);
} }
}, },
}); });
@@ -54,7 +119,7 @@ export class OpenSketch extends SketchContribution {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: OpenSketch.Commands.OPEN_SKETCH.id, commandId: OpenSketch.Commands.OPEN_SKETCH.id,
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'), label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
order: '2', order: '1',
}); });
} }
@@ -65,40 +130,30 @@ export class OpenSketch extends SketchContribution {
}); });
} }
private async openSketch(toOpen: SketchLocation | undefined): Promise<void> { async openSketch(
if (!toOpen) { toOpen: MaybePromise<Sketch | undefined> = this.selectSketch()
return; ): Promise<void> {
const sketch = await toOpen;
if (sketch) {
this.workspaceService.open(new URI(sketch.uri));
} }
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);
} }
private async selectSketch(): Promise<Sketch | undefined> { protected async selectSketch(): Promise<Sketch | undefined> {
const config = await this.configService.getConfiguration(); const config = await this.configService.getConfiguration();
const defaultPath = await this.fileService.fsPath( const defaultPath = await this.fileService.fsPath(
new URI(config.sketchDirUri) new URI(config.sketchDirUri)
); );
const { filePaths } = await remote.dialog.showOpenDialog( const { filePaths } = await remote.dialog.showOpenDialog({
remote.getCurrentWindow(), defaultPath,
{ properties: ['createDirectory', 'openFile'],
defaultPath, filters: [
properties: ['createDirectory', 'openFile'], {
filters: [ name: nls.localize('arduino/sketch/sketch', 'Sketch'),
{ extensions: ['ino', 'pde'],
name: nls.localize('arduino/sketch/sketch', 'Sketch'), },
extensions: ['ino', 'pde'], ],
}, });
],
}
);
if (!filePaths.length) { if (!filePaths.length) {
return undefined; return undefined;
} }
@@ -114,11 +169,45 @@ export class OpenSketch extends SketchContribution {
return sketch; return sketch;
} }
if (Sketch.isSketchFile(sketchFileUri)) { if (Sketch.isSketchFile(sketchFileUri)) {
return promptMoveSketch(sketchFileUri, { const name = new URI(sketchFileUri).path.name;
fileService: this.fileService, const nameWithExt = this.labelProvider.getName(new URI(sketchFileUri));
sketchService: this.sketchService, const { response } = await remote.dialog.showMessageBox({
labelProvider: this.labelProvider, 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 = 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());
}
} }
} }
} }
@@ -128,57 +217,8 @@ export namespace OpenSketch {
export const OPEN_SKETCH: Command = { export const OPEN_SKETCH: Command = {
id: 'arduino-open-sketch', 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());
} }
} }

View File

@@ -50,7 +50,7 @@ export class SaveAsSketch extends SketchContribution {
/** /**
* Resolves `true` if the sketch was successfully saved as something. * Resolves `true` if the sketch was successfully saved as something.
*/ */
private async saveAs( async saveAs(
{ {
execOnlyIfTemp, execOnlyIfTemp,
openAfterMove, openAfterMove,
@@ -58,10 +58,7 @@ export class SaveAsSketch extends SketchContribution {
markAsRecentlyOpened, markAsRecentlyOpened,
}: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT }: SaveAsSketch.Options = SaveAsSketch.Options.DEFAULT
): Promise<boolean> { ): Promise<boolean> {
const [sketch, configuration] = await Promise.all([ const sketch = await this.sketchServiceClient.currentSketch();
this.sketchServiceClient.currentSketch(),
this.configService.getConfiguration(),
]);
if (!CurrentSketch.isValid(sketch)) { if (!CurrentSketch.isValid(sketch)) {
return false; return false;
} }
@@ -71,38 +68,27 @@ export class SaveAsSketch extends SketchContribution {
return false; 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 does not exist, propose a `directories.user`/${sketch.name} path
// If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss} // If target exists, propose `directories.user`/${sketch.name}_copy_${yyyymmddHHMMss}
const defaultUri = containerDirUri.resolve( const sketchDirUri = new URI(
(await this.configService.getConfiguration()).sketchDirUri
);
const exists = await this.fileService.exists(
sketchDirUri.resolve(sketch.name)
);
const defaultUri = sketchDirUri.resolve(
exists exists
? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}` ? `${sketch.name}_copy_${dateFormat(new Date(), 'yyyymmddHHMMss')}`
: sketch.name : sketch.name
); );
const defaultPath = await this.fileService.fsPath(defaultUri); const defaultPath = await this.fileService.fsPath(defaultUri);
const { filePath, canceled } = await remote.dialog.showSaveDialog( const { filePath, canceled } = await remote.dialog.showSaveDialog({
remote.getCurrentWindow(), title: nls.localize(
{ 'arduino/sketch/saveFolderAs',
title: nls.localize( 'Save sketch folder as...'
'arduino/sketch/saveFolderAs', ),
'Save sketch folder as...' defaultPath,
), });
defaultPath,
}
);
if (!filePath || canceled) { if (!filePath || canceled) {
return false; return false;
} }

View File

@@ -1,6 +1,7 @@
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { SaveAsSketch } from './save-as-sketch'; import { SaveAsSketch } from './save-as-sketch';
import { import {
SketchContribution, SketchContribution,
@@ -18,13 +19,19 @@ export class SaveSketch extends SketchContribution {
registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, { registry.registerCommand(SaveSketch.Commands.SAVE_SKETCH, {
execute: () => this.saveSketch(), 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 { override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, { registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
commandId: SaveSketch.Commands.SAVE_SKETCH.id, commandId: SaveSketch.Commands.SAVE_SKETCH.id,
label: nls.localize('vscode/fileCommands/save', 'Save'), label: nls.localize('vscode/fileCommands/save', 'Save'),
order: '7', order: '6',
}); });
} }
@@ -61,5 +68,8 @@ export namespace SaveSketch {
export const SAVE_SKETCH: Command = { export const SAVE_SKETCH: Command = {
id: 'arduino-save-sketch', id: 'arduino-save-sketch',
}; };
export const SAVE_SKETCH__TOOLBAR: Command = {
id: 'arduino-save-sketch--toolbar',
};
} }
} }

View File

@@ -176,7 +176,7 @@ export class SketchControl extends SketchContribution {
{ {
commandId: command.id, commandId: command.id,
label: this.labelProvider.getName(uri), label: this.labelProvider.getName(uri),
order: String(i).padStart(4), order: `${i}`,
} }
); );
this.toDisposeBeforeCreateNewContextMenu.push( this.toDisposeBeforeCreateNewContextMenu.push(

View File

@@ -1,14 +1,32 @@
import { injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { CommandHandler } from '@theia/core/lib/common/command'; import { CommandHandler } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from './contribution'; import { CommandRegistry, MenuModelRegistry } from './contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager';
import { NotificationCenter } from '../notification-center';
import { Examples } from './examples'; import { Examples } from './examples';
import { SketchContainer, SketchesError } from '../../common/protocol'; import {
SketchContainer,
SketchesError,
SketchRef,
} from '../../common/protocol';
import { OpenSketch } from './open-sketch'; import { OpenSketch } from './open-sketch';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common';
@injectable() @injectable()
export class Sketchbook extends Examples { 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 { override onStart(): void {
this.sketchServiceClient.onSketchbookDidChange(() => this.update()); this.sketchServiceClient.onSketchbookDidChange(() => this.update());
} }
@@ -17,10 +35,10 @@ export class Sketchbook extends Examples {
this.update(); this.update();
} }
protected override update(): void { private update() {
this.sketchService.getSketches({}).then((container) => { this.sketchService.getSketches({}).then((container) => {
this.register(container); this.register(container);
this.menuManager.update(); this.mainMenuManager.update();
}); });
} }
@@ -32,7 +50,7 @@ export class Sketchbook extends Examples {
); );
} }
private register(container: SketchContainer): void { protected register(container: SketchContainer): void {
this.toDispose.dispose(); this.toDispose.dispose();
this.registerRecursively( this.registerRecursively(
[...container.children, ...container.sketches], [...container.children, ...container.sketches],
@@ -44,19 +62,24 @@ export class Sketchbook extends Examples {
protected override createHandler(uri: string): CommandHandler { protected override createHandler(uri: string): CommandHandler {
return { return {
execute: async () => { execute: async () => {
let sketch: SketchRef | undefined = undefined;
try { try {
await this.commandService.executeCommand( sketch = await this.sketchService.loadSketch(uri);
OpenSketch.Commands.OPEN_SKETCH.id,
uri
);
} catch (err) { } catch (err) {
if (SketchesError.NotFound.is(err)) { if (SketchesError.NotFound.is(err)) {
// Force update the menu items to remove the absent sketch. // 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(); this.update();
} else {
throw err;
} }
} }
if (sketch) {
await this.commandService.executeCommand(
OpenSketch.Commands.OPEN_SKETCH.id,
sketch
);
}
}, },
}; };
} }

View File

@@ -1,193 +0,0 @@
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',
};
}
}

View File

@@ -1,7 +1,7 @@
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event'; import { Emitter } from '@theia/core/lib/common/event';
import { CoreService, Port } from '../../common/protocol'; import { BoardUserField, CoreService, Port } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import { import {
Command, Command,
@@ -11,36 +11,96 @@ import {
TabBarToolbarRegistry, TabBarToolbarRegistry,
CoreServiceContribution, CoreServiceContribution,
} from './contribution'; } from './contribution';
import { deepClone, nls } from '@theia/core/lib/common'; import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog';
import { deepClone, DisposableCollection, nls } from '@theia/core/lib/common';
import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl'; import { CurrentSketch } from '../../common/protocol/sketches-service-client-impl';
import type { VerifySketchParams } from './verify-sketch'; import type { VerifySketchParams } from './verify-sketch';
import { UserFields } from './user-fields';
@injectable() @injectable()
export class UploadSketch extends CoreServiceContribution { 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 onDidChangeEmitter = new Emitter<void>();
private readonly onDidChange = this.onDidChangeEmitter.event; private readonly onDidChange = this.onDidChangeEmitter.event;
private uploadInProgress = false; private uploadInProgress = false;
@inject(UserFields) protected override init(): void {
private readonly userFields: UserFields; 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;
}
override registerCommands(registry: CommandRegistry): void { override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, { registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, {
execute: async () => { execute: async () => {
if (await this.userFields.checkUserFieldsDialog()) { const key = this.selectedFqbnAddress();
this.uploadSketch(); 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);
} }
this.uploadSketch();
}, },
isEnabled: () => !this.uploadInProgress, isEnabled: () => !this.uploadInProgress,
}); });
registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, { registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, {
execute: async () => { execute: async () => {
if (await this.userFields.checkUserFieldsDialog(true)) { const key = this.selectedFqbnAddress();
this.uploadSketch(); if (!key) {
return;
} }
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.userFields.isRequired(), isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields,
}); });
registry.registerCommand( registry.registerCommand(
UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER, UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER,
@@ -60,20 +120,45 @@ export class UploadSketch extends CoreServiceContribution {
} }
override registerMenus(registry: MenuModelRegistry): void { override registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { this.menuActionsDisposables.dispose();
commandId: UploadSketch.Commands.UPLOAD_SKETCH.id, this.menuActionsDisposables.push(
label: nls.localize('arduino/sketch/upload', 'Upload'), registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
order: '1', 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( if (this.boardRequiresUserFields) {
'arduino/sketch/uploadUsingProgrammer', this.menuActionsDisposables.push(
'Upload Using Programmer' registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
), commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id,
order: '3', 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',
})
);
} }
override registerKeybindings(registry: KeybindingRegistry): void { override registerKeybindings(registry: KeybindingRegistry): void {
@@ -130,7 +215,18 @@ export class UploadSketch extends CoreServiceContribution {
return; return;
} }
if (!this.userFields.checkUserFieldsForUpload()) { // 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"
)
);
return; return;
} }
@@ -146,7 +242,6 @@ export class UploadSketch extends CoreServiceContribution {
{ timeout: 3000 } { timeout: 3000 }
); );
} catch (e) { } catch (e) {
this.userFields.notifyFailedWithError(e);
this.handleError(e); this.handleError(e);
} finally { } finally {
this.uploadInProgress = false; this.uploadInProgress = false;
@@ -163,7 +258,7 @@ export class UploadSketch extends CoreServiceContribution {
if (!CurrentSketch.isValid(sketch)) { if (!CurrentSketch.isValid(sketch)) {
return undefined; return undefined;
} }
const userFields = this.userFields.getUserFields(); const userFields = this.userFields();
const { boardsConfig } = this.boardsServiceProvider; const { boardsConfig } = this.boardsServiceProvider;
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] = const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([ await Promise.all([
@@ -206,6 +301,10 @@ export class UploadSketch extends CoreServiceContribution {
return port; 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 * Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
* `VENDOR:ARCHITECTURE:BOARD_ID` format. * `VENDOR:ARCHITECTURE:BOARD_ID` format.
@@ -229,7 +328,7 @@ export namespace UploadSketch {
id: 'arduino-upload-with-configuration-sketch', id: 'arduino-upload-with-configuration-sketch',
label: nls.localize( label: nls.localize(
'arduino/sketch/configureAndUpload', 'arduino/sketch/configureAndUpload',
'Configure and Upload' 'Configure And Upload'
), ),
category: 'Arduino', category: 'Arduino',
}; };

View File

@@ -1,147 +0,0 @@
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;
}
}
}

View File

@@ -7,9 +7,7 @@ export namespace CreateUri {
export const scheme = 'arduino-create'; export const scheme = 'arduino-create';
export const root = toUri(posix.sep); export const root = toUri(posix.sep);
export function toUri( export function toUri(posixPathOrResource: string | Create.Resource): URI {
posixPathOrResource: string | Create.Resource | Create.Sketch
): URI {
const posixPath = const posixPath =
typeof posixPathOrResource === 'string' typeof posixPathOrResource === 'string'
? posixPathOrResource ? posixPathOrResource

View File

@@ -171,9 +171,6 @@ export class UploadCertificateDialog extends AbstractDialog<void> {
Widget.detach(this.widget); Widget.detach(this.widget);
} }
Widget.attach(this.widget, this.contentNode); Widget.attach(this.widget, this.contentNode);
const firstButton = this.widget.node.querySelector('button');
firstButton?.focus();
this.widget.busyCallback = this.busyCallback.bind(this); this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg); super.onAfterAttach(msg);
this.update(); this.update();

View File

@@ -115,8 +115,6 @@ export class UploadFirmwareDialog extends AbstractDialog<void> {
Widget.detach(this.widget); Widget.detach(this.widget);
} }
Widget.attach(this.widget, this.contentNode); Widget.attach(this.widget, this.contentNode);
const firstButton = this.widget.node.querySelector('button');
firstButton?.focus();
this.widget.busyCallback = this.busyCallback.bind(this); this.widget.busyCallback = this.busyCallback.bind(this);
super.onAfterAttach(msg); super.onAfterAttach(msg);
this.update(); this.update();

View File

@@ -10,7 +10,6 @@ import { FileDialogService } from '@theia/filesystem/lib/browser/file-dialog/fil
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { import {
AdditionalUrls, AdditionalUrls,
CompilerWarnings,
CompilerWarningLiterals, CompilerWarningLiterals,
Network, Network,
ProxySettings, ProxySettings,
@@ -23,22 +22,14 @@ import {
LanguageInfo, LanguageInfo,
} from '@theia/core/lib/common/i18n/localization'; } from '@theia/core/lib/common/i18n/localization';
import SettingsStepInput from './settings-step-input'; import SettingsStepInput from './settings-step-input';
import { InterfaceScale } from '../../contributions/interface-scale';
const maxScale = InterfaceScale.ZoomLevel.toPercentage( const maxScale = 280;
InterfaceScale.ZoomLevel.MAX const minScale = -60;
); const scaleStep = 20;
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< export class SettingsComponent extends React.Component<
SettingsComponent.Props, SettingsComponent.Props,
SettingsComponent.State SettingsComponent.State
@@ -180,8 +171,7 @@ export class SettingsComponent extends React.Component<
<div className="column"> <div className="column">
<div className="flex-line"> <div className="flex-line">
<SettingsStepInput <SettingsStepInput
key={`font-size-stepper-${String(this.state.editorFontSize)}`} value={this.state.editorFontSize}
initialValue={this.state.editorFontSize}
setSettingsStateValue={this.setFontSize} setSettingsStateValue={this.setFontSize}
step={fontSizeStep} step={fontSizeStep}
maxValue={maxFontSize} maxValue={maxFontSize}
@@ -200,18 +190,13 @@ export class SettingsComponent extends React.Component<
</label> </label>
<div> <div>
<SettingsStepInput <SettingsStepInput
key={`scale-stepper-${String(scalePercentage)}`} value={scalePercentage}
initialValue={scalePercentage}
setSettingsStateValue={this.setInterfaceScale} setSettingsStateValue={this.setInterfaceScale}
step={scaleStep} step={scaleStep}
maxValue={maxScale} maxValue={maxScale}
minValue={minScale} minValue={minScale}
unitOfMeasure="%" unitOfMeasure="%"
classNames={{ classNames={{ input: 'theia-input small with-margin' }}
input: 'theia-input small with-margin',
buttonsContainer:
'settings-step-input-buttons-container-perc',
}}
/> />
</div> </div>
</div> </div>
@@ -275,7 +260,7 @@ export class SettingsComponent extends React.Component<
> >
{CompilerWarningLiterals.map((value) => ( {CompilerWarningLiterals.map((value) => (
<option key={value} value={value}> <option key={value} value={value}>
{CompilerWarnings.labelOf(value)} {value}
</option> </option>
))} ))}
</select> </select>
@@ -413,22 +398,10 @@ export class SettingsComponent extends React.Component<
</form> </form>
<div className="flex-line proxy-settings"> <div className="flex-line proxy-settings">
<div className="column"> <div className="column">
<div className="flex-line">{`${nls.localize( <div className="flex-line">Host name:</div>
'arduino/preferences/proxySettings/hostname', <div className="flex-line">Port number:</div>
'Host name' <div className="flex-line">Username:</div>
)}:`}</div> <div className="flex-line">Password:</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>
<div className="column stretch"> <div className="column stretch">
<div className="flex-line"> <div className="flex-line">
@@ -529,7 +502,6 @@ export class SettingsComponent extends React.Component<
canSelectFiles: false, canSelectFiles: false,
canSelectMany: false, canSelectMany: false,
canSelectFolders: true, canSelectFolders: true,
modal: true,
}); });
if (uri) { if (uri) {
const sketchbookPath = await this.props.fileService.fsPath(uri); const sketchbookPath = await this.props.fileService.fsPath(uri);
@@ -568,7 +540,8 @@ export class SettingsComponent extends React.Component<
}; };
private setInterfaceScale = (percentage: number) => { private setInterfaceScale = (percentage: number) => {
const interfaceScale = InterfaceScale.ZoomLevel.fromPercentage(percentage); const interfaceScale = (percentage - 100) / 20;
this.setState({ interfaceScale }); this.setState({ interfaceScale });
}; };

View File

@@ -155,6 +155,7 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
this.textArea = document.createElement('textarea'); this.textArea = document.createElement('textarea');
this.textArea.className = 'theia-input'; this.textArea.className = 'theia-input';
this.textArea.setAttribute('style', 'flex: 0;');
this.textArea.value = urls this.textArea.value = urls
.filter((url) => url.trim()) .filter((url) => url.trim())
.filter((url) => !!url) .filter((url) => !!url)
@@ -180,10 +181,10 @@ export class AdditionalUrlsDialog extends AbstractDialog<string[]> {
); );
this.contentNode.appendChild(anchor); this.contentNode.appendChild(anchor);
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
this.appendCloseButton( this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel') nls.localize('vscode/issueMainService/cancel', 'Cancel')
); );
this.appendAcceptButton(nls.localize('vscode/issueMainService/ok', 'OK'));
} }
get value(): string[] { get value(): string[] {

View File

@@ -2,7 +2,7 @@ import * as React from '@theia/core/shared/react';
import classnames from 'classnames'; import classnames from 'classnames';
interface SettingsStepInputProps { interface SettingsStepInputProps {
initialValue: number; value: number;
setSettingsStateValue: (value: number) => void; setSettingsStateValue: (value: number) => void;
step: number; step: number;
maxValue: number; maxValue: number;
@@ -15,7 +15,7 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
props: SettingsStepInputProps props: SettingsStepInputProps
) => { ) => {
const { const {
initialValue, value,
setSettingsStateValue, setSettingsStateValue,
step, step,
maxValue, maxValue,
@@ -24,35 +24,18 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
classNames, classNames,
} = props; } = 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 => { const clamp = (value: number, min: number, max: number): number => {
return Math.min(Math.max(value, min), max); return Math.min(Math.max(value, min), max);
}; };
const resetToInitialState = (): void => {
setValueState({
currentValue: initialValue,
isEmptyString: false,
});
};
const onStep = ( const onStep = (
roundingOperation: 'ceil' | 'floor', roundingOperation: 'ceil' | 'floor',
stepOperation: (a: number, b: number) => number stepOperation: (a: number, b: number) => number
): void => { ): void => {
const valueRoundedToScale = const valueRoundedToScale = Math[roundingOperation](value / step) * step;
Math[roundingOperation](currentValue / step) * step;
const calculatedValue = const calculatedValue =
valueRoundedToScale === currentValue valueRoundedToScale === value
? stepOperation(currentValue, step) ? stepOperation(value, step)
: valueRoundedToScale; : valueRoundedToScale;
const newValue = clamp(calculatedValue, minValue, maxValue); const newValue = clamp(calculatedValue, minValue, maxValue);
@@ -69,53 +52,33 @@ const SettingsStepInput: React.FC<SettingsStepInputProps> = (
const onUserInput = (event: React.ChangeEvent<HTMLInputElement>): void => { const onUserInput = (event: React.ChangeEvent<HTMLInputElement>): void => {
const { value: eventValue } = event.target; const { value: eventValue } = event.target;
setValueState({
currentValue: Number(eventValue),
isEmptyString: eventValue === '',
});
};
/* Prevent the user from entering invalid values */ if (eventValue === '') {
const onBlur = (event: React.FocusEvent): void => { setSettingsStateValue(0);
if (
(currentValue === initialValue && !isEmptyString) ||
event.currentTarget.contains(event.relatedTarget as Node)
) {
return;
} }
const clampedValue = clamp(currentValue, minValue, maxValue); const number = Number(eventValue);
if (clampedValue === initialValue || isNaN(currentValue) || isEmptyString) {
resetToInitialState();
return;
}
setSettingsStateValue(clampedValue); if (!isNaN(number) && number !== value) {
const newValue = clamp(number, minValue, maxValue);
setSettingsStateValue(newValue);
}
}; };
const valueIsNotWithinRange = const upDisabled = value >= maxValue;
currentValue < minValue || currentValue > maxValue; const downDisabled = value <= minValue;
const isDisabledException =
valueIsNotWithinRange || isEmptyString || isNaN(currentValue);
const upDisabled = isDisabledException || currentValue >= maxValue;
const downDisabled = isDisabledException || currentValue <= minValue;
return ( return (
<div className="settings-step-input-container" onBlur={onBlur}> <div className="settings-step-input-container">
<input <input
className={classnames('settings-step-input-element', classNames?.input)} className={classnames('settings-step-input-element', classNames?.input)}
value={isEmptyString ? '' : String(currentValue)} value={value.toString()}
onChange={onUserInput} onChange={onUserInput}
type="number" type="number"
pattern="[0-9]+" pattern="[0-9]+"
/> />
<div <div className="settings-step-input-buttons-container">
className={classnames(
'settings-step-input-buttons-container',
classNames?.buttonsContainer
)}
>
<button <button
className="settings-step-input-button settings-step-input-up-button" className="settings-step-input-button settings-step-input-up-button"
disabled={upDisabled} disabled={upDisabled}

View File

@@ -16,9 +16,9 @@ export const UserFieldsComponent = ({
const [boardUserFields, setBoardUserFields] = React.useState< const [boardUserFields, setBoardUserFields] = React.useState<
BoardUserField[] BoardUserField[]
>(initialBoardUserFields); >(initialBoardUserFields);
const [uploadButtonDisabled, setUploadButtonDisabled] = const [uploadButtonDisabled, setUploadButtonDisabled] =
React.useState<boolean>(true); React.useState<boolean>(true);
const firstInputElement = React.useRef<HTMLInputElement>(null);
React.useEffect(() => { React.useEffect(() => {
setBoardUserFields(initialBoardUserFields); setBoardUserFields(initialBoardUserFields);
@@ -48,10 +48,7 @@ export const UserFieldsComponent = ({
React.useEffect(() => { React.useEffect(() => {
updateUserFields(boardUserFields); updateUserFields(boardUserFields);
setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields)); setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields));
if (firstInputElement.current) { }, [boardUserFields]);
firstInputElement.current.focus();
}
}, [boardUserFields, updateUserFields]);
return ( return (
<div> <div>
@@ -74,7 +71,6 @@ export const UserFieldsComponent = ({
field.label field.label
)} )}
onChange={updateUserField(index)} onChange={updateUserField(index)}
ref={index === 0 ? firstInputElement : undefined}
/> />
</div> </div>
</div> </div>

View File

@@ -13,7 +13,7 @@ import { BoardUserField } from '../../../common/protocol';
@injectable() @injectable()
export class UserFieldsDialogWidget extends ReactWidget { export class UserFieldsDialogWidget extends ReactWidget {
private _currentUserFields: BoardUserField[] = []; protected _currentUserFields: BoardUserField[] = [];
constructor(private cancel: () => void, private accept: () => Promise<void>) { constructor(private cancel: () => void, private accept: () => Promise<void>) {
super(); super();
@@ -34,7 +34,7 @@ export class UserFieldsDialogWidget extends ReactWidget {
}); });
} }
private setUserFields(userFields: BoardUserField[]): void { protected setUserFields(userFields: BoardUserField[]): void {
this._currentUserFields = userFields; this._currentUserFields = userFields;
} }

View File

@@ -119,16 +119,20 @@ export class LibraryListWidget extends ListWidget<
message.appendChild(question); message.appendChild(question);
const result = await new MessageBoxDialog({ const result = await new MessageBoxDialog({
title: nls.localize( title: nls.localize(
'arduino/library/installLibraryDependencies', 'arduino/library/dependenciesForLibrary',
'Install library dependencies' 'Dependencies for library {0}:{1}',
item.name,
version
), ),
message, message,
buttons: [ buttons: [
nls.localize('arduino/library/installAll', 'Install all'),
nls.localize( nls.localize(
'arduino/library/installWithoutDependencies', 'arduino/library/installOnly',
'Install without dependencies' 'Install {0} only',
item.name
), ),
nls.localize('arduino/library/installAll', 'Install All'), nls.localize('vscode/issueMainService/cancel', 'Cancel'),
], ],
maxWidth: 740, // Aligned with `settings-dialog.css`. maxWidth: 740, // Aligned with `settings-dialog.css`.
}).open(); }).open();
@@ -136,11 +140,11 @@ export class LibraryListWidget extends ListWidget<
if (result) { if (result) {
const { response } = result; const { response } = result;
if (response === 0) { if (response === 0) {
// Current only
installDependencies = false;
} else if (response === 1) {
// All // All
installDependencies = true; installDependencies = true;
} else if (response === 1) {
// Current only
installDependencies = false;
} }
} }
} else { } else {
@@ -197,13 +201,7 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
options.buttons || [nls.localize('vscode/issueMainService/ok', 'OK')] options.buttons || [nls.localize('vscode/issueMainService/ok', 'OK')]
).forEach((text, index) => { ).forEach((text, index) => {
const button = this.createButton(text); const button = this.createButton(text);
const isPrimaryButton = button.classList.add(index === 0 ? 'main' : 'secondary');
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.controlPanel.appendChild(button);
this.toDisposeOnDetach.push( this.toDisposeOnDetach.push(
addEventListener(button, 'click', () => { addEventListener(button, 'click', () => {

View File

@@ -1,17 +1,16 @@
import { nls } from '@theia/core/lib/common';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { injectable } from '@theia/core/shared/inversify'; import { injectable } from '@theia/core/shared/inversify';
import { LibraryPackage, LibrarySearch } from '../../common/protocol'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { URI } from '../contributions/contribution'; import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ArduinoMenus } from '../menu/arduino-menus'; import { MenuModelRegistry } from '@theia/core';
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
import { LibraryListWidget } from './library-list-widget'; import { LibraryListWidget } from './library-list-widget';
import { ArduinoMenus } from '../menu/arduino-menus';
import { nls } from '@theia/core/lib/common';
@injectable() @injectable()
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution< export class LibraryListWidgetFrontendContribution
LibraryPackage, extends AbstractViewContribution<LibraryListWidget>
LibrarySearch implements FrontendApplicationContribution
> { {
constructor() { constructor() {
super({ super({
widgetId: LibraryListWidget.WIDGET_ID, widgetId: LibraryListWidget.WIDGET_ID,
@@ -25,6 +24,10 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
}); });
} }
async initializeLayout(): Promise<void> {
this.openView();
}
override registerMenus(menus: MenuModelRegistry): void { override registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) { if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, { menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, {
@@ -37,17 +40,4 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
}); });
} }
} }
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);
}
} }

View File

@@ -34,6 +34,7 @@ export class LocalCacheFsProvider
@inject(AuthenticationClientService) @inject(AuthenticationClientService)
protected readonly authenticationService: AuthenticationClientService; protected readonly authenticationService: AuthenticationClientService;
// TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
readonly ready = new Deferred<void>(); readonly ready = new Deferred<void>();
private _localCacheRoot: URI; private _localCacheRoot: URI;
@@ -152,7 +153,7 @@ export class LocalCacheFsProvider
return uri; return uri;
} }
toUri(session: AuthenticationSession): URI { private 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. // 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]); return this._localCacheRoot.resolve(session.id.split('|')[1]);
} }

View File

@@ -46,7 +46,6 @@ export class MonitorManagerProxyClientImpl
private wsPort?: number; private wsPort?: number;
private lastConnectedBoard: BoardsConfig.Config; private lastConnectedBoard: BoardsConfig.Config;
private onBoardsConfigChanged: Disposable | undefined; private onBoardsConfigChanged: Disposable | undefined;
private isMonitorWidgetOpen = false;
getWebSocketPort(): number | undefined { getWebSocketPort(): number | undefined {
return this.wsPort; return this.wsPort;
@@ -175,14 +174,6 @@ export class MonitorManagerProxyClientImpl
return this.server().getCurrentSettings(board, port); return this.server().getCurrentSettings(board, port);
} }
setMonitorWidgetStatus(value: boolean): void {
this.isMonitorWidgetOpen = value;
}
getMonitorWidgetStatus(): boolean {
return this.isMonitorWidgetOpen;
}
send(message: string): void { send(message: string): void {
if (!this.webSocket) { if (!this.webSocket) {
return; return;

View File

@@ -8,9 +8,6 @@ import { JsonRpcProxy } from '@theia/core/lib/common/messaging/proxy-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { import {
IndexUpdateDidCompleteParams,
IndexUpdateDidFailParams,
IndexUpdateWillStartParams,
NotificationServiceClient, NotificationServiceClient,
NotificationServiceServer, NotificationServiceServer,
} from '../common/protocol/notification-service'; } from '../common/protocol/notification-service';
@@ -32,48 +29,48 @@ export class NotificationCenter
implements NotificationServiceClient, FrontendApplicationContribution implements NotificationServiceClient, FrontendApplicationContribution
{ {
@inject(NotificationServiceServer) @inject(NotificationServiceServer)
private readonly server: JsonRpcProxy<NotificationServiceServer>; protected readonly server: JsonRpcProxy<NotificationServiceServer>;
@inject(FrontendApplicationStateService) @inject(FrontendApplicationStateService)
private readonly appStateService: FrontendApplicationStateService; private readonly appStateService: FrontendApplicationStateService;
private readonly indexUpdateDidCompleteEmitter = protected readonly indexDidUpdateEmitter = new Emitter<string>();
new Emitter<IndexUpdateDidCompleteParams>(); protected readonly indexWillUpdateEmitter = new Emitter<string>();
private readonly indexUpdateWillStartEmitter = protected readonly indexUpdateDidProgressEmitter =
new Emitter<IndexUpdateWillStartParams>();
private readonly indexUpdateDidProgressEmitter =
new Emitter<ProgressMessage>(); new Emitter<ProgressMessage>();
private readonly indexUpdateDidFailEmitter = protected readonly indexUpdateDidFailEmitter = new Emitter<{
new Emitter<IndexUpdateDidFailParams>(); progressId: string;
private readonly daemonDidStartEmitter = new Emitter<string>(); message: string;
private readonly daemonDidStopEmitter = new Emitter<void>(); }>();
private readonly configDidChangeEmitter = new Emitter<{ protected readonly daemonDidStartEmitter = new Emitter<string>();
protected readonly daemonDidStopEmitter = new Emitter<void>();
protected readonly configDidChangeEmitter = new Emitter<{
config: Config | undefined; config: Config | undefined;
}>(); }>();
private readonly platformDidInstallEmitter = new Emitter<{ protected readonly platformDidInstallEmitter = new Emitter<{
item: BoardsPackage; item: BoardsPackage;
}>(); }>();
private readonly platformDidUninstallEmitter = new Emitter<{ protected readonly platformDidUninstallEmitter = new Emitter<{
item: BoardsPackage; item: BoardsPackage;
}>(); }>();
private readonly libraryDidInstallEmitter = new Emitter<{ protected readonly libraryDidInstallEmitter = new Emitter<{
item: LibraryPackage; item: LibraryPackage;
}>(); }>();
private readonly libraryDidUninstallEmitter = new Emitter<{ protected readonly libraryDidUninstallEmitter = new Emitter<{
item: LibraryPackage; item: LibraryPackage;
}>(); }>();
private readonly attachedBoardsDidChangeEmitter = protected readonly attachedBoardsDidChangeEmitter =
new Emitter<AttachedBoardsChangeEvent>(); new Emitter<AttachedBoardsChangeEvent>();
private readonly recentSketchesChangedEmitter = new Emitter<{ protected readonly recentSketchesChangedEmitter = new Emitter<{
sketches: Sketch[]; sketches: Sketch[];
}>(); }>();
private readonly onAppStateDidChangeEmitter = private readonly onAppStateDidChangeEmitter =
new Emitter<FrontendApplicationState>(); new Emitter<FrontendApplicationState>();
private readonly toDispose = new DisposableCollection( protected readonly toDispose = new DisposableCollection(
this.indexUpdateWillStartEmitter, this.indexWillUpdateEmitter,
this.indexUpdateDidProgressEmitter, this.indexUpdateDidProgressEmitter,
this.indexUpdateDidCompleteEmitter, this.indexDidUpdateEmitter,
this.indexUpdateDidFailEmitter, this.indexUpdateDidFailEmitter,
this.daemonDidStartEmitter, this.daemonDidStartEmitter,
this.daemonDidStopEmitter, this.daemonDidStopEmitter,
@@ -85,8 +82,8 @@ export class NotificationCenter
this.attachedBoardsDidChangeEmitter this.attachedBoardsDidChangeEmitter
); );
readonly onIndexUpdateDidComplete = this.indexUpdateDidCompleteEmitter.event; readonly onIndexDidUpdate = this.indexDidUpdateEmitter.event;
readonly onIndexUpdateWillStart = this.indexUpdateWillStartEmitter.event; readonly onIndexWillUpdate = this.indexDidUpdateEmitter.event;
readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event; readonly onIndexUpdateDidProgress = this.indexUpdateDidProgressEmitter.event;
readonly onIndexUpdateDidFail = this.indexUpdateDidFailEmitter.event; readonly onIndexUpdateDidFail = this.indexUpdateDidFailEmitter.event;
readonly onDaemonDidStart = this.daemonDidStartEmitter.event; readonly onDaemonDidStart = this.daemonDidStartEmitter.event;
@@ -115,20 +112,26 @@ export class NotificationCenter
this.toDispose.dispose(); this.toDispose.dispose();
} }
notifyIndexUpdateWillStart(params: IndexUpdateWillStartParams): void { notifyIndexWillUpdate(progressId: string): void {
this.indexUpdateWillStartEmitter.fire(params); this.indexWillUpdateEmitter.fire(progressId);
} }
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void { notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
this.indexUpdateDidProgressEmitter.fire(progressMessage); this.indexUpdateDidProgressEmitter.fire(progressMessage);
} }
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void { notifyIndexDidUpdate(progressId: string): void {
this.indexUpdateDidCompleteEmitter.fire(params); this.indexDidUpdateEmitter.fire(progressId);
} }
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void { notifyIndexUpdateDidFail({
this.indexUpdateDidFailEmitter.fire(params); progressId,
message,
}: {
progressId: string;
message: string;
}): void {
this.indexUpdateDidFailEmitter.fire({ progressId, message });
} }
notifyDaemonDidStart(port: string): void { notifyDaemonDidStart(port: string): void {

View File

@@ -74,10 +74,6 @@ export class MonitorWidget extends ReactWidget {
this.monitorManagerProxy.startMonitor(); this.monitorManagerProxy.startMonitor();
} }
protected override onAfterAttach(msg: Message): void {
this.monitorManagerProxy.setMonitorWidgetStatus(this.isAttached);
}
onMonitorSettingsDidChange(settings: MonitorSettings): void { onMonitorSettingsDidChange(settings: MonitorSettings): void {
this.settings = { this.settings = {
...this.settings, ...this.settings,
@@ -95,7 +91,6 @@ export class MonitorWidget extends ReactWidget {
} }
override dispose(): void { override dispose(): void {
this.monitorManagerProxy.setMonitorWidgetStatus(this.isAttached);
super.dispose(); super.dispose();
} }

View File

@@ -6,53 +6,6 @@ import { BoardsServiceProvider } from '../../boards/boards-service-provider';
import { MonitorModel } from '../../monitor-model'; import { MonitorModel } from '../../monitor-model';
import { Unknown } from '../../../common/nls'; 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 namespace SerialMonitorSendInput {
export interface Props { export interface Props {
readonly boardsServiceProvider: BoardsServiceProvider; readonly boardsServiceProvider: BoardsServiceProvider;
@@ -63,7 +16,6 @@ export namespace SerialMonitorSendInput {
export interface State { export interface State {
text: string; text: string;
connected: boolean; connected: boolean;
history: HistoryList;
} }
} }
@@ -75,7 +27,7 @@ export class SerialMonitorSendInput extends React.Component<
constructor(props: Readonly<SerialMonitorSendInput.Props>) { constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props); super(props);
this.state = { text: '', connected: true, history: new HistoryList() }; this.state = { text: '', connected: true };
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this); this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this); this.onKeyDown = this.onKeyDown.bind(this);
@@ -138,7 +90,7 @@ export class SerialMonitorSendInput extends React.Component<
); );
} }
protected setRef = (element: HTMLElement | null): void => { protected setRef = (element: HTMLElement | null) => {
if (this.props.resolveFocus) { if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined); this.props.resolveFocus(element || undefined);
} }
@@ -158,17 +110,7 @@ export class SerialMonitorSendInput extends React.Component<
if (keyCode) { if (keyCode) {
const { key } = keyCode; const { key } = keyCode;
if (key === Key.ENTER) { if (key === Key.ENTER) {
const { text } = this.state;
this.onSend(); 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: '' });
} }
} }
} }

View File

@@ -18,7 +18,6 @@ import {
CLOSE_PLOTTER_WINDOW, CLOSE_PLOTTER_WINDOW,
SHOW_PLOTTER_WINDOW, SHOW_PLOTTER_WINDOW,
} from '../../../common/ipc-communication'; } from '../../../common/ipc-communication';
import { nls } from '@theia/core/lib/common/nls';
const queryString = require('query-string'); const queryString = require('query-string');
@@ -65,9 +64,6 @@ export class PlotterFrontendContribution extends Contribution {
ipcRenderer.on(CLOSE_PLOTTER_WINDOW, async () => { ipcRenderer.on(CLOSE_PLOTTER_WINDOW, async () => {
if (!!this.window) { if (!!this.window) {
if (!this.monitorManagerProxy.getMonitorWidgetStatus()) {
this.monitorManagerProxy.disconnect();
}
this.window = null; this.window = null;
} }
}); });
@@ -111,12 +107,7 @@ export class PlotterFrontendContribution extends Contribution {
if (wsPort) { if (wsPort) {
this.open(wsPort); this.open(wsPort);
} else { } else {
this.messageService.error( this.messageService.error(`Couldn't open serial plotter`);
nls.localize(
'arduino/contributions/plotter/couldNotOpen',
"Couldn't open serial plotter"
)
);
} }
} }

View File

@@ -1,11 +1,5 @@
#select-board-dialog-container > .dialogBlock {
width: 640px;
height: 500px;
}
div#select-board-dialog { div#select-board-dialog {
margin: 5px; margin: 5px;
height: 100%;
} }
div#select-board-dialog .selectBoardContainer { div#select-board-dialog .selectBoardContainer {
@@ -13,17 +7,12 @@ div#select-board-dialog .selectBoardContainer {
gap: 10px; gap: 10px;
overflow: hidden; overflow: hidden;
max-height: 100%; max-height: 100%;
height: 100%;
} }
.select-board-dialog .head { .select-board-dialog .head {
margin: 5px; margin: 5px;
} }
.dialogContent.select-board-dialog {
height: 100%;
}
div.dialogContent.select-board-dialog > div.head .title { div.dialogContent.select-board-dialog > div.head .title {
font-weight: 400; font-weight: 400;
letter-spacing: 0.02em; letter-spacing: 0.02em;
@@ -74,7 +63,6 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 100%; max-height: 100%;
height: 100%;
} }
#select-board-dialog .selectBoardContainer .left.container .content { #select-board-dialog .selectBoardContainer .left.container .content {
@@ -143,7 +131,6 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
#select-board-dialog .selectBoardContainer .list { #select-board-dialog .selectBoardContainer .list {
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
flex: 1;
} }
#select-board-dialog .selectBoardContainer .ports.list { #select-board-dialog .selectBoardContainer .ports.list {
@@ -295,11 +282,3 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
display: none; display: none;
} }
} }
#select-board-dialog .no-result {
text-transform: uppercase;
height: 100%;
user-select: none;
padding: 10px 5px;
overflow-wrap: break-word;
}

View File

@@ -9,8 +9,7 @@
total = padding + margin = 96px total = padding + margin = 96px
*/ */
max-width: calc(100% - 96px) !important; max-width: calc(100% - 96px) !important;
min-width: unset;
min-width: 424px;
max-height: 560px; max-height: 560px;
padding: 0 28px; padding: 0 28px;
} }
@@ -55,7 +54,6 @@
align-items: center; align-items: center;
} }
.p-Widget.dialogOverlay .dialogControl .spinner,
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner { .p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner {
background: var(--theia-icon-loading) center center no-repeat; background: var(--theia-icon-loading) center center no-repeat;
animation: theia-spin 1.25s linear infinite; animation: theia-spin 1.25s linear infinite;
@@ -64,11 +62,11 @@
} }
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow:first-child { .p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow:first-child {
margin-top: 0px; margin-top: 0px;
height: 32px; height: 32px;
} }
.fl1 { .fl1{
flex: 1; flex: 1;
} }
@@ -81,13 +79,9 @@
opacity: .4; opacity: .4;
} }
@media only screen and (max-height: 560px) { @media only screen and (max-height: 560px) {
.p-Widget.dialogOverlay .dialogBlock { .p-Widget.dialogOverlay .dialogBlock {
max-height: 400px; max-height: 400px;
} }
} }
.p-Widget.dialogOverlay .error.progress {
color: var(--theia-button-background);
align-self: center;
}

View File

@@ -7,7 +7,7 @@
} }
.firmware-uploader-dialog .arduino-select__control { .firmware-uploader-dialog .arduino-select__control {
height: 31px; height: 31px;
background: var(--theia-input-background) !important; background: var(--theia-menubar-selectionBackground) !important;
} }
.firmware-uploader-dialog .dialogRow > button{ .firmware-uploader-dialog .dialogRow > button{
@@ -28,4 +28,4 @@
.firmware-uploader-dialog .status-icon { .firmware-uploader-dialog .status-icon {
margin-right: 10px; margin-right: 10px;
} }

View File

@@ -55,8 +55,7 @@
/* Makes the sidepanel a bit wider when opening the widget */ /* Makes the sidepanel a bit wider when opening the widget */
.p-DockPanel-widget { .p-DockPanel-widget {
min-width: 200px; min-width: 200px;
min-height: 20px; min-height: 200px;
height: 200px;
} }
/* Overrule the default Theia CSS button styles. */ /* Overrule the default Theia CSS button styles. */
@@ -109,13 +108,6 @@ button.secondary[disabled], .theia-button.secondary[disabled] {
background-color: var(--theia-secondaryButton-background); 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 */ /* To make the progress-bar slightly thicker, and use the color from the status bar */
.theia-progress-bar-container { .theia-progress-bar-container {
width: 100%; width: 100%;
@@ -144,9 +136,6 @@ button.theia-button.message-box-dialog-button {
font-size: 14px; font-size: 14px;
} }
.uppercase {
text-transform: uppercase;
}
/* High Contrast Theme rules */ /* High Contrast Theme rules */
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/ /* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/

View File

@@ -111,13 +111,13 @@
font-weight: bold; font-weight: bold;
max-height: calc(1em + 4px); max-height: calc(1em + 4px);
color: var(--theia-button-foreground); color: var(--theia-button-foreground);
content: attr(install); content: 'INSTALLED';
} }
.component-list-item .header .installed:hover:before { .component-list-item .header .installed:hover:before {
background-color: var(--theia-button-foreground); background-color: var(--theia-button-foreground);
color: var(--theia-button-background); color: var(--theia-button-background);
content: attr(uninstall); content: 'UNINSTALL';
} }
.component-list-item[min-width~="170px"] .footer { .component-list-item[min-width~="170px"] .footer {
@@ -131,7 +131,7 @@
flex-direction: column-reverse; flex-direction: column-reverse;
} }
.component-list-item .footer > * { .component-list-item:hover .footer > * {
display: inline-block; display: inline-block;
margin: 5px 0px 0px 10px; margin: 5px 0px 0px 10px;
} }
@@ -160,4 +160,4 @@
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before { .hc-black.hc-theia.theia-hc .component-list-item .header .installed:before {
border: 1px solid var(--theia-button-border); border: 1px solid var(--theia-button-border);
} }

View File

@@ -2,16 +2,6 @@
background: var(--theia-editorGroupHeader-tabsBackground); 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 { .p-TabBar-toolbar .item.arduino-tool-item {
margin-left: 0; margin-left: 0;
} }
@@ -97,7 +87,8 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: var(--theia-titleBar-activeBackground); background-color: var(--theia-titleBar-activeBackground);
} }
#arduino-toolbar-container { #arduino-toolbar-container {
@@ -252,10 +243,3 @@
outline: 1px solid var(--theia-contrastBorder); outline: 1px solid var(--theia-contrastBorder);
outline-offset: -1px; 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;
}

View File

@@ -14,10 +14,6 @@
font-family: monospace font-family: monospace
} }
.serial-monitor-messages pre {
margin: 0px;
}
.serial-monitor .head { .serial-monitor .head {
display: flex; display: flex;
padding: 5px; padding: 5px;

View File

@@ -88,13 +88,9 @@
} }
.additional-urls-dialog textarea { .additional-urls-dialog textarea {
resize: none; width: 100%;
white-space: nowrap;
} }
.p-Widget.dialogOverlay .dialogBlock .dialogContent.additional-urls-dialog { .p-Widget.dialogOverlay .dialogBlock .dialogContent.additional-urls-dialog {
display: flex; display: block;
overflow: hidden;
padding: 0 1px;
margin: 0 -1px;
} }

View File

@@ -2,17 +2,17 @@
position: relative 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 { .settings-step-input-element::-webkit-outer-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
margin: 0; margin: 0;
} }
.settings-step-input-buttons-container { .settings-step-input-buttons-container {
display: none; display: none;
flex-direction: column; flex-direction: column;
position: absolute; position: absolute;
right: 0px; right: 14px;
top: 50%; top: 50%;
transform: translate(0px, -50%); transform: translate(0px, -50%);
height: calc(100% - 4px); height: calc(100% - 4px);
@@ -21,11 +21,7 @@
background: var(--theia-input-background); background: var(--theia-input-background);
} }
.settings-step-input-buttons-container-perc { .settings-step-input-container:hover > .settings-step-input-buttons-container {
right: 14px;
}
.settings-step-input-container:hover>.settings-step-input-buttons-container {
display: flex; display: flex;
} }
@@ -47,4 +43,4 @@
.settings-step-input-button:hover { .settings-step-input-button:hover {
background: rgba(128, 128, 128, 0.8); background: rgba(128, 128, 128, 0.8);
} }

View File

@@ -33,22 +33,6 @@
height: 100%; 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 { .sketchbook-tree__opts {
background-color: var(--theia-foreground); background-color: var(--theia-foreground);
-webkit-mask: url(./sketchbook-opts-icon.svg); -webkit-mask: url(./sketchbook-opts-icon.svg);

View File

@@ -1,6 +1,8 @@
import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog'; import { AboutDialog as TheiaAboutDialog } from '@theia/core/lib/browser/about-dialog';
import { duration } from '../../../common/decorators';
export class AboutDialog extends TheiaAboutDialog { export class AboutDialog extends TheiaAboutDialog {
@duration({ name: 'theia-about#init' })
protected override async init(): Promise<void> { protected override async init(): Promise<void> {
// NOOP // NOOP
// IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time. // IDE2 has a custom about dialog, so it does not make sense to collect Theia extensions at startup time.

View File

@@ -0,0 +1,44 @@
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'));
}
}

View File

@@ -0,0 +1,80 @@
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>
);
}
}

View File

@@ -17,6 +17,7 @@ import {
SketchesServiceClientImpl, SketchesServiceClientImpl,
} from '../../../common/protocol/sketches-service-client-impl'; } from '../../../common/protocol/sketches-service-client-impl';
import { SaveAsSketch } from '../../contributions/save-as-sketch'; import { SaveAsSketch } from '../../contributions/save-as-sketch';
import { SingleTextInputDialog } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
@injectable() @injectable()
@@ -160,26 +161,20 @@ export class WorkspaceCommandContribution extends TheiaWorkspaceCommandContribut
return; return;
} }
const initialValue = uri.path.base; const initialValue = uri.path.base;
const parentUri = parent.resource; const dialog = new SingleTextInputDialog({
title: nls.localize('theia/workspace/newFileName', 'New name for file'),
const dialog = new WorkspaceInputDialog( initialValue,
{ initialSelectionRange: {
title: nls.localize('theia/workspace/newFileName', 'New name for file'), start: 0,
initialValue, end: uri.path.name.length,
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);
},
}, },
this.labelProvider validate: (name, mode) => {
); if (initialValue === name && mode === 'preview') {
return false;
}
return this.validateFileName(name, parent, false);
},
});
const newName = await dialog.open(); const newName = await dialog.open();
const newNameWithExt = this.maybeAppendInoExt(newName); const newNameWithExt = this.maybeAppendInoExt(newName);
if (newNameWithExt) { if (newNameWithExt) {

View File

@@ -14,11 +14,9 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
constructor( constructor(
@inject(WorkspaceInputDialogProps) @inject(WorkspaceInputDialogProps)
protected override readonly props: WorkspaceInputDialogProps, protected override readonly props: WorkspaceInputDialogProps,
@inject(LabelProvider) @inject(LabelProvider) protected override readonly labelProvider: LabelProvider
protected override readonly labelProvider: LabelProvider
) { ) {
super(props, labelProvider); super(props, labelProvider);
this.node.classList.add('workspace-input-dialog');
this.appendCloseButton( this.appendCloseButton(
nls.localize('vscode/issueMainService/cancel', 'Cancel') nls.localize('vscode/issueMainService/cancel', 'Cancel')
); );
@@ -43,14 +41,4 @@ export class WorkspaceInputDialog extends TheiaWorkspaceInputDialog {
this.errorMessageNode.innerText = DialogError.getMessage(error); 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;
}
} }

View File

@@ -16,7 +16,6 @@ import {
import { import {
SketchesService, SketchesService,
Sketch, Sketch,
SketchesError,
} from '../../../common/protocol/sketches-service'; } from '../../../common/protocol/sketches-service';
import { FileStat } from '@theia/filesystem/lib/common/files'; import { FileStat } from '@theia/filesystem/lib/common/files';
import { import {
@@ -39,7 +38,6 @@ export class WorkspaceService extends TheiaWorkspaceService {
private readonly providers: ContributionProvider<StartupTaskProvider>; private readonly providers: ContributionProvider<StartupTaskProvider>;
private version?: string; private version?: string;
private _workspaceError: Error | undefined;
async onStart(application: FrontendApplication): Promise<void> { async onStart(application: FrontendApplication): Promise<void> {
const info = await this.applicationServer.getApplicationInfo(); const info = await this.applicationServer.getApplicationInfo();
@@ -53,10 +51,6 @@ export class WorkspaceService extends TheiaWorkspaceService {
this.onCurrentWidgetChange({ newValue, oldValue: null }); this.onCurrentWidgetChange({ newValue, oldValue: null });
} }
get workspaceError(): Error | undefined {
return this._workspaceError;
}
protected override async toFileStat( protected override async toFileStat(
uri: string | URI | undefined uri: string | URI | undefined
): Promise<FileStat | undefined> { ): Promise<FileStat | undefined> {
@@ -65,31 +59,6 @@ export class WorkspaceService extends TheiaWorkspaceService {
const newSketchUri = await this.sketchService.createNewSketch(); const newSketchUri = await this.sketchService.createNewSketch();
return this.toFileStat(newSketchUri.uri); 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; return stat;
} }

View File

@@ -1,78 +1,78 @@
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import * as ReactDOM from '@theia/core/shared/react-dom'; import * as ReactDOM from '@theia/core/shared/react-dom';
import { import { inject, injectable } from '@theia/core/shared/inversify';
inject, import { Widget } from '@theia/core/shared/@phosphor/widgets';
injectable, import { Message, MessageLoop } from '@theia/core/shared/@phosphor/messaging';
postConstruct, import { Disposable } from '@theia/core/lib/common/disposable';
} from '@theia/core/shared/inversify'; import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
import { UserStatus } from './cloud-user-status'; import { UserStatus } from './cloud-user-status';
import { nls } from '@theia/core/lib/common/nls';
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget'; import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
import { AuthenticationClientService } from '../../auth/authentication-client-service'; import { AuthenticationClientService } from '../../auth/authentication-client-service';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model'; import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget'; import { nls } from '@theia/core/lib/common';
import { CreateNew } from '../sketchbook/create-new';
import { AuthenticationSession } from '../../../node/auth/types';
@injectable() @injectable()
export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidget<CloudSketchbookTreeWidget> { export class CloudSketchbookCompositeWidget extends BaseWidget {
@inject(AuthenticationClientService) @inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService; protected readonly authenticationService: AuthenticationClientService;
@inject(CloudSketchbookTreeWidget) @inject(CloudSketchbookTreeWidget)
private readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget; protected readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget;
private _session: AuthenticationSession | undefined;
private compositeNode: HTMLElement;
private cloudUserStatusNode: HTMLElement;
constructor() { constructor() {
super(); super();
this.id = 'cloud-sketchbook-composite-widget'; 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.title.caption = nls.localize( this.title.caption = nls.localize(
'arduino/cloud/remoteSketchbook', 'arduino/cloud/remoteSketchbook',
'Remote Sketchbook' 'Remote Sketchbook'
); );
this.title.iconClass = 'cloud-sketchbook-tree-icon'; this.title.iconClass = 'cloud-sketchbook-tree-icon';
this.title.closable = false;
this.id = 'cloud-sketchbook-composite-widget';
} }
@postConstruct() public getTreeWidget(): CloudSketchbookTreeWidget {
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; return this.cloudSketchbookTreeWidget;
} }
protected renderFooter(footerNode: HTMLElement): void { protected override onAfterAttach(message: Message): void {
super.onAfterAttach(message);
Widget.attach(this.cloudSketchbookTreeWidget, this.compositeNode);
ReactDOM.render( ReactDOM.render(
<> <UserStatus
{this._session && ( model={this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel}
<CreateNew authenticationService={this.authenticationService}
label={nls.localize( />,
'arduino/sketchbook/newRemoteSketch', this.cloudUserStatusNode
'New Remote Sketch' );
)} this.toDisposeOnDetach.push(
onClick={this.onDidClickCreateNew} Disposable.create(() => Widget.detach(this.cloudSketchbookTreeWidget))
/>
)}
<UserStatus
model={
this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel
}
authenticationService={this.authenticationService}
/>
</>,
footerNode
); );
} }
private onDidClickCreateNew: () => void = () => { protected override onActivateRequest(msg: Message): void {
this.commandService.executeCommand('arduino-new-cloud-sketch'); 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
);
}
} }

View File

@@ -1,26 +1,20 @@
import { import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
inject, import { TreeNode } from '@theia/core/lib/browser/tree';
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { CompositeTreeNode, TreeNode } from '@theia/core/lib/browser/tree';
import { posixSegments, splitSketchPath } from '../../create/create-paths'; import { posixSegments, splitSketchPath } from '../../create/create-paths';
import { CreateApi } from '../../create/create-api'; import { CreateApi } from '../../create/create-api';
import { CloudSketchbookTree } from './cloud-sketchbook-tree'; import { CloudSketchbookTree } from './cloud-sketchbook-tree';
import { AuthenticationClientService } from '../../auth/authentication-client-service'; import { AuthenticationClientService } from '../../auth/authentication-client-service';
import { SketchbookTreeModel } from '../sketchbook/sketchbook-tree-model'; import { SketchbookTreeModel } from '../sketchbook/sketchbook-tree-model';
import { ArduinoPreferences } from '../../arduino-preferences';
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree'; import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
import { CreateUri } from '../../create/create-uri'; import { CreateUri } from '../../create/create-uri';
import { FileChangesEvent, FileStat } from '@theia/filesystem/lib/common/files'; import { FileStat } from '@theia/filesystem/lib/common/files';
import { import { LocalCacheFsProvider } from '../../local-cache/local-cache-fs-provider';
LocalCacheFsProvider, import { FileService } from '@theia/filesystem/lib/browser/file-service';
LocalCacheUri,
} from '../../local-cache/local-cache-fs-provider';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { SketchCache } from './cloud-sketch-cache'; import { SketchCache } from './cloud-sketch-cache';
import { Create } from '../../create/typings'; import { Create } from '../../create/typings';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
export function sketchBaseDir(sketch: Create.Sketch): FileStat { export function sketchBaseDir(sketch: Create.Sketch): FileStat {
// extract the sketch path // extract the sketch path
@@ -58,16 +52,26 @@ export function sketchesToFileStats(sketches: Create.Sketch[]): FileStat[] {
@injectable() @injectable()
export class CloudSketchbookTreeModel extends SketchbookTreeModel { export class CloudSketchbookTreeModel extends SketchbookTreeModel {
@inject(CreateApi) @inject(FileService)
private readonly createApi: CreateApi; protected override readonly fileService: FileService;
@inject(AuthenticationClientService)
private readonly authenticationService: AuthenticationClientService;
@inject(LocalCacheFsProvider)
private readonly localCacheFsProvider: LocalCacheFsProvider;
@inject(SketchCache)
private readonly sketchCache: SketchCache;
private _localCacheFsProviderReady: Deferred<void> | undefined; @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;
@inject(LocalCacheFsProvider)
protected readonly localCacheFsProvider: LocalCacheFsProvider;
@inject(SketchCache)
protected readonly sketchCache: SketchCache;
@postConstruct() @postConstruct()
protected override init(): void { protected override init(): void {
@@ -77,50 +81,6 @@ 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> { override async createRoot(): Promise<TreeNode | undefined> {
const { session } = this.authenticationService; const { session } = this.authenticationService;
if (!session) { if (!session) {
@@ -129,10 +89,7 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
} }
this.createApi.init(this.authenticationService, this.arduinoPreferences); this.createApi.init(this.authenticationService, this.arduinoPreferences);
this.sketchCache.init(); this.sketchCache.init();
const [sketches] = await Promise.all([ const sketches = await this.createApi.sketches();
this.createApi.sketches(),
this.ensureLocalFsProviderReady(),
]);
const rootFileStats = sketchesToFileStats(sketches); const rootFileStats = sketchesToFileStats(sketches);
if (this.workspaceService.opened) { if (this.workspaceService.opened) {
const workspaceNode = WorkspaceNode.createRoot( const workspaceNode = WorkspaceNode.createRoot(
@@ -151,9 +108,7 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
return this.tree as CloudSketchbookTree; return this.tree as CloudSketchbookTree;
} }
protected override recursivelyFindSketchRoot( protected override recursivelyFindSketchRoot(node: TreeNode): any {
node: TreeNode
): TreeNode | false {
if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) { if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) {
return node; return node;
} }
@@ -167,25 +122,13 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
} }
override async revealFile(uri: URI): Promise<TreeNode | undefined> { override async revealFile(uri: URI): Promise<TreeNode | undefined> {
await this.localCacheFsProvider.ready.promise;
// we use remote uris as keys for the tree // we use remote uris as keys for the tree
// convert local URIs // convert local URIs
const remoteUri = this.localCacheFsProvider.from(uri); const remoteuri = this.localCacheFsProvider.from(uri);
if (remoteUri) { if (remoteuri) {
return super.revealFile(remoteUri); return super.revealFile(remoteuri);
} else { } else {
return super.revealFile(uri); 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;
}
} }

View File

@@ -1,5 +1,5 @@
import * as React from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react';
import { inject, injectable } from '@theia/core/shared/inversify'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { TreeModel } from '@theia/core/lib/browser/tree/tree-model'; import { TreeModel } from '@theia/core/lib/browser/tree/tree-model';
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model'; import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
import { AuthenticationClientService } from '../../auth/authentication-client-service'; import { AuthenticationClientService } from '../../auth/authentication-client-service';
@@ -27,6 +27,12 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
@inject(CloudSketchbookTree) @inject(CloudSketchbookTree)
protected readonly cloudSketchbookTree: 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 { protected override renderTree(model: TreeModel): React.ReactNode {
if (this.shouldShowWelcomeView()) return this.renderViewWelcome(); if (this.shouldShowWelcomeView()) return this.renderViewWelcome();
if (this.shouldShowEmptyView()) return this.renderEmptyView(); if (this.shouldShowEmptyView()) return this.renderEmptyView();
@@ -55,10 +61,10 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
</div> </div>
</div> </div>
<button <button
className="theia-button uppercase" className="theia-button"
onClick={() => shell.openExternal('https://create.arduino.cc/editor')} onClick={() => shell.openExternal('https://create.arduino.cc/editor')}
> >
{nls.localize('arduino/cloud/goToCloud', 'Go to Cloud')} {nls.localize('cloud/GoToCloud', 'GO TO CLOUD')}
</button> </button>
<div className="center item"></div> <div className="center item"></div>
</div> </div>

View File

@@ -136,7 +136,7 @@ export class CloudSketchbookTree extends SketchbookTree {
return; return;
} }
} }
return this.runWithState(node, 'pulling', async (node) => { this.runWithState(node, 'pulling', async (node) => {
const commandsCopy = node.commands; const commandsCopy = node.commands;
node.commands = []; node.commands = [];
@@ -196,7 +196,7 @@ export class CloudSketchbookTree extends SketchbookTree {
return; return;
} }
} }
return this.runWithState(node, 'pushing', async (node) => { this.runWithState(node, 'pushing', async (node) => {
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) { if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
throw new Error( throw new Error(
nls.localize( nls.localize(
@@ -269,7 +269,7 @@ export class CloudSketchbookTree extends SketchbookTree {
return prev; return prev;
} }
// do not map "do_not_sync" files/directories and their descendants // do not map "do_not_sync" files/directoris and their descendants
const segments = path[1].split(posix.sep) || []; const segments = path[1].split(posix.sep) || [];
if ( if (
segments.some((segment) => Create.do_not_sync_files.includes(segment)) segments.some((segment) => Create.do_not_sync_files.includes(segment))

View File

@@ -2,7 +2,6 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'
import { CloudSketchbookCompositeWidget } from './cloud-sketchbook-composite-widget'; import { CloudSketchbookCompositeWidget } from './cloud-sketchbook-composite-widget';
import { SketchbookWidget } from '../sketchbook/sketchbook-widget'; import { SketchbookWidget } from '../sketchbook/sketchbook-widget';
import { ArduinoPreferences } from '../../arduino-preferences'; import { ArduinoPreferences } from '../../arduino-preferences';
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget';
@injectable() @injectable()
export class CloudSketchbookWidget extends SketchbookWidget { export class CloudSketchbookWidget extends SketchbookWidget {
@@ -20,8 +19,8 @@ export class CloudSketchbookWidget extends SketchbookWidget {
override getTreeWidget(): any { override getTreeWidget(): any {
const widget: any = this.sketchbookTreesContainer.selectedWidgets().next(); const widget: any = this.sketchbookTreesContainer.selectedWidgets().next();
if (widget instanceof BaseSketchbookCompositeWidget) { if (widget && typeof widget.getTreeWidget !== 'undefined') {
return widget.treeWidget; return (widget as CloudSketchbookCompositeWidget).getTreeWidget();
} }
return widget; return widget;
} }
@@ -31,7 +30,7 @@ export class CloudSketchbookWidget extends SketchbookWidget {
this.sketchbookTreesContainer.activateWidget(this.widget); this.sketchbookTreesContainer.activateWidget(this.widget);
} else { } else {
this.sketchbookTreesContainer.activateWidget( this.sketchbookTreesContainer.activateWidget(
this.sketchbookCompositeWidget this.localSketchbookTreeWidget
); );
} }
this.setDocumentMode(); this.setDocumentMode();

View File

@@ -14,21 +14,34 @@ export class ComponentListItem<
)[0]; )[0];
this.state = { this.state = {
selectedVersion: version, 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 { override render(): React.ReactNode {
const { item, itemRenderer } = this.props; const { item, itemRenderer } = this.props;
return ( return (
<> <div
onMouseEnter={() => this.setState({ focus: true })}
onMouseLeave={() => this.setState({ focus: false })}
>
{itemRenderer.renderItem( {itemRenderer.renderItem(
Object.assign(this.state, { item }), Object.assign(this.state, { item }),
this.install.bind(this), this.install.bind(this),
this.uninstall.bind(this), this.uninstall.bind(this),
this.onVersionChange.bind(this) this.onVersionChange.bind(this)
)} )}
</> </div>
); );
} }
@@ -64,9 +77,11 @@ export namespace ComponentListItem {
readonly install: (item: T, version?: Installable.Version) => Promise<void>; readonly install: (item: T, version?: Installable.Version) => Promise<void>;
readonly uninstall: (item: T) => Promise<void>; readonly uninstall: (item: T) => Promise<void>;
readonly itemRenderer: ListItemRenderer<T>; readonly itemRenderer: ListItemRenderer<T>;
readonly onFocusDidChange: () => void;
} }
export interface State { export interface State {
selectedVersion?: Installable.Version; selectedVersion?: Installable.Version;
focus: boolean;
} }
} }

View File

@@ -125,7 +125,7 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
rowIndex={index} rowIndex={index}
parent={parent} parent={parent}
> >
{({ registerChild }) => ( {({ measure, registerChild }) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
<div ref={registerChild} style={style}> <div ref={registerChild} style={style}>
@@ -135,6 +135,7 @@ export class ComponentList<T extends ArduinoComponent> extends React.Component<
itemRenderer={this.props.itemRenderer} itemRenderer={this.props.itemRenderer}
install={this.props.install} install={this.props.install}
uninstall={this.props.uninstall} uninstall={this.props.uninstall}
onFocusDidChange={() => measure()}
/> />
</div> </div>
)} )}

View File

@@ -111,7 +111,19 @@ export class FilterableListContainer<
const { searchable } = this.props; const { searchable } = this.props;
searchable searchable
.search(searchOptions) .search(searchOptions)
.then((items) => this.setState({ items: this.props.sort(items) })); .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));
});
} }
protected async install( protected async install(
@@ -127,7 +139,7 @@ export class FilterableListContainer<
run: ({ progressId }) => install({ item, progressId, version }), run: ({ progressId }) => install({ item, progressId, version }),
}); });
const items = await searchable.search(this.state.searchOptions); const items = await searchable.search(this.state.searchOptions);
this.setState({ items: this.props.sort(items) }); this.setState({ items: this.sort(items) });
} }
protected async uninstall(item: T): Promise<void> { protected async uninstall(item: T): Promise<void> {
@@ -155,7 +167,7 @@ export class FilterableListContainer<
run: ({ progressId }) => uninstall({ item, progressId }), run: ({ progressId }) => uninstall({ item, progressId }),
}); });
const items = await searchable.search(this.state.searchOptions); const items = await searchable.search(this.state.searchOptions);
this.setState({ items: this.props.sort(items) }); this.setState({ items: this.sort(items) });
} }
} }
@@ -192,7 +204,6 @@ export namespace FilterableListContainer {
progressId: string; progressId: string;
}) => Promise<void>; }) => Promise<void>;
readonly commandService: CommandService; readonly commandService: CommandService;
readonly sort: (items: T[]) => T[];
} }
export interface State<T, S extends Searchable.Options> { export interface State<T, S extends Searchable.Options> {

View File

@@ -28,7 +28,7 @@ export class ListItemRenderer<T extends ArduinoComponent> {
uninstall: (item: T) => Promise<void>, uninstall: (item: T) => Promise<void>,
onVersionChange: (version: Installable.Version) => void onVersionChange: (version: Installable.Version) => void
): React.ReactNode { ): React.ReactNode {
const { item } = input; const { item, focus } = input;
let nameAndAuthor: JSX.Element; let nameAndAuthor: JSX.Element;
if (item.name && item.author) { if (item.name && item.author) {
const name = <span className="name">{item.name}</span>; const name = <span className="name">{item.name}</span>;
@@ -55,14 +55,7 @@ export class ListItemRenderer<T extends ArduinoComponent> {
item.installedVersion item.installedVersion
)} )}
</span> </span>
<span <span className="installed" onClick={onClickUninstall} />
className="installed uppercase"
onClick={onClickUninstall}
{...{
install: nls.localize('arduino/component/installed', 'Installed'),
uninstall: nls.localize('arduino/component/uninstall', 'Uninstall'),
}}
/>
</div> </div>
); );
@@ -77,10 +70,10 @@ export class ListItemRenderer<T extends ArduinoComponent> {
const onClickInstall = () => install(item); const onClickInstall = () => install(item);
const installButton = item.installable && ( const installButton = item.installable && (
<button <button
className="theia-button secondary install uppercase" className="theia-button secondary install"
onClick={onClickInstall} onClick={onClickInstall}
> >
{nls.localize('arduino/component/install', 'Install')} {nls.localize('arduino/component/install', 'INSTALL')}
</button> </button>
); );
@@ -127,10 +120,12 @@ export class ListItemRenderer<T extends ArduinoComponent> {
{description} {description}
</div> </div>
<div className="info">{moreInfo}</div> <div className="info">{moreInfo}</div>
<div className="footer"> {focus && (
{versions} <div className="footer">
{installButton} {versions}
</div> {installButton}
</div>
)}
</div> </div>
); );
} }

View File

@@ -1,15 +1,9 @@
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 { injectable } from '@theia/core/shared/inversify';
import { Searchable } from '../../../common/protocol'; import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { ArduinoComponent } from '../../../common/protocol/arduino-component'; import { ArduinoComponent } from '../../../common/protocol/arduino-component';
import { ListWidget } from './list-widget'; import { ListWidget } from './list-widget';
import { Searchable } from '../../../common/protocol';
@injectable() @injectable()
export abstract class ListWidgetFrontendContribution< export abstract class ListWidgetFrontendContribution<
@@ -17,49 +11,14 @@ export abstract class ListWidgetFrontendContribution<
S extends Searchable.Options S extends Searchable.Options
> >
extends AbstractViewContribution<ListWidget<T, S>> extends AbstractViewContribution<ListWidget<T, S>>
implements FrontendApplicationContribution, OpenHandler implements FrontendApplicationContribution
{ {
readonly id: string = `http-opener-${this.viewId}`;
async initializeLayout(): Promise<void> { async initializeLayout(): Promise<void> {
this.openView(); // TS requires at least one method from `FrontendApplicationContribution`.
// Expected to be empty.
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars override registerMenus(): void {
override registerMenus(_: MenuModelRegistry): void {
// NOOP // 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;
} }

View File

@@ -51,11 +51,9 @@ export abstract class ListWidget<
*/ */
protected firstActivate = true; protected firstActivate = true;
protected readonly defaultSortComparator: (left: T, right: T) => number;
constructor(protected options: ListWidget.Options<T, S>) { constructor(protected options: ListWidget.Options<T, S>) {
super(); super();
const { id, label, iconClass, itemDeprecated, itemLabel } = options; const { id, label, iconClass } = options;
this.id = id; this.id = id;
this.title.label = label; this.title.label = label;
this.title.caption = label; this.title.caption = label;
@@ -65,23 +63,12 @@ export abstract class ListWidget<
this.node.tabIndex = 0; // To be able to set the focus on the widget. this.node.tabIndex = 0; // To be able to set the focus on the widget.
this.scrollOptions = undefined; this.scrollOptions = undefined;
this.toDispose.push(this.searchOptionsChangeEmitter); 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() @postConstruct()
protected init(): void { protected init(): void {
this.toDispose.pushAll([ this.toDispose.pushAll([
this.notificationCenter.onIndexUpdateDidComplete(() => this.notificationCenter.onIndexDidUpdate(() => this.refresh(undefined)),
this.refresh(undefined)
),
this.notificationCenter.onDaemonDidStart(() => this.refresh(undefined)), this.notificationCenter.onDaemonDidStart(() => this.refresh(undefined)),
this.notificationCenter.onDaemonDidStop(() => this.refresh(undefined)), this.notificationCenter.onDaemonDidStop(() => this.refresh(undefined)),
]); ]);
@@ -141,30 +128,6 @@ export abstract class ListWidget<
return this.options.installable.uninstall({ item, progressId }); 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 { render(): React.ReactNode {
return ( return (
<FilterableListContainer<T, S> <FilterableListContainer<T, S>
@@ -182,7 +145,6 @@ export abstract class ListWidget<
messageService={this.messageService} messageService={this.messageService}
commandService={this.commandService} commandService={this.commandService}
responseService={this.responseService} responseService={this.responseService}
sort={this.filterableListSort}
/> />
); );
} }

View File

@@ -1,20 +0,0 @@
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;
}
}

View File

@@ -1,93 +0,0 @@
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');
};
}

View File

@@ -59,7 +59,6 @@ export class SketchbookTreeWidget extends FileTreeWidget {
'Local Sketchbook' 'Local Sketchbook'
); );
this.title.closable = false; this.title.closable = false;
this.addClass('tree-container'); // Adds `height: 100%` to the tree. Otherwise you cannot see it.
} }
@postConstruct() @postConstruct()

View File

@@ -11,21 +11,15 @@ import { Disposable } from '@theia/core/lib/common/disposable';
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget'; import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
import { SketchbookTreeWidget } from './sketchbook-tree-widget'; import { SketchbookTreeWidget } from './sketchbook-tree-widget';
import { nls } from '@theia/core/lib/common'; import { nls } from '@theia/core/lib/common';
import { CloudSketchbookCompositeWidget } from '../cloud-sketchbook/cloud-sketchbook-composite-widget';
import { URI } from '../../contributions/contribution'; import { URI } from '../../contributions/contribution';
import {
BaseSketchbookCompositeWidget,
SketchbookCompositeWidget,
} from './sketchbook-composite-widget';
@injectable() @injectable()
export class SketchbookWidget extends BaseWidget { export class SketchbookWidget extends BaseWidget {
static readonly LABEL = nls.localize( static LABEL = nls.localize('arduino/sketch/titleSketchbook', 'Sketchbook');
'arduino/sketch/titleSketchbook',
'Sketchbook'
);
@inject(SketchbookCompositeWidget) @inject(SketchbookTreeWidget)
protected readonly sketchbookCompositeWidget: SketchbookCompositeWidget; protected readonly localSketchbookTreeWidget: SketchbookTreeWidget;
protected readonly sketchbookTreesContainer: DockPanel; protected readonly sketchbookTreesContainer: DockPanel;
@@ -42,7 +36,7 @@ export class SketchbookWidget extends BaseWidget {
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
this.sketchbookTreesContainer.addWidget(this.sketchbookCompositeWidget); this.sketchbookTreesContainer.addWidget(this.localSketchbookTreeWidget);
} }
protected override onAfterAttach(message: Message): void { protected override onAfterAttach(message: Message): void {
@@ -54,7 +48,7 @@ export class SketchbookWidget extends BaseWidget {
} }
getTreeWidget(): SketchbookTreeWidget { getTreeWidget(): SketchbookTreeWidget {
return this.sketchbookCompositeWidget.treeWidget; return this.localSketchbookTreeWidget;
} }
activeTreeWidgetId(): string | undefined { activeTreeWidgetId(): string | undefined {
@@ -86,8 +80,8 @@ export class SketchbookWidget extends BaseWidget {
if (widget instanceof SketchbookTreeWidget) { if (widget instanceof SketchbookTreeWidget) {
return widget; return widget;
} }
if (widget instanceof BaseSketchbookCompositeWidget) { if (widget instanceof CloudSketchbookCompositeWidget) {
return widget.treeWidget; return widget.getTreeWidget();
} }
return undefined; return undefined;
}; };

View File

@@ -3,14 +3,7 @@ import { Searchable } from './searchable';
import { Installable } from './installable'; import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component'; import { ArduinoComponent } from './arduino-component';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { import { All, Contributed, Partner, Type, Updatable } from '../nls';
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 type AvailablePorts = Record<string, [Port, Array<Board>]>;
export namespace AvailablePorts { export namespace AvailablePorts {
@@ -148,7 +141,6 @@ export interface BoardsService
fqbn: string; fqbn: string;
}): Promise<BoardsPackage | undefined>; }): Promise<BoardsPackage | undefined>;
searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]>; searchBoards({ query }: { query?: string }): Promise<BoardWithPackage[]>;
getInstalledBoards(): Promise<BoardWithPackage[]>;
getBoardUserFields(options: { getBoardUserFields(options: {
fqbn: string; fqbn: string;
protocol: string; protocol: string;
@@ -159,7 +151,6 @@ export interface BoardSearch extends Searchable.Options {
readonly type?: BoardSearch.Type; readonly type?: BoardSearch.Type;
} }
export namespace BoardSearch { export namespace BoardSearch {
export const Default: BoardSearch = { type: 'All' };
export const TypeLiterals = [ export const TypeLiterals = [
'All', 'All',
'Updatable', 'Updatable',
@@ -170,11 +161,6 @@ export namespace BoardSearch {
'Arduino@Heart', 'Arduino@Heart',
] as const; ] as const;
export type Type = typeof TypeLiterals[number]; 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> = { export const TypeLabels: Record<Type, string> = {
All: All, All: All,
Updatable: Updatable, Updatable: Updatable,
@@ -191,41 +177,8 @@ export namespace BoardSearch {
keyof Omit<BoardSearch, 'query'>, keyof Omit<BoardSearch, 'query'>,
string string
> = { > = {
type: TypeLabel, type: Type,
}; };
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 { export interface Port {
@@ -332,29 +285,6 @@ export namespace Port {
return false; 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 { export interface BoardsPackage extends ArduinoComponent {

View File

@@ -1,4 +1,3 @@
import { nls } from '@theia/core/lib/common/nls';
import { ApplicationError } from '@theia/core/lib/common/application-error'; import { ApplicationError } from '@theia/core/lib/common/application-error';
import type { import type {
Location, Location,
@@ -11,7 +10,6 @@ import type {
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import type { Programmer } from './boards-service'; import type { Programmer } from './boards-service';
import type { Sketch } from './sketches-service'; import type { Sketch } from './sketches-service';
import { IndexUpdateSummary } from './notification-service';
export const CompilerWarningLiterals = [ export const CompilerWarningLiterals = [
'None', 'None',
@@ -20,17 +18,6 @@ export const CompilerWarningLiterals = [
'All', 'All',
] as const; ] as const;
export type CompilerWarnings = typeof CompilerWarningLiterals[number]; 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 namespace CoreError {
export interface ErrorLocationRef { export interface ErrorLocationRef {
readonly message: string; readonly message: string;
@@ -109,37 +96,6 @@ export interface CoreService {
compile(options: CoreService.Options.Compile): Promise<void>; compile(options: CoreService.Options.Compile): Promise<void>;
upload(options: CoreService.Options.Upload): Promise<void>; upload(options: CoreService.Options.Upload): Promise<void>;
burnBootloader(options: CoreService.Options.Bootloader): 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 { export namespace CoreService {

View File

@@ -8,10 +8,9 @@ import {
Partner, Partner,
Recommended, Recommended,
Retired, Retired,
Type as TypeLabel, Type,
Updatable, Updatable,
} from '../nls'; } from '../nls';
import URI from '@theia/core/lib/common/uri';
export const LibraryServicePath = '/services/library-service'; export const LibraryServicePath = '/services/library-service';
export const LibraryService = Symbol('LibraryService'); export const LibraryService = Symbol('LibraryService');
@@ -56,7 +55,6 @@ export interface LibrarySearch extends Searchable.Options {
readonly topic?: LibrarySearch.Topic; readonly topic?: LibrarySearch.Topic;
} }
export namespace LibrarySearch { export namespace LibrarySearch {
export const Default: LibrarySearch = { type: 'All', topic: 'All' };
export const TypeLiterals = [ export const TypeLiterals = [
'All', 'All',
'Updatable', 'Updatable',
@@ -68,11 +66,6 @@ export namespace LibrarySearch {
'Retired', 'Retired',
] as const; ] as const;
export type Type = typeof TypeLiterals[number]; 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> = { export const TypeLabels: Record<Type, string> = {
All: All, All: All,
Updatable: Updatable, Updatable: Updatable,
@@ -97,11 +90,6 @@ export namespace LibrarySearch {
'Uncategorized', 'Uncategorized',
] as const; ] as const;
export type Topic = typeof TopicLiterals[number]; 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> = { export const TopicLabels: Record<Topic, string> = {
All: All, All: All,
Communication: nls.localize( Communication: nls.localize(
@@ -138,60 +126,8 @@ export namespace LibrarySearch {
string string
> = { > = {
topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'), topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
type: TypeLabel, type: Type,
}; };
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 { export namespace LibraryService {
@@ -226,6 +162,12 @@ export enum LibraryLocation {
} }
export interface LibraryPackage extends ArduinoComponent { 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. * 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` * For example, including `SD` will prepend `#include <SD.h>` to the `ino` file. While including `Bridge`

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