diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 298a30258921d..4cf7f4872f462 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -13,21 +13,21 @@ jobs:
lint:
name: Lint
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- - name: Use Node.js 18.x
- uses: actions/setup-node@v3
+ - name: Use Node.js 20.x
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
- node-version: 18.x
+ node-version: 20.x
registry-url: 'https://registry.npmjs.org'
- name: Use Python 3.11
- uses: actions/setup-python@v4
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
with:
python-version: '3.11'
@@ -51,24 +51,24 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [windows-2019, ubuntu-latest, macos-11]
- node: [16.x, 18.x, 20.x]
+ os: [windows-2019, ubuntu-22.04, macos-14]
+ node: [18.x, 20.x]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Use Node.js ${{ matrix.node }}
- uses: actions/setup-node@v3
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
node-version: ${{ matrix.node }}
registry-url: 'https://registry.npmjs.org'
- name: Use Python 3.11
- uses: actions/setup-python@v4
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
with:
python-version: '3.11'
@@ -86,7 +86,7 @@ jobs:
if: runner.os == 'Linux'
shell: bash
run: |
- yarn -s download:plugins
+ yarn -s download:plugins --rate-limit 3
- name: Build
shell: bash
diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml
index 82a562fa2adeb..b299055bd4415 100644
--- a/.github/workflows/license-check.yml
+++ b/.github/workflows/license-check.yml
@@ -19,8 +19,8 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-latest]
- node: ['18.x']
+ os: [ubuntu-22.04]
+ node: ['20.x']
java: ['11']
runs-on: ${{ matrix.os }}
@@ -28,18 +28,18 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
fetch-depth: 2
- name: Use Node.js ${{ matrix.node }}
- uses: actions/setup-node@v3
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
node-version: ${{ matrix.node }}
registry-url: 'https://registry.npmjs.org'
- name: Use Java ${{ matrix.java }}
- uses: actions/setup-java@v3
+ uses: actions/setup-java@1df8dbefe2a8cbc99770194893dd902763bee34b # v3.9.0
with:
distribution: 'adopt'
java-version: ${{ matrix.java }}
diff --git a/.github/workflows/native-dependencies.yml b/.github/workflows/native-dependencies.yml
index 095ff9912e9fc..009aa57436497 100644
--- a/.github/workflows/native-dependencies.yml
+++ b/.github/workflows/native-dependencies.yml
@@ -10,17 +10,17 @@ jobs:
os: ['ubuntu-20.04', 'windows-latest', 'macos-latest']
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
# Update the node version here after every Electron upgrade
- name: Use Node.js 18.17.0
- uses: actions/setup-node@v3
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
node-version: '18.17.0'
registry-url: 'https://registry.npmjs.org'
- name: Use Python 3.11
- uses: actions/setup-python@v4
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
with:
python-version: '3.11'
@@ -44,7 +44,7 @@ jobs:
run: yarn zip:native:dependencies
- name: Upload Artifacts
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: native-dependencies
path: ./scripts/native-dependencies-*.zip
diff --git a/.github/workflows/performance-tests.yml b/.github/workflows/performance-tests.yml
index 065de46991490..559c580fa6d02 100644
--- a/.github/workflows/performance-tests.yml
+++ b/.github/workflows/performance-tests.yml
@@ -7,23 +7,23 @@ jobs:
build-and-test-performance:
name: Performance Tests
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- - name: Use Node.js 18.x
- uses: actions/setup-node@v3
+ - name: Use Node.js 20.x
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
- node-version: "18.x"
+ node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- - name: Use Python 3.x
- uses: actions/setup-python@v4
+ - name: Use Python 3.11
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
with:
- python-version: "3.x"
+ python-version: '3.11'
- name: Build
shell: bash
@@ -36,17 +36,15 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # https://github.com/microsoft/vscode-ripgrep/issues/9
- name: Performance (browser)
- uses: GabrielBB/xvfb-action@v1
- with:
- run: yarn performance:startup:browser
+ shell: bash
+ run: yarn performance:startup:browser
- name: Performance (Electron)
- uses: GabrielBB/xvfb-action@v1
- with:
- run: yarn performance:startup:electron
+ shell: bash
+ run: xvfb-run yarn performance:startup:electron
- name: Analyze performance results
- uses: benchmark-action/github-action-benchmark@v1
+ uses: benchmark-action/github-action-benchmark@fd31771ce86cc65eab85653da103f71ab1b4479c # v1.9.0
with:
name: Performance Benchmarks
tool: "customSmallerIsBetter"
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 953096017ee3d..96be450becff5 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -13,31 +13,38 @@ on:
jobs:
build-and-test-playwright:
- name: Playwright Tests (ubuntu-latest, Node.js 18.x)
+ name: Playwright Tests (ubuntu-22.04, Node.js 18.x)
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- - name: Use Node.js "18.x"
- uses: actions/setup-node@v3
+ - name: Use Node.js "20.x"
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
- node-version: "18.x"
+ node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- name: Use Python 3.11
- uses: actions/setup-python@v4
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
with:
python-version: "3.11"
+ - name: Install IPython Kernel
+ shell: bash
+ run: |
+ python3 -m pip install ipykernel==6.15.2
+ python3 -m ipykernel install --user
+
- name: Build Browser
shell: bash
run: |
yarn global add node-gyp
yarn --skip-integrity-check --network-timeout 100000
+ yarn download:plugins
yarn browser build
env:
NODE_OPTIONS: --max_old_space_size=4096
@@ -49,6 +56,15 @@ jobs:
yarn --cwd examples/playwright build
- name: Test (playwright)
- uses: GabrielBB/xvfb-action@v1
+ shell: bash
+ run: yarn --cwd examples/playwright ui-tests-ci
+
+ - name: Archive test results
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 #v4
+ if: ${{ !cancelled() }}
with:
- run: yarn --cwd examples/playwright ui-tests-ci
+ name: playwright-test-results
+ path: |
+ examples/playwright/test-results/
+ examples/playwright/playwright-report/
+ retention-days: 7
diff --git a/.github/workflows/production-smoke-test.yml b/.github/workflows/production-smoke-test.yml
index 216d39f841422..ab80575b9e67a 100644
--- a/.github/workflows/production-smoke-test.yml
+++ b/.github/workflows/production-smoke-test.yml
@@ -11,23 +11,23 @@ on:
jobs:
build-and-test-playwright:
- name: Smoke Test for Browser Example Production Build on ubuntu-latest with Node.js 18.x
+ name: Smoke Test for Browser Example Production Build on ubuntu-22.04 with Node.js 18.x
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- - name: Use Node.js "18.x"
- uses: actions/setup-node@v3
+ - name: Use Node.js "20.x"
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
- node-version: "18.x"
+ node-version: "20.x"
registry-url: "https://registry.npmjs.org"
- name: Use Python 3.11
- uses: actions/setup-python@v4
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
with:
python-version: "3.11"
@@ -47,6 +47,5 @@ jobs:
yarn --cwd examples/playwright build
- name: Run Smoke Test (examples/playwright/src/tests/theia-app)
- uses: GabrielBB/xvfb-action@v1
- with:
- run: yarn test:playwright theia-app
+ shell: bash
+ run: yarn test:playwright theia-app
diff --git a/.github/workflows/publish-gh-pages.yml b/.github/workflows/publish-gh-pages.yml
index 68e5311ca3004..6eae492ec3289 100644
--- a/.github/workflows/publish-gh-pages.yml
+++ b/.github/workflows/publish-gh-pages.yml
@@ -7,7 +7,7 @@ jobs:
publish:
name: Publish to NPM and GitHub pages
needs: build
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
# The current approach is silly. We should be smarter and use `actions/upload-artifact` and `actions/download-artifact` instead of rebuilding
# everything from scratch again. (git checkout, Node.js install, yarn, etc.) It was not possible to share artifacts on Travis CI without an
@@ -15,18 +15,18 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
fetch-depth: 0 # To fetch all history for all branches and tags. (Will be required for caching with lerna: https://github.com/markuplint/markuplint/pull/111)
- - name: Use Node.js 18.x
- uses: actions/setup-node@v3
+ - name: Use Node.js 20.x
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
- node-version: '18.x'
+ node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Use Python 3.x
- uses: actions/setup-python@v4
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
with:
python-version: '3.x'
@@ -45,14 +45,14 @@ jobs:
NODE_OPTIONS: --max_old_space_size=9216
- name: Publish GH Pages
- uses: peaceiris/actions-gh-pages@v3
+ uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./gh-pages
force_orphan: true # will only keep latest commit on branch gh-pages
- name: Publish NPM
- uses: nick-invision/retry@v2
+ uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0
with:
timeout_minutes: 5
retry_wait_seconds: 30
diff --git a/.github/workflows/publish-next.yml b/.github/workflows/publish-next.yml
new file mode 100644
index 0000000000000..6ae2e3e9c79ad
--- /dev/null
+++ b/.github/workflows/publish-next.yml
@@ -0,0 +1,47 @@
+name: Publish Next
+
+permissions:
+ id-token: write
+
+on: workflow_dispatch
+
+jobs:
+ publish:
+ name: Perform Publishing
+ runs-on: ubuntu-22.04
+ timeout-minutes: 60
+ steps:
+ - name: Checkout
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
+ with:
+ # To fetch all history for all branches and tags.
+ # Required for lerna to determine the version of the next package.
+ fetch-depth: 0
+
+ - name: Use Node.js 20.x
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
+ with:
+ node-version: 20.x
+ registry-url: "https://registry.npmjs.org"
+
+ - name: Use Python 3.11
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
+ with:
+ python-version: "3.11"
+
+ - name: Install
+ shell: bash
+ run: |
+ yarn global add node-gyp
+ yarn --skip-integrity-check --network-timeout 100000
+ env:
+ NODE_OPTIONS: --max_old_space_size=4096
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # https://github.com/microsoft/vscode-ripgrep/issues/9
+
+ - name: Publish NPM
+ shell: bash
+ run: |
+ yarn publish:next
+ env:
+ NPM_CONFIG_PROVENANCE: "true"
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
new file mode 100644
index 0000000000000..7ed5e983851ce
--- /dev/null
+++ b/.github/workflows/publish-release.yml
@@ -0,0 +1,73 @@
+name: Publish Release
+
+permissions:
+ id-token: write
+
+on:
+ workflow_dispatch:
+ inputs:
+ release_type:
+ description: 'Release Type'
+ required: true
+ default: 'minor'
+ type: choice
+ options:
+ - 'minor'
+ - 'patch'
+
+jobs:
+ publish:
+ name: Perform Publishing
+ runs-on: ubuntu-22.04
+ timeout-minutes: 60
+ steps:
+ - name: Checkout
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
+
+ - name: Use Node.js 20.x
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
+ with:
+ node-version: 20.x
+ registry-url: "https://registry.npmjs.org"
+
+ - name: Use Python 3.11
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
+ with:
+ python-version: "3.11"
+
+ - name: Install
+ shell: bash
+ run: |
+ yarn global add node-gyp
+ yarn --skip-integrity-check --network-timeout 100000
+ env:
+ NODE_OPTIONS: --max_old_space_size=4096
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # https://github.com/microsoft/vscode-ripgrep/issues/9
+
+ - name: Publish NPM
+ shell: bash
+ run: |
+ yarn publish:latest -- ${{ inputs.release_type }}
+ yarn publish:check
+ env:
+ NPM_CONFIG_PROVENANCE: "true" # enable provenance check
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
+
+ - name: Get Actor User Data
+ uses: octokit/request-action@21d174fc38ff59af9cf4d7e07347d29df6dbaa99 # v2.3.0
+ id: actor_user_data
+ with:
+ route: GET /users/{user}
+ user: ${{ github.actor }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5
+ with:
+ commiter: ${{ github.actor }} <${{ fromJson(steps.actor_user_data.outputs.data).email }}>
+ author: ${{ github.actor }} <${{ fromJson(steps.actor_user_data.outputs.data).email }}>
+ branch: bot/package-update
+ title: Package update for version ${{ env.NEXT_VERSION_NUMBER }}
+ commit-message: Package update for version ${{ env.NEXT_VERSION_NUMBER }}
+ body: Automated package update for Theia version ${{ env.NEXT_VERSION_NUMBER }}. Triggered by @${{ github.actor }}.
diff --git a/.github/workflows/set-milestone-on-pr.yml b/.github/workflows/set-milestone-on-pr.yml
index 226b68474c4f3..2a040fdacaf95 100644
--- a/.github/workflows/set-milestone-on-pr.yml
+++ b/.github/workflows/set-milestone-on-pr.yml
@@ -22,16 +22,16 @@ on:
jobs:
set-milestone:
if: github.event.pull_request.merged == true
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
steps:
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- id: compute-milestone
run: |
export THEIA_CORE_VERSION=$(node -p "require(\"./packages/core/package.json\").version")
echo "MILESTONE_NUMBER=$(npx -q semver@7 --increment minor $THEIA_CORE_VERSION)" >> $GITHUB_ENV
- id: set
- uses: actions/github-script@v3
+ uses: actions/github-script@ffc2c79a5b2490bd33e0a41c1de74b877714d736 # v3.2.0
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
diff --git a/.github/workflows/translation.yml b/.github/workflows/translation.yml
index 815165c7f202e..e4a21d3858995 100644
--- a/.github/workflows/translation.yml
+++ b/.github/workflows/translation.yml
@@ -5,21 +5,21 @@ on: workflow_dispatch
jobs:
translation:
name: Translation Update
- runs-on: ubuntu-latest
+ runs-on: ubuntu-22.04
timeout-minutes: 60
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- - name: Use Node.js 18.x
- uses: actions/setup-node@v3
+ - name: Use Node.js 20.x
+ uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
with:
- node-version: 18.x
+ node-version: 20.x
registry-url: "https://registry.npmjs.org"
- name: Use Python 3.x
- uses: actions/setup-python@v4
+ uses: actions/setup-python@b64ffcaf5b410884ad320a9cfac8866006a109aa # v4.8.0
with:
python-version: "3.11"
@@ -44,7 +44,7 @@ jobs:
DEEPL_API_TOKEN: ${{ secrets.DEEPL_API_TOKEN }}
- name: Get Actor User Data
- uses: octokit/request-action@v2.x
+ uses: octokit/request-action@21d174fc38ff59af9cf4d7e07347d29df6dbaa99 # v2.3.0
id: actor_user_data
with:
route: GET /users/{user}
@@ -53,7 +53,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Pull Request
- uses: peter-evans/create-pull-request@v4
+ uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5
with:
commiter: ${{ github.actor }} <${{ fromJson(steps.actor_user_data.outputs.data).email }}>
author: ${{ github.actor }} <${{ fromJson(steps.actor_user_data.outputs.data).email }}>
diff --git a/.gitignore b/.gitignore
index d8cde8f9d7d9a..aab5678aabd83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,8 +15,6 @@ errorShots
examples/*/src-gen
examples/*/gen-webpack.config.js
examples/*/gen-webpack.node.config.js
-examples/*/.theia
-examples/*/.vscode
examples/*/.test
.browser_modules
**/docs/api
diff --git a/.gitpod.yml b/.gitpod.yml
index a03d38cc692cc..1deab0c9a8306 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -12,13 +12,10 @@ ports:
- port: 9339 # Node.js debug port
onOpen: ignore
tasks:
- - init: yarn --network-timeout 100000 && yarn build:examples && yarn download:plugins
+ - init: yarn --network-timeout 100000 && yarn browser build && yarn download:plugins
command: >
jwm &
- yarn --cwd examples/browser start ../.. --hostname=0.0.0.0
-github:
- prebuilds:
- pullRequestsFromForks: true
+ yarn browser start ../.. --hostname=0.0.0.0
vscode:
extensions:
- - dbaeumer.vscode-eslint@2.0.0:CwAMx4wYz1Kq39+1Aul4VQ==
+ - dbaeumer.vscode-eslint
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 00e3862ace5e0..3539be4173f25 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -58,12 +58,13 @@
"request": "launch",
"name": "Launch Browser Backend",
"program": "${workspaceFolder}/examples/browser/lib/backend/main.js",
+ "cwd": "${workspaceFolder}/examples/browser",
"args": [
"--hostname=0.0.0.0",
"--port=3000",
"--no-cluster",
"--app-project-path=${workspaceFolder}/examples/browser",
- "--plugins=local-dir:plugins",
+ "--plugins=local-dir:../../plugins",
"--hosted-plugin-inspect=9339",
"--ovsx-router-config=${workspaceFolder}/examples/ovsx-router-config.json"
],
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 5b108a74ccc8b..8166ed2cb0656 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -59,4 +59,9 @@
180
],
"typescript.preferences.quoteStyle": "single",
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "vscode.typescript-language-features",
+ "typescript.preferences.quoteStyle": "single",
+ "editor.tabSize": 4,
+ }
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0599a3bed35b0..8aed2f4a66368 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,421 @@
- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)
+## 1.54.0 - 09/26/2024
+
+- [ai] add Theia AI LLM Support [Experimental] [#14048](https://github.com/eclipse-theia/theia/pull/14048)
+- [ai] adapted default LLM for Theia AI to gpt-4o [#14165](https://github.com/eclipse-theia/theia/pull/14165)
+- [ai] add enable state of agent to preferences [#14206](https://github.com/eclipse-theia/theia/pull/14206)
+- [ai] chore: polished AI code completion [#14192](https://github.com/eclipse-theia/theia/pull/14192)
+- [ai] consistently named agents and added tags [#14182](https://github.com/eclipse-theia/theia/pull/14182)
+- [ai] feat: added toolbar actions for chat nodes [#14181](https://github.com/eclipse-theia/theia/pull/14181) - Contributed on behalf of STMicroelectronics
+- [ai] feat: show variables and functions on AI agent configuration [#14177](https://github.com/eclipse-theia/theia/pull/14177)
+- [ai] feat: supported models served via OpenAI API [#14172](https://github.com/eclipse-theia/theia/pull/14172)
+- [ai] fixed ai-settings retrieval [#14221](https://github.com/eclipse-theia/theia/pull/14221)
+- [ai] fixed enablement of AI support [#14166](https://github.com/eclipse-theia/theia/pull/14166)
+- [ai] improved prompt of workspace agent [#14159](https://github.com/eclipse-theia/theia/pull/14159)
+- [ai] refined AI settings [#14202](https://github.com/eclipse-theia/theia/pull/14202)
+- [ai] refined experimental message for AI features [#14187](https://github.com/eclipse-theia/theia/pull/14187)
+- [ai] removed type duplication for kind property [#14207](https://github.com/eclipse-theia/theia/pull/14207)
+- [ai] consistent prompt ids [#14162](https://github.com/eclipse-theia/theia/pull/14162)
+- [ai] fix: disabled an agent also disabled its UIContribution [#14184](https://github.com/eclipse-theia/theia/pull/14184)
+- [application-package] bumped API version to 1.93.1 [#14224](https://github.com/eclipse-theia/theia/pull/14224) - Contributed on behalf of STMicroelectronics
+- [core] fixed selection of contributed menu action argument adapters [#14132](https://github.com/eclipse-theia/theia/pull/14132) - Contributed on behalf of STMicroelectronics
+- [core] supported proxy env variable for schema catalog download [#14130](https://github.com/eclipse-theia/theia/pull/14130)
+- [core] supported workbench.editorAssociations preference [#14139](https://github.com/eclipse-theia/theia/pull/14139)
+- [editor] aligned active text and notebook editor more towards vscode [#14190](https://github.com/eclipse-theia/theia/pull/14190)
+- [filesystem] fixed FileResource sometimes sending contents change event during writing [#14043](https://github.com/eclipse-theia/theia/pull/14043) - Contributed on behalf of Toro Cloud
+- [notebook] focused notebook cell container correctly [#14175](https://github.com/eclipse-theia/theia/pull/14175)
+- [notebook] fixed notebook context selection [#14179](https://github.com/eclipse-theia/theia/pull/14179)
+- [notebook] made the cell editor border grey when not focused [#14195](https://github.com/eclipse-theia/theia/pull/14195)
+- [plugin] removed stub tag from TerminalOptions#color [#14171](https://github.com/eclipse-theia/theia/pull/14171)
+- [plugin] moved stubbed API TerminalShellIntegration into main API [#14168](https://github.com/eclipse-theia/theia/pull/14168) - Contributed on behalf of STMicroelectronics
+- [plugin] supported evolution on proposed API extensionAny [#14199](https://github.com/eclipse-theia/theia/pull/14199) - Contributed on behalf of STMicroelectronics
+- [plugin] updated TreeView reveal options to be readonly [#14198](https://github.com/eclipse-theia/theia/pull/14198) - Contributed on behalf of STMicroelectronics
+- [plugin-ext] properly supported executeDocumentSymbolProvider command [#14173](https://github.com/eclipse-theia/theia/pull/14173)
+- [plugin-ext] fixed leak in tabs-main.ts [#14186](https://github.com/eclipse-theia/theia/pull/14186)
+- [preferences] expanded plugin preferences on scroll correctly [#14170](https://github.com/eclipse-theia/theia/pull/14170)
+- [test] supported TestMessage stack traces [#14154](https://github.com/eclipse-theia/theia/pull/14154) - Contributed on behalf of STMicroelectronics
+- [workspace] handled only the user workspace security settings [#14147](https://github.com/eclipse-theia/theia/pull/14147)
+
+[Breaking Changes:](#breaking_changes_1.54.0)
+
+- [ai] added toolbar actions on chat nodes [#14181](https://github.com/eclipse-theia/theia/pull/14181) - Contributed on behalf of STMicroelectronics
+- [core] updated AuthenticationService to handle multiple accounts per provider [#14149](https://github.com/eclipse-theia/theia/pull/14149) - Contributed on behalf of STMicroelectronics
+
+## 1.53.0 - 08/29/2024
+
+- [application-package] bumpped API version to 1.92.2 [#14076](https://github.com/eclipse-theia/theia/pull/14076) - Contributed on behalf of STMicroelectronics
+- [collaboration] added support for collaboration feature [#13309](https://github.com/eclipse-theia/theia/pull/13309)
+- [core] added `testing/profiles/context` menu contribution [#14028](https://github.com/eclipse-theia/theia/pull/14028) - Contributed on behalf of STMicroelectronics
+- [core] added support for reverting a composite saveable [#14079](https://github.com/eclipse-theia/theia/pull/14079)
+- [core] aligned available locales to VS Code [#14039](https://github.com/eclipse-theia/theia/pull/14039)
+- [core] dropped support for Node 16.x [#14027](https://github.com/eclipse-theia/theia/pull/14027) - Contributed on behalf of STMicroelectronics
+- [core] refactored undo-redo action for editors [#13963](https://github.com/eclipse-theia/theia/pull/13963)
+- [core] updated logic to correctly revert saveable on widget close [#14062](https://github.com/eclipse-theia/theia/pull/14062)
+- [core] updated logic to download json schema catalog at build-time [#14065](https://github.com/eclipse-theia/theia/pull/14065) - Contributed on behalf of STMicroelectronics
+- [electron] updated electron to version 30.1.2 [#14041](https://github.com/eclipse-theia/theia/pull/14041) - Contributed on behalf of STMicroelectronics
+- [monaco] updated logic to rely on `IConfigurationService` change event to update model options [#13994](https://github.com/eclipse-theia/theia/pull/13994) - Contributed on behalf of STMicroelectronics
+- [notebook] added aliases for `list.focusUp` and `list.focusDown` for notebooks [#14042](https://github.com/eclipse-theia/theia/pull/14042)
+- [notebook] added logic to support Alt+Enter in notebooks - run the current cell and insert a new below [#14022](https://github.com/eclipse-theia/theia/pull/14022)
+- [notebook] added notebook selected cell status bar item and center selected cell command [#14046](https://github.com/eclipse-theia/theia/pull/14046)
+- [notebook] added support to find widget in notebooks [#13982](https://github.com/eclipse-theia/theia/pull/13982)
+- [notebook] enhanced notebook cell divider [#14081](https://github.com/eclipse-theia/theia/pull/14081)
+- [notebook] fixed notebook output scrolling and text rendering [#14016](https://github.com/eclipse-theia/theia/pull/14016)
+- [notebook] fixed vscode api notebook selection property [#14087](https://github.com/eclipse-theia/theia/pull/14087)
+- [notebook] updated logic to make sure notebook model created when calling `openNotebookDocument` [#14029](https://github.com/eclipse-theia/theia/pull/14029)
+- [notebook] updated logic to use correct cell type for selected language [#13983](https://github.com/eclipse-theia/theia/pull/13983)
+- [playwright] fixed flaky playwright Theia Main Menu test [#13951](https://github.com/eclipse-theia/theia/pull/13951) - Contributed on behalf of STMicroelectronics
+- [plugin] added `executeFoldingRangeProvider`, `executeCodeActionProvider`, and `executeWorkspaceSymbolProvider` command implementations [#14093](https://github.com/eclipse-theia/theia/pull/14093)
+- [plugin] added support for `--headless-hosted-plugin-inspect` cmd argument [#13918](https://github.com/eclipse-theia/theia/pull/13918)
+- [plugin] fixed issue when creating new untitled notebook doesn't work [#14031](https://github.com/eclipse-theia/theia/pull/14031)
+- [plugin] implemented previously stubbed API `window.registerUriHandler()` [#13306](https://github.com/eclipse-theia/theia/pull/13306) - Contributed on behalf of STMicroelectronics
+- [plugin] stubbed Terminal Shell Integration VS Code API [#14058](https://github.com/eclipse-theia/theia/pull/14058)
+- [plugin] updated logic to allow opening changes for files associated with custom editors [#13916](https://github.com/eclipse-theia/theia/pull/13916)
+- [plugin] upated code to not use `ChannelMultiplexer` in `RPCProtocol` [#13980](https://github.com/eclipse-theia/theia/pull/13980) - Contributed on behalf of STMicroelectronics
+- [preferences] fixed preference tree for plugins [#14036](https://github.com/eclipse-theia/theia/pull/14036)
+- [vsx-registry] fixed `429` errors on OVSX requests [#14030](https://github.com/eclipse-theia/theia/pull/14030)
+
+[Breaking Changes:](#breaking_changes_1.53.0)
+
+- [dependencies] Updated electron to version 30.1.2 - [#14041](https://github.com/eclipse-theia/theia/pull/14041) - Contributed on behalf of STMicroelectronics
+- [dependencies] increased minimum node version to 18. [#14027](https://github.com/eclipse-theia/theia/pull/14027) - Contributed on behalf of STMicroelectronics
+
+## 1.52.0 - 07/25/2024
+
+- [application-package] bumped the default supported API from `1.90.2` to `1.91.1` [#13955](https://github.com/eclipse-theia/theia/pull/13955) - Contributed on behalf of STMicroelectronics
+- [cli] added logging to download:plugins script [#13905](https://github.com/eclipse-theia/theia/pull/13905) - Contributed on behalf of STMicroelectronics
+- [core] bug fix: "core.saveAll" command only saved dirty widgets [#13942](https://github.com/eclipse-theia/theia/pull/13942)
+- [core] downgrade jsdom to 22.1.0 [#13944](https://github.com/eclipse-theia/theia/pull/13944)
+- [core] fixed reload for remote feature and added option to the electron window to change URL on reload [#13891](https://github.com/eclipse-theia/theia/pull/13891)
+- [core] improved implementation around widget management [#13818](https://github.com/eclipse-theia/theia/pull/13818)
+- [core] introduced `FRONTEND_CONNECTION_TIMEOUT` environment variable to override application connection settings [#13936](https://github.com/eclipse-theia/theia/pull/13936) - Contributed on behalf of STMicroelectronics
+- [core] made sure UI loaded when minimized [#13887](https://github.com/eclipse-theia/theia/pull/13887) - Contributed on behalf of STMicroelectronics
+- [core] prevented the rendering of the tab bar tooltip if no caption was provided [#13945](https://github.com/eclipse-theia/theia/pull/13945)
+- [core] tab selected should be adjacent when closing the last one [#13912](https://github.com/eclipse-theia/theia/pull/13912) - Contributed on behalf of STMicroelectronics
+- [core] upgraded ws to 8.18.0 [#13903](https://github.com/eclipse-theia/theia/pull/13903)
+- [debug] added DebugSessionOptions.testRun [#13939](https://github.com/eclipse-theia/theia/pull/13939) - Contributed on behalf of STMicroelectronics
+- [debug] implemented activeStackItem and related change event in debug namespace [#13900](https://github.com/eclipse-theia/theia/pull/13900) - Contributed on behalf of STMicroelectronics
+- [filesystem] fixed FileResource not adding event listener to the disposable collection [#13880](https://github.com/eclipse-theia/theia/pull/13880)
+- [notebook] changed cell type when selecting markdown as a code cell's language [#13933](https://github.com/eclipse-theia/theia/pull/13933)
+- [notebook] made Notebook preferences registration substitutable [#13926](https://github.com/eclipse-theia/theia/pull/13926)
+- [ovsx-client] fixed plugin version comparison [#13907](https://github.com/eclipse-theia/theia/pull/13907)
+- [plugin-ext] codicon color and URI support to TerminalOptions [#13413](https://github.com/eclipse-theia/theia/pull/13413)
+- [plugin-ext] used relative paths for ctx.importScripts() [#13854](https://github.com/eclipse-theia/theia/pull/13854)
+- [preferences] refactored preference tree layouting [#13819](https://github.com/eclipse-theia/theia/pull/13819)
+- [terminal] added support for 256 truecolor [#13853](https://github.com/eclipse-theia/theia/pull/13853)
+- [workflows] updated Mac OS version to 14 in CI [#13908](https://github.com/eclipse-theia/theia/pull/13908)
+
+## 1.51.0 - 06/27/2024
+
+- [application-manager] updated logic to load correct messaging module in browser-only mode [#13827](https://github.com/eclipse-theia/theia/pull/13827)
+- [application-package] bumped the default supported API from `1.89.1` to `1.90.2` [#13849](https://github.com/eclipse-theia/theia/pull/13849) - Contributed on behalf of STMicroelectronics
+- [core] added support for dynamic menu contributions [#13720](https://github.com/eclipse-theia/theia/pull/13720)
+- [core] fixed account menu order, icon and badge [#13771](https://github.com/eclipse-theia/theia/pull/13771)
+- [core] fixed overflow behavior of sidebars [#13483](https://github.com/eclipse-theia/theia/pull/13483) - Contributed on behalf of STMicroelectronics
+- [core] improved shown keybindings in context menu [#13830](https://github.com/eclipse-theia/theia/pull/13830)
+- [core] introduced optional serialize method in Saveable [#13833](https://github.com/eclipse-theia/theia/pull/13833)
+- [core] updated doc comments on service-connection-provider.ts [#13805](https://github.com/eclipse-theia/theia/pull/13805) - Contributed on behalf of STMicroelectronics
+- [core] updated logic of links to block local navigation and open new windows externally in electron [#13782](https://github.com/eclipse-theia/theia/pull/13782) - Contributed on behalf of STMicroelectronics
+- [core] updated logic to propagate "Save As" operation to plugin host [#13689](https://github.com/eclipse-theia/theia/pull/13689)
+- [core] updated logic to use 'openWithSystemApp' to open uri when 'env.openExternal' requested [#13676](https://github.com/eclipse-theia/theia/pull/13676)
+- [electron] switched single instance on per default. [#13831](https://github.com/eclipse-theia/theia/pull/13831) - Contributed on behalf of STMicroelectronics
+- [filesystem] improved Upload Command [#13775](https://github.com/eclipse-theia/theia/pull/13775)
+- [markers] fixed data race in problem view tree [#13841](https://github.com/eclipse-theia/theia/pull/13841)
+- [messages] updated logic to always resolve existing before showing new notification [#13668](https://github.com/eclipse-theia/theia/pull/13668)
+- [monaco] fixed editors theme change and widget not attached error [#13757](https://github.com/eclipse-theia/theia/pull/13757)
+- [notebook] added an indicator for loading notebooks [#13843](https://github.com/eclipse-theia/theia/pull/13843)
+- [notebook] added notebook output options and tag preference search [#13773](https://github.com/eclipse-theia/theia/pull/13773)
+- [notebook] disabled cell editor search widget [#13836](https://github.com/eclipse-theia/theia/pull/13836)
+- [notebook] improved ability to overwrite notebook services [#13776](https://github.com/eclipse-theia/theia/pull/13776)
+- [notebook] improved notebook cell drag images [#13791](https://github.com/eclipse-theia/theia/pull/13791)
+- [notebook] improved support for creating new notebooks [#13696](https://github.com/eclipse-theia/theia/pull/13696)
+- [notebook] updated logic to set notebook editor as active when opening in foreground [#13828](https://github.com/eclipse-theia/theia/pull/13828)
+- [notebook] updated logic to stop moving to next cell when suggestion widget is visible [#13774](https://github.com/eclipse-theia/theia/pull/13774)
+- [playwright] fixed type definition of TheiaAppFactory [#13799](https://github.com/eclipse-theia/theia/pull/13799) - Contributed on behalf of STMicroelectronics
+- [plugin] added stub for `registerMappedEditProvider` [#13681](https://github.com/eclipse-theia/theia/pull/13681) - Contributed on behalf of STMicroelectronics
+- [plugin] added support for PluginExt#extensionKind [#13763](https://github.com/eclipse-theia/theia/pull/13763)
+- [plugin] added support for TestRunRequest preserveFocus API [#13839](https://github.com/eclipse-theia/theia/pull/13839) - Contributed on behalf of STMicroelectronics
+- [plugin] fixed RPC proxy handler notifications and requests order [#13810](https://github.com/eclipse-theia/theia/pull/13810)
+- [plugin] fixed programmatic save for custom text editors [#13684](https://github.com/eclipse-theia/theia/pull/13684)
+- [plugin] fixed tab group API event order [#13812](https://github.com/eclipse-theia/theia/pull/13812)
+- [plugin] stubbed Chat and Language Model API [#13778](https://github.com/eclipse-theia/theia/pull/13778)
+- [plugin] stubbed activeStackItem and related change event in debug namespace [#13847](https://github.com/eclipse-theia/theia/pull/13847) - Contributed on behalf of STMicroelectronics
+- [plugin] updated logic to avoid pollution of all toolbars by actions contributed by tree views in extensions [#13768](https://github.com/eclipse-theia/theia/pull/13768) - Contributed on behalf of STMicroelectronics
+- [plugin] updated logic to return empty appRoot in web plugin host [#13762](https://github.com/eclipse-theia/theia/pull/13762)
+- [scm] updated jsdiff and simplify diff computation [#13787](https://github.com/eclipse-theia/theia/pull/13787) - Contributed on behalf of STMicroelectronics
+- [vsx-registry] updated logic to use targetPlatform when installing plugin from open-vsx [#13825](https://github.com/eclipse-theia/theia/pull/13825)
+
+[Breaking Changes:](#breaking_changes_1.51.0)
+
+- [electron] switched single instance on per default. [#13831](https://github.com/eclipse-theia/theia/pull/13831) - Contributed on behalf of STMicroelectronics
+- [filesystem] adjusted the "Save As" mechanism to assume that `Saveable.getSnapshot()` returns a full snapshot of the editor model [#13689](https://github.com/eclipse-theia/theia/pull/13689).
+
+## 1.50.0 - 06/03/2024
+
+- [application-package] bumped the default supported API from `1.88.1` to `1.89.1` [#13738](https://github.com/eclipse-theia/theia/pull/13738) - contributed on behalf of STMicroelectronics
+- [cli] upgrade the Theia build to use Typescript 5.4.5 [#13628](https://github.com/eclipse-theia/theia/pull/13628) - Contributed on behalf of STMicroelectronics
+- [core] added logic to delegate showing help to the back end process. [#13729](https://github.com/eclipse-theia/theia/pull/13729) - Contributed on behalf of STMicroelectronics
+- [core] added logic to don't reveal the focused element when updating the tree rows [#13703](https://github.com/eclipse-theia/theia/pull/13703) - Contributed on behalf of STMicroelectronics
+- [core] added logic to ensure globalSelection is correctly set when opening context menu on a tree widget [#13710](https://github.com/eclipse-theia/theia/pull/13710)
+- [core] added to logic to ensure usage of user-defined `THEIA_CONFIG_DIR` [#13708](https://github.com/eclipse-theia/theia/pull/13708) - Contributed on behalf of STMicroelectronics
+- [core] fixed hex editor by updating `msgpckr` to 1.10.2 [#13722](https://github.com/eclipse-theia/theia/pull/13722)
+- [core] improved `WebSocketConnectionProvider` deprecation message [#13713](https://github.com/eclipse-theia/theia/pull/13713) - Contributed on behalf of STMicroelectronics
+- [core] refactored auto save mechanism via a central service [#13683](https://github.com/eclipse-theia/theia/pull/13683)
+- [core] updated logic to make browserWindow of splashScreen transparent [#13699](https://github.com/eclipse-theia/theia/pull/13699)
+- [dev-container] added support for four previously unsupported dev container properties [#13714](https://github.com/eclipse-theia/theia/pull/13714)
+- [dev-container] improved logic to show dev-container label in status bar [#13744](https://github.com/eclipse-theia/theia/pull/13744)
+- [electron] updated electron to ^28.2.8 [#13580](https://github.com/eclipse-theia/theia/pull/13580)
+- [navigator] added logic to handle `isFileSystemResource` context key [#13664](https://github.com/eclipse-theia/theia/pull/13664)
+- [navigator] added logic to not show the new "Open With..." command on folders [#13678](https://github.com/eclipse-theia/theia/pull/13678)
+- [notebook] added additional css to notebook output webviews [#13666](https://github.com/eclipse-theia/theia/pull/13666)
+- [notebook] added basics for notebook cell drag image renderers [#13698](https://github.com/eclipse-theia/theia/pull/13698)
+- [notebook] added logic to select next notebook cell on first or last line of editor [#13656](https://github.com/eclipse-theia/theia/pull/13656)
+- [notebook] added logic to select the last cell when deleting selected last cell [#13715](https://github.com/eclipse-theia/theia/pull/13715)
+- [notebook] added logic to stop execution when deleting cell [#13701](https://github.com/eclipse-theia/theia/pull/13701)
+- [notebook] added responsive design for the main notebook toolbar [#13663](https://github.com/eclipse-theia/theia/pull/13663)
+- [notebook] aligned commands with vscode notebook commands [#13645](https://github.com/eclipse-theia/theia/pull/13645)
+- [notebook] aligned notebook scroll into view behaviour with vscode [#13742](https://github.com/eclipse-theia/theia/pull/13742)
+- [notebook] fixed focus loss of the notebook editor widget when bluring a cell editor [#13741](https://github.com/eclipse-theia/theia/pull/13741)
+- [notebook] fixed notebook cell divider size [#13745](https://github.com/eclipse-theia/theia/pull/13745)
+- [notebook] fixed storing of the notebook-outlineview state data [#13648](https://github.com/eclipse-theia/theia/pull/13648)
+- [notebook] improved notebook cell model lifecycle [#13675](https://github.com/eclipse-theia/theia/pull/13675)
+- [notebook] improved support for creating new notebooks [#13696](https://github.com/eclipse-theia/theia/pull/13696)
+- [plugin] added stub for `registerMappedEditProvider` [#13681](https://github.com/eclipse-theia/theia/pull/13681) - Contributed on behalf of STMicroelectronics
+- [plugin] added support `WindowState` active API [#13718](https://github.com/eclipse-theia/theia/pull/13718) - contributed on behalf of STMicroelectronics
+- [plugin] fixed github authentication built-in for electron case [#13611](https://github.com/eclipse-theia/theia/pull/13611) - Contributed on behalof of STMicroelectronics
+- [plugin] fixed incorrect URI conversions in custom-editors-main [#13653](https://github.com/eclipse-theia/theia/pull/13653)
+- [plugin] fixed quick pick separators from plugins [#13740](https://github.com/eclipse-theia/theia/pull/13740)
+- [plugin] improved vscode tab API [#13730](https://github.com/eclipse-theia/theia/pull/13730)
+- [plugin] updated `DropMetada` and `documentPaste` proposed API for 1.89 compatibility [#13733](https://github.com/eclipse-theia/theia/pull/13733) - contributed on behalf of STMicroelectronics
+- [plugin] updated nls metadata for VSCode API 1.89.0 [#13743](https://github.com/eclipse-theia/theia/pull/13743)
+- [remote] added logic to support plugin copying for remote feature [#13369](https://github.com/eclipse-theia/theia/pull/13369)
+- [terminal] fixed performance issues in terminal [#13735](https://github.com/eclipse-theia/theia/pull/13735) - Contributed on behalf of STMicroelectronics
+- [terminal] updated logic to allow transitive binding for TerminalFrontendContribution [#13667](https://github.com/eclipse-theia/theia/pull/13667)
+
+[Breaking Changes:](#breaking_changes_1.50.0)
+
+- [core] Classes implementing the `Saveable` interface no longer need to implement the `autoSave` field. However, a new `onContentChanged` event has been added instead.
+- [navigator] The `Open With...` command now uses a dedicated `OpenWithHandler` to populate the quick pick.
+ Adopters contributing an open handler need to explicitly add the handler to the `OpenWithHandler` ([#13573](https://github.com/eclipse-theia/theia/pull/13573)).
+
+## v1.49.0 - 04/29/2024
+
+- [application-manager] added logic to generate Extension Info in server application to avoid empty About Dialog [#13590](https://github.com/eclipse-theia/theia/pull/13590) - contributed on behalf of STMicroelectronics
+- [application-manager] fixed spawn calls for node LTS versions [#13614](https://github.com/eclipse-theia/theia/pull/13614)
+- [application-package] bumped the default supported API from `1.87.2` to `1.88.1` [#13646](https://github.com/eclipse-theia/theia/pull/13646) - contributed on behalf of STMicroelectronics
+- [cli] added "patches" folder to package.json "files" field [#13554](https://github.com/eclipse-theia/theia/pull/13554) - contributed on behalf of STMicroelectronics
+- [core] added a new built-in handler to open files with system application [#13601](https://github.com/eclipse-theia/theia/pull/13601)
+- [core] added logic to always consider the "passthrough" commmand enabled for keybindings [#13564](https://github.com/eclipse-theia/theia/pull/13564) - contributed on behalf of STMicroelectronics
+- [core] added Splash Screen Support for Electron [#13505](https://github.com/eclipse-theia/theia/pull/13505) - contributed on behalf of Pragmatiqu IT GmbH
+- [core] fixed window revealing when navigating with multiple windows [#13561](https://github.com/eclipse-theia/theia/pull/13561) - contributed on behalf of STMicroelectronics
+- [core] improved "Open With..." command UX [#13573](https://github.com/eclipse-theia/theia/pull/13573)
+- [filesystem] added logic to open editor on file upload [#13578](https://github.com/eclipse-theia/theia/pull/13578)
+- [monaco] added logic to prevent duplicate Clipboard actions in editor context menu [#13626](https://github.com/eclipse-theia/theia/pull/13626)
+- [monaco] fixed monaco localization [#13557](https://github.com/eclipse-theia/theia/pull/13557)
+- [notebook] added additional keybings to the notebook editor [#13594](https://github.com/eclipse-theia/theia/pull/13594)
+- [notebook] added logic to force notebook scrollbar update after content change [#13575](https://github.com/eclipse-theia/theia/pull/13575)
+- [notebook] added logic to read execution summary [#13567](https://github.com/eclipse-theia/theia/pull/13567)
+- [notebook] added logic to select notebook cell language [#13615](https://github.com/eclipse-theia/theia/pull/13615)
+- [notebook] added logic to show short title for notebook toolbar commands [#13586](https://github.com/eclipse-theia/theia/pull/13586)
+- [notebook] added logic to use notebook URI as context for toolbar commands [#13585](https://github.com/eclipse-theia/theia/pull/13585)
+- [notebook] added shift+enter keybinding for markdown cells [#13563](https://github.com/eclipse-theia/theia/pull/13563)
+- [notebook] added support for Outline-View and Breadcrumbs [#13562](https://github.com/eclipse-theia/theia/pull/13562)
+- [notebook] added support for truncated notebook output commands [#13555](https://github.com/eclipse-theia/theia/pull/13555)
+- [notebook] disabled clear all outputs in notebook main toolbar [#13569](https://github.com/eclipse-theia/theia/pull/13569)
+- [notebook] fixed clear cell outputs command [#13640](https://github.com/eclipse-theia/theia/pull/13640)
+- [notebook] fixed kernel autobind for on startup opened notebooks [#13598](https://github.com/eclipse-theia/theia/pull/13598)
+- [notebook] fixed logic to set context for multiple notebooks [#13566](https://github.com/eclipse-theia/theia/pull/13566)
+- [notebook] fixed notebook cell EOL splitting [#13574](https://github.com/eclipse-theia/theia/pull/13574)
+- [notebook] fixed notebook model/cell disposal [#13606](https://github.com/eclipse-theia/theia/pull/13606)
+- [notebook] fixed notebook widget icon on reload [#13612](https://github.com/eclipse-theia/theia/pull/13612)
+- [notebook] improved notebook cell context key handling [#13572](https://github.com/eclipse-theia/theia/pull/13572)
+- [notebook] improved notebook markdown cell rendering [#13577](https://github.com/eclipse-theia/theia/pull/13577)
+- [plugin] added logic to hide empty plugin view containers from user [#13581](https://github.com/eclipse-theia/theia/pull/13581)
+- [plugin] added logic to ignore vsix files in local-plugins dir [#13435](https://github.com/eclipse-theia/theia/pull/13435) - contributed on behalf of STMicroelectronics
+- [plugin] fixed `onLanguage` activation event [#13630](https://github.com/eclipse-theia/theia/pull/13630)
+- [plugin] fixed issue with webview communication for Safari [#13587](https://github.com/eclipse-theia/theia/pull/13587)
+- [plugin] updated `DropMetada` and `documentPaste` proposed API for 1.88 compatibility [#13632](https://github.com/eclipse-theia/theia/pull/13632)
+- [plugin] updated back-end plugin deployment logic [#13643](https://github.com/eclipse-theia/theia/pull/13643) - contributed on behalf of STMicroelectronics
+- [process] fixed spawn calls for node LTS versions [#13614](https://github.com/eclipse-theia/theia/pull/13614)
+- [remote] fixed remote support in packaged apps [#13584](https://github.com/eclipse-theia/theia/pull/13584)
+- [scm] added support for dirty diff peek view [#13104](https://github.com/eclipse-theia/theia/pull/13104)
+- [terminal] fixed spawn calls for node LTS versions [#13614](https://github.com/eclipse-theia/theia/pull/13614)
+- [test] stubbed VS Code `Test Coverage` API [#13631](https://github.com/eclipse-theia/theia/pull/13631) - contributed on behalf of STMicroelectronics
+- [vsx-registry] fixed logic to bind Extension search bar within view container [#13623](https://github.com/eclipse-theia/theia/pull/13623)
+
+[Breaking Changes:](#breaking_changes_1.49.0)
+
+- [scm] revised some of the dirty diff related types [#13104](https://github.com/eclipse-theia/theia/pull/13104)
+ - replaced `DirtyDiff.added/removed/modified` with `changes`, which provides more detailed information about the changes
+ - changed the semantics of `LineRange` to represent a range that spans up to but not including the `end` line (previously, it included the `end` line)
+ - changed the signature of `DirtyDiffDecorator.toDeltaDecoration(LineRange | number, EditorDecorationOptions)` to `toDeltaDecoration(Change)`
+
+## v1.48.0 - 03/28/2024
+
+- [application-package] bumped the default supported API from `1.86.2` to `1.87.2` [#13514](https://github.com/eclipse-theia/theia/pull/13514) - contributed on behalf of STMicroelectronics
+- [core] added "New File" default implementation [#13344](https://github.com/eclipse-theia/theia/pull/13344)
+- [core] added logic to check for disposed before sending update message in toolbars [#13454](https://github.com/eclipse-theia/theia/pull/13454) - contributed on behalf of STMicroelectronics
+- [core] fixed default translation of Close Editor command [#13412](https://github.com/eclipse-theia/theia/pull/13412)
+- [core] fixed logic to allow reopening secondary windows [#13509](https://github.com/eclipse-theia/theia/pull/13509) - contributed on behalf of STMicroelectronics
+- [core] fixed rending of quickpick buttons [#13342](https://github.com/eclipse-theia/theia/pull/13342) - contributed on behalf of STMicroelectronics
+- [core] updated logic to remove unneeded URI conversion [#13415](https://github.com/eclipse-theia/theia/pull/13415)
+- [dev-container] added first version of dev-container support [#13372](https://github.com/eclipse-theia/theia/pull/13372)
+- [editor] added secondary window support for text editors [#13493](https://github.com/eclipse-theia/theia/pull/13493) - contributed on behalf of STMicroelectronics
+- [git] fixed detecting changes after git init [#13487](https://github.com/eclipse-theia/theia/pull/13487)
+- [metrics] allowed accessing the metrics endpoint for performance analysis in electron [#13380](https://github.com/eclipse-theia/theia/pull/13380) - contributed on behalf of STMicroelectronics
+- [monaco] fixed monaco quickpick [#13451](https://github.com/eclipse-theia/theia/pull/13451) - contributed on behalf of STMicroelectronics
+- [monaco] fixed rending of quickpick buttons [#13342](https://github.com/eclipse-theia/theia/pull/13342) - contributed on behalf of STMicroelectronics
+- [notebook] added execute cells above/below commands [#13528](https://github.com/eclipse-theia/theia/pull/13528)
+- [notebook] added execution order display to code cells [#13502](https://github.com/eclipse-theia/theia/pull/13502)
+- [notebook] added keybindings to notebook editor [#13497](https://github.com/eclipse-theia/theia/pull/13497)
+- [notebook] added support for custom widget types for notebook outputs [#13517](https://github.com/eclipse-theia/theia/pull/13517)
+- [notebook] fixed cell execution height styling [#13515](https://github.com/eclipse-theia/theia/pull/13515)
+- [notebook] fixed context keys for notebook editor context [#13448](https://github.com/eclipse-theia/theia/pull/13448)
+- [notebook] fixed keybindings triggers when cell editor is focused [#13500](https://github.com/eclipse-theia/theia/pull/13500)
+- [notebook] fixed notebook document metadata edit [#13528](https://github.com/eclipse-theia/theia/pull/13528)
+- [notebook] fixed renaming and moving of open notebooks [#13467](https://github.com/eclipse-theia/theia/pull/13467)
+- [notebook] fixed undo redo keybindings for notebook editor [#13518](https://github.com/eclipse-theia/theia/pull/13518)
+- [notebook] improved focusing of the notebook cell editors [#13516](https://github.com/eclipse-theia/theia/pull/13516)
+- [notebook] improved performance when opening notebooks [#13488](https://github.com/eclipse-theia/theia/pull/13488)
+- [notebook] updated logic to only initialize notebook cell editor when in viewport [#13476](https://github.com/eclipse-theia/theia/pull/13476)
+- [plugin] added `Interval` `TextEditorLineNumbersStyle` [#13458](https://github.com/eclipse-theia/theia/pull/13458) - contributed on behalf of STMicroelectronics
+- [plugin] added terminal observer API [#13402](https://github.com/eclipse-theia/theia/pull/13402)
+- [plugin] changed logic to ensure that showOpenDialog returns correct file URI [#13208](https://github.com/eclipse-theia/theia/pull/13208) - contributed on behalf of STMicroelectronics
+- [plugin] fixed quickpick [#13451](https://github.com/eclipse-theia/theia/pull/13451) - contributed on behalf of STMicroelectronics
+- [plugin] made `acquireVsCodeApi` function available on global objects [#13411](https://github.com/eclipse-theia/theia/pull/13411)
+- [plugin] updated logic to avoid disposal of `QuickInputExt` on hide [#13485](https://github.com/eclipse-theia/theia/pull/13485) - contributed on behalf of STMicroelectronics
+- [remote] added logic to support remote port forwarding [#13439](https://github.com/eclipse-theia/theia/pull/13439)
+- [terminal] added logic to resolve links to workspace files in terminal [#13498](https://github.com/eclipse-theia/theia/pull/13498) - contributed on behalf of STMicroelectronics
+- [terminal] added terminal observer API [#13402](https://github.com/eclipse-theia/theia/pull/13402)
+
+[Breaking Changes:](#breaking_changes_1.48.0)
+- [core] Add secondary windows support for text editors. [#13493](https://github.com/eclipse-theia/theia/pull/13493 ). The changes in require more extensive patches for our dependencies than before. For this purpose, we are using the `patch-package` library. However, this change requires adopters to add the line `"postinstall": "theia-patch"` to the `package.json` at the root of their monorepo (where the `node_modules` folder is located). - contributed on behalf of STMicroelectronics
+
+## v1.47.0 - 02/29/2024
+
+- [application-package] bumped the default supported API from `1.85.1` to `1.86.2` [#13429](https://github.com/eclipse-theia/theia/pull/13429) - contributed on behalf of STMicroelectronics
+- [core] added logic to show decorations in the editor tabs [#13371](https://github.com/eclipse-theia/theia/pull/13371)
+- [core] added ts-docs for several key utility classes [#13324](https://github.com/eclipse-theia/theia/pull/13324)
+- [core] fixed core localizations for electron [#13331](https://github.com/eclipse-theia/theia/pull/13331)
+- [core] fixed memory leak in `DockPanelRenderer` and `ToolbarAwareTabBar` [#13327](https://github.com/eclipse-theia/theia/pull/13327)
+- [core] fixed update of CompositeMenuNode properties [#13425](https://github.com/eclipse-theia/theia/pull/13425)
+- [core] improved title rendering on menu bar change [#13317](https://github.com/eclipse-theia/theia/pull/13317)
+- [core] updated code to use common uuid generator everywhere [#13255](https://github.com/eclipse-theia/theia/pull/13255)
+- [core] updated logic to use `tslib` in order to reduce bundle size [#13350](https://github.com/eclipse-theia/theia/pull/13350)
+- [core] upgraded msgpackr to 1.10.1 [#13365](https://github.com/eclipse-theia/theia/pull/13365) - contributed on behalf of STMicroelectronics
+- [debug] fixed issue with unexpected breakpoint in python [#12543](https://github.com/eclipse-theia/theia/pull/12543)
+- [documentation] extended custom plugin API documentation [#13358](https://github.com/eclipse-theia/theia/pull/13358)
+- [editor] improved readonly editor behaviour [#13403](https://github.com/eclipse-theia/theia/pull/13403)
+- [filesystem] fixed issue with non recursive folder deletion [#13361](https://github.com/eclipse-theia/theia/pull/13361)
+- [filesystem] implemented readonly markdown message for file system providers [#13414](https://github.com/eclipse-theia/theia/pull/13414) - contributed on behalf of STMicroelectronics
+- [monaco] upgraded Monaco to 1.83.1 [#13217](https://github.com/eclipse-theia/theia/pull/13217)
+- [notebook] added support for proposed notebook kernel messaging and preload contribution point [#13401](https://github.com/eclipse-theia/theia/pull/13401)
+- [notebook] fixed notebook renderer messaging [#13401](https://github.com/eclipse-theia/theia/pull/13401)
+- [notebook] fixed race condition in notebook kernel association [#13364](https://github.com/eclipse-theia/theia/pull/13364)
+- [notebook] improved logic to update notebook execution timer [#13366](https://github.com/eclipse-theia/theia/pull/13366)
+- [notebook] improved notebook scrolling behaviour [#13338](https://github.com/eclipse-theia/theia/pull/13338)
+- [notebook] improved styling for notebook toolbar items [#13334](https://github.com/eclipse-theia/theia/pull/13334)
+- [notebook] fixed scroll behaviour of Notebooks [#13430](https://github.com/eclipse-theia/theia/pull/13430)
+- [plugin] added command to install plugins from the command line [#13406](https://github.com/eclipse-theia/theia/issues/13406) - contributed on behalf of STMicroelectronics
+- [plugin] added logic to support `workspace.save(URI)` and `workspace.saveAs(URI)` [#13393](https://github.com/eclipse-theia/theia/pull/13393) - contributed on behalf of STMicroelectronics
+- [plugin] added support for `extension/context`, `terminal/context`, and `terminal/title/context` menu contribution points [#13226](https://github.com/eclipse-theia/theia/pull/13226)
+- [plugin] fixed custom editors asset loading [#13382](https://github.com/eclipse-theia/theia/pull/13382)
+- [plugin] fixed logic to use correct path for hosted plugin deployer handler [#13427](https://github.com/eclipse-theia/theia/pull/13427) - contributed on behalf of STMicroelectronics
+- [plugin] fixed regressions from headless plugins introduction [#13337](https://github.com/eclipse-theia/theia/pull/13337) - contributed on behalf of STMicroelectronics
+- [plugin] support TestRunProfile onDidChangeDefault introduced in VS Code 1.86.0 [#13388](https://github.com/eclipse-theia/theia/pull/13388) - contributed on behalf of STMicroelectronics
+- [plugin] updated `WorkspaceEdit` metadata typing [#13395](https://github.com/eclipse-theia/theia/pull/13395) - contributed on behalf of STMicroelectronics
+- [search-in-workspace] added logic to focus on next and previous search results [#12703](https://github.com/eclipse-theia/theia/pull/12703)
+- [task] fixed logic to configure tasks [#13367](https://github.com/eclipse-theia/theia/pull/13367) - contributed on behalf of STMicroelectronics
+- [terminal] updated to latest xterm version [#12691](https://github.com/eclipse-theia/theia/pull/12691)
+- [vsx-registry] added `--install-plugin` cli command [#13421](https://github.com/eclipse-theia/theia/pull/13421) - contributed on behalf of STMicroelectronics
+- [vsx-registry] added possibility to install vsix files from the explorer view [#13291](https://github.com/eclipse-theia/theia/pull/13291)
+
+[Breaking Changes:](#breaking_changes_1.47.0)
+
+- [monaco] Upgrade Monaco dependency to 1.83.1 [#13217](https://github.com/eclipse-theia/theia/pull/13217)- contributed on behalf of STMicroelectronics\
+ There are a couple of breaking changes that come with this monaco update
+ - Moved `ThemaIcon` and `ThemeColor` to the common folder
+ - Minor typing adjustments in QuickPickService: in parti
+ - FileUploadService: moved id field from data transfer item to the corresponding file info
+ - The way we instantiate monaco services has changed completely: if you touch monaco services in your code, please read the description in the
+ file comment in `monaco-init.ts`.
+
+## v1.46.0 - 01/25/2024
+- [plugin] Add prefix to contributed view container ids [#13362](https://github.com/eclipse-theia/theia/pull/13362) - contributed on behalf of STMicroelectronics
+- [application-manager] updated message for missing Electron main entries [#13242](https://github.com/eclipse-theia/theia/pull/13242)
+- [application-package] bumped the default supported API from `1.84.2` to `1.85.1` [#13276](https://github.com/eclipse-theia/theia/pull/13276) - contributed on behalf of STMicroelectronics
+- [browser-only] added support for 'browser-only' Theia [#12853](https://github.com/eclipse-theia/theia/pull/12853)
+- [builtins] update built-ins to version 1.83.1 [#13298](https://github.com/eclipse-theia/theia/pull/13298) - contributed on behalf of STMicroelectronics
+- [core] added keybindings to toggle the tree checkbox [#13271](https://github.com/eclipse-theia/theia/pull/13271)
+- [core] added logic to dispose cancellation event listeners [#13254](https://github.com/eclipse-theia/theia/pull/13254)
+- [core] added preference 'workbench.tree.indent' to control the indentation in the tree widget [#13179](https://github.com/eclipse-theia/theia/pull/13179) - contributed on behalf of STMicroelectronics
+- [core] fixed copy/paste from a menu in electron [#13220](https://github.com/eclipse-theia/theia/pull/13220) - contributed on behalf of STMicroelectronics
+- [core] fixed file explorer progress bar issue [#13268](https://github.com/eclipse-theia/theia/pull/13268)
+- [core] fixed issue with cyclic menu contributions [#13264](https://github.com/eclipse-theia/theia/pull/13264)
+- [core] fixed leak when reconnecting to back end without reload [#13250](https://github.com/eclipse-theia/theia/pull/13250) - contributed on behalf of STMicroelectronics
+- [core] fixed SelectComponent to render dropdown correctly in dialog [#13261](https://github.com/eclipse-theia/theia/pull/13261)
+- [core] removed error logs from RpcProxyFactory [#13191](https://github.com/eclipse-theia/theia/pull/13191)
+- [documentation] improved documentation about 'ContributionProvider' use [#13278](https://github.com/eclipse-theia/theia/pull/13278)
+- [docuemtnation] improved documentation on passing objects across RPC [#13238](https://github.com/eclipse-theia/theia/pull/13238)
+- [documentation] updated plugin API docs for headless plugins and Inversify DI [#13299](https://github.com/eclipse-theia/theia/pull/13299)
+- [filesystem] updated logic to only read unbuffered when we read the whole file [#13197](https://github.com/eclipse-theia/theia/pull/13197)
+- [headless-plugin] added support for "headless plugins" in a new plugin host [#13138](https://github.com/eclipse-theia/theia/pull/13138)
+- [monaco] updated logic to add document URI as context to getDefaultFormatter [#13280](https://github.com/eclipse-theia/theia/pull/13280) - contributed on behalf of STMicroelectronics
+- [notebook] fixed dynamic notebook widgets resizing [#13289](https://github.com/eclipse-theia/theia/pull/13289)
+- [notebook] fixed multiple problems with the notebook output rendering [#13239](https://github.com/eclipse-theia/theia/pull/13239)
+- [notebook] improved notebook error logging [#13256](https://github.com/eclipse-theia/theia/pull/13256)
+- [plugin] added logic to synchronize messages sent via different proxies [#13180](https://github.com/eclipse-theia/theia/pull/13180)
+- [remote] added support for specifying the port of a remote SSH connection [#13296](https://github.com/eclipse-theia/theia/pull/13296) - contributed on behalf of STMicroelectronics
+- [plugin] fixed inputbox onTriggerButton() event [#13207](https://github.com/eclipse-theia/theia/pull/13207) - contributed on behalf of STMicroelectronics
+- [plugin] fixed localization for the removeSession method [#13257](https://github.com/eclipse-theia/theia/pull/13257)
+- [plugin] fixed `vscode.env.appRoot` path [#13285](https://github.com/eclipse-theia/theia/pull/13285)
+- [plugin] stubbed multiDocumentHighlightProvider proposed API [#13248](https://github.com/eclipse-theia/theia/pull/13248) - contributed on behalf of STMicroelectronics
+- [plugin] updated logic to handle activeCustomEditorId [#13267](https://github.com/eclipse-theia/theia/pull/13267)
+- [plugin] updated logic to pass context to webview context menu action [#13228](https://github.com/eclipse-theia/theia/pull/13228)
+- [plugin] updated logic to use more stable hostname for webviews [#13092](https://github.com/eclipse-theia/theia/pull/13225) [#13258](https://github.com/eclipse-theia/theia/pull/13265)
+- [terminal] fixed wording in error message [#13245](https://github.com/eclipse-theia/theia/pull/13245) - contributed on behalf of STMicroelectronics
+- [terminal] renamed terminal.sendText() parameter from addNewLine to shouldExecute [#13236](https://github.com/eclipse-theia/theia/pull/13236) - contributed on behalf of STMicroelectronics
+- [terminal] updated logic to resize terminal [#13281](https://github.com/eclipse-theia/theia/pull/13281)
+- [terminal] updated terminalQuickFixProvider proposed API according to vscode 1.85 version [#13240](https://github.com/eclipse-theia/theia/pull/13240) - contributed on behalf of STMicroelectronics
+- [vsx-registry] implemented verified extension filtering [#12995](https://github.com/eclipse-theia/theia/pull/12995)
+
+[Breaking Changes:](#breaking_changes_1.46.0)
+
+- [core] moved `FileUri` from `node` package to `common` [#12853](https://github.com/eclipse-theia/theia/pull/12853)
+- [plugin] introduced new common interfaces/classes for reuse by different plugin hosts [#13138](https://github.com/eclipse-theia/theia/pull/13138)
+
+## v1.45.0 - 12/21/2023
+
+- [application-manager] updated logic to allow rebinding messaging services in preload [#13199](https://github.com/eclipse-theia/theia/pull/13199)
+- [application-package] bumped the default supported API from `1.83.1` to `1.84.2` [#13198](https://github.com/eclipse-theia/theia/pull/13198)
+- [core] added cli parameter `--electronUserData` to control `userDataPath` [#13155](https://github.com/eclipse-theia/theia/pull/13155)
+- [core] added logic to control the size and position of secondary windows [#13201](https://github.com/eclipse-theia/theia/pull/13201)
+- [core] added logic to save untitled files to the last active folder [#13184](https://github.com/eclipse-theia/theia/pull/13184)
+- [core] fixed regression preventing closing the application when a dirty editor is present [#13173](https://github.com/eclipse-theia/theia/pull/13173)
+- [core] fixed styling for compressed navigator indents [#13162](https://github.com/eclipse-theia/theia/pull/13162)
+- [core] introduced timeout logic for keeping connection contexts alive [#13082](https://github.com/eclipse-theia/theia/pull/13082)
+- [core] updated `nls.metadata.json` for `1.84.2` [#13200](https://github.com/eclipse-theia/theia/pull/13200)
+- [debug] fixed issue where debug configuration providers would replace other providers [#13196](https://github.com/eclipse-theia/theia/pull/13196)
+- [documentation] improved documentation regarding the addition of the plugin API in the plugin host [#13153](https://github.com/eclipse-theia/theia/pull/13153)
+- [notebook] fixed notebook kernel selection [#13171](https://github.com/eclipse-theia/theia/pull/13171)
+- [notebook] implemented general API improvements [#13012](https://github.com/eclipse-theia/theia/pull/13012)
+- [notebook] optimized output logic [#13137](https://github.com/eclipse-theia/theia/pull/13137)
+- [plugin] added documentation about adding custom activation events [#13190](https://github.com/eclipse-theia/theia/pull/13190)
+- [plugin] added logic to deploy plugins asynchronously [#13134](https://github.com/eclipse-theia/theia/pull/13134)
+- [plugin] added logic to not reject unknown schemas in `WindowStateExt.asExternalUri` [#13057](https://github.com/eclipse-theia/theia/pull/13057)
+- [plugin] added support for the `TestMessage.contextValue` VS Code API [#13176](https://github.com/eclipse-theia/theia/pull/13176) - contributed on behalf of STMicroelectronics
+- [plugin] added support for the `webview/context` menu contribution point [#13166](https://github.com/eclipse-theia/theia/pull/13166)
+- [plugin] fixed incorrect `unsupported activation error` in stdout [#13095](https://github.com/eclipse-theia/theia/pull/13095)
+- [plugin] fixed issue where the `onView` activation event was incorrectly generated [#13091](https://github.com/eclipse-theia/theia/pull/13091)
+- [plugin] fixed plugin icon styling [#13101](https://github.com/eclipse-theia/theia/pull/13101)
+- [terminal] updated logic to use `ApplicationShell` when expanding/collapsing the bottom panel [#13131](https://github.com/eclipse-theia/theia/pull/13131)
+- [workspace] added logic to create an empty workspace if no workspace is active on `updateWorkspaceFolders` event [#13181](https://github.com/eclipse-theia/theia/pull/13181) - contributed on behalf of STMicroelectronics
+
+[Breaking Changes:](#breaking_changes_1.45.0)
+
+- [plugin] updated VS Code extension locations: deployment dir switched to `$CONFDIR/deployedPlugin`, `.vsix` files from `$CONFDIR/extensions` are deployed automatically [#13178](https://github.com/eclipse-theia/theia/pull/13178) - Contributed on behalf of STMicroelectronics
+
## v1.44.0 - 11/30/2023
- [application-manager] added option to copy `trash` dependency to the bundle [#13112](https://github.com/eclipse-theia/theia/pull/13112)
@@ -149,6 +564,8 @@
- [deps] bumped supported Node.js version from 16.x to >=18, you may need to update your environments [#12711](https://github.com/eclipse-theia/theia/pull/12711)
- [preferences] removed the `welcome.alwaysShowWelcomePage` preference in favor of `workbench.startupEditor` [#12813](https://github.com/eclipse-theia/theia/pull/12813)
+- [terminal] deprecated `terminal.integrated.rendererType` preference [#12691](https://github.com/eclipse-theia/theia/pull/12691)
+- [terminal] removed protected method `TerminalWidgetImpl.getTerminalRendererType` [#12691](https://github.com/eclipse-theia/theia/pull/12691)
## v1.40.0 - 07/27/2023
diff --git a/LICENSE-vscode.txt b/LICENSE-vscode.txt
new file mode 100644
index 0000000000000..701c01487abff
--- /dev/null
+++ b/LICENSE-vscode.txt
@@ -0,0 +1,29 @@
+Below is the full text of vscode's MIT license, copied from the link below (it has not changed except cosmetically, since the Theia project was started):
+
+https://github.com/microsoft/vscode/blob/2dd03eaebe21473f3af6df2a229199b9aa138e97/LICENSE.txt
+
+This license covers code originally copied from the vscode repository and integrated in this project.
+
+-----
+
+MIT License
+
+Copyright (c) 2015 - present Microsoft Corporation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index d925522f2df2b..7787064ed858a 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,6 @@
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-curved)](https://github.com/eclipse-theia/theia/labels/help%20wanted)
- [![Discourse status](https://img.shields.io/discourse/status?label=Chat&server=https%3A%2F%2Fcommunity.theia-ide.org%2F)](https://community.theia-ide.org/)
[![Build Status](https://github.com/eclipse-theia/theia/workflows/Build/badge.svg?branch=master)](https://github.com/eclipse-theia/theia/actions?query=branch%3Amaster+event%3Apush+event%3Aschedule)
[![Publish VS Code Built-in Extensions](https://github.com/eclipse-theia/vscode-builtin-extensions/actions/workflows/publish-vsx-latest.yml/badge.svg?branch=master)](https://github.com/eclipse-theia/vscode-builtin-extensions/actions/workflows/publish-vsx-latest.yml)
[![Open questions](https://img.shields.io/badge/Open-questions-blue.svg?style=flat-curved)](https://github.com/eclipse-theia/theia/discussions/categories/q-a)
@@ -20,6 +19,7 @@ Eclipse Theia is an extensible framework to develop full-fledged multi-language
- [Website](#website)
+- [Repositories](#repositories)
- [Releases](#releases)
- [Scope](#scope)
- [Roadmap](#roadmap)
@@ -38,7 +38,10 @@ Eclipse Theia is an extensible framework to develop full-fledged multi-language
## Website
-[Visit the Eclipse Theia website](http://www.theia-ide.org) for more information and [the Theia documentation](http://www.theia-ide.org/doc).
+[Visit the Eclipse Theia website](http://www.theia-ide.org) for more information and [the Theia documentation](http://www.theia-ide.org/docs).
+
+## Repositories
+This is the main repository for the Eclipse Theia project, containing the sources of the Theia Platform. Please open generic discussions, bug reports and feature requests about Theia on this repository. The Theia project also includes additional repositories, e.g. for the [artifacts building the Theia IDE](https://github.com/eclipse-theia/theia-blueprint) and the [Theia website](https://github.com/eclipse-theia/theia-website). Please also see the [overview of all Theia project repositories](https://github.com/eclipse-theia).
## Releases
@@ -65,10 +68,10 @@ See [our roadmap](https://github.com/eclipse-theia/theia/wiki/Eclipse-Theia-Road
Here you can find guides and examples for common scenarios to adopt Theia:
- [Get an overview of how to get started](https://theia-ide.org/#gettingstarted) on the Theia website
-- [Develop a Theia application - your own IDE/Tool](https://www.theia-ide.org/doc/Composing_Applications.html)
+- [Develop a Theia application - your own IDE/Tool](https://theia-ide.org/docs/composing_applications/)
- [Learn about Theia's extension mechanisms](https://theia-ide.org/docs/extensions/)
- [Develop a VS Code like extension](https://theia-ide.org/docs/authoring_vscode_extensions/)
-- [Develop a Theia extension](http://www.theia-ide.org/doc/Authoring_Extensions.html)
+- [Develop a Theia extension](https://theia-ide.org/docs/authoring_extensions/)
- [Test a VS Code extension in Theia](https://github.com/eclipse-theia/theia/wiki/Testing-VS-Code-extensions)
- [Package a desktop Theia application with Electron](https://theia-ide.org/docs/blueprint_documentation/)
@@ -81,14 +84,14 @@ Read below to learn how to take part in improving Theia:
- Find an issue to work on and submit a pull request
- First time contributing to open source? Pick a [good first issue](https://github.com/eclipse-theia/theia/labels/good%20first%20issue) to get you familiar with GitHub contributing process.
- First time contributing to Theia? Pick a [beginner friendly issue](https://github.com/eclipse-theia/theia/labels/beginners) to get you familiar with codebase and our contributing process.
- - Want to become a Committer? Solve an issue showing that you understand Theia objectives and architecture. [Here](https://github.com/eclipse-theia/theia/labels/help%20wanted) is a good list to start. Further, have a look at our [roadmap](https://github.com/eclipse-theia/theia/wiki/Roadmap) to align your contributions with the current project goals.
+ - Want to become a Committer? Solve an issue showing that you understand Theia objectives and architecture. [Here](https://github.com/eclipse-theia/theia/labels/help%20wanted) is a good list to start. Further, have a look at our [roadmap](https://github.com/eclipse-theia/theia/wiki/Eclipse-Theia-Roadmap) to align your contributions with the current project goals.
- Could not find an issue? Look for bugs, typos, and missing features.
## Feedback
Read below how to engage with Theia community:
-- Join the discussion on [Discourse](https://community.theia-ide.org/).
+- Join the discussion on [GitHub](https://github.com/eclipse-theia/theia/discussions).
- Ask a question, request a new feature and file a bug with [GitHub issues](https://github.com/eclipse-theia/theia/issues/new/choose).
- Vote on existing GitHub issues by reacting with a 👍. We regularly check issues with votes!
- Star the repository to show your support.
@@ -112,9 +115,8 @@ Read below how to engage with Theia community:
## License
- [Eclipse Public License 2.0](LICENSE-EPL)
-- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](LICENSE-GPL-W-CLASSPATH-EXCEPTION)
+- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](LICENSE-GPL-2.0-ONLY-CLASSPATH-EXCEPTION)
## Trademark
-"Theia" is a trademark of the Eclipse Foundation
-
+"Theia" is a **trademark of the Eclipse Foundation**. [Learn More](https://www.eclipse.org/theia)
diff --git a/configs/base.tsconfig.json b/configs/base.tsconfig.json
index d6b6e42db7c55..731dcdfd7106f 100644
--- a/configs/base.tsconfig.json
+++ b/configs/base.tsconfig.json
@@ -12,6 +12,7 @@
"strictNullChecks": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
+ "importHelpers": true,
"downlevelIteration": true,
"resolveJsonModule": true,
"module": "CommonJS",
diff --git a/dependency-check-baseline.json b/dependency-check-baseline.json
index a2d415608c293..b4146da9836f4 100644
--- a/dependency-check-baseline.json
+++ b/dependency-check-baseline.json
@@ -1,9 +1,3 @@
{
- "npm/npmjs/-/allure-commandline/2.23.0": "Approved https://gitlab.eclipse.org/eclipsefdn/emo-team/iplab/-/issues/9379",
- "npm/npmjs/-/eslint-plugin-deprecation/1.2.1": "Approved as 'works-with': https://dev.eclipse.org/ipzilla/show_bug.cgi?id=22573",
- "npm/npmjs/-/jschardet/2.3.0": "Approved for Eclipse Theia: https://dev.eclipse.org/ipzilla/show_bug.cgi?id=22481",
- "npm/npmjs/-/jsdom/11.12.0": "Approved as 'works-with': https://dev.eclipse.org/ipzilla/show_bug.cgi?id=23640https://dev.eclipse.org/ipzilla/show_bug.cgi?id=23640",
- "npm/npmjs/-/lzma-native/8.0.6": "Approved as 'works-with': https://gitlab.eclipse.org/eclipsefdn/emo-team/iplab/-/issues/1850",
- "npm/npmjs/-/normalize-package-data/3.0.3": "Under review, licence believed to be BSD-2-Clause: https://gitlab.eclipse.org/eclipsefdn/emo-team/iplab/-/issues/11614",
- "npm/npmjs/-/playwright-core/1.22.2": "Approved as 'works-with': https://gitlab.eclipse.org/eclipsefdn/emo-team/iplab/-/issues/2734"
+ "npm/npmjs/-/advanced-mark.js/2.6.0": "Manually approved"
}
diff --git a/dev-packages/application-manager/package.json b/dev-packages/application-manager/package.json
index 1a468a1c92396..3ab87e9160235 100644
--- a/dev-packages/application-manager/package.json
+++ b/dev-packages/application-manager/package.json
@@ -1,6 +1,6 @@
{
"name": "@theia/application-manager",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia application manager API.",
"publishConfig": {
"access": "public"
@@ -33,9 +33,9 @@
"@babel/plugin-transform-classes": "^7.10.0",
"@babel/plugin-transform-runtime": "^7.10.0",
"@babel/preset-env": "^7.10.0",
- "@theia/application-package": "1.44.0",
- "@theia/ffmpeg": "1.44.0",
- "@theia/native-webpack-plugin": "1.44.0",
+ "@theia/application-package": "1.54.0",
+ "@theia/ffmpeg": "1.54.0",
+ "@theia/native-webpack-plugin": "1.54.0",
"@types/fs-extra": "^4.0.2",
"@types/semver": "^7.5.0",
"babel-loader": "^8.2.2",
@@ -45,6 +45,7 @@
"css-loader": "^6.2.0",
"electron-rebuild": "^3.2.7",
"fs-extra": "^4.0.2",
+ "http-server": "^14.1.1",
"ignore-loader": "^0.1.2",
"less": "^3.0.3",
"mini-css-extract-plugin": "^2.6.1",
@@ -56,8 +57,8 @@
"source-map": "^0.6.1",
"source-map-loader": "^2.0.1",
"source-map-support": "^0.5.19",
- "string-replace-loader": "^3.1.0",
"style-loader": "^2.0.0",
+ "tslib": "^2.6.2",
"umd-compat-loader": "^2.1.2",
"webpack": "^5.76.0",
"webpack-cli": "4.7.0",
@@ -73,7 +74,7 @@
}
},
"devDependencies": {
- "@theia/ext-scripts": "1.44.0",
+ "@theia/ext-scripts": "1.54.0",
"@types/node-abi": "*"
},
"nyc": {
diff --git a/dev-packages/application-manager/src/application-package-manager.ts b/dev-packages/application-manager/src/application-package-manager.ts
index beec9f99ec70c..09d3d26956789 100644
--- a/dev-packages/application-manager/src/application-package-manager.ts
+++ b/dev-packages/application-manager/src/application-package-manager.ts
@@ -119,10 +119,32 @@ export class ApplicationPackageManager {
start(args: string[] = []): cp.ChildProcess {
if (this.pck.isElectron()) {
return this.startElectron(args);
+ } else if (this.pck.isBrowserOnly()) {
+ return this.startBrowserOnly(args);
}
return this.startBrowser(args);
}
+ startBrowserOnly(args: string[]): cp.ChildProcess {
+ const { command, mainArgs, options } = this.adjustBrowserOnlyArgs(args);
+ return this.__process.spawnBin(command, mainArgs, options);
+ }
+
+ adjustBrowserOnlyArgs(args: string[]): Readonly<{ command: string, mainArgs: string[]; options: cp.SpawnOptions }> {
+ let { mainArgs, options } = this.adjustArgs(args);
+
+ // first parameter: path to generated frontend
+ // second parameter: disable cache to support watching
+ mainArgs = ['lib/frontend', '-c-1', ...mainArgs];
+
+ const portIndex = mainArgs.findIndex(v => v.startsWith('--port'));
+ if (portIndex === -1) {
+ mainArgs.push('--port=3000');
+ }
+
+ return { command: 'http-server', mainArgs, options };
+ }
+
startElectron(args: string[]): cp.ChildProcess {
// If possible, pass the project root directory to electron rather than the script file so that Electron
// can determine the app name. This requires that the package.json has a main field.
@@ -139,7 +161,7 @@ export class ApplicationPackageManager {
console.warn(
`WARNING: ${this.pck.packagePath} does not have a "main" entry.\n` +
'Please add the following line:\n' +
- ' "main": "src-gen/frontend/electron-main.js"'
+ ' "main": "lib/backend/electron-main.js"'
);
}
diff --git a/dev-packages/application-manager/src/application-process.ts b/dev-packages/application-manager/src/application-process.ts
index 47fda1c743ba1..2befd37bcfc50 100644
--- a/dev-packages/application-manager/src/application-process.ts
+++ b/dev-packages/application-manager/src/application-process.ts
@@ -32,7 +32,10 @@ export class ApplicationProcess {
) { }
spawn(command: string, args?: string[], options?: cp.SpawnOptions): cp.ChildProcess {
- return cp.spawn(command, args || [], Object.assign({}, this.defaultOptions, options));
+ return cp.spawn(command, args || [], Object.assign({}, this.defaultOptions, {
+ ...options,
+ shell: true
+ }));
}
fork(modulePath: string, args?: string[], options?: cp.ForkOptions): cp.ChildProcess {
@@ -50,7 +53,10 @@ export class ApplicationProcess {
spawnBin(command: string, args: string[], options?: cp.SpawnOptions): cp.ChildProcess {
const binPath = this.resolveBin(command);
- return this.spawn(binPath, args, options);
+ return this.spawn(binPath, args, {
+ ...options,
+ shell: true
+ });
}
protected resolveBin(command: string): string {
diff --git a/dev-packages/application-manager/src/generator/abstract-generator.ts b/dev-packages/application-manager/src/generator/abstract-generator.ts
index 2ff52527ef3ec..da35c5d588849 100644
--- a/dev-packages/application-manager/src/generator/abstract-generator.ts
+++ b/dev-packages/application-manager/src/generator/abstract-generator.ts
@@ -37,6 +37,10 @@ export abstract class AbstractGenerator {
return this.pck.ifElectron(value, defaultValue);
}
+ protected ifBrowserOnly(value: string, defaultValue: string = ''): string {
+ return this.pck.ifBrowserOnly(value, defaultValue);
+ }
+
protected async write(path: string, content: string): Promise {
await fs.ensureFile(path);
await fs.writeFile(path, content);
diff --git a/dev-packages/application-manager/src/generator/backend-generator.ts b/dev-packages/application-manager/src/generator/backend-generator.ts
index 5f457d0d9dba5..afa538291a6a6 100644
--- a/dev-packages/application-manager/src/generator/backend-generator.ts
+++ b/dev-packages/application-manager/src/generator/backend-generator.ts
@@ -20,6 +20,10 @@ import { AbstractGenerator } from './abstract-generator';
export class BackendGenerator extends AbstractGenerator {
async generate(): Promise {
+ if (this.pck.isBrowserOnly()) {
+ // no backend generation in case of browser-only target
+ return;
+ }
const backendModules = this.pck.targetBackendModules;
await this.write(this.pck.backend('server.js'), this.compileServer(backendModules));
await this.write(this.pck.backend('main.js'), this.compileMain(backendModules));
@@ -48,10 +52,12 @@ if (process.env.LC_ALL) {
}
process.env.LC_NUMERIC = 'C';
+const { resolve } = require('path');
+const theiaAppProjectPath = resolve(__dirname, '..', '..');
+process.env.THEIA_APP_PROJECT_PATH = theiaAppProjectPath;
const { default: electronMainApplicationModule } = require('@theia/core/lib/electron-main/electron-main-application-module');
const { ElectronMainApplication, ElectronMainApplicationGlobals } = require('@theia/core/lib/electron-main/electron-main-application');
const { Container } = require('inversify');
-const { resolve } = require('path');
const { app } = require('electron');
const config = ${this.prettyStringify(this.pck.props.frontend.config)};
@@ -67,9 +73,10 @@ const isSingleInstance = ${this.pck.props.backend.config.singleInstance === true
const container = new Container();
container.load(electronMainApplicationModule);
container.bind(ElectronMainApplicationGlobals).toConstantValue({
- THEIA_APP_PROJECT_PATH: resolve(__dirname, '..', '..'),
+ THEIA_APP_PROJECT_PATH: theiaAppProjectPath,
THEIA_BACKEND_MAIN_PATH: resolve(__dirname, 'main.js'),
THEIA_FRONTEND_HTML_PATH: resolve(__dirname, '..', '..', 'lib', 'frontend', 'index.html'),
+ THEIA_SECONDARY_WINDOW_HTML_PATH: resolve(__dirname, '..', '..', 'lib', 'frontend', 'secondary-window.html')
});
function load(raw) {
@@ -115,6 +122,7 @@ if ('ELECTRON_RUN_AS_NODE' in process.env) {
}
const path = require('path');
+process.env.THEIA_APP_PROJECT_PATH = path.resolve(__dirname, '..', '..')
const express = require('express');
const { Container } = require('inversify');
const { BackendApplication, BackendApplicationServer, CliManager } = require('@theia/core/lib/node');
@@ -178,6 +186,8 @@ const main = require('@theia/core/lib/node/main');
BackendApplicationConfigProvider.set(${this.prettyStringify(this.pck.props.backend.config)});
+globalThis.extensionInfo = ${this.prettyStringify(this.pck.extensionPackages.map(({ name, version }) => ({ name, version }))) };
+
const serverModule = require('./server');
const serverAddress = main.start(serverModule());
diff --git a/dev-packages/application-manager/src/generator/frontend-generator.ts b/dev-packages/application-manager/src/generator/frontend-generator.ts
index 7b121dd635b64..bbfd134bdb3b8 100644
--- a/dev-packages/application-manager/src/generator/frontend-generator.ts
+++ b/dev-packages/application-manager/src/generator/frontend-generator.ts
@@ -24,7 +24,7 @@ export class FrontendGenerator extends AbstractGenerator {
async generate(options?: GeneratorOptions): Promise {
await this.write(this.pck.frontend('index.html'), this.compileIndexHtml(this.pck.targetFrontendModules));
- await this.write(this.pck.frontend('index.js'), this.compileIndexJs(this.pck.targetFrontendModules, this.pck.frontendPreloadModules));
+ await this.write(this.pck.frontend('index.js'), this.compileIndexJs(this.pck.targetFrontendModules, this.pck.targetFrontendPreloadModules));
await this.write(this.pck.frontend('secondary-window.html'), this.compileSecondaryWindowHtml());
await this.write(this.pck.frontend('secondary-index.js'), this.compileSecondaryIndexJs(this.pck.secondaryWindowModules));
if (this.pck.isElectron()) {
@@ -92,9 +92,7 @@ function load(container, jsModule) {
.then(containerModule => container.load(containerModule.default));
}
-async function preload(parent) {
- const container = new Container();
- container.parent = parent;
+async function preload(container) {
try {
${Array.from(frontendPreloadModules.values(), jsModulePath => `\
await load(container, ${this.importOrRequire()}('${jsModulePath}'));`).join(EOL)}
@@ -110,22 +108,38 @@ ${Array.from(frontendPreloadModules.values(), jsModulePath => `\
}
module.exports = (async () => {
- const { messagingFrontendModule } = require('@theia/core/lib/${this.pck.isBrowser()
- ? 'browser/messaging/messaging-frontend-module'
- : 'electron-browser/messaging/electron-messaging-frontend-module'}');
+ const { messagingFrontendModule } = require('@theia/core/lib/${this.pck.isBrowser() || this.pck.isBrowserOnly()
+ ? 'browser/messaging/messaging-frontend-module'
+ : 'electron-browser/messaging/electron-messaging-frontend-module'}');
const container = new Container();
container.load(messagingFrontendModule);
+ ${this.ifBrowserOnly(`const { messagingFrontendOnlyModule } = require('@theia/core/lib/browser-only/messaging/messaging-frontend-only-module');
+ container.load(messagingFrontendOnlyModule);`)}
+
await preload(container);
+
+ ${this.ifMonaco(() => `
+ const { MonacoInit } = require('@theia/monaco/lib/browser/monaco-init');
+ `)};
+
const { FrontendApplication } = require('@theia/core/lib/browser');
const { frontendApplicationModule } = require('@theia/core/lib/browser/frontend-application-module');
const { loggerFrontendModule } = require('@theia/core/lib/browser/logger-frontend-module');
container.load(frontendApplicationModule);
+ ${this.pck.ifBrowserOnly(`const { frontendOnlyApplicationModule } = require('@theia/core/lib/browser-only/frontend-only-application-module');
+ container.load(frontendOnlyApplicationModule);`)}
+
container.load(loggerFrontendModule);
+ ${this.ifBrowserOnly(`const { loggerFrontendOnlyModule } = require('@theia/core/lib/browser-only/logger-frontend-only-module');
+ container.load(loggerFrontendOnlyModule);`)}
try {
${Array.from(frontendModules.values(), jsModulePath => `\
await load(container, ${this.importOrRequire()}('${jsModulePath}'));`).join(EOL)}
+ ${this.ifMonaco(() => `
+ MonacoInit.init(container);
+ `)};
await start();
} catch (reason) {
console.error('Failed to start the frontend application.');
@@ -174,15 +188,6 @@ ${Array.from(frontendModules.values(), jsModulePath => `\
}
-
diff --git a/dev-packages/application-manager/src/generator/webpack-generator.ts b/dev-packages/application-manager/src/generator/webpack-generator.ts
index e650c7a7dbd09..9cad52452cfb5 100644
--- a/dev-packages/application-manager/src/generator/webpack-generator.ts
+++ b/dev-packages/application-manager/src/generator/webpack-generator.ts
@@ -22,7 +22,9 @@ export class WebpackGenerator extends AbstractGenerator {
async generate(): Promise {
await this.write(this.genConfigPath, this.compileWebpackConfig());
- await this.write(this.genNodeConfigPath, this.compileNodeWebpackConfig());
+ if (!this.pck.isBrowserOnly()) {
+ await this.write(this.genNodeConfigPath, this.compileNodeWebpackConfig());
+ }
if (await this.shouldGenerateUserWebpackConfig()) {
await this.write(this.configPath, this.compileUserWebpackConfig());
}
@@ -126,24 +128,6 @@ module.exports = [{
cache: staticCompression,
module: {
rules: [
- {
- // Removes the host check in PhosphorJS to enable moving widgets to secondary windows.
- test: /widget\\.js$/,
- loader: 'string-replace-loader',
- include: /node_modules[\\\\/]@phosphor[\\\\/]widgets[\\\\/]lib/,
- options: {
- multiple: [
- {
- search: /document\\.body\\.contains\\(widget.node\\)/gm,
- replace: 'widget.node.ownerDocument.body.contains(widget.node)'
- },
- {
- search: /\\!document\\.body\\.contains\\(host\\)/gm,
- replace: ' !host.ownerDocument.body.contains(host)'
- }
- ]
- }
- },
{
test: /\\.css$/,
exclude: /materialcolors\\.css$|\\.useable\\.css$/,
@@ -278,6 +262,10 @@ module.exports = [{
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"]
+ },
+ {
+ test: /\.wasm$/,
+ type: 'asset/resource'
}
]
},
@@ -332,7 +320,7 @@ module.exports = [{
*/
// @ts-check
const configs = require('./${paths.basename(this.genConfigPath)}');
-const nodeConfig = require('./${paths.basename(this.genNodeConfigPath)}');
+${this.ifBrowserOnly('', `const nodeConfig = require('./${paths.basename(this.genNodeConfigPath)}');`)}
/**
* Expose bundled modules on window.theia.moduleName namespace, e.g.
@@ -343,10 +331,11 @@ configs[0].module.rules.push({
loader: require.resolve('@theia/application-manager/lib/expose-loader')
}); */
-module.exports = [
+${this.ifBrowserOnly('module.exports = configs;', `module.exports = [
...configs,
nodeConfig.config
-];`;
+];`)}
+`;
}
protected compileNodeWebpackConfig(): string {
@@ -373,8 +362,10 @@ const production = mode === 'production';
const commonJsLibraries = {};
for (const [entryPointName, entryPointPath] of Object.entries({
${this.ifPackage('@theia/plugin-ext', "'backend-init-theia': '@theia/plugin-ext/lib/hosted/node/scanners/backend-init-theia',")}
- ${this.ifPackage('@theia/filesystem', "'nsfw-watcher': '@theia/filesystem/lib/node/nsfw-watcher',")}
+ ${this.ifPackage('@theia/filesystem', "'parcel-watcher': '@theia/filesystem/lib/node/parcel-watcher',")}
${this.ifPackage('@theia/plugin-ext-vscode', "'plugin-vscode-init': '@theia/plugin-ext-vscode/lib/node/plugin-vscode-init',")}
+ ${this.ifPackage('@theia/api-provider-sample', "'gotd-api-init': '@theia/api-provider-sample/lib/plugin/gotd-api-init',")}
+ ${this.ifPackage('@theia/git', "'git-locator-host': '@theia/git/lib/node/git-locator/git-locator-host',")}
})) {
commonJsLibraries[entryPointName] = {
import: require.resolve(entryPointPath),
@@ -426,11 +417,13 @@ const config = {
'ipc-bootstrap': require.resolve('@theia/core/lib/node/messaging/ipc-bootstrap'),
${this.ifPackage('@theia/plugin-ext', () => `// VS Code extension support:
'plugin-host': require.resolve('@theia/plugin-ext/lib/hosted/node/plugin-host'),`)}
+ ${this.ifPackage('@theia/plugin-ext-headless', () => `// Theia Headless Plugin support:
+ 'plugin-host-headless': require.resolve('@theia/plugin-ext-headless/lib/hosted/node/plugin-host-headless'),`)}
${this.ifPackage('@theia/process', () => `// Make sure the node-pty thread worker can be executed:
- 'worker/conoutSocketWorker': require.resolve('node-pty/lib/worker/conoutSocketWorker'),`)}
- ${this.ifPackage('@theia/git', () => `// Ensure the git locator process can the started
- 'git-locator-host': require.resolve('@theia/git/lib/node/git-locator/git-locator-host'),`)}
+ 'worker/conoutSocketWorker': require.resolve('node-pty/lib/worker/conoutSocketWorker'),`)}
${this.ifElectron("'electron-main': require.resolve('./src-gen/backend/electron-main'),")}
+ ${this.ifPackage('@theia/dev-container', () => `// VS Code Dev-Container communication:
+ 'dev-container-server': require.resolve('@theia/dev-container/lib/dev-container-server/dev-container-server'),`)}
...commonJsLibraries
},
module: {
@@ -493,6 +486,8 @@ const config = {
module: /express/
}, {
module: /cross-spawn/
+ }, {
+ module: /@parcel\\/watcher/
}
]
};
diff --git a/dev-packages/application-manager/src/rebuild.ts b/dev-packages/application-manager/src/rebuild.ts
index 0784c315558ba..5b0e8010f090c 100644
--- a/dev-packages/application-manager/src/rebuild.ts
+++ b/dev-packages/application-manager/src/rebuild.ts
@@ -19,7 +19,7 @@ import fs = require('fs-extra');
import path = require('path');
import os = require('os');
-export type RebuildTarget = 'electron' | 'browser';
+export type RebuildTarget = 'electron' | 'browser' | 'browser-only';
const EXIT_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'];
@@ -32,7 +32,6 @@ type NodeABI = string | number;
export const DEFAULT_MODULES = [
'node-pty',
- 'nsfw',
'native-keymap',
'find-git-repositories',
'drivelist',
@@ -304,7 +303,12 @@ async function guardExit(run: (token: ExitToken) => Promise): Promise {
return await run(token);
} finally {
for (const signal of EXIT_SIGNALS) {
- process.off(signal, signalListener);
+ // FIXME we have a type clash here between Node, Electron and Mocha.
+ // Typescript is resolving here to Electron's Process interface which extends the NodeJS.EventEmitter interface
+ // However instead of the actual NodeJS.EventEmitter interface it resolves to an empty stub of Mocha
+ // Therefore it can't find the correct "off" signature and throws an error
+ // By casting to the NodeJS.EventEmitter ourselves, we short circuit the resolving and it succeeds
+ (process as NodeJS.EventEmitter).off(signal, signalListener);
}
}
}
diff --git a/dev-packages/application-package/package.json b/dev-packages/application-package/package.json
index 28d4a3f4ae04b..642f2d38ae863 100644
--- a/dev-packages/application-package/package.json
+++ b/dev-packages/application-package/package.json
@@ -1,6 +1,6 @@
{
"name": "@theia/application-package",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia application package API.",
"publishConfig": {
"access": "public"
@@ -29,20 +29,21 @@
"watch": "theiaext watch"
},
"dependencies": {
- "@theia/request": "1.44.0",
+ "@theia/request": "1.54.0",
"@types/fs-extra": "^4.0.2",
"@types/semver": "^7.5.0",
"@types/write-json-file": "^2.2.1",
"deepmerge": "^4.2.2",
"fs-extra": "^4.0.2",
"is-electron": "^2.1.0",
- "nano": "^9.0.5",
+ "nano": "^10.1.3",
"resolve-package-path": "^4.0.3",
"semver": "^7.5.4",
+ "tslib": "^2.6.2",
"write-json-file": "^2.2.0"
},
"devDependencies": {
- "@theia/ext-scripts": "1.44.0"
+ "@theia/ext-scripts": "1.54.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
diff --git a/dev-packages/application-package/src/api.ts b/dev-packages/application-package/src/api.ts
index 5e86da406d12a..b824ab9a72c55 100644
--- a/dev-packages/application-package/src/api.ts
+++ b/dev-packages/application-package/src/api.ts
@@ -18,4 +18,4 @@
* The default supported API version the framework supports.
* The version should be in the format `x.y.z`.
*/
-export const DEFAULT_SUPPORTED_API_VERSION = '1.83.1';
+export const DEFAULT_SUPPORTED_API_VERSION = '1.93.1';
diff --git a/dev-packages/application-package/src/application-package.ts b/dev-packages/application-package/src/application-package.ts
index 68b5cb896c6ec..700dec43bb2d8 100644
--- a/dev-packages/application-package/src/application-package.ts
+++ b/dev-packages/application-package/src/application-package.ts
@@ -73,7 +73,7 @@ export class ApplicationPackage {
theia.target = this.options.appTarget;
}
- if (theia.target && !(theia.target in ApplicationProps.ApplicationTarget)) {
+ if (theia.target && !(Object.values(ApplicationProps.ApplicationTarget).includes(theia.target))) {
const defaultTarget = ApplicationProps.ApplicationTarget.browser;
console.warn(`Unknown application target '${theia.target}', '${defaultTarget}' to be used instead`);
theia.target = defaultTarget;
@@ -140,10 +140,24 @@ export class ApplicationPackage {
return this._frontendPreloadModules ??= this.computeModules('frontendPreload');
}
+ get frontendOnlyPreloadModules(): Map {
+ if (!this._frontendPreloadModules) {
+ this._frontendPreloadModules = this.computeModules('frontendOnlyPreload', 'frontendPreload');
+ }
+ return this._frontendPreloadModules;
+ }
+
get frontendModules(): Map {
return this._frontendModules ??= this.computeModules('frontend');
}
+ get frontendOnlyModules(): Map {
+ if (!this._frontendModules) {
+ this._frontendModules = this.computeModules('frontendOnly', 'frontend');
+ }
+ return this._frontendModules;
+ }
+
get frontendElectronModules(): Map {
return this._frontendElectronModules ??= this.computeModules('frontendElectron', 'frontend');
}
@@ -227,6 +241,10 @@ export class ApplicationPackage {
return this.target === ApplicationProps.ApplicationTarget.electron;
}
+ isBrowserOnly(): boolean {
+ return this.target === ApplicationProps.ApplicationTarget.browserOnly;
+ }
+
ifBrowser(value: T): T | undefined;
ifBrowser(value: T, defaultValue: T): T;
ifBrowser(value: T, defaultValue?: T): T | undefined {
@@ -239,14 +257,33 @@ export class ApplicationPackage {
return this.isElectron() ? value : defaultValue;
}
+ ifBrowserOnly(value: T): T | undefined;
+ ifBrowserOnly(value: T, defaultValue: T): T;
+ ifBrowserOnly(value: T, defaultValue?: T): T | undefined {
+ return this.isBrowserOnly() ? value : defaultValue;
+ }
+
get targetBackendModules(): Map {
+ if (this.isBrowserOnly()) {
+ return new Map();
+ }
return this.ifBrowser(this.backendModules, this.backendElectronModules);
}
get targetFrontendModules(): Map {
+ if (this.isBrowserOnly()) {
+ return this.frontendOnlyModules;
+ }
return this.ifBrowser(this.frontendModules, this.frontendElectronModules);
}
+ get targetFrontendPreloadModules(): Map {
+ if (this.isBrowserOnly()) {
+ return this.frontendOnlyPreloadModules;
+ }
+ return this.frontendPreloadModules;
+ }
+
get targetElectronMainModules(): Map {
return this.ifElectron(this.electronMainModules, new Map());
}
diff --git a/dev-packages/application-package/src/application-props.ts b/dev-packages/application-package/src/application-props.ts
index e0c9ffca031fa..ca843a7e6729e 100644
--- a/dev-packages/application-package/src/application-props.ts
+++ b/dev-packages/application-package/src/application-props.ts
@@ -33,8 +33,37 @@ export type ElectronFrontendApplicationConfig = RequiredRecursive;
export namespace BackendApplicationConfig {
export const DEFAULT: BackendApplicationConfig = {
- singleInstance: false,
+ singleInstance: true,
+ frontendConnectionTimeout: 0
};
export interface Partial extends ApplicationConfig {
@@ -151,6 +201,11 @@ export namespace BackendApplicationConfig {
* Defaults to `false`.
*/
readonly singleInstance?: boolean;
+
+ /**
+ * The time in ms the connection context will be preserved for reconnection after a front end disconnects.
+ */
+ readonly frontendConnectionTimeout?: number;
}
}
@@ -228,10 +283,11 @@ export interface ApplicationProps extends NpmRegistryProps {
};
}
export namespace ApplicationProps {
- export type Target = keyof typeof ApplicationTarget;
+ export type Target = `${ApplicationTarget}`;
export enum ApplicationTarget {
browser = 'browser',
- electron = 'electron'
+ electron = 'electron',
+ browserOnly = 'browser-only'
};
export const DEFAULT: ApplicationProps = {
...NpmRegistryProps.DEFAULT,
diff --git a/dev-packages/application-package/src/extension-package.ts b/dev-packages/application-package/src/extension-package.ts
index 8ccd8d661513a..dced483c99674 100644
--- a/dev-packages/application-package/src/extension-package.ts
+++ b/dev-packages/application-package/src/extension-package.ts
@@ -21,7 +21,9 @@ import { NpmRegistry, PublishedNodePackage, NodePackage } from './npm-registry';
export interface Extension {
frontendPreload?: string;
+ frontendOnlyPreload?: string;
frontend?: string;
+ frontendOnly?: string;
frontendElectron?: string;
secondaryWindow?: string;
backend?: string;
diff --git a/dev-packages/cli/bin/theia-patch.js b/dev-packages/cli/bin/theia-patch.js
new file mode 100755
index 0000000000000..a6a511fa9b2dd
--- /dev/null
+++ b/dev-packages/cli/bin/theia-patch.js
@@ -0,0 +1,37 @@
+#!/usr/bin/env node
+
+// *****************************************************************************
+// Copyright (C) 2024 STMicroelectronics and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+// @ts-check
+const path = require('path');
+const cp = require('child_process');
+
+const patchPackage = require.resolve('patch-package');
+console.log(`patch-package = ${patchPackage}`);
+
+const patchesDir = path.join('.', 'node_modules', '@theia', 'cli', 'patches');
+
+console.log(`patchesdir = ${patchesDir}`);
+
+const env = Object.assign({}, process.env);
+
+const scriptProcess = cp.exec(`node "${patchPackage}" --patch-dir "${patchesDir}"`, {
+ cwd: process.cwd(),
+ env
+});
+
+scriptProcess.stdout.pipe(process.stdout);
+scriptProcess.stderr.pipe(process.stderr);
diff --git a/dev-packages/cli/package.json b/dev-packages/cli/package.json
index b84bb17fb0576..27b9cce50b361 100644
--- a/dev-packages/cli/package.json
+++ b/dev-packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@theia/cli",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia CLI.",
"publishConfig": {
"access": "public"
@@ -17,10 +17,12 @@
"files": [
"bin",
"lib",
- "src"
+ "src",
+ "patches"
],
"bin": {
- "theia": "./bin/theia"
+ "theia": "./bin/theia",
+ "theia-patch": "./bin/theia-patch.js"
},
"scripts": {
"prepare": "tsc -b",
@@ -30,12 +32,12 @@
"clean": "theiaext clean"
},
"dependencies": {
- "@theia/application-manager": "1.44.0",
- "@theia/application-package": "1.44.0",
- "@theia/ffmpeg": "1.44.0",
- "@theia/localization-manager": "1.44.0",
- "@theia/ovsx-client": "1.44.0",
- "@theia/request": "1.44.0",
+ "@theia/application-manager": "1.54.0",
+ "@theia/application-package": "1.54.0",
+ "@theia/ffmpeg": "1.54.0",
+ "@theia/localization-manager": "1.54.0",
+ "@theia/ovsx-client": "1.54.0",
+ "@theia/request": "1.54.0",
"@types/chai": "^4.2.7",
"@types/mocha": "^10.0.0",
"@types/node-fetch": "^2.5.7",
@@ -44,13 +46,16 @@
"decompress": "^4.2.1",
"escape-string-regexp": "4.0.0",
"glob": "^8.0.3",
+ "http-server": "^14.1.1",
"limiter": "^2.1.0",
"log-update": "^4.0.0",
"mocha": "^10.1.0",
- "puppeteer": "19.7.2",
- "puppeteer-core": "19.7.2",
+ "patch-package": "^8.0.0",
+ "puppeteer": "23.1.0",
+ "puppeteer-core": "23.1.0",
"puppeteer-to-istanbul": "1.4.0",
"temp": "^0.9.1",
+ "tslib": "^2.6.2",
"yargs": "^15.3.1"
},
"devDependencies": {
diff --git a/dev-packages/cli/patches/@phosphor+widgets+1.9.3.patch b/dev-packages/cli/patches/@phosphor+widgets+1.9.3.patch
new file mode 100644
index 0000000000000..40f582ab14dcf
--- /dev/null
+++ b/dev-packages/cli/patches/@phosphor+widgets+1.9.3.patch
@@ -0,0 +1,157 @@
+diff --git a/node_modules/@phosphor/widgets/lib/menu.d.ts b/node_modules/@phosphor/widgets/lib/menu.d.ts
+index 5d5053c..7802167 100644
+--- a/node_modules/@phosphor/widgets/lib/menu.d.ts
++++ b/node_modules/@phosphor/widgets/lib/menu.d.ts
+@@ -195,7 +195,7 @@ export declare class Menu extends Widget {
+ *
+ * This is a no-op if the menu is already attached to the DOM.
+ */
+- open(x: number, y: number, options?: Menu.IOpenOptions): void;
++ open(x: number, y: number, options?: Menu.IOpenOptions, anchor?: HTMLElement): void;
+ /**
+ * Handle the DOM events for the menu.
+ *
+diff --git a/node_modules/@phosphor/widgets/lib/menu.js b/node_modules/@phosphor/widgets/lib/menu.js
+index de23022..a8b15b1 100644
+--- a/node_modules/@phosphor/widgets/lib/menu.js
++++ b/node_modules/@phosphor/widgets/lib/menu.js
+@@ -13,7 +13,7 @@ var __extends = (this && this.__extends) || (function () {
+ };
+ })();
+ var __assign = (this && this.__assign) || function () {
+- __assign = Object.assign || function(t) {
++ __assign = Object.assign || function (t) {
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
+ s = arguments[i];
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
+@@ -424,7 +424,7 @@ var Menu = /** @class */ (function (_super) {
+ *
+ * This is a no-op if the menu is already attached to the DOM.
+ */
+- Menu.prototype.open = function (x, y, options) {
++ Menu.prototype.open = function (x, y, options, node) {
+ if (options === void 0) { options = {}; }
+ // Bail early if the menu is already attached.
+ if (this.isAttached) {
+@@ -434,7 +434,7 @@ var Menu = /** @class */ (function (_super) {
+ var forceX = options.forceX || false;
+ var forceY = options.forceY || false;
+ // Open the menu as a root menu.
+- Private.openRootMenu(this, x, y, forceX, forceY);
++ Private.openRootMenu(this, x, y, forceX, forceY, node);
+ // Activate the menu to accept keyboard input.
+ this.activate();
+ };
+@@ -484,8 +484,16 @@ var Menu = /** @class */ (function (_super) {
+ this.node.addEventListener('mouseenter', this);
+ this.node.addEventListener('mouseleave', this);
+ this.node.addEventListener('contextmenu', this);
+- document.addEventListener('mousedown', this, true);
+ };
++
++ Menu.prototype.onAfterAttach = function (msg) {
++ this.node.ownerDocument.addEventListener('mousedown', this, true);
++ }
++
++ Menu.prototype.onBeforeDetach = function (msg) {
++ this.node.ownerDocument.removeEventListener('mousedown', this, true);
++ }
++
+ /**
+ * A message handler invoked on an `'after-detach'` message.
+ */
+@@ -496,7 +504,6 @@ var Menu = /** @class */ (function (_super) {
+ this.node.removeEventListener('mouseenter', this);
+ this.node.removeEventListener('mouseleave', this);
+ this.node.removeEventListener('contextmenu', this);
+- document.removeEventListener('mousedown', this, true);
+ };
+ /**
+ * A message handler invoked on an `'activate-request'` message.
+@@ -1124,14 +1131,15 @@ var Private;
+ /**
+ * Open a menu as a root menu at the target location.
+ */
+- function openRootMenu(menu, x, y, forceX, forceY) {
++ function openRootMenu(menu, x, y, forceX, forceY, element) {
+ // Ensure the menu is updated before attaching and measuring.
+ messaging_1.MessageLoop.sendMessage(menu, widget_1.Widget.Msg.UpdateRequest);
+ // Get the current position and size of the main viewport.
++ var doc = element ? element.ownerDocument : document;
+ var px = window.pageXOffset;
+ var py = window.pageYOffset;
+- var cw = document.documentElement.clientWidth;
+- var ch = document.documentElement.clientHeight;
++ var cw = doc.documentElement.clientWidth;
++ var ch = doc.documentElement.clientHeight;
+ // Compute the maximum allowed height for the menu.
+ var maxHeight = ch - (forceY ? y : 0);
+ // Fetch common variables.
+@@ -1145,7 +1153,7 @@ var Private;
+ style.visibility = 'hidden';
+ style.maxHeight = maxHeight + "px";
+ // Attach the menu to the document.
+- widget_1.Widget.attach(menu, document.body);
++ widget_1.Widget.attach(menu, doc.body);
+ // Measure the size of the menu.
+ var _a = node.getBoundingClientRect(), width = _a.width, height = _a.height;
+ // Adjust the X position of the menu to fit on-screen.
+@@ -1177,8 +1185,8 @@ var Private;
+ // Get the current position and size of the main viewport.
+ var px = window.pageXOffset;
+ var py = window.pageYOffset;
+- var cw = document.documentElement.clientWidth;
+- var ch = document.documentElement.clientHeight;
++ var cw = itemNode.ownerDocument.documentElement.clientWidth;
++ var ch = itemNode.ownerDocument.documentElement.clientHeight;
+ // Compute the maximum allowed height for the menu.
+ var maxHeight = ch;
+ // Fetch common variables.
+@@ -1192,7 +1200,7 @@ var Private;
+ style.visibility = 'hidden';
+ style.maxHeight = maxHeight + "px";
+ // Attach the menu to the document.
+- widget_1.Widget.attach(submenu, document.body);
++ widget_1.Widget.attach(submenu, itemNode.ownerDocument.body);
+ // Measure the size of the menu.
+ var _a = node.getBoundingClientRect(), width = _a.width, height = _a.height;
+ // Compute the box sizing for the menu.
+diff --git a/node_modules/@phosphor/widgets/lib/menubar.js b/node_modules/@phosphor/widgets/lib/menubar.js
+index a8e10f4..da2ee82 100644
+--- a/node_modules/@phosphor/widgets/lib/menubar.js
++++ b/node_modules/@phosphor/widgets/lib/menubar.js
+@@ -521,7 +521,7 @@ var MenuBar = /** @class */ (function (_super) {
+ // Get the positioning data for the new menu.
+ var _a = itemNode.getBoundingClientRect(), left = _a.left, bottom = _a.bottom;
+ // Open the new menu at the computed location.
+- newMenu.open(left, bottom, { forceX: true, forceY: true });
++ newMenu.open(left, bottom, { forceX: true, forceY: true }, this.node);
+ };
+ /**
+ * Close the child menu immediately.
+diff --git a/node_modules/@phosphor/widgets/lib/widget.js b/node_modules/@phosphor/widgets/lib/widget.js
+index 01241fa..62da27c 100644
+--- a/node_modules/@phosphor/widgets/lib/widget.js
++++ b/node_modules/@phosphor/widgets/lib/widget.js
+@@ -906,10 +906,10 @@ exports.Widget = Widget;
+ if (widget.parent) {
+ throw new Error('Cannot attach a child widget.');
+ }
+- if (widget.isAttached || document.body.contains(widget.node)) {
++ if (widget.isAttached || widget.node.ownerDocument.body.contains(widget.node)) {
+ throw new Error('Widget is already attached.');
+ }
+- if (!document.body.contains(host)) {
++ if (!host.ownerDocument.body.contains(host)) {
+ throw new Error('Host is not attached.');
+ }
+ messaging_1.MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
+@@ -930,7 +930,7 @@ exports.Widget = Widget;
+ if (widget.parent) {
+ throw new Error('Cannot detach a child widget.');
+ }
+- if (!widget.isAttached || !document.body.contains(widget.node)) {
++ if (!widget.isAttached || !widget.node.ownerDocument.body.contains(widget.node)) {
+ throw new Error('Widget is not attached.');
+ }
+ messaging_1.MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
diff --git a/dev-packages/cli/patches/@theia+monaco-editor-core+1.83.101.patch b/dev-packages/cli/patches/@theia+monaco-editor-core+1.83.101.patch
new file mode 100644
index 0000000000000..da1e72d06ac90
--- /dev/null
+++ b/dev-packages/cli/patches/@theia+monaco-editor-core+1.83.101.patch
@@ -0,0 +1,32 @@
+diff --git a/node_modules/@theia/monaco-editor-core/esm/vs/base/browser/ui/sash/sash.js b/node_modules/@theia/monaco-editor-core/esm/vs/base/browser/ui/sash/sash.js
+index 111dec4..b196066 100644
+--- a/node_modules/@theia/monaco-editor-core/esm/vs/base/browser/ui/sash/sash.js
++++ b/node_modules/@theia/monaco-editor-core/esm/vs/base/browser/ui/sash/sash.js
+@@ -47,14 +47,15 @@ function setGlobalHoverDelay(size) {
+ }
+ exports.setGlobalHoverDelay = setGlobalHoverDelay;
+ class MouseEventFactory {
+- constructor() {
++ constructor(el) {
++ this.el = el;
+ this.disposables = new lifecycle_1.DisposableStore();
+ }
+ get onPointerMove() {
+- return this.disposables.add(new event_1.DomEmitter(window, 'mousemove')).event;
++ return this.disposables.add(new event_1.DomEmitter(this.el.ownerDocument.defaultView, 'mousemove')).event;
+ }
+ get onPointerUp() {
+- return this.disposables.add(new event_1.DomEmitter(window, 'mouseup')).event;
++ return this.disposables.add(new event_1.DomEmitter(this.el.ownerDocument.defaultView, 'mouseup')).event;
+ }
+ dispose() {
+ this.disposables.dispose();
+@@ -243,7 +244,7 @@ class Sash extends lifecycle_1.Disposable {
+ this.el.classList.add('mac');
+ }
+ const onMouseDown = this._register(new event_1.DomEmitter(this.el, 'mousedown')).event;
+- this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory()), this));
++ this._register(onMouseDown(e => this.onPointerStart(e, new MouseEventFactory(this.el)), this));
+ const onMouseDoubleClick = this._register(new event_1.DomEmitter(this.el, 'dblclick')).event;
+ this._register(onMouseDoubleClick(this.onPointerDoublePress, this));
+ const onMouseEnter = this._register(new event_1.DomEmitter(this.el, 'mouseenter')).event;
diff --git a/dev-packages/cli/src/download-plugins.ts b/dev-packages/cli/src/download-plugins.ts
index 1f4e3208a0900..ca17aa88cc989 100644
--- a/dev-packages/cli/src/download-plugins.ts
+++ b/dev-packages/cli/src/download-plugins.ts
@@ -16,7 +16,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { OVSXApiFilterImpl, OVSXClient } from '@theia/ovsx-client';
+import { OVSXApiFilterImpl, OVSXClient, VSXTargetPlatform } from '@theia/ovsx-client';
import * as chalk from 'chalk';
import * as decompress from 'decompress';
import { promises as fs } from 'fs';
@@ -55,8 +55,6 @@ export interface DownloadPluginsOptions {
* Fetch plugins in parallel
*/
parallel?: boolean;
-
- rateLimit?: number;
}
interface PluginDownload {
@@ -65,17 +63,20 @@ interface PluginDownload {
version?: string | undefined
}
-export default async function downloadPlugins(ovsxClient: OVSXClient, requestService: RequestService, options: DownloadPluginsOptions = {}): Promise {
+export default async function downloadPlugins(
+ ovsxClient: OVSXClient,
+ rateLimiter: RateLimiter,
+ requestService: RequestService,
+ options: DownloadPluginsOptions = {}
+): Promise {
const {
packed = false,
ignoreErrors = false,
apiVersion = DEFAULT_SUPPORTED_API_VERSION,
- rateLimit = 15,
parallel = true
} = options;
- const rateLimiter = new RateLimiter({ tokensPerInterval: rateLimit, interval: 'second' });
- const apiFilter = new OVSXApiFilterImpl(apiVersion);
+ const apiFilter = new OVSXApiFilterImpl(ovsxClient, apiVersion);
// Collect the list of failures to be appended at the end of the script.
const failures: string[] = [];
@@ -129,8 +130,11 @@ export default async function downloadPlugins(ovsxClient: OVSXClient, requestSer
await parallelOrSequence(Array.from(ids, id => async () => {
try {
await rateLimiter.removeTokens(1);
- const { extensions } = await ovsxClient.query({ extensionId: id, includeAllVersions: true });
- const extension = apiFilter.getLatestCompatibleExtension(extensions);
+ const extension = await apiFilter.findLatestCompatibleExtension({
+ extensionId: id,
+ includeAllVersions: true,
+ targetPlatform
+ });
const version = extension?.version;
const downloadUrl = extension?.files.download;
if (downloadUrl) {
@@ -140,6 +144,7 @@ export default async function downloadPlugins(ovsxClient: OVSXClient, requestSer
failures.push(`No download url for extension pack ${id} (${version})`);
}
} catch (err) {
+ console.error(err);
failures.push(err.message);
}
}));
@@ -170,8 +175,10 @@ export default async function downloadPlugins(ovsxClient: OVSXClient, requestSer
}
}
+const targetPlatform = `${process.platform}-${process.arch}` as VSXTargetPlatform;
+
const placeholders: Record = {
- targetPlatform: `${process.platform}-${process.arch}`
+ targetPlatform
};
function resolveDownloadUrlPlaceholders(url: string): string {
for (const [name, value] of Object.entries(placeholders)) {
diff --git a/dev-packages/cli/src/run-test.ts b/dev-packages/cli/src/run-test.ts
index 2895d63481fae..57a6e3aecc6b9 100644
--- a/dev-packages/cli/src/run-test.ts
+++ b/dev-packages/cli/src/run-test.ts
@@ -84,4 +84,5 @@ export default async function runTest(options: TestOptions): Promise {
? `http://[${address}]:${port}`
: `http://${address}:${port}`;
await testPage.goto(url);
+ await testPage.bringToFront();
}
diff --git a/dev-packages/cli/src/test-page.ts b/dev-packages/cli/src/test-page.ts
index fe158c48f4882..95d5ba7df5d0e 100644
--- a/dev-packages/cli/src/test-page.ts
+++ b/dev-packages/cli/src/test-page.ts
@@ -113,7 +113,8 @@ export default async function newTestPage(options: TestPageOptions): Promise(cli: yargs.Argv): yargs.Argv {
return cli
.option('app-target', {
description: 'The target application type. Overrides `theia.target` in the application\'s package.json',
- choices: ['browser', 'electron'] as const,
+ choices: ['browser', 'electron', 'browser-only'] as const,
});
}
@@ -389,7 +390,7 @@ async function theiaCli(): Promise {
'rate-limit': {
describe: 'Amount of maximum open-vsx requests per second',
number: true,
- default: 15
+ default: OVSX_RATE_LIMIT
},
'proxy-url': {
describe: 'Proxy URL'
@@ -415,6 +416,7 @@ async function theiaCli(): Promise {
strictSSL: strictSsl
});
let client: OVSXClient | undefined;
+ const rateLimiter = new RateLimiter({ tokensPerInterval: options.rateLimit, interval: 'second' });
if (ovsxRouterConfig) {
const routerConfig = await fs.promises.readFile(ovsxRouterConfig, 'utf8').then(JSON.parse, error => {
console.error(error);
@@ -422,15 +424,15 @@ async function theiaCli(): Promise {
if (routerConfig) {
client = await OVSXRouterClient.FromConfig(
routerConfig,
- OVSXHttpClient.createClientFactory(requestService),
+ OVSXHttpClient.createClientFactory(requestService, rateLimiter),
[RequestContainsFilterFactory, ExtensionIdMatchesFilterFactory]
);
}
}
if (!client) {
- client = new OVSXHttpClient(apiUrl, requestService);
+ client = new OVSXHttpClient(apiUrl, requestService, rateLimiter);
}
- await downloadPlugins(client, requestService, options);
+ await downloadPlugins(client, rateLimiter, requestService, options);
},
})
.command<{
@@ -464,13 +466,16 @@ async function theiaCli(): Promise {
}
},
handler: async ({ freeApi, deeplKey, file, sourceLanguage, languages = [] }) => {
- await localizationManager.localize({
+ const success = await localizationManager.localize({
sourceFile: file,
freeApi: freeApi ?? true,
authKey: deeplKey,
targetLanguages: languages,
sourceLanguage
});
+ if (!success) {
+ process.exit(1);
+ }
}
})
.command<{
@@ -583,6 +588,10 @@ async function theiaCli(): Promise {
if (!process.env.THEIA_CONFIG_DIR) {
process.env.THEIA_CONFIG_DIR = temp.track().mkdirSync('theia-test-config-dir');
}
+ const args = ['--no-sandbox'];
+ if (!testInspect) {
+ args.push('--headless=old');
+ }
await runTest({
start: () => new Promise((resolve, reject) => {
const serverProcess = manager.start(toStringArray(theiaArgs));
@@ -591,11 +600,13 @@ async function theiaCli(): Promise {
serverProcess.on('close', (code, signal) => reject(`Server process exited unexpectedly: ${code ?? signal}`));
}),
launch: {
- args: ['--no-sandbox'],
+ args: args,
// eslint-disable-next-line no-null/no-null
defaultViewport: null, // view port can take available space instead of 800x600 default
devtools: testInspect,
- executablePath: executablePath()
+ headless: testInspect ? false : 'shell',
+ executablePath: executablePath(),
+ protocolTimeout: 600000
},
files: {
extension: testExtension,
diff --git a/dev-packages/ffmpeg/package.json b/dev-packages/ffmpeg/package.json
index 34081b3c658de..8e516fe0f99a2 100644
--- a/dev-packages/ffmpeg/package.json
+++ b/dev-packages/ffmpeg/package.json
@@ -1,6 +1,6 @@
{
"name": "@theia/ffmpeg",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia FFMPEG reader utility.",
"publishConfig": {
"access": "public"
@@ -29,6 +29,7 @@
},
"dependencies": {
"@electron/get": "^2.0.0",
+ "tslib": "^2.6.2",
"unzipper": "^0.9.11"
},
"devDependencies": {
diff --git a/dev-packages/localization-manager/package.json b/dev-packages/localization-manager/package.json
index d71a049f7781b..d902aff6c108c 100644
--- a/dev-packages/localization-manager/package.json
+++ b/dev-packages/localization-manager/package.json
@@ -1,6 +1,6 @@
{
"name": "@theia/localization-manager",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia localization manager API.",
"publishConfig": {
"access": "public"
@@ -36,10 +36,11 @@
"deepmerge": "^4.2.2",
"fs-extra": "^4.0.2",
"glob": "^7.2.0",
- "typescript": "~4.5.5"
+ "tslib": "^2.6.2",
+ "typescript": "~5.4.5"
},
"devDependencies": {
- "@theia/ext-scripts": "1.44.0"
+ "@theia/ext-scripts": "1.54.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
diff --git a/dev-packages/localization-manager/src/deepl-api.ts b/dev-packages/localization-manager/src/deepl-api.ts
index 95ee5df4635c3..aa3b620b435e5 100644
--- a/dev-packages/localization-manager/src/deepl-api.ts
+++ b/dev-packages/localization-manager/src/deepl-api.ts
@@ -54,7 +54,9 @@ export async function deepl(
*/
function coerceLanguage(parameters: DeeplParameters): void {
if (parameters.target_lang === 'ZH-CN') {
- parameters.target_lang = 'ZH';
+ parameters.target_lang = 'ZH-HANS';
+ } else if (parameters.target_lang === 'ZH-TW') {
+ parameters.target_lang = 'ZH-HANT';
}
}
@@ -101,10 +103,13 @@ export type DeeplLanguage =
| 'FI'
| 'FR'
| 'HU'
+ | 'ID'
| 'IT'
| 'JA'
+ | 'KO'
| 'LT'
| 'LV'
+ | 'NB'
| 'NL'
| 'PL'
| 'PT-PT'
@@ -115,14 +120,24 @@ export type DeeplLanguage =
| 'SK'
| 'SL'
| 'SV'
+ | 'TR'
+ | 'UK'
| 'ZH-CN'
+ | 'ZH-TW'
+ | 'ZH-HANS'
+ | 'ZH-HANT'
| 'ZH';
export const supportedLanguages = [
- 'BG', 'CS', 'DA', 'DE', 'EL', 'EN-GB', 'EN-US', 'EN', 'ES', 'ET', 'FI', 'FR', 'HU', 'IT',
- 'JA', 'LT', 'LV', 'NL', 'PL', 'PT-PT', 'PT-BR', 'PT', 'RO', 'RU', 'SK', 'SL', 'SV', 'ZH-CN'
+ 'BG', 'CS', 'DA', 'DE', 'EL', 'EN-GB', 'EN-US', 'EN', 'ES', 'ET', 'FI', 'FR', 'HU', 'ID', 'IT',
+ 'JA', 'KO', 'LT', 'LV', 'NL', 'PL', 'PT-PT', 'PT-BR', 'PT', 'RO', 'RU', 'SK', 'SL', 'SV', 'TR', 'UK', 'ZH-CN', 'ZH-TW'
];
+// From https://code.visualstudio.com/docs/getstarted/locales#_available-locales
+export const defaultLanguages = [
+ 'ZH-CN', 'ZH-TW', 'FR', 'DE', 'IT', 'ES', 'JA', 'KO', 'RU', 'PT-BR', 'TR', 'PL', 'CS', 'HU'
+] as const;
+
export function isSupportedLanguage(language: string): language is DeeplLanguage {
return supportedLanguages.includes(language.toUpperCase());
}
diff --git a/dev-packages/localization-manager/src/localization-extractor.ts b/dev-packages/localization-manager/src/localization-extractor.ts
index 387a54d90c641..1f2af7c7cd0ce 100644
--- a/dev-packages/localization-manager/src/localization-extractor.ts
+++ b/dev-packages/localization-manager/src/localization-extractor.ts
@@ -52,6 +52,14 @@ class SingleFileServiceHost implements ts.LanguageServiceHost {
getScriptSnapshot = (name: string) => name === this.filename ? this.file : this.lib;
getCurrentDirectory = () => '';
getDefaultLibFileName = () => 'lib.d.ts';
+ readFile(file: string, encoding?: string | undefined): string | undefined {
+ if (file === this.filename) {
+ return this.file.getText(0, this.file.getLength());
+ }
+ }
+ fileExists(file: string): boolean {
+ return this.filename === file;
+ }
}
class TypeScriptError extends Error {
@@ -81,8 +89,9 @@ export async function extract(options: ExtractionOptions): Promise {
const errors: string[] = [];
for (const file of files) {
const filePath = path.resolve(cwd, file);
+ const fileName = path.relative(cwd, file).split(path.sep).join('/');
const content = await fs.readFile(filePath, 'utf8');
- const fileLocalization = await extractFromFile(file, content, errors, options);
+ const fileLocalization = await extractFromFile(fileName, content, errors, options);
localization = deepmerge(localization, fileLocalization);
}
if (errors.length > 0 && options.logs) {
diff --git a/dev-packages/localization-manager/src/localization-manager.ts b/dev-packages/localization-manager/src/localization-manager.ts
index 90d650e59f225..34081a0d3d9f9 100644
--- a/dev-packages/localization-manager/src/localization-manager.ts
+++ b/dev-packages/localization-manager/src/localization-manager.ts
@@ -18,7 +18,7 @@ import * as chalk from 'chalk';
import * as fs from 'fs-extra';
import * as path from 'path';
import { Localization, sortLocalization } from './common';
-import { deepl, DeeplLanguage, DeeplParameters, isSupportedLanguage, supportedLanguages } from './deepl-api';
+import { deepl, DeeplLanguage, DeeplParameters, defaultLanguages, isSupportedLanguage } from './deepl-api';
export interface LocalizationOptions {
freeApi: Boolean
@@ -34,7 +34,7 @@ export class LocalizationManager {
constructor(private localizationFn = deepl) { }
- async localize(options: LocalizationOptions): Promise {
+ async localize(options: LocalizationOptions): Promise {
let source: Localization = {};
const cwd = process.env.INIT_CWD || process.cwd();
const sourceFile = path.resolve(cwd, options.sourceFile);
@@ -52,8 +52,10 @@ export class LocalizationManager {
languages.push(targetLanguage);
}
}
- if (languages.length !== options.targetLanguages.length) {
- console.log('Supported languages: ' + supportedLanguages.join(', '));
+ if (languages.length === 0) {
+ // No supported languages were found, default to all supported languages
+ console.log('No languages were specified, defaulting to all supported languages for VS Code');
+ languages.push(...defaultLanguages);
}
const existingTranslations: Map = new Map();
for (const targetLanguage of languages) {
@@ -64,7 +66,8 @@ export class LocalizationManager {
existingTranslations.set(targetLanguage, {});
}
}
- await Promise.all(languages.map(language => this.translateLanguage(source, existingTranslations.get(language)!, language, options)));
+ const results = await Promise.all(languages.map(language => this.translateLanguage(source, existingTranslations.get(language)!, language, options)));
+ let result = results.reduce((acc, val) => acc && val, true);
for (const targetLanguage of languages) {
const targetPath = this.translationFileName(sourceFile, targetLanguage);
@@ -73,8 +76,10 @@ export class LocalizationManager {
await fs.writeJson(targetPath, sortLocalization(translation), { spaces: 2 });
} catch {
console.error(chalk.red(`Error writing translated file to '${targetPath}'`));
+ result = false;
}
}
+ return result;
}
protected translationFileName(original: string, language: string): string {
@@ -83,7 +88,7 @@ export class LocalizationManager {
return path.join(directory, `${fileName}.${language.toLowerCase()}.json`);
}
- async translateLanguage(source: Localization, target: Localization, targetLanguage: string, options: LocalizationOptions): Promise {
+ async translateLanguage(source: Localization, target: Localization, targetLanguage: string, options: LocalizationOptions): Promise {
const map = this.buildLocalizationMap(source, target);
if (map.text.length > 0) {
try {
@@ -100,11 +105,14 @@ export class LocalizationManager {
map.localize(i, this.removeIgnoreTags(text));
});
console.log(chalk.green(`Successfully translated ${map.text.length} value${map.text.length > 1 ? 's' : ''} for language "${targetLanguage}"`));
+ return true;
} catch (e) {
console.log(chalk.red(`Could not translate into language "${targetLanguage}"`), e);
+ return false;
}
} else {
console.log(`No translation necessary for language "${targetLanguage}"`);
+ return true;
}
}
diff --git a/dev-packages/native-webpack-plugin/package.json b/dev-packages/native-webpack-plugin/package.json
index 1a287aa69a4ee..cf9c289074dcb 100644
--- a/dev-packages/native-webpack-plugin/package.json
+++ b/dev-packages/native-webpack-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@theia/native-webpack-plugin",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Webpack Plugin for native dependencies of Theia.",
"publishConfig": {
"access": "public"
@@ -29,6 +29,8 @@
"watch": "theiaext watch"
},
"dependencies": {
+ "detect-libc": "^2.0.2",
+ "tslib": "^2.6.2",
"webpack": "^5.76.0"
}
}
diff --git a/dev-packages/native-webpack-plugin/src/native-webpack-plugin.ts b/dev-packages/native-webpack-plugin/src/native-webpack-plugin.ts
index eafa1a01ac386..18ae43c186138 100644
--- a/dev-packages/native-webpack-plugin/src/native-webpack-plugin.ts
+++ b/dev-packages/native-webpack-plugin/src/native-webpack-plugin.ts
@@ -24,6 +24,7 @@ const REQUIRE_RIPGREP = '@vscode/ripgrep';
const REQUIRE_VSCODE_WINDOWS_CA_CERTS = '@vscode/windows-ca-certs';
const REQUIRE_BINDINGS = 'bindings';
const REQUIRE_KEYMAPPING = './build/Release/keymapping';
+const REQUIRE_PARCEL_WATCHER = './build/Release/watcher.node';
export interface NativeWebpackPluginOptions {
out: string;
@@ -72,7 +73,8 @@ export class NativeWebpackPlugin {
[REQUIRE_RIPGREP]: ripgrepFile,
[REQUIRE_BINDINGS]: bindingsFile,
[REQUIRE_KEYMAPPING]: keymappingFile,
- [REQUIRE_VSCODE_WINDOWS_CA_CERTS]: windowsCaCertsFile
+ [REQUIRE_VSCODE_WINDOWS_CA_CERTS]: windowsCaCertsFile,
+ [REQUIRE_PARCEL_WATCHER]: findNativeWatcherFile()
};
});
compiler.hooks.normalModuleFactory.tap(
@@ -154,6 +156,19 @@ export class NativeWebpackPlugin {
}
}
+function findNativeWatcherFile(): string {
+ let name = `@parcel/watcher-${process.platform}-${process.arch}`;
+ if (process.platform === 'linux') {
+ const { MUSL, family } = require('detect-libc');
+ if (family === MUSL) {
+ name += '-musl';
+ } else {
+ name += '-glibc';
+ }
+ }
+ return require.resolve(name);
+}
+
async function buildFile(root: string, name: string, content: string): Promise {
const tmpFile = path.join(root, name);
await fs.promises.writeFile(tmpFile, content);
diff --git a/dev-packages/ovsx-client/package.json b/dev-packages/ovsx-client/package.json
index a6b4e9b38ef9a..3a16cb7fd5918 100644
--- a/dev-packages/ovsx-client/package.json
+++ b/dev-packages/ovsx-client/package.json
@@ -1,6 +1,6 @@
{
"name": "@theia/ovsx-client",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia Open-VSX Client",
"publishConfig": {
"access": "public"
@@ -29,7 +29,9 @@
"watch": "theiaext watch"
},
"dependencies": {
- "@theia/request": "1.44.0",
- "semver": "^7.5.4"
+ "@theia/request": "1.54.0",
+ "limiter": "^2.1.0",
+ "semver": "^7.5.4",
+ "tslib": "^2.6.2"
}
}
diff --git a/dev-packages/ovsx-client/src/index.ts b/dev-packages/ovsx-client/src/index.ts
index 8d0d7318fe398..bc836bde8ce98 100644
--- a/dev-packages/ovsx-client/src/index.ts
+++ b/dev-packages/ovsx-client/src/index.ts
@@ -14,8 +14,8 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
-export { OVSXApiFilter, OVSXApiFilterImpl } from './ovsx-api-filter';
-export { OVSXHttpClient } from './ovsx-http-client';
+export { OVSXApiFilter, OVSXApiFilterImpl, OVSXApiFilterProvider } from './ovsx-api-filter';
+export { OVSXHttpClient, OVSX_RATE_LIMIT } from './ovsx-http-client';
export { OVSXMockClient } from './ovsx-mock-client';
export { OVSXRouterClient, OVSXRouterConfig, OVSXRouterFilterFactory as FilterFactory } from './ovsx-router-client';
export * from './ovsx-router-filters';
diff --git a/dev-packages/ovsx-client/src/ovsx-api-filter.ts b/dev-packages/ovsx-client/src/ovsx-api-filter.ts
index 0c5c505491713..17a882b1f9796 100644
--- a/dev-packages/ovsx-client/src/ovsx-api-filter.ts
+++ b/dev-packages/ovsx-client/src/ovsx-api-filter.ts
@@ -15,7 +15,11 @@
// *****************************************************************************
import * as semver from 'semver';
-import { VSXAllVersions, VSXBuiltinNamespaces, VSXExtensionRaw, VSXSearchEntry } from './ovsx-types';
+import { OVSXClient, VSXAllVersions, VSXBuiltinNamespaces, VSXExtensionRaw, VSXQueryOptions, VSXSearchEntry } from './ovsx-types';
+
+export const OVSXApiFilterProvider = Symbol('OVSXApiFilterProvider');
+
+export type OVSXApiFilterProvider = () => Promise;
export const OVSXApiFilter = Symbol('OVSXApiFilter');
/**
@@ -23,6 +27,7 @@ export const OVSXApiFilter = Symbol('OVSXApiFilter');
*/
export interface OVSXApiFilter {
supportedApiVersion: string;
+ findLatestCompatibleExtension(query: VSXQueryOptions): Promise;
/**
* Get the latest compatible extension version:
* - A builtin extension is fetched based on the extension version which matches the API.
@@ -38,14 +43,58 @@ export interface OVSXApiFilter {
export class OVSXApiFilterImpl implements OVSXApiFilter {
constructor(
+ public client: OVSXClient,
public supportedApiVersion: string
) { }
+ async findLatestCompatibleExtension(query: VSXQueryOptions): Promise {
+ const targetPlatform = query.targetPlatform;
+ if (!targetPlatform) {
+ return this.queryLatestCompatibleExtension(query);
+ }
+ const latestWithTargetPlatform = await this.queryLatestCompatibleExtension(query);
+ let latestUniversal: VSXExtensionRaw | undefined;
+ if (targetPlatform !== 'universal' && targetPlatform !== 'web') {
+ // Additionally query the universal version, as there might be a newer one available
+ latestUniversal = await this.queryLatestCompatibleExtension({ ...query, targetPlatform: 'universal' });
+ }
+ if (latestWithTargetPlatform && latestUniversal) {
+ // Prefer the version with the target platform if it's greater or equal to the universal version
+ return this.versionGreaterThanOrEqualTo(latestWithTargetPlatform.version, latestUniversal.version) ? latestWithTargetPlatform : latestUniversal;
+ }
+ return latestWithTargetPlatform ?? latestUniversal;
+ }
+
+ protected async queryLatestCompatibleExtension(query: VSXQueryOptions): Promise {
+ let offset = 0;
+ let size = 5;
+ let loop = true;
+ while (loop) {
+ const queryOptions: VSXQueryOptions = {
+ ...query,
+ offset,
+ size // there is a great chance that the newest version will work
+ };
+ const results = await this.client.query(queryOptions);
+ const compatibleExtension = this.getLatestCompatibleExtension(results.extensions);
+ if (compatibleExtension) {
+ return compatibleExtension;
+ }
+ // Adjust offset by the amount of returned extensions
+ offset += results.extensions.length;
+ // Continue querying if there are more extensions available
+ loop = results.totalSize > offset;
+ // Adjust the size to fetch more extensions next time
+ size = Math.min(size * 2, 100);
+ }
+ return undefined;
+ }
+
getLatestCompatibleExtension(extensions: VSXExtensionRaw[]): VSXExtensionRaw | undefined {
if (extensions.length === 0) {
return;
} else if (this.isBuiltinNamespace(extensions[0].namespace.toLowerCase())) {
- return extensions.find(extension => this.versionGreaterThanOrEqualTo(extension.version, this.supportedApiVersion));
+ return extensions.find(extension => this.versionGreaterThanOrEqualTo(this.supportedApiVersion, extension.version));
} else {
return extensions.find(extension => this.supportedVscodeApiSatisfies(extension.engines?.vscode ?? '*'));
}
@@ -63,7 +112,7 @@ export class OVSXApiFilterImpl implements OVSXApiFilter {
}
}
if (this.isBuiltinNamespace(searchEntry.namespace)) {
- return getLatestCompatibleVersion(allVersions => this.versionGreaterThanOrEqualTo(allVersions.version, this.supportedApiVersion));
+ return getLatestCompatibleVersion(allVersions => this.versionGreaterThanOrEqualTo(this.supportedApiVersion, allVersions.version));
} else {
return getLatestCompatibleVersion(allVersions => this.supportedVscodeApiSatisfies(allVersions.engines?.vscode ?? '*'));
}
@@ -82,7 +131,7 @@ export class OVSXApiFilterImpl implements OVSXApiFilter {
if (!versionA || !versionB) {
return false;
}
- return semver.lte(versionA, versionB);
+ return semver.gte(versionA, versionB);
}
protected supportedVscodeApiSatisfies(vscodeApiRange: string): boolean {
diff --git a/dev-packages/ovsx-client/src/ovsx-http-client.ts b/dev-packages/ovsx-client/src/ovsx-http-client.ts
index 84629e61f02df..e6e5c3298827b 100644
--- a/dev-packages/ovsx-client/src/ovsx-http-client.ts
+++ b/dev-packages/ovsx-client/src/ovsx-http-client.ts
@@ -16,6 +16,9 @@
import { OVSXClient, VSXQueryOptions, VSXQueryResult, VSXSearchOptions, VSXSearchResult } from './ovsx-types';
import { RequestContext, RequestService } from '@theia/request';
+import { RateLimiter } from 'limiter';
+
+export const OVSX_RATE_LIMIT = 15;
export class OVSXHttpClient implements OVSXClient {
@@ -23,45 +26,46 @@ export class OVSXHttpClient implements OVSXClient {
* @param requestService
* @returns factory that will cache clients based on the requested input URL.
*/
- static createClientFactory(requestService: RequestService): (url: string) => OVSXClient {
+ static createClientFactory(requestService: RequestService, rateLimiter?: RateLimiter): (url: string) => OVSXClient {
// eslint-disable-next-line no-null/no-null
const cachedClients: Record = Object.create(null);
- return url => cachedClients[url] ??= new this(url, requestService);
+ return url => cachedClients[url] ??= new this(url, requestService, rateLimiter);
}
constructor(
protected vsxRegistryUrl: string,
- protected requestService: RequestService
+ protected requestService: RequestService,
+ protected rateLimiter = new RateLimiter({ tokensPerInterval: OVSX_RATE_LIMIT, interval: 'second' })
) { }
- async search(searchOptions?: VSXSearchOptions): Promise {
- try {
- return await this.requestJson(this.buildUrl('api/-/search', searchOptions));
- } catch (err) {
- return {
- error: err?.message || String(err),
- offset: -1,
- extensions: []
- };
- }
+ search(searchOptions?: VSXSearchOptions): Promise {
+ return this.requestJson(this.buildUrl('api/-/search', searchOptions));
}
- async query(queryOptions?: VSXQueryOptions): Promise {
- try {
- return await this.requestJson(this.buildUrl('api/-/query', queryOptions));
- } catch (error) {
- console.warn(error);
- return {
- extensions: []
- };
- }
+ query(queryOptions?: VSXQueryOptions): Promise {
+ return this.requestJson(this.buildUrl('api/v2/-/query', queryOptions));
}
protected async requestJson(url: string): Promise {
- return RequestContext.asJson(await this.requestService.request({
- url,
- headers: { 'Accept': 'application/json' }
- }));
+ const attempts = 5;
+ for (let i = 0; i < attempts; i++) {
+ // Use 1, 2, 4, 8, 16 tokens for each attempt
+ const tokenCount = Math.pow(2, i);
+ await this.rateLimiter.removeTokens(tokenCount);
+ const context = await this.requestService.request({
+ url,
+ headers: { 'Accept': 'application/json' }
+ });
+ if (context.res.statusCode === 429) {
+ console.warn('OVSX rate limit exceeded. Consider reducing the rate limit.');
+ // If there are still more attempts left, retry the request with a higher token count
+ if (i < attempts - 1) {
+ continue;
+ }
+ }
+ return RequestContext.asJson(context);
+ }
+ throw new Error('Failed to fetch data from OVSX.');
}
protected buildUrl(url: string, query?: object): string {
diff --git a/dev-packages/ovsx-client/src/ovsx-mock-client.ts b/dev-packages/ovsx-client/src/ovsx-mock-client.ts
index f7366f841b62d..05388cce9e24c 100644
--- a/dev-packages/ovsx-client/src/ovsx-mock-client.ts
+++ b/dev-packages/ovsx-client/src/ovsx-mock-client.ts
@@ -61,21 +61,26 @@ export class OVSXMockClient implements OVSXClient {
reviewsUrl: url.extensionReviewsUrl(namespace, name),
timestamp: new Date(now - ids.length + i + 1).toISOString(),
version,
- description: `Mock VS Code Extension for ${id}`
+ description: `Mock VS Code Extension for ${id}`,
+ namespaceDisplayName: name,
+ preRelease: false
};
});
return this;
}
async query(queryOptions?: VSXQueryOptions): Promise {
+ const extensions = this.extensions
+ .filter(extension => typeof queryOptions === 'object' && (
+ this.compare(queryOptions.extensionId, this.id(extension)) &&
+ this.compare(queryOptions.extensionName, extension.name) &&
+ this.compare(queryOptions.extensionVersion, extension.version) &&
+ this.compare(queryOptions.namespaceName, extension.namespace)
+ ));
return {
- extensions: this.extensions
- .filter(extension => typeof queryOptions === 'object' && (
- this.compare(queryOptions.extensionId, this.id(extension)) &&
- this.compare(queryOptions.extensionName, extension.name) &&
- this.compare(queryOptions.extensionVersion, extension.version) &&
- this.compare(queryOptions.namespaceName, extension.namespace)
- ))
+ offset: 0,
+ totalSize: extensions.length,
+ extensions
};
}
diff --git a/dev-packages/ovsx-client/src/ovsx-router-client.ts b/dev-packages/ovsx-client/src/ovsx-router-client.ts
index f5121b3e18b09..6b524b9491b62 100644
--- a/dev-packages/ovsx-client/src/ovsx-router-client.ts
+++ b/dev-packages/ovsx-client/src/ovsx-router-client.ts
@@ -152,6 +152,8 @@ export class OVSXRouterClient implements OVSXClient {
protected emptyQueryResult(queryOptions?: VSXQueryOptions): VSXQueryResult {
return {
+ offset: 0,
+ totalSize: 0,
extensions: []
};
}
@@ -183,8 +185,11 @@ export class OVSXRouterClient implements OVSXClient {
results.forEach((result, sourceUri) => {
result.extensions.forEach(extension => filtering.push(this.filterExtension(sourceUri, extension)));
});
+ const extensions = removeNullValues(await Promise.all(filtering));
return {
- extensions: removeNullValues(await Promise.all(filtering))
+ offset: 0,
+ totalSize: extensions.length,
+ extensions
};
}
diff --git a/dev-packages/ovsx-client/src/ovsx-types.ts b/dev-packages/ovsx-client/src/ovsx-types.ts
index 1c0c2a197c252..ec747ebf91e8e 100644
--- a/dev-packages/ovsx-client/src/ovsx-types.ts
+++ b/dev-packages/ovsx-client/src/ovsx-types.ts
@@ -49,7 +49,7 @@ export interface OVSXClient {
*/
search(searchOptions?: VSXSearchOptions): Promise;
/**
- * GET https://openvsx.org/api/-/query
+ * GET https://openvsx.org/api/v2/-/query
*
* Fetch one or all versions of an extension.
*/
@@ -105,13 +105,10 @@ export interface VSXSearchOptions {
* Should be aligned with https://github.com/eclipse/openvsx/blob/e8f64fe145fc05d2de1469735d50a7a90e400bc4/server/src/main/java/org/eclipse/openvsx/json/SearchResultJson.java
*/
export interface VSXSearchResult {
- error?: string;
offset: number;
extensions: VSXSearchEntry[];
}
-/** @deprecated since 1.31.0 use {@link VSXQueryOptions} instead */
-export type VSXQueryParam = VSXQueryOptions;
/**
* The possible options when performing a search.
*
@@ -126,10 +123,25 @@ export interface VSXQueryOptions {
extensionId?: string;
extensionUuid?: string;
namespaceUuid?: string;
- includeAllVersions?: boolean;
+ includeAllVersions?: boolean | 'links';
+ targetPlatform?: VSXTargetPlatform;
+ size?: number;
+ offset?: number;
}
+export type VSXTargetPlatform =
+ 'universal' | 'web' |
+ 'win32-x64' | 'win32-ia32' | 'win32-arm64' |
+ 'darwin-x64' | 'darwin-arm64' |
+ 'linux-x64' | 'linux-arm64' | 'linux-armhf' |
+ 'alpine-x64' | 'alpine-arm64' | (string & {});
+
export interface VSXQueryResult {
+ success?: string;
+ warning?: string;
+ error?: string;
+ offset: number;
+ totalSize: number;
extensions: VSXExtensionRaw[];
}
@@ -199,35 +211,65 @@ export interface VSXExtensionRaw {
reviewsUrl: string;
name: string;
namespace: string;
- publishedBy: VSXUser
+ targetPlatform?: VSXTargetPlatform;
+ publishedBy: VSXUser;
+ preRelease: boolean;
namespaceAccess: VSXExtensionNamespaceAccess;
files: VSXExtensionRawFiles;
allVersions: {
[version: string]: string;
};
+ allVersionsUrl?: string;
averageRating?: number;
downloadCount: number;
reviewCount: number;
version: string;
timestamp: string;
preview?: boolean;
+ verified?: boolean;
displayName?: string;
+ namespaceDisplayName: string;
description?: string;
categories?: string[];
+ extensionKind?: string[];
tags?: string[];
license?: string;
homepage?: string;
repository?: string;
+ sponsorLink?: string;
bugs?: string;
markdown?: string;
galleryColor?: string;
galleryTheme?: string;
+ localizedLanguages?: string[];
qna?: string;
+ badges?: VSXBadge[];
+ dependencies?: VSXExtensionReference[];
+ bundledExtensions?: VSXExtensionReference[];
+ allTargetPlatformVersions?: VSXTargetPlatforms[];
+ url?: string;
engines?: {
[engine: string]: string;
};
}
+export interface VSXBadge {
+ url?: string;
+ href?: string;
+ description?: string;
+}
+
+export interface VSXExtensionReference {
+ url: string;
+ namespace: string;
+ extension: string;
+}
+
+export interface VSXTargetPlatforms {
+ version: string;
+ targetPlatforms: VSXTargetPlatform[];
+}
+
export interface VSXResponseError extends Error {
statusCode: number;
}
diff --git a/dev-packages/private-eslint-plugin/package.json b/dev-packages/private-eslint-plugin/package.json
index b1daa89267ea3..df56642978462 100644
--- a/dev-packages/private-eslint-plugin/package.json
+++ b/dev-packages/private-eslint-plugin/package.json
@@ -1,16 +1,16 @@
{
"private": true,
"name": "@theia/eslint-plugin",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Custom ESLint rules for developing Theia extensions and applications",
"main": "index.js",
"scripts": {
"prepare": "tsc -b"
},
"dependencies": {
- "@theia/core": "1.44.0",
- "@theia/ext-scripts": "1.44.0",
- "@theia/re-exports": "1.44.0",
+ "@theia/core": "1.54.0",
+ "@theia/ext-scripts": "1.54.0",
+ "@theia/re-exports": "1.54.0",
"js-levenshtein": "^1.1.6"
}
}
diff --git a/dev-packages/private-ext-scripts/package.json b/dev-packages/private-ext-scripts/package.json
index 4e2787d56772e..7cbc0f55acee9 100644
--- a/dev-packages/private-ext-scripts/package.json
+++ b/dev-packages/private-ext-scripts/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@theia/ext-scripts",
- "version": "1.44.0",
+ "version": "1.54.0",
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"description": "NPM scripts for Theia packages.",
"bin": {
diff --git a/dev-packages/private-ext-scripts/theia-ts-clean.js b/dev-packages/private-ext-scripts/theia-ts-clean.js
index 62c3ba9e751c6..9bdaa0c860c3e 100755
--- a/dev-packages/private-ext-scripts/theia-ts-clean.js
+++ b/dev-packages/private-ext-scripts/theia-ts-clean.js
@@ -21,7 +21,7 @@
const _glob = require('glob');
const debug = require('debug')('ts-clean');
const fs = require('fs');
-const nsfw = require('nsfw');
+const parcelWatcher = require('@parcel/watcher');
const path = require('path');
const util = require('util');
const yargs = require('yargs');
@@ -121,13 +121,11 @@ async function tsClean() {
*/
async function tsCleanWatch(src, dst, dry) {
await tsCleanRun(src, dst, dry);
- const watcher = await nsfw(src, async events => {
+ await parcelWatcher.subscribe(src, async (_err, events) => {
for (const event of events) {
let absolute;
- if (event.action === nsfw.actions.DELETED) {
- absolute = path.resolve(event.directory, event.file);
- } else if (event.action === nsfw.actions.RENAMED) {
- absolute = path.resolve(event.directory, event.oldFile);
+ if (event.type === 'delete') {
+ absolute = event.path;
} else {
continue;
}
@@ -143,7 +141,6 @@ async function tsCleanWatch(src, dst, dry) {
}));
}
});
- await watcher.start();
}
/**
diff --git a/dev-packages/private-re-exports/package.json b/dev-packages/private-re-exports/package.json
index 83769f80bae9e..d5076c0f8e950 100644
--- a/dev-packages/private-re-exports/package.json
+++ b/dev-packages/private-re-exports/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@theia/re-exports",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia re-export helper functions and scripts.",
"main": "lib/index.js",
"engines": {
@@ -26,6 +26,7 @@
"dependencies": {
"mustache": "^4.2.0",
"semver": "^7.5.4",
+ "tslib": "^2.6.2",
"yargs": "^15.3.1"
},
"devDependencies": {
diff --git a/dev-packages/private-re-exports/src/package-re-exports.ts b/dev-packages/private-re-exports/src/package-re-exports.ts
index 40aebc7a91db4..ca76c6b816532 100644
--- a/dev-packages/private-re-exports/src/package-re-exports.ts
+++ b/dev-packages/private-re-exports/src/package-re-exports.ts
@@ -172,12 +172,13 @@ export class PackageReExports {
// To get around this, we can spawn a sub NodeJS process that will run the asynchronous
// logic and then synchronously wait for the serialized result on the standard output.
const scriptPath = require.resolve('./bin-package-re-exports-from-package.js');
- const { stdout } = cp.spawnSync(process.argv0, [...process.execArgv, scriptPath, packageName], {
+ const { stdout } = cp.spawnSync(process.platform === 'win32' ? `"${process.argv0}"` : process.argv0, [...process.execArgv, scriptPath, packageName], {
env: {
ELECTRON_RUN_AS_NODE: '1'
},
encoding: 'utf8',
- stdio: ['ignore', 'pipe', 'inherit']
+ stdio: ['ignore', 'pipe', 'inherit'],
+ shell: true
});
const [packageRoot, reExports] = JSON.parse(stdout) as [string, ReExport[]];
return new PackageReExports(packageName, packageRoot, reExports);
diff --git a/dev-packages/request/package.json b/dev-packages/request/package.json
index 3c3ae2d1d41e9..f55e01682458e 100644
--- a/dev-packages/request/package.json
+++ b/dev-packages/request/package.json
@@ -1,6 +1,6 @@
{
"name": "@theia/request",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia Proxy-Aware Request Service",
"publishConfig": {
"access": "public"
@@ -30,6 +30,7 @@
},
"dependencies": {
"http-proxy-agent": "^5.0.0",
- "https-proxy-agent": "^5.0.0"
+ "https-proxy-agent": "^5.0.0",
+ "tslib": "^2.6.2"
}
}
diff --git a/dev-packages/request/src/node-request-service.ts b/dev-packages/request/src/node-request-service.ts
index 4126647f6451a..2a5e491ec37e8 100644
--- a/dev-packages/request/src/node-request-service.ts
+++ b/dev-packages/request/src/node-request-service.ts
@@ -31,7 +31,6 @@ export interface NodeRequestOptions extends RequestOptions {
};
export class NodeRequestService implements RequestService {
-
protected proxyUrl?: string;
protected strictSSL?: boolean;
protected authorization?: string;
@@ -107,9 +106,14 @@ export class NodeRequestService implements RequestService {
opts.auth = options.user + ':' + options.password;
}
+ const timeoutHandler = () => {
+ reject('timeout');
+ };
+
const req = rawRequest(opts, async res => {
const followRedirects = options.followRedirects ?? 3;
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && followRedirects > 0 && res.headers.location) {
+ req.off('timeout', timeoutHandler);
this.request({
...options,
url: res.headers.location,
@@ -125,6 +129,7 @@ export class NodeRequestService implements RequestService {
});
stream.on('end', () => {
+ req.off('timeout', timeoutHandler);
const buffer = Buffer.concat(chunks);
resolve({
url: options.url,
@@ -136,11 +141,17 @@ export class NodeRequestService implements RequestService {
});
});
- stream.on('error', reject);
+ stream.on('error', err => {
+ reject(err);
+ });
}
});
- req.on('error', reject);
+ req.on('error', err => {
+ reject(err);
+ });
+
+ req.on('timeout', timeoutHandler);
if (options.timeout) {
req.setTimeout(options.timeout);
@@ -153,7 +164,7 @@ export class NodeRequestService implements RequestService {
req.end();
token?.onCancellationRequested(() => {
- req.abort();
+ req.destroy();
reject();
});
});
diff --git a/doc/Developing.md b/doc/Developing.md
index bce52b7b21bbd..f8c111a41a656 100644
--- a/doc/Developing.md
+++ b/doc/Developing.md
@@ -508,15 +508,14 @@ etc.) by opening `packages//coverage/index.html`.
- Install [`scoop`](https://github.com/lukesampson/scoop#installation).
- Install [`nvm`](https://github.com/coreybutler/nvm-windows) with scoop: `scoop install nvm`.
- - Install Node.js with `nvm`: `nvm install 18.17.0`, then use it: `nvm use 18.17.0`. You can list all available Node.js versions with `nvm list available` if you want to pick another version.
+ - Install Node.js with `nvm`: `nvm install lts`, then use it: `nvm use lts`. You can list all available Node.js versions with `nvm list available` if you want to pick another version.
- Install `yarn`: `scoop install yarn`.
- If you need to install `windows-build-tools`, see [`Installing Windows Build Tools`](#installing-windows-build-tools).
- If you run into problems with installing the required build tools, the `node-gyp` documentation offers a useful [guide](https://github.com/nodejs/node-gyp#on-windows) how to install the dependencies manually. The versions required for building Theia are:
- - Python 3.6 or higher
+ - Python 3.6 to 3.11
- Visual Studio [build tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) 17
- - If you have multiple versions of either python or Visual Studio installed or if the tool is not found, you may adjust the used version via the npm config:
- - `npm config set python /path/to/executable/python --global`
- - `npm config set msvs_version 2017 --global`
+ - If you have multiple versions of either python or Visual Studio installed, or if the tool is not found, you may adjust the version used as described
+ [here](https://github.com/nodejs/node-gyp?tab=readme-ov-file#configuring-python-dependency)
Clone, build and run Theia.
Using Git Bash as administrator:
diff --git a/doc/Plugin-API.md b/doc/Plugin-API.md
index 729619fc5b04d..54c0f91e27a64 100644
--- a/doc/Plugin-API.md
+++ b/doc/Plugin-API.md
@@ -5,13 +5,22 @@ Therefore, it supports [three extension mechanisms: VS Code extensions, Theia ex
In the following, we focus on the mechanics of Theia plugins and Theia’s compatibility with the [VS Code Extension API](https://code.visualstudio.com/api) in order to support running VS Code extensions in Theia.
This documentation aims to support developers extending Theia’s plugin API to either enhance the extensibility of Theia via plugins and/or increase Theia’s coverage of the VS Code Extension API – and with that the number of VS Code extensions that can be used in Theia.
-Theia plugins, as well as VS Code extensions, can be installed and removed from a Theia installation at runtime and may extend many different capabilities of Theia, such as theming, language support, debuggers, tree views, etc., via a clearly defined API.
+Theia plugins, as well as VS Code extensions, can be installed and removed from a Theia installation at runtime.
+There are three kinds of plugins that address different extensibility use cases:
+
+- VS Code plugins may extend many different user-facing capabilities of Theia, such as theming, language support, debuggers, tree views, etc., via a clearly defined API.
+_In the context of VS Code itself these are called "extensions", not to be confused with build-time Theia extensions._
+- Theia plugins are a superset of VS Code plugins using a largely compatible API with some additional capabilities not applicable to VS Code
+- Headless plugins address application-specific extension points.
+They do not extend the user-facing capabilities of Theia as the other two categories of plugins do, but support application-specific services that, in turn, often do.
+More information about headless plugins [is detailed below](#headless-plugins).
+
A plugin runs inside a "host process".
-This is a sub-process spawned by Theia's backend to isolate the plugin from the main process.
+This is a subprocess spawned by Theia's backend to isolate the plugin from the main process.
This encapsulates the plugin to prevent it from arbitrarily accessing Theia services and potentially harm performance or functionality of Theia’s main functionality.
-Instead, a plugin accesses Theia’s state and services via the plugin API.
+Instead, a plugin accesses Theia’s state and services via the plugin API, if any, provided within its host process.
-Theia’s plugin API thrives to be a super set of VS Code’s extension API to enable running VS Code extensions as Theia plugins.
+Theia’s plugin API strives to be a superset of VS Code’s extension API to enable running VS Code extensions as Theia plugins.
For many cases this already works well.
A report on API compatibility is generated daily in the [vscode-theia-comparator repository](https://github.com/eclipse-theia/vscode-theia-comparator).
Please note that the report only checks the API on an interface level – and not the compatibility of the interfaces’ implementation behaviour.
@@ -23,10 +32,11 @@ The report can be found here:
## Relevant Theia source code
-- [plugin](https://github.com/eclipse-theia/theia/tree/master/packages/plugin): Contains the API declaration of the theia plugin namespace
-- [plugin-ext](https://github.com/eclipse-theia/theia/tree/master/packages/plugin-ext): Contains both the mechanisms for running plugins and providing them with an API namespace and the implementation of the ‘theia’ plugin API
+- [plugin](https://github.com/eclipse-theia/theia/tree/master/packages/plugin): Contains the API declaration of the theia plugin namespace.
+- [plugin-ext](https://github.com/eclipse-theia/theia/tree/master/packages/plugin-ext): Contains both the mechanisms for running plugins and providing them with an API namespace and the implementation of the ‘theia’ plugin API.
- [plugin-ext-vscode](https://github.com/eclipse-theia/theia/tree/master/packages/plugin-ext-vscode): Contains an implementation of the VS Code plugin API.
-Since VS Code and Theia APIs are largely compatible, the initialization passes on the Theia plugin API and overrides a few members in the API object to be compatible to VS Code extensions (see [plugin-ext-vscode/src/node/plugin-vscode-init.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts))
+Since VS Code and Theia APIs are largely compatible, the initialization passes on the Theia plugin API and overrides a few members in the API object to be compatible to VS Code extensions (see [plugin-ext-vscode/src/node/plugin-vscode-init.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts)).
+- [plugin-ext-headless](https://github.com/eclipse-theia/theia/tree/master/packages/plugin-ext-headless): Contains the mechanism for running "headless" plugins in a dedicated backend plugin host process.
## API definition and exposure
@@ -39,8 +49,8 @@ For VS Code plugins, the same API is available via the `vscode` namespace as exp
Plugin containers are node processes (see [plugin-ext/src/hosted/node/plugin-host.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/hosted/node/plugin-host.ts))and web workers ([plugin-ext/src/hosted/browser/worker/worker-main.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts)).
These expose the API in the following places:
-- Browser: assign API object to `window['theia']` in [plugin-ext/src/hosted/browser/worker/worker-main.ts](https://github.com/eclipse-theia/theia/blob/541b300adc029ab1dd729da1ca49179ace1447b2/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts#L192)
-- Back-end/Node: Override module loading for Theia plugins in [plugin-ext/src/hosted/node/scanners/backend-init-theia.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts) and for VS Code plugins in [plugin-ext-vscode/src/node/plugin-vscode-init.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts)
+- Browser: assign API object to `window['theia']` in [plugin-ext/src/hosted/browser/worker/worker-main.ts](https://github.com/eclipse-theia/theia/blob/541b300adc029ab1dd729da1ca49179ace1447b2/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts#L192).
+- Back-end/Node: Override module loading for Theia plugins in [plugin-ext/src/hosted/node/scanners/backend-init-theia.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/hosted/node/scanners/backend-init-theia.ts) and for VS Code plugins in [plugin-ext-vscode/src/node/plugin-vscode-init.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts).
**Note** that it is not necessary to adapt these for implementing new plugin API.
@@ -48,34 +58,52 @@ These expose the API in the following places:
As the plugin runs in a separate process, the plugin API cannot directly communicate with Theia.
Instead, the plugin process and Theia’s main process communicate via RPC.
-Therefore, the following "Main-Ext" pattern is used.
-![Communication between Theia and Plugin API](./images/plugin-api-diagram.png)
+For VS Code plugins and Theia plugins, the following "Main-Ext" pattern is used.
+There is one instance of the plugin host process for each connected frontend.
+
+![Communication between Theia and Plugin API for VS Code Plugins](./images/plugin-api-diagram.svg)
`Ext` refers to the code running on the plugin side inside the isolated host process.
Therefore, this code cannot directly use any Theia services (e.g. via dependency injection).
`Main` refers to code running inside the Theia frontend in the **browser** context.
Therefore, it can access any Theia service just like a [build time Theia extension](https://theia-ide.org/docs/authoring_extensions/).
-As the lifecycle of a plugin starts inside its process on the `Ext` side, anything that the plugin needs from Theia (e.g. state, command execution, access to services) has to be invoked over RCP via an implementation on the `Main` side.
+> [!NOTE]
+> As the plugin hosts for VS Code plugins are scoped per frontend connection and the `Main` side resides in the browser, there is actually an indirection of the RPC communication channel via the Theia backend.
+> This simply relays the messages in both directions as depicted in the diagram.
+
+For headless plugins, "Main-Ext" pattern is very similar, except that there is only one instance of the plugin host and the `Main` code runs in the **node** context, as there is no associated frontend context for headless plugins.
+
+![Communication between Theia and Plugin API for Headless Plugins](./images/headless-plugin-diagram.svg)
+
+As the lifecycle of a plugin starts inside its process on the `Ext` side, anything that the plugin needs from Theia (e.g. state, command execution, access to services) has to be invoked over RPC via an implementation on the `Main` side.
In the inverse direction, the same is true for code that runs on the `Main` side and that needs something from the plugin side (e.g. changing plugin state after a user input).
-It needs to be invoked over RCP via an implementation on the `Ext` side.
+It needs to be invoked over RPC via an implementation on the `Ext` side.
Therefore, `Main` and `Ext` interfaces usually come in pairs (e.g. [LanguagesExt](https://github.com/eclipse-theia/theia/blob/541b300adc029ab1dd729da1ca49179ace1447b2/packages/plugin-ext/src/common/plugin-api-rpc.ts#L1401) and [LanguagesMain](https://github.com/eclipse-theia/theia/blob/541b300adc029ab1dd729da1ca49179ace1447b2/packages/plugin-ext/src/common/plugin-api-rpc.ts#L1474)).
To communicate with each other, the implementation of each side of the API - `Main` and `Ext` - has an RPC proxy of its corresponding counterpart.
The proxy is based on the interface of the other side: `Main` implementation has a proxy of the `Ext` interface and vice versa.
The implementations do not have explicit dependencies to each other.
-Communication via RPC only supports transferring plain JSON objects: Only pure DTO objects without any functions can be transmitted.
-Consequently, objects with functions need to be cached and references to such objects need to be transmitted as handles (ids) that are resolved to the original object later on to invoke functions.
+### Encoding and Decoding RPC Messages
+
+The communication between each side of the API is governed by proxies that use an `RpcProtocol` on a given channel to transmit RPC messages such as requests and notifications.
+In Theia, the encoding and decoding process of RPC messages can be customized through dedicated `RpcMessageEncoder` and `RpcMessageDecoder` classes that can be provided when a new RpcProtocol is created.
+The `RpcMessageEncoder` writes RPC messages to a buffer whereas the `RpcMessageDecoder` parses a binary message from a buffer into a RPC message.
-For instance, in [LanguagesExtImpl](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/languages.ts)#registerCodeActionsProvider a new code action provider is cached on the `Ext` side and then registered on the `Main` side via its handle.
-When the code action provider’s methods are later invoked on the `Main` side (e.g. in [LanguagesMainImpl](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/main/browser/languages-main.ts)#provideCodeActions), it calls the `Ext` side with this handle.
-The `Ext` side then gets the cached object, executes appropriate functions and returns the results back to the `Main` side (e.g. in [LanguagesExtImpl](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/languages.ts)#$provideCodeActions).
+By default, Theia uses an encoder and decoder based on [msgpackr](https://www.npmjs.com/package/msgpackr) that already properly handles many JavaScript built-in types, such as arrays and maps.
+We can separately extend the encoding and decoding of our own classes by installing extensions using the `MsgPackExtensionManager` singleton, accessible from both ends of the channel.
+Examples of this can be found in the [extension for Errors](https://github.com/eclipse-theia/theia/blob/72421be24d0461f811a39324579913e91056d7c4/packages/core/src/common/message-rpc/rpc-message-encoder.ts#L171) and the [extension for URI, Range and other classes](https://github.com/eclipse-theia/theia/blob/72421be24d0461f811a39324579913e91056d7c4/packages/plugin-ext/src/common/rpc-protocol.ts#L223).
+We call the registration of these extensions in `index.ts` to ensure they are available early on.
+Please note that msgpackr [always registers extensions globally](https://github.com/kriszyp/msgpackr/issues/93) so the extensions leak into all connections within Theia, i.e., also non-API related areas such as the frontend-backend connection.
+And while the number of custom extensions is [limited to 100](https://github.com/kriszyp/msgpackr?tab=readme-ov-file#custom-extensions), checking the extensions for every single message may impact performance if there are many extensions or the conversion is very expensive.
+Another hurdle is that we need to ensure that we always detect the correct type of object purely from the object shape which may prove difficult if there are hundreds of objects and custom instances from user code.
+Consequently, we mainly use the msgpackr extension mechanism for very few common classes but rely on custom data transfer objects (DTOs) for the Theia plugin API, see also [Complex objects and RPC](#complex-objects-and-rpc).
## Adding new API
-This section gives an introduction to extending Theia’s plugin API. If you want to add a whole new namespace in your own extension, see this [readme](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/doc/how-to-add-new-plugin-namespace.md).
+This section gives an introduction to extending Theia’s plugin API. If you want to add a complete custom plugin API in your own extension, see this [readme](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/doc/how-to-add-new-custom-plugin-api.md).
For adding new API, the first step is to declare it in the [theia.d.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin/src/theia.d.ts) file in the plugin package.
In a second step, the implementation for the new API must be made available in the returned object of the API factory in [plugin-context.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/plugin-context.ts).
@@ -132,48 +160,82 @@ export function createAPIFactory(
### Adding new Ext and Main interfaces with implementations
-`Ext` and `Main` interfaces only contain the functions called over RCP.
+`Ext` and `Main` interfaces only contain the functions called over RPC.
Further functions are just part of the implementations.
-Functions to be called over RCP must start with `$`, e.g. `$executeStuff`.
+Functions to be called over RPC must start with `$`, e.g. `$executeStuff`.
- Define `Ext` and `Main` interfaces in [plugin-ext/src/common/plugin-api-rpc.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/common/plugin-api-rpc.ts).
-The interfaces should be suffixed with `Ext` and `Main` correspondingly (e.g. `LanguagesMain` and `LanguagesExt`)
+The interfaces should be suffixed with `Ext` and `Main` correspondingly (e.g. `LanguagesMain` and `LanguagesExt`).
- In [plugin-ext/src/common/plugin-api-rpc.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/common/plugin-api-rpc.ts), add a proxy identifier for the `Ext` interface to `MAIN_RPC_CONTEXT` and one for the `Main` interface to `PLUGIN_RPC_CONTEXT`
-- Create the `Ext` implementation in folder [plugin-ext/src/plugin](https://github.com/eclipse-theia/theia/tree/master/packages/plugin-ext/src/plugin)
-- Create the `Main` implementation in folder [plugin-ext/src/main/browser](https://github.com/eclipse-theia/theia/tree/master/packages/plugin-ext/src/main/browser)
+- Create the `Ext` implementation in folder [plugin-ext/src/plugin](https://github.com/eclipse-theia/theia/tree/master/packages/plugin-ext/src/plugin).
+- Create the `Main` implementation in folder [plugin-ext/src/main/browser](https://github.com/eclipse-theia/theia/tree/master/packages/plugin-ext/src/main/browser).
- To communicate via RPC, each implementation has a proxy depending on the interface on the other side.
For instance, see `LanguagesExtImpl` in [plugin-ext/src/plugin/languages.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/languages.ts) and `LanguagesMainImpl` in [plugin-ext/src/main/browser/languages-main.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/main/browser/languages-main.ts).
They each create the proxy to the other side in their constructors by using the proxy identifiers.
### Complex objects and RPC
-Only pure DTO objects without any functions or references to other objects can be transmitted via RPC.
-This often makes it impossible to just transfer objects provided by a plugin directly via RPC.
-In this case a DTO interface is necessary.
-These are defined [plugin-ext/src/common/plugin-api-rpc.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/common/plugin-api-rpc.ts).
-Utility functions to convert between DTO and API types on the `Ext` side are usually added to [plugin-ext/src/plugin/type-converters.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/type-converters.ts).
-Thus, this is also a good starting point to look for conversion utilities for existing types.
+When [encoding and decoding RPC messages](#encoding-and-decoding-rpc-messages) pure DTO objects that only carry properties can always be transmitted safely.
+However, due to the necessary serialization process, functions and references to other objects can never be transmitted safely.
If functions of objects need to be invoked on the opposite side of their creation, the object needs to be cached on the creation side.
-The other side receives a handle (basically an id) that can be used to invoke the functionality on the creation side.
+The other side receives a handle (usually an id) that can be used to invoke the functionality on the creation side.
As all cached objects are kept in memory, they should be disposed of when they are no longer needed.
-
-For instance, in [LanguagesExtImpl](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/languages.ts)#registerCodeActionsProvider a new code action provider is created and cached on the `Ext` side and then registered on the `Main` side via its handle.
-When the code action provider’s methods are later invoked on the `Main` side (e.g. in [LanguagesMainImpl](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/main/browser/languages-main.ts)#provideCodeActions), it calls the `Ext` side with this handle.
-The `Ext` side then gets the cached object, executes appropriate functions and returns the results back to the `Main` side (e.g. in [LanguageExtImpl](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/languages.ts)#$provideCodeActions).
-
+For instance, in [LanguagesExtImpl#registerCodeActionsProvider](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/languages.ts) a new code action provider is created and cached on the `Ext` side and then registered on the `Main` side via its handle.
+When the code action provider’s methods are later invoked on the `Main` side (e.g. in [LanguagesMainImpl#provideCodeActions](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/main/browser/languages-main.ts)), it calls the `Ext` side with this handle.
+The `Ext` side then gets the cached object, executes appropriate functions and returns the results back to the `Main` side (e.g. in [LanguagesExtImpl#$provideCodeActions](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/languages.ts)).
Another example to browse are the [TaskExtImpl](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/tasks/tasks.ts) and [TaskMainImpl](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/main/browser/tasks-main.ts) classes.
+To [ensure correct type conversion](#encoding-and-decoding-rpc-messages) between the Theia backend and the plugin host we define an API protocol based on types and DTOs that can be transmitted safely.
+The plugin API and its types are defined in [plugin-ext/src/common/plugin-api-rpc.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/common/plugin-api-rpc.ts) with some additional conversion on the `Ext` side being defined in [plugin-ext/src/plugin/type-converters.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/type-converters.ts).
+Thus, this is also a good starting point to look for conversion utilities for existing types.
+
### Adding new types
New classes and other types such as enums are usually implemented in [plugin-ext/src/plugin/types-impl.ts](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/src/plugin/types-impl.ts).
-They can be added here and the added to the API object created in the API factory.
+They can be added there and then we can add them to the API object created in the API factory.
+
+## Headless Plugins
+
+The majority of plugin use cases are for extension of the Theia user experience via the VS Code compatible API provided by Theia.
+These plugins use either the `vscode` API object if they use the `"vscode"` engine type in their package manifests or else the `theia` object if they use the `"theiaPlugin"` engine.
+The lifecycle of these kinds of plugins is bound to _frontend connections_: for each connected frontend, the Theia backend spawns a plugin host host process dedicated to it in which these plugins are loaded and activated (as applicable to their declared activation events).
+In the plugin host for a frontend connection these plugins have access to Theia services as discussed above, isolated from the main Theia backend process and from all of the other instances of the same plugin running in plugin hosts for all other frontend connections.
+
+Headless plugins, by contrast, are quite different in most respects:
+
+- They are encapsulated in a single plugin host process in the backend, separate from all frontend-connection plugin hosts.
+This host is spun up only if there are any headless plugins to run in it.
+- Theia does not export any default API object, analogous to `vscode` or `theia` for other plugins, as Theia itself defines no use cases for headless plugins.
+Such use cases are entirely defined by the Theia-based application's requirements and reflected in its custom APIs defined [as described in this how-to document][custom-api-howto].
+- Theia supports neither any contribution points for headless plugins nor any non-trivial activation events (only `'*'` and `'onStartupFinished'`).
+This is a corollary of the use cases being entirely application-specific: the application needs to define its own contribution points and activation events.
+Currently this requires an application to enumerate the available deployed plugins via the [HostedPluginServer](https://github.com/eclipse-theia/theia/blob/1a56ba96fdc9b6df3a230df7b44e22e5785e3abd/packages/plugin-ext/src/common/plugin-protocol.ts#L1005) to parse their package manifests to extract application-specific contribution points and activation events, and to activate plugins via the [PluginManager::activatePlugin(pluginId)](https://github.com/eclipse-theia/theia/blob/1a56ba96fdc9b6df3a230df7b44e22e5785e3abd/packages/plugin-ext/src/common/plugin-api-rpc.ts#L182) API on the appropriate triggers.
+
+Thus, headless plugins are best suited to the contribution of third-party extensions of a Theia application's custom backend services, where those service are shared by all connected frontends or serve some kind of headless scenario like a CLI.
+
+A headless plugin may be restricted to only the headless deployment, in which case it may make this explicit by declaring the `"theiaHeadlessPlugin"` engine in its package manifest.
+Alternatively, a VS Code or Theia plugin that extends the frontend user experience may also contribute a headless entrypoint for a headless deployment by identifying such entrypoint script in the `"headless"` property of the `"theiaPlugin"` object in its package manifest in addition to the `"main"` entrypoint (for VS Code plugins) or the `"theiaPlugin.backend"` entrypoint (for Theia plugins).
+
+The only API namespaces that are available to headless plugins are those custom APIs that are contributed by the application's custom build-time Theia extensions or by other headless plugins via the return results of their `activate()` functions.
+For details of how to contribute custom API, see the [pertinent documentation][custom-api-howto].
+
+[custom-api-howto]: https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/doc/how-to-add-new-custom-plugin-api.md
+
+## Dependency Injection
+
+Both the `Main` and the `Ext` sides of the plugin API are configured using [InversifyJS](https://inversify.io) dependency injection.
+
+On the `Main` side, the usual mechanism is used to bind implementations of the API objects, consisting of `ContainerModule`s registered in `package.json` and loaded at start-up into Theia's Inversify `Container` by a generated script.
+
+On the `Ext` side, the plugin host initialization script creates and configures its Inversify `Container`.
+You are encouraged to leverage this dependency injection in the definition of new API objects and the maintenance of existing ones, to promote reuse and substitutability of the various interface implementations.
## Additional Links
Talk by Thomas Maeder on writing plugin API:
-Adding a new plugin API namespace outside of theia plugin API: [how-to-add-new-plugin-namespace.md](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/doc/how-to-add-new-plugin-namespace.md)
+Adding a new custom plugin API outside of Theia plugin API: [how-to-add-new-custom-plugin-api.md](https://github.com/eclipse-theia/theia/blob/master/packages/plugin-ext/doc/how-to-add-new-custom-plugin-api.md)
Theia Plugin Implementation wiki page:
@@ -183,4 +245,6 @@ Theia versus VS Code API Comparator:
-Example of creating a custom namespace API and using in VS Code extensions: https://github.com/thegecko/vscode-theia-extension
+Example of creating a custom namespace API and using in VS Code extensions:
+
+Example of a Theia extension defining a custom namespace API and a headless plugin that uses it: [Greeting-of-the-Day API Provider Sample](https://github.com/eclipse-theia/theia/blob/master/examples/api-provider-sample) and [Greeting-of-the-Day Client Sample Plugin](https://github.com/eclipse-theia/theia/blob/master/sample-plugins/sample-namespace/plugin-gotd)
diff --git a/doc/Publishing.md b/doc/Publishing.md
index 6ca655a3a7d5a..16072f28a12a3 100644
--- a/doc/Publishing.md
+++ b/doc/Publishing.md
@@ -40,7 +40,7 @@ In order to successfully perform a `yarn upgrade` one must:
### Announce Release
It is a good idea to give a heads-up to developers and the community some hours before a release.
-At the time of writing this is [Discourse](https://community.theia-ide.org/). Here is an [example](https://community.theia-ide.org/t/eclipse-theia-v1-40-0-release/3112/5).
+At the time of writing this is [GitHub Discussions](https://github.com/eclipse-theia/theia/discussions). Here is an [example](https://github.com/eclipse-theia/theia/discussions/13314).
### Localization
diff --git a/doc/coding-guidelines.md b/doc/coding-guidelines.md
index fae57fc42dcfd..b3706d79e0aa7 100644
--- a/doc/coding-guidelines.md
+++ b/doc/coding-guidelines.md
@@ -301,11 +301,12 @@ export namespace DirtyDiffModel {
}
```
-* [5.](#no-multi-inject) Don't use multi-inject, use `ContributionProvider` to inject multiple instances.
+* [5.](#no-multi-inject) Don't use InversifyJS's `@multiInject`, use Theia's utility `ContributionProvider` to inject multiple instances.
> Why?
> - `ContributionProvider` is a documented way to introduce contribution points. See `Contribution-Points`: https://www.theia-ide.org/docs/services_and_contributions
> - If nothing is bound to an identifier, multi-inject resolves to `undefined`, not an empty array. `ContributionProvider` provides an empty array.
> - Multi-inject does not guarantee the same instances are injected if an extender does not use `inSingletonScope`. `ContributionProvider` caches instances to ensure uniqueness.
+> - `ContributionProvider` supports filtering. See `ContributionFilterRegistry`.
## CSS
diff --git a/doc/images/headless-plugin-diagram.drawio b/doc/images/headless-plugin-diagram.drawio
new file mode 100644
index 0000000000000..70518fc142f8a
--- /dev/null
+++ b/doc/images/headless-plugin-diagram.drawio
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/images/headless-plugin-diagram.svg b/doc/images/headless-plugin-diagram.svg
new file mode 100644
index 0000000000000..139b275bc2b9a
--- /dev/null
+++ b/doc/images/headless-plugin-diagram.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/doc/images/plugin-api-diagram.drawio.xml b/doc/images/plugin-api-diagram.drawio.xml
index da096d9200eab..666f59d9f38e8 100644
--- a/doc/images/plugin-api-diagram.drawio.xml
+++ b/doc/images/plugin-api-diagram.drawio.xml
@@ -1 +1,209 @@
-5Vxbc5s4FP41nuk+xAMIcXmM7aTtTrrNNDuzbV92iFFsthh5hBzb/fUrgcAIYRs7XJwkL0EHSYFzvnMXGYDxYvOReMv5F+yjcGBo/mYAJgPD0IHtsF+csk0pjgZSwowEvpi0IzwEv5EgaoK6CnwUSxMpxiENljJxiqMITalE8wjBa3naEw7lv7r0ZkghPEy9UKX+E/h0Lqi65e5ufELBbC7+tGPY6Y2Fl00WbxLPPR+vCyRwMwBjgjFNrxabMQo58zK+pOtu99zNH4ygiNZZ8Pvfn2Bz+/HT2vPHP3/drCd3K/fKtMTD0W32xshnDBBDTOgcz3DkhTc76ojgVeQjvq3GRrs5dxgvGVFnxP8QpVshTW9FMSPN6SIUd9EmoN8L1z/4VkMDiuFkI7ZOBttsEFGy/Z7MhNnwR/HeblkyytalL8jfai/jBCnGKzJFB7iVAdAjM0QPzYO5fJliILxA7IHYQoJCjwbP8oN4AqGzfJ5Yek2Ity1MWOIgonFh53tOYBOEshmOwKRQNSADgl2kO2ajwqPtSAloTgCQeIlnL1yJ11IAJcNlPQ8oelh6CaPXzGjI0HgKwnCMQ0ySteD2djx23VyKz4hQtDksR5XtGX80mT+GKTRzXVBsG6a0eUGnM0ZWiqrA3tO5Z7TNPdcFoBnuuZlFPsQ9qHXJPbtd7mnaeKzxZTEl+BeS7vCfZvhqOcf5CitAqVttsdXpiK0tMM/U+mae+3rsoQ3gpdnDzMYcYh+K/Gse2LFRhCMej/hePE/4qcu8kxldNw5Q+VUEU4WBy2ineXfFfUNLxrKllbichidiVTHQO7KRUd4ojV+UjRoLCdSY4M+Hr38xyrf7sSJOhl8qi002t0LGRT0QJC8MZhEbTpn8EKOPuDYELHS/FjcWge8nAWuVjlWB40XqBA2Z7bqtqpNT5R/bUqYsRekntteGrivH9w5wzgrw9SMBPt/kHpGAcY3DoPGgX6RIR6N+aFXjo7ZdeJnptNr1PJZ1y34a8jyWzbM9yUg5UHU+RoW6tKcvhmq3vnhBxCj3BG+2F2q5CKYMXZivunKbCqssY1gST4Uxc7s0ZhmaC8K52XCnd33/+T1JxrZLkjEtVXGsTiUD2zY8I6NFwwMsFdkgh3+Rg05rHFRN9zc0C+Js7/cBbRuWjQ6o8AndQltNhe/D1SzxCZdrd5qQhcv0xJQ9QEV22K0w1NT6lmDGrsh/u4Iw9uVyxTBJ71IMQE3Rc534hGP6dmXhgIuTRY3qe6FcMg29OA6mh4okefYmZW41GzN5olZY1U1jRlSCj6ZoZrWAOyrwmDJ+TLeEi9oFHre0kVnaqOUCjwkU2JEkXkHkw9yLuEoabGdt8vfXP1Q8MnnfeY8olGFYX9cJioPf3mOyH4eRaIuxzeFoACeHdF00icXiQd6aLULugKLttQwac5aWLsnkymwEM8CSd7XkDfDTU4xakTFQ862GTIt+jmnRejMtsKZpcfo0LbZdTgrPNC0mPLJR26bFVGC3JPg58NFnihaxZF7Yk8Rvxb4YB+3LlTYEjtOMRYFyWJ91+9q3KKZTKauuytFnWZ3eTpqYNa3Oi0+avCj+VJVVEfGldIqBczyd7bZTbKiJlCg3X25poY26D3D0crG5oiTXaakh88p9dc5gwVjpNU1VbwGSXjdC6tVU6WqVWmjb58UybNRsNdkpM0DpTJ9TUXPQKnSjnBI2pxuv1+b3fzpIV2uYaRfrvXUYgS3j2uzd5DdfSjuzLFYdserQOOwILuQQhF0t9q7SYLkwphxUq11hg/1W2GBr1RcZVXbNRIgloJpmyDh2TPsYkvmoRVC6NTGp7zmY0xUobRmUZt7CPxWWdqkbZJZb0i3D0rqguFirh9z8GNoOuAwO/eIW1jWm+4Db7nckriOXsoEwPq1+RwLVMxBjzOQQPK6S2MOwQh4KPRJ2NeNXH9BwNuRvldTdfMQvp+lUTZQMiVodPCk0bSDOcUGJl2Z+yOFYBK/rcL9gXxbrqB9O5C1cvrOhsaAURXHCzIsMSZuQTKnSnZv7viJQq0ZmdZrPl87Fn1vUzNs2LAJwBnJIar+KkNTYU3joxv07TjkmBec5f+juOR11xPmfav7tUtbcyWeEVo3Ty+23GxtEp1G7KtZrcGodO0pQOzQtHW4A5cMNLYemtpq/p5WVyvKe8GKnuq4QPdFDjitmPiuIZnfJtIm5o3wTguMkzJY/hQmM52whYjuUeowjxroxxyqc8E4nHOm7cdKAXLIomkVH7PG9IAEm8mK6RjHNqjWF9mUDrtJ24dCRkxizog5pGcPKr8Va+/xz/zGUNxy2lL4yAhU1zU7DFvtwx/79SMKs+ESiW0moPnTE/wHGW1aH/LOh9k8pD/hx4+yfdaRua/cvT8DN/w==
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/images/plugin-api-diagram.png b/doc/images/plugin-api-diagram.png
deleted file mode 100644
index 1c43cc297f06f..0000000000000
Binary files a/doc/images/plugin-api-diagram.png and /dev/null differ
diff --git a/doc/images/plugin-api-diagram.svg b/doc/images/plugin-api-diagram.svg
new file mode 100644
index 0000000000000..a919537e3497f
--- /dev/null
+++ b/doc/images/plugin-api-diagram.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/api-provider-sample/.eslintrc.js b/examples/api-provider-sample/.eslintrc.js
new file mode 100644
index 0000000000000..13089943582b6
--- /dev/null
+++ b/examples/api-provider-sample/.eslintrc.js
@@ -0,0 +1,10 @@
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ extends: [
+ '../../configs/build.eslintrc.json'
+ ],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: 'tsconfig.json'
+ }
+};
diff --git a/examples/api-provider-sample/README.md b/examples/api-provider-sample/README.md
new file mode 100644
index 0000000000000..dff79d5296502
--- /dev/null
+++ b/examples/api-provider-sample/README.md
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
ECLIPSE THEIA - API PROVIDER SAMPLE
+
+
+
+
+
+## Description
+
+The `@theia/api-provider-sample` extension is a programming example showing how to define and provide a custom API object for _plugins_ to use.
+The purpose of the extension is to:
+- provide developers with realistic coding examples of providing custom API objects
+- provide easy-to-use and test examples for features when reviewing pull requests
+
+The extension is for reference and test purposes only and is not published on `npm` (`private: true`).
+
+### Greeting of the Day
+
+The sample defines a `gotd` API that plugins can import and use to obtain tailored messages with which to greet the world, for example in their activation function.
+
+The source code is laid out in the `src/` tree as follows:
+
+- `gotd.d.ts` — the TypeScript definition of the `gotd` API object that plugins import to interact with the "Greeting of the Day" service
+- `plugin/` — the API initialization script and the implementation of the API objects (`GreetingExt` and similar interfaces).
+ All code in this directory runs exclusively in the separate plugin-host Node process, isolated from the main Theia process, together with either headless plugins or the backend of VS Code plugins.
+ The `GreetingExtImpl` and similar classes communicate with the actual API implementation (`GreetingMainImpl` etc.) classes in the main Theia process via RPC
+- `node/` — the API classes implementing `GreetingMain` and similar interfaces and the Inversify bindings that register the API provider.
+ All code in this directory runs in the main Theia Node process
+ - `common/` — the RPC API Ext/Main interface definitions corresponding to the backend of the `gotd` plugin API
+
+## Additional Information
+
+- [Theia - GitHub](https://github.com/eclipse-theia/theia)
+- [Theia - Website](https://theia-ide.org/)
+
+## License
+
+- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
+- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
+
+## Trademark
+"Theia" is a trademark of the Eclipse Foundation
+https://www.eclipse.org/theia
diff --git a/examples/api-provider-sample/package.json b/examples/api-provider-sample/package.json
new file mode 100644
index 0000000000000..410a1d085df1e
--- /dev/null
+++ b/examples/api-provider-sample/package.json
@@ -0,0 +1,42 @@
+{
+ "private": true,
+ "name": "@theia/api-provider-sample",
+ "version": "1.54.0",
+ "description": "Theia - Example code to demonstrate Theia API Provider Extensions",
+ "dependencies": {
+ "@theia/core": "1.54.0",
+ "@theia/plugin-ext": "1.54.0",
+ "@theia/plugin-ext-headless": "1.54.0"
+ },
+ "theiaExtensions": [
+ {
+ "backend": "lib/node/gotd-backend-module"
+ }
+ ],
+ "keywords": [
+ "theia-extension"
+ ],
+ "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/eclipse-theia/theia.git"
+ },
+ "bugs": {
+ "url": "https://github.com/eclipse-theia/theia/issues"
+ },
+ "homepage": "https://github.com/eclipse-theia/theia",
+ "files": [
+ "lib",
+ "src"
+ ],
+ "types": "src/gotd.d.ts",
+ "scripts": {
+ "lint": "theiaext lint",
+ "build": "theiaext build",
+ "watch": "theiaext watch",
+ "clean": "theiaext clean"
+ },
+ "devDependencies": {
+ "@theia/ext-scripts": "1.54.0"
+ }
+}
diff --git a/examples/api-provider-sample/src/common/plugin-api-rpc.ts b/examples/api-provider-sample/src/common/plugin-api-rpc.ts
new file mode 100644
index 0000000000000..87f34925ce0c0
--- /dev/null
+++ b/examples/api-provider-sample/src/common/plugin-api-rpc.ts
@@ -0,0 +1,70 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+import { createProxyIdentifier } from '@theia/plugin-ext/lib/common/rpc-protocol';
+import type { greeting } from '../gotd';
+import { Event } from '@theia/core';
+
+export enum GreetingKind {
+ DIRECT = 1,
+ QUIRKY = 2,
+ SNARKY = 3,
+}
+
+export interface GreeterData {
+ readonly uuid: string;
+ greetingKinds: greeting.GreetingKind[];
+};
+
+export const GreetingMain = Symbol('GreetingMain');
+export interface GreetingMain {
+ $getMessage(greeterId: string): Promise;
+
+ $createGreeter(): Promise;
+ $destroyGreeter(greeterId: GreeterData['uuid']): Promise;
+
+ $updateGreeter(data: GreeterData): void;
+}
+
+export const GreetingExt = Symbol('GreetingExt');
+export interface GreetingExt {
+
+ //
+ // External protocol
+ //
+
+ registerGreeter(): Promise;
+ unregisterGreeter(uuid: string): Promise;
+
+ getMessage(greeterId: string): Promise;
+ getGreetingKinds(greeterId: string): readonly greeting.GreetingKind[];
+ setGreetingKindEnabled(greeterId: string, greetingKind: greeting.GreetingKind, enable: boolean): void;
+ onGreetingKindsChanged(greeterId: string): Event;
+
+ //
+ // Internal protocol
+ //
+
+ $greeterUpdated(data: GreeterData): void;
+
+}
+
+export const PLUGIN_RPC_CONTEXT = {
+ GREETING_MAIN: createProxyIdentifier('GreetingMain'),
+};
+
+export const MAIN_RPC_CONTEXT = {
+ GREETING_EXT: createProxyIdentifier('GreetingExt'),
+};
diff --git a/examples/api-provider-sample/src/gotd.d.ts b/examples/api-provider-sample/src/gotd.d.ts
new file mode 100644
index 0000000000000..52bbc7a469a5e
--- /dev/null
+++ b/examples/api-provider-sample/src/gotd.d.ts
@@ -0,0 +1,49 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+// Strictly speaking, the 'greeting' namespace is an unnecessary level of organization
+// but it serves to illustrate how API namespaces are implemented in the backend.
+export namespace greeting {
+ export function createGreeter(): Promise;
+
+ export enum GreetingKind {
+ DIRECT = 1,
+ QUIRKY = 2,
+ SNARKY = 3,
+ }
+
+ export interface Greeter extends Disposable {
+ greetingKinds: readonly GreetingKind[];
+
+ getMessage(): Promise;
+
+ setGreetingKind(kind: GreetingKind, enable = true): void;
+
+ onGreetingKindsChanged: Event;
+ }
+}
+
+export interface Event {
+ (listener: (e: T) => unknown, thisArg?: unknown): Disposable;
+}
+
+export interface Disposable {
+ dispose(): void;
+}
+
+namespace Disposable {
+ export function create(func: () => void): Disposable;
+}
diff --git a/examples/api-provider-sample/src/node/ext-plugin-gotd-api-provider.ts b/examples/api-provider-sample/src/node/ext-plugin-gotd-api-provider.ts
new file mode 100644
index 0000000000000..e16c7d902e879
--- /dev/null
+++ b/examples/api-provider-sample/src/node/ext-plugin-gotd-api-provider.ts
@@ -0,0 +1,33 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+import * as path from 'path';
+import { injectable } from '@theia/core/shared/inversify';
+import { ExtPluginApi, ExtPluginApiProvider } from '@theia/plugin-ext-headless';
+
+@injectable()
+export class ExtPluginGotdApiProvider implements ExtPluginApiProvider {
+ provideApi(): ExtPluginApi {
+ // We can support both backend plugins and headless plugins, so we have only one
+ // entry-point script. Moreover, the application build packages that script in
+ // the `../backend/` directory from its source `../plugin/` location, alongside
+ // the scripts for all other plugin API providers.
+ const universalInitPath = path.join(__dirname, '../backend/gotd-api-init');
+ return {
+ backendInitPath: universalInitPath,
+ headlessInitPath: universalInitPath
+ };
+ }
+}
diff --git a/examples/api-provider-sample/src/node/gotd-backend-module.ts b/examples/api-provider-sample/src/node/gotd-backend-module.ts
new file mode 100644
index 0000000000000..08b54a7f556f1
--- /dev/null
+++ b/examples/api-provider-sample/src/node/gotd-backend-module.ts
@@ -0,0 +1,28 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+import { ContainerModule } from '@theia/core/shared/inversify';
+import { ExtPluginApiProvider } from '@theia/plugin-ext';
+import { ExtPluginGotdApiProvider } from './ext-plugin-gotd-api-provider';
+import { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution';
+import { GotdMainPluginApiProvider } from './gotd-main-plugin-provider';
+import { GreetingMain } from '../common/plugin-api-rpc';
+import { GreetingMainImpl } from './greeting-main-impl';
+
+export default new ContainerModule(bind => {
+ bind(Symbol.for(ExtPluginApiProvider)).to(ExtPluginGotdApiProvider).inSingletonScope();
+ bind(MainPluginApiProvider).to(GotdMainPluginApiProvider).inSingletonScope();
+ bind(GreetingMain).to(GreetingMainImpl).inSingletonScope();
+});
diff --git a/examples/api-provider-sample/src/node/gotd-main-plugin-provider.ts b/examples/api-provider-sample/src/node/gotd-main-plugin-provider.ts
new file mode 100644
index 0000000000000..14778112cb4a9
--- /dev/null
+++ b/examples/api-provider-sample/src/node/gotd-main-plugin-provider.ts
@@ -0,0 +1,29 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+import { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution';
+import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { GreetingMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
+
+@injectable()
+export class GotdMainPluginApiProvider implements MainPluginApiProvider {
+ @inject(GreetingMain)
+ protected readonly greetingMain: GreetingMain;
+
+ initialize(rpc: RPCProtocol): void {
+ rpc.set(PLUGIN_RPC_CONTEXT.GREETING_MAIN, this.greetingMain);
+ }
+}
diff --git a/examples/api-provider-sample/src/node/greeting-main-impl.ts b/examples/api-provider-sample/src/node/greeting-main-impl.ts
new file mode 100644
index 0000000000000..02bf2d2eb9f45
--- /dev/null
+++ b/examples/api-provider-sample/src/node/greeting-main-impl.ts
@@ -0,0 +1,72 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { generateUuid } from '@theia/core/lib/common/uuid';
+import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { GreetingKind, GreeterData, GreetingExt, GreetingMain, MAIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
+
+const GREETINGS = {
+ [GreetingKind.DIRECT]: ['Hello, world!', "I'm here!", 'Good day!'],
+ [GreetingKind.QUIRKY]: ['Howdy doody, world?', "What's crack-a-lackin'?", 'Wazzup werld?'],
+ [GreetingKind.SNARKY]: ["Oh, it's you, world.", 'You again, world?!', 'Whatever.'],
+} as const;
+
+@injectable()
+export class GreetingMainImpl implements GreetingMain {
+ protected proxy: GreetingExt;
+
+ private greeterData: Record = {};
+
+ constructor(@inject(RPCProtocol) rpc: RPCProtocol) {
+ this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.GREETING_EXT);
+ }
+
+ async $createGreeter(): Promise {
+ const result: GreeterData = {
+ uuid: generateUuid(),
+ greetingKinds: [GreetingKind.DIRECT]
+ };
+ this.greeterData[result.uuid] = result;
+ return result;
+ }
+
+ async $destroyGreeter(greeterId: string): Promise {
+ delete this.greeterData[greeterId];
+ }
+
+ $updateGreeter(data: GreeterData): void {
+ const myData = this.greeterData[data.uuid];
+ if (myData) {
+ myData.greetingKinds = [...data.greetingKinds];
+ this.proxy.$greeterUpdated({ ...myData });
+ }
+ }
+
+ async $getMessage(greeterId: string): Promise {
+ const data = this.greeterData[greeterId];
+ if (data.greetingKinds.length === 0) {
+ throw new Error(`No greetings are available for greeter ${greeterId}`);
+ }
+
+ // Get a random one of our supported greeting kinds.
+ const kind = data.greetingKinds[(Math.floor(Math.random() * data.greetingKinds.length))];
+ // And a random greeting of that kind
+ const greetingIdx = Math.floor(Math.random() * GREETINGS[kind].length);
+
+ return GREETINGS[kind][greetingIdx];
+ }
+}
diff --git a/examples/api-provider-sample/src/plugin/gotd-api-init.ts b/examples/api-provider-sample/src/plugin/gotd-api-init.ts
new file mode 100644
index 0000000000000..cae7b4c0a3e7f
--- /dev/null
+++ b/examples/api-provider-sample/src/plugin/gotd-api-init.ts
@@ -0,0 +1,97 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
+import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
+import { Plugin } from '@theia/plugin-ext/lib/common/plugin-api-rpc';
+import type * as gotd from '../gotd';
+import { GreetingKind, GreetingExt, MAIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
+import { GreetingExtImpl } from './greeting-ext-impl';
+import { Disposable, DisposableCollection } from '@theia/core';
+import { PluginContainerModule } from '@theia/plugin-ext/lib/plugin/node/plugin-container-module';
+
+// This script is responsible for creating and returning the extension's
+// custom API object when a plugin's module imports it. Keep in mind that
+// all of the code here runs in the plugin-host node process, whether that
+// be the backend host dedicated to some frontend connection or the single
+// host for headless plugins, which is where the plugin itself is running.
+
+type Gotd = typeof gotd;
+const GotdApiFactory = Symbol('GotdApiFactory');
+
+// Retrieved by Theia to configure the Inversify DI container when the plugin is initialized.
+// This is called when the plugin-host process is forked.
+export const containerModule = PluginContainerModule.create(({ bind, bindApiFactory }) => {
+ bind(GreetingExt).to(GreetingExtImpl).inSingletonScope();
+ bindApiFactory('@theia/api-provider-sample', GotdApiFactory, GotdApiFactoryImpl);
+});
+
+// Creates the Greeting of the Day API object
+@injectable()
+class GotdApiFactoryImpl {
+ @inject(RPCProtocol)
+ protected readonly rpc: RPCProtocol;
+
+ @inject(GreetingExt)
+ protected readonly greetingExt: GreetingExt;
+
+ @postConstruct()
+ initialize(): void {
+ this.rpc.set(MAIN_RPC_CONTEXT.GREETING_EXT, this.greetingExt);
+ }
+
+ createApi(plugin: Plugin): Gotd {
+ const self = this;
+ async function createGreeter(): Promise {
+ const toDispose = new DisposableCollection();
+
+ const uuid = await self.greetingExt.registerGreeter();
+ toDispose.push(Disposable.create(() => self.greetingExt.unregisterGreeter(uuid)));
+
+ const onGreetingKindsChanged = self.greetingExt.onGreetingKindsChanged(uuid);
+
+ const result: gotd.greeting.Greeter = {
+ get greetingKinds(): readonly GreetingKind[] {
+ return self.greetingExt.getGreetingKinds(uuid);
+ },
+
+ setGreetingKind(greetingKind: GreetingKind, enable = true): void {
+ self.greetingExt.setGreetingKindEnabled(uuid, greetingKind, enable);
+ },
+
+ getMessage(): Promise {
+ return self.greetingExt.getMessage(uuid);
+ },
+
+ onGreetingKindsChanged,
+
+ dispose: toDispose.dispose.bind(toDispose),
+ };
+
+ return result;
+ }
+
+ const greeting: Gotd['greeting'] = {
+ createGreeter,
+ GreetingKind
+ };
+
+ return {
+ greeting,
+ Disposable,
+ };
+ };
+}
diff --git a/examples/api-provider-sample/src/plugin/greeting-ext-impl.ts b/examples/api-provider-sample/src/plugin/greeting-ext-impl.ts
new file mode 100644
index 0000000000000..56b5dabb35f03
--- /dev/null
+++ b/examples/api-provider-sample/src/plugin/greeting-ext-impl.ts
@@ -0,0 +1,86 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { GreetingKind, GreeterData, GreetingExt, GreetingMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
+import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
+import { Event, Emitter } from '@theia/core';
+
+type LocalGreeterData = GreeterData & {
+ onGreetingKindsChangedEmitter: Emitter
+};
+
+@injectable()
+export class GreetingExtImpl implements GreetingExt {
+ private readonly proxy: GreetingMain;
+
+ private greeterData: Record = {};
+
+ constructor(@inject(RPCProtocol) rpc: RPCProtocol) {
+ this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.GREETING_MAIN);
+ }
+
+ async registerGreeter(): Promise {
+ const newGreeter = await this.proxy.$createGreeter();
+ this.greeterData[newGreeter.uuid] = {
+ ...newGreeter,
+ onGreetingKindsChangedEmitter: new Emitter()
+ };
+ return newGreeter.uuid;
+ }
+
+ unregisterGreeter(uuid: string): Promise {
+ delete this.greeterData[uuid];
+ return this.proxy.$destroyGreeter(uuid);
+ }
+
+ getGreetingKinds(greeterId: string): readonly GreetingKind[] {
+ const data = this.greeterData[greeterId];
+ return data ? [...data.greetingKinds] : [];
+ }
+
+ setGreetingKindEnabled(greeterId: string, greetingKind: GreetingKind, enable: boolean): void {
+ const data = this.greeterData[greeterId];
+
+ if (data.greetingKinds.includes(greetingKind) === enable) {
+ return; // Nothing to change
+ }
+
+ if (enable) {
+ data.greetingKinds.push(greetingKind);
+ } else {
+ const index = data.greetingKinds.indexOf(greetingKind);
+ data.greetingKinds.splice(index, 1);
+ }
+
+ this.proxy.$updateGreeter({uuid: greeterId, greetingKinds: [...data.greetingKinds] });
+ }
+
+ onGreetingKindsChanged(greeterId: string): Event {
+ return this.greeterData[greeterId].onGreetingKindsChangedEmitter.event;
+ }
+
+ getMessage(greeterId: string): Promise {
+ return this.proxy.$getMessage(greeterId);
+ }
+
+ $greeterUpdated(data: GreeterData): void {
+ const myData = this.greeterData[data.uuid];
+ if (myData) {
+ myData.greetingKinds = [...data.greetingKinds];
+ myData.onGreetingKindsChangedEmitter.fire([...data.greetingKinds]);
+ }
+ }
+}
diff --git a/examples/api-provider-sample/tsconfig.json b/examples/api-provider-sample/tsconfig.json
new file mode 100644
index 0000000000000..a13a4e663a43d
--- /dev/null
+++ b/examples/api-provider-sample/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "extends": "../../configs/base.tsconfig",
+ "compilerOptions": {
+ "composite": true,
+ "rootDir": "src",
+ "outDir": "lib"
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "../../packages/core"
+ },
+ {
+ "path": "../../packages/plugin-ext"
+ },
+ {
+ "path": "../../packages/plugin-ext-headless"
+ }
+ ]
+}
diff --git a/examples/api-samples/package.json b/examples/api-samples/package.json
index 4f6bfc0da7da3..410372c39b76e 100644
--- a/examples/api-samples/package.json
+++ b/examples/api-samples/package.json
@@ -1,21 +1,22 @@
{
"private": true,
"name": "@theia/api-samples",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia - Example code to demonstrate Theia API",
"dependencies": {
- "@theia/core": "1.44.0",
- "@theia/file-search": "1.44.0",
- "@theia/filesystem": "1.44.0",
- "@theia/monaco": "1.44.0",
- "@theia/monaco-editor-core": "1.72.3",
- "@theia/output": "1.44.0",
- "@theia/ovsx-client": "1.44.0",
- "@theia/search-in-workspace": "1.44.0",
- "@theia/test": "1.44.0",
- "@theia/toolbar": "1.44.0",
- "@theia/vsx-registry": "1.44.0",
- "@theia/workspace": "1.44.0"
+ "@theia/ai-chat-ui": "1.54.0",
+ "@theia/core": "1.54.0",
+ "@theia/file-search": "1.54.0",
+ "@theia/filesystem": "1.54.0",
+ "@theia/monaco": "1.54.0",
+ "@theia/monaco-editor-core": "1.83.101",
+ "@theia/output": "1.54.0",
+ "@theia/ovsx-client": "1.54.0",
+ "@theia/search-in-workspace": "1.54.0",
+ "@theia/test": "1.54.0",
+ "@theia/toolbar": "1.54.0",
+ "@theia/vsx-registry": "1.54.0",
+ "@theia/workspace": "1.54.0"
},
"theiaExtensions": [
{
@@ -29,6 +30,9 @@
{
"electronMain": "lib/electron-main/update/sample-updater-main-module",
"frontendElectron": "lib/electron-browser/updater/sample-updater-frontend-module"
+ },
+ {
+ "frontendOnly": "lib/browser-only/api-samples-frontend-only-module"
}
],
"keywords": [
@@ -54,6 +58,6 @@
"clean": "theiaext clean"
},
"devDependencies": {
- "@theia/ext-scripts": "1.44.0"
+ "@theia/ext-scripts": "1.54.0"
}
}
diff --git a/examples/api-samples/src/browser-only/api-samples-frontend-only-module.ts b/examples/api-samples/src/browser-only/api-samples-frontend-only-module.ts
new file mode 100644
index 0000000000000..2fe1703ffa8b1
--- /dev/null
+++ b/examples/api-samples/src/browser-only/api-samples-frontend-only-module.ts
@@ -0,0 +1,27 @@
+// *****************************************************************************
+// Copyright (C) 2023 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
+import { bindBrowserFSInitialization } from './filesystem/example-filesystem-initialization';
+
+export default new ContainerModule((
+ bind: interfaces.Bind,
+ _unbind: interfaces.Unbind,
+ _isBound: interfaces.IsBound,
+ rebind: interfaces.Rebind,
+) => {
+ bindBrowserFSInitialization(bind, rebind);
+});
diff --git a/examples/api-samples/src/browser-only/filesystem/example-filesystem-initialization.ts b/examples/api-samples/src/browser-only/filesystem/example-filesystem-initialization.ts
new file mode 100644
index 0000000000000..f048231f977a5
--- /dev/null
+++ b/examples/api-samples/src/browser-only/filesystem/example-filesystem-initialization.ts
@@ -0,0 +1,51 @@
+// *****************************************************************************
+// Copyright (C) 2023 EclipseSource and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { URI } from '@theia/core';
+import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
+import { EncodingService } from '@theia/core/lib/common/encoding-service';
+import { BrowserFSInitialization, DefaultBrowserFSInitialization } from '@theia/filesystem/lib/browser-only/browserfs-filesystem-initialization';
+import { BrowserFSFileSystemProvider } from '@theia/filesystem/lib/browser-only/browserfs-filesystem-provider';
+import type { FSModule } from 'browserfs/dist/node/core/FS';
+
+@injectable()
+export class ExampleBrowserFSInitialization extends DefaultBrowserFSInitialization {
+
+ @inject(EncodingService)
+ protected encodingService: EncodingService;
+
+ override async initializeFS(fs: FSModule, provider: BrowserFSFileSystemProvider): Promise {
+ try {
+ if (!fs.existsSync('/home/workspace')) {
+ await provider.mkdir(new URI('/home/workspace'));
+ await provider.writeFile(new URI('/home/workspace/my-file.txt'), this.encodingService.encode('foo').buffer, { create: true, overwrite: false });
+ await provider.writeFile(new URI('/home/workspace/my-file2.txt'), this.encodingService.encode('bar').buffer, { create: true, overwrite: false });
+ }
+ if (!fs.existsSync('/home/workspace2')) {
+ await provider.mkdir(new URI('/home/workspace2'));
+ await provider.writeFile(new URI('/home/workspace2/my-file.json'), this.encodingService.encode('{ foo: true }').buffer, { create: true, overwrite: false });
+ await provider.writeFile(new URI('/home/workspace2/my-file2.json'), this.encodingService.encode('{ bar: false }').buffer, { create: true, overwrite: false });
+ }
+ } catch (e) {
+ console.error('An error occurred while initializing the demo workspaces', e);
+ }
+ }
+}
+
+export const bindBrowserFSInitialization = (bind: interfaces.Bind, rebind: interfaces.Rebind): void => {
+ bind(ExampleBrowserFSInitialization).toSelf();
+ rebind(BrowserFSInitialization).toService(ExampleBrowserFSInitialization);
+};
diff --git a/examples/api-samples/src/browser/api-samples-frontend-module.ts b/examples/api-samples/src/browser/api-samples-frontend-module.ts
index eae887e3fa360..fc41efb1bce03 100644
--- a/examples/api-samples/src/browser/api-samples-frontend-module.ts
+++ b/examples/api-samples/src/browser/api-samples-frontend-module.ts
@@ -29,6 +29,8 @@ import { bindMonacoPreferenceExtractor } from './monaco-editor-preferences/monac
import { rebindOVSXClientFactory } from '../common/vsx/sample-ovsx-client-factory';
import { bindSampleAppInfo } from './vsx/sample-frontend-app-info';
import { bindTestSample } from './test/sample-test-contribution';
+import { bindSampleFileSystemCapabilitiesCommands } from './file-system/sample-file-system-capabilities';
+import { bindChatNodeToolbarActionContribution } from './chat/chat-node-toolbar-action-contribution';
export default new ContainerModule((
bind: interfaces.Bind,
@@ -36,6 +38,7 @@ export default new ContainerModule((
isBound: interfaces.IsBound,
rebind: interfaces.Rebind,
) => {
+ bindChatNodeToolbarActionContribution(bind);
bindDynamicLabelProvider(bind);
bindSampleUnclosableView(bind);
bindSampleOutputChannelWithSeverity(bind);
@@ -47,5 +50,6 @@ export default new ContainerModule((
bindMonacoPreferenceExtractor(bind);
bindSampleAppInfo(bind);
bindTestSample(bind);
+ bindSampleFileSystemCapabilitiesCommands(bind);
rebindOVSXClientFactory(rebind);
});
diff --git a/examples/api-samples/src/browser/chat/chat-node-toolbar-action-contribution.ts b/examples/api-samples/src/browser/chat/chat-node-toolbar-action-contribution.ts
new file mode 100644
index 0000000000000..4494f753c81bd
--- /dev/null
+++ b/examples/api-samples/src/browser/chat/chat-node-toolbar-action-contribution.ts
@@ -0,0 +1,41 @@
+// *****************************************************************************
+// Copyright (C) 2024 STMicroelectronics and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import {
+ ChatNodeToolbarActionContribution
+} from '@theia/ai-chat-ui/lib/browser/chat-node-toolbar-action-contribution';
+import {
+ isResponseNode,
+ RequestNode,
+ ResponseNode
+} from '@theia/ai-chat-ui/lib/browser/chat-tree-view';
+import { interfaces } from '@theia/core/shared/inversify';
+
+export function bindChatNodeToolbarActionContribution(bind: interfaces.Bind): void {
+ bind(ChatNodeToolbarActionContribution).toDynamicValue(context => ({
+ getToolbarActions: (args: RequestNode | ResponseNode) => {
+ if (isResponseNode(args)) {
+ return [{
+ commandId: 'sample-command',
+ icon: 'codicon codicon-feedback',
+ tooltip: 'Example command'
+ }];
+ } else {
+ return [];
+ }
+ }
+ }));
+}
diff --git a/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts b/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts
new file mode 100644
index 0000000000000..7d25426de8993
--- /dev/null
+++ b/examples/api-samples/src/browser/file-system/sample-file-system-capabilities.ts
@@ -0,0 +1,68 @@
+/********************************************************************************
+ * Copyright (C) 2024 TypeFox and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { CommandContribution, CommandRegistry } from '@theia/core';
+import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
+import { RemoteFileSystemProvider } from '@theia/filesystem/lib/common/remote-file-system-provider';
+import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files';
+import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
+
+@injectable()
+export class SampleFileSystemCapabilities implements CommandContribution {
+
+ @inject(RemoteFileSystemProvider)
+ protected readonly remoteFileSystemProvider: RemoteFileSystemProvider;
+
+ registerCommands(commands: CommandRegistry): void {
+ commands.registerCommand({
+ id: 'toggleFileSystemReadonly',
+ label: 'Toggle File System Readonly'
+ }, {
+ execute: () => {
+ const readonly = (this.remoteFileSystemProvider.capabilities & FileSystemProviderCapabilities.Readonly) !== 0;
+ if (readonly) {
+ this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities & ~FileSystemProviderCapabilities.Readonly);
+ } else {
+ this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities | FileSystemProviderCapabilities.Readonly);
+ }
+ }
+ });
+
+ commands.registerCommand({
+ id: 'addFileSystemReadonlyMessage',
+ label: 'Add a File System ReadonlyMessage for readonly'
+ }, {
+ execute: () => {
+ const readonlyMessage = new MarkdownStringImpl(`Added new **Markdown** string '+${Date.now()}`);
+ this.remoteFileSystemProvider['setReadOnlyMessage'](readonlyMessage);
+ }
+ });
+
+ commands.registerCommand({
+ id: 'removeFileSystemReadonlyMessage',
+ label: 'Remove File System ReadonlyMessage for readonly'
+ }, {
+ execute: () => {
+ this.remoteFileSystemProvider['setReadOnlyMessage'](undefined);
+ }
+ });
+ }
+
+}
+
+export function bindSampleFileSystemCapabilitiesCommands(bind: interfaces.Bind): void {
+ bind(CommandContribution).to(SampleFileSystemCapabilities).inSingletonScope();
+}
diff --git a/examples/api-samples/src/browser/menu/sample-menu-contribution.ts b/examples/api-samples/src/browser/menu/sample-menu-contribution.ts
index a8a2590623497..e1972603d9068 100644
--- a/examples/api-samples/src/browser/menu/sample-menu-contribution.ts
+++ b/examples/api-samples/src/browser/menu/sample-menu-contribution.ts
@@ -14,12 +14,16 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
-import { ConfirmDialog, QuickInputService } from '@theia/core/lib/browser';
+import { ConfirmDialog, Dialog, QuickInputService } from '@theia/core/lib/browser';
+import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
+import { SelectComponent } from '@theia/core/lib/browser/widgets/select-component';
import {
Command, CommandContribution, CommandRegistry, MAIN_MENU_BAR,
MenuContribution, MenuModelRegistry, MenuNode, MessageService, SubMenuOptions
} from '@theia/core/lib/common';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
+import * as React from '@theia/core/shared/react';
+import { ReactNode } from '@theia/core/shared/react';
const SampleCommand: Command = {
id: 'sample-command',
@@ -50,6 +54,10 @@ const SampleQuickInputCommand: Command = {
category: 'Quick Input',
label: 'Test Positive Integer'
};
+const SampleSelectDialog: Command = {
+ id: 'sample-command-select-dialog',
+ label: 'Sample Select Component Dialog'
+};
@injectable()
export class SampleCommandContribution implements CommandContribution {
@@ -114,6 +122,25 @@ export class SampleCommandContribution implements CommandContribution {
this.messageService.info(`Sample confirm dialog returned with: \`${JSON.stringify(choice)}\``);
}
});
+ commands.registerCommand(SampleSelectDialog, {
+ execute: async () => {
+ await new class extends ReactDialog {
+ constructor() {
+ super({ title: 'Sample Select Component Dialog' });
+ this.appendAcceptButton(Dialog.OK);
+ }
+ protected override render(): ReactNode {
+ return React.createElement(SelectComponent, {
+ options: Array.from(Array(10).keys()).map(i => ({ label: 'Option ' + ++i })),
+ defaultValue: 0
+ });
+ }
+ override get value(): boolean {
+ return true;
+ }
+ }().open();
+ }
+ });
commands.registerCommand(SampleQuickInputCommand, {
execute: async () => {
const result = await this.quickInputService.input({
diff --git a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts
index d9f7e657660a7..d8e3e75183e2a 100644
--- a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts
+++ b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts
@@ -29,12 +29,13 @@ class SampleElectronMainMenuFactory extends ElectronMainMenuFactory {
protected override fillMenuTemplate(parentItems: MenuDto[],
menu: MenuNode,
args: unknown[] = [],
- options: ElectronMenuOptions
+ options: ElectronMenuOptions,
+ skipRoot: boolean
): MenuDto[] {
if (menu instanceof PlaceholderMenuNode) {
parentItems.push({ label: menu.label, enabled: false, visible: true });
} else {
- super.fillMenuTemplate(parentItems, menu, args, options);
+ super.fillMenuTemplate(parentItems, menu, args, options, skipRoot);
}
return parentItems;
}
diff --git a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts
index bb4c38e0d1903..844bfd3feff88 100644
--- a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts
+++ b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-module.ts
@@ -15,7 +15,7 @@
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
-import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-provider';
+import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-source';
import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
import { SampleUpdater, SampleUpdaterPath, SampleUpdaterClient } from '../../common/updater/sample-updater';
import { SampleUpdaterFrontendContribution, ElectronMenuUpdater, SampleUpdaterClientImpl } from './sample-updater-frontend-contribution';
diff --git a/examples/api-samples/src/electron-main/update/sample-updater-main-module.ts b/examples/api-samples/src/electron-main/update/sample-updater-main-module.ts
index b92e76576bf37..6eac1611ef2fb 100644
--- a/examples/api-samples/src/electron-main/update/sample-updater-main-module.ts
+++ b/examples/api-samples/src/electron-main/update/sample-updater-main-module.ts
@@ -17,15 +17,15 @@
import { ContainerModule } from '@theia/core/shared/inversify';
import { RpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
-import { ElectronConnectionHandler } from '@theia/core/lib/electron-common/messaging/electron-connection-handler';
import { SampleUpdaterPath, SampleUpdater, SampleUpdaterClient } from '../../common/updater/sample-updater';
import { SampleUpdaterImpl } from './sample-updater-impl';
+import { ConnectionHandler } from '@theia/core';
export default new ContainerModule(bind => {
bind(SampleUpdaterImpl).toSelf().inSingletonScope();
bind(SampleUpdater).toService(SampleUpdaterImpl);
bind(ElectronMainApplicationContribution).toService(SampleUpdater);
- bind(ElectronConnectionHandler).toDynamicValue(context =>
+ bind(ConnectionHandler).toDynamicValue(context =>
new RpcConnectionHandler(SampleUpdaterPath, client => {
const server = context.container.get(SampleUpdater);
server.setClient(client);
diff --git a/examples/api-samples/src/node/sample-mock-open-vsx-server.ts b/examples/api-samples/src/node/sample-mock-open-vsx-server.ts
index 4a60f8cbf47eb..4cb4af8616a4c 100644
--- a/examples/api-samples/src/node/sample-mock-open-vsx-server.ts
+++ b/examples/api-samples/src/node/sample-mock-open-vsx-server.ts
@@ -69,7 +69,7 @@ export class SampleMockOpenVsxServer implements BackendApplicationContribution {
app.use(
this.mockServerPath + '/api',
express.Router()
- .get('/-/query', async (req, res) => {
+ .get('/v2/-/query', async (req, res) => {
await this.ready;
res.json(await this.mockClient.query(this.sanitizeQuery(req.query)));
})
@@ -170,6 +170,8 @@ export class SampleMockOpenVsxServer implements BackendApplicationContribution {
reviewsUrl: url.extensionReviewsUrl(namespace, name),
timestamp: new Date().toISOString(),
version,
+ namespaceDisplayName: name,
+ preRelease: false
}
});
}));
diff --git a/examples/api-samples/tsconfig.json b/examples/api-samples/tsconfig.json
index 8a1dfa8803322..551c17de9f91b 100644
--- a/examples/api-samples/tsconfig.json
+++ b/examples/api-samples/tsconfig.json
@@ -12,6 +12,9 @@
{
"path": "../../dev-packages/ovsx-client"
},
+ {
+ "path": "../../packages/ai-chat-ui"
+ },
{
"path": "../../packages/core"
},
diff --git a/examples/api-tests/package.json b/examples/api-tests/package.json
index 4f2ca4d13bd70..ffad1dc080294 100644
--- a/examples/api-tests/package.json
+++ b/examples/api-tests/package.json
@@ -1,9 +1,9 @@
{
"name": "@theia/api-tests",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "Theia API tests",
"dependencies": {
- "@theia/core": "1.44.0"
+ "@theia/core": "1.54.0"
},
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
diff --git a/examples/api-tests/src/explorer-open-close.spec.js b/examples/api-tests/src/explorer-open-close.spec.js
index d7c9d1f77bf6c..b7195e77f49f9 100644
--- a/examples/api-tests/src/explorer-open-close.spec.js
+++ b/examples/api-tests/src/explorer-open-close.spec.js
@@ -123,7 +123,6 @@ describe('Explorer and Editor - open and close', function () {
async function openEditor() {
await editorManager.open(fileUri, { mode: 'activate' });
- await waitLanguageServerReady();
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isDefined(activeEditor);
assert.equal(activeEditor.uri.resolveToAbsolute().toString(), fileUri.resolveToAbsolute().toString());
@@ -135,21 +134,4 @@ describe('Explorer and Editor - open and close', function () {
assert.isUndefined(activeEditor);
}
- async function waitLanguageServerReady() {
- // quite a bit of jitter in the "Initializing LS" status bar entry,
- // so we want to read a few times in a row that it's done (undefined)
- const MAX_N = 5
- let n = MAX_N;
- while (n > 0) {
- await pause(1);
- if (progressStatusBarItem.currentProgress) {
- n = MAX_N;
- } else {
- n--;
- }
- if (n < MAX_N) {
- console.debug('n = ' + n);
- }
- }
- }
});
diff --git a/examples/api-tests/src/find-replace.spec.js b/examples/api-tests/src/find-replace.spec.js
index 943f68cc7efbb..af37bfb56e0a6 100644
--- a/examples/api-tests/src/find-replace.spec.js
+++ b/examples/api-tests/src/find-replace.spec.js
@@ -143,28 +143,9 @@ describe('Find and Replace', function () {
async function openEditor() {
await editorManager.open(fileUri, { mode: 'activate' });
- await waitLanguageServerReady();
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isDefined(activeEditor);
// @ts-ignore
assert.equal(activeEditor.uri.resolveToAbsolute().toString(), fileUri.resolveToAbsolute().toString());
}
-
- async function waitLanguageServerReady() {
- // quite a bit of jitter in the "Initializing LS" status bar entry,
- // so we want to read a few times in a row that it's done (undefined)
- const MAX_N = 5
- let n = MAX_N;
- while (n > 0) {
- await pause(1);
- if (progressStatusBarItem.currentProgress) {
- n = MAX_N;
- } else {
- n--;
- }
- if (n < 5) {
- console.debug('n = ' + n);
- }
- }
- }
});
diff --git a/examples/api-tests/src/menus.spec.js b/examples/api-tests/src/menus.spec.js
index 542c533bc6ff9..905e60422dfaa 100644
--- a/examples/api-tests/src/menus.spec.js
+++ b/examples/api-tests/src/menus.spec.js
@@ -44,8 +44,8 @@ describe('Menus', function () {
const container = window.theia.container;
const shell = container.get(ApplicationShell);
+ /** @type {BrowserMenuBarContribution} */
const menuBarContribution = container.get(BrowserMenuBarContribution);
- const menuBar = /** @type {import('@theia/core/lib/browser/menu/browser-menu-plugin').MenuBarWidget} */ (menuBarContribution.menuBar);
const pluginService = container.get(HostedPluginSupport);
const menus = container.get(MenuModelRegistry);
const commands = container.get(CommandRegistry);
@@ -54,6 +54,9 @@ describe('Menus', function () {
before(async function () {
await pluginService.didStart;
await pluginService.activateByViewContainer('explorer');
+ // Updating the menu interferes with our ability to programmatically test it
+ // We simply disable the menu updating
+ menus.isReady = false;
});
const toTearDown = new DisposableCollection();
@@ -73,7 +76,7 @@ describe('Menus', function () {
]) {
it(`should toggle '${contribution.viewLabel}' view`, async () => {
await contribution.closeView();
- await menuBar.triggerMenuItem('View', contribution.viewLabel);
+ await menuBarContribution.menuBar.triggerMenuItem('View', contribution.viewLabel);
await shell.waitForActivation(contribution.viewId);
});
}
diff --git a/examples/api-tests/src/monaco-api.spec.js b/examples/api-tests/src/monaco-api.spec.js
index e5dc9cd238836..438d10ea53bf5 100644
--- a/examples/api-tests/src/monaco-api.spec.js
+++ b/examples/api-tests/src/monaco-api.spec.js
@@ -26,7 +26,7 @@ describe('Monaco API', async function () {
const { MonacoResolvedKeybinding } = require('@theia/monaco/lib/browser/monaco-resolved-keybinding');
const { MonacoTextmateService } = require('@theia/monaco/lib/browser/textmate/monaco-textmate-service');
const { CommandRegistry } = require('@theia/core/lib/common/command');
- const { SimpleKeybinding } = require('@theia/monaco-editor-core/esm/vs/base/common/keybindings');
+ const { KeyCodeChord, ResolvedChord } = require('@theia/monaco-editor-core/esm/vs/base/common/keybindings');
const { IKeybindingService } = require('@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybinding');
const { StandaloneServices } = require('@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices');
const { TokenizationRegistry } = require('@theia/monaco-editor-core/esm/vs/editor/common/languages');
@@ -59,10 +59,10 @@ describe('Monaco API', async function () {
});
it('KeybindingService.resolveKeybinding', () => {
- const simpleKeybinding = new SimpleKeybinding(true, true, true, true, 41 /* KeyCode.KeyK */);
- const chordKeybinding = simpleKeybinding.toChord();
- assert.equal(chordKeybinding.parts.length, 1);
- assert.equal(chordKeybinding.parts[0], simpleKeybinding);
+ const chord = new KeyCodeChord(true, true, true, true, 41 /* KeyCode.KeyK */);
+ const chordKeybinding = chord.toKeybinding();
+ assert.equal(chordKeybinding.chords.length, 1);
+ assert.equal(chordKeybinding.chords[0], chord);
const resolvedKeybindings = StandaloneServices.get(IKeybindingService).resolveKeybinding(chordKeybinding);
assert.equal(resolvedKeybindings.length, 1);
@@ -74,9 +74,8 @@ describe('Monaco API', async function () {
const electronAccelerator = resolvedKeybinding.getElectronAccelerator();
const userSettingsLabel = resolvedKeybinding.getUserSettingsLabel();
const WYSIWYG = resolvedKeybinding.isWYSIWYG();
- const chord = resolvedKeybinding.isChord();
- const parts = resolvedKeybinding.getParts();
- const dispatchParts = resolvedKeybinding.getDispatchParts();
+ const parts = resolvedKeybinding.getChords();
+ const dispatchParts = resolvedKeybinding.getDispatchChords().map(str => str === null ? '' : str);
const platform = window.navigator.platform;
let expected;
@@ -88,15 +87,14 @@ describe('Monaco API', async function () {
electronAccelerator: 'Ctrl+Shift+Alt+Cmd+K',
userSettingsLabel: 'ctrl+shift+alt+cmd+K',
WYSIWYG: true,
- chord: false,
- parts: [{
- altKey: true,
- ctrlKey: true,
- keyAriaLabel: 'K',
- keyLabel: 'K',
- metaKey: true,
- shiftKey: true
- }],
+ parts: [new ResolvedChord(
+ true,
+ true,
+ true,
+ true,
+ 'K',
+ 'K',
+ )],
dispatchParts: [
'ctrl+shift+alt+meta+K'
]
@@ -108,15 +106,14 @@ describe('Monaco API', async function () {
electronAccelerator: 'Ctrl+Shift+Alt+K',
userSettingsLabel: 'ctrl+shift+alt+K',
WYSIWYG: true,
- chord: false,
- parts: [{
- altKey: true,
- ctrlKey: true,
- keyAriaLabel: 'K',
- keyLabel: 'K',
- metaKey: false,
- shiftKey: true
- }],
+ parts: [new ResolvedChord(
+ true,
+ true,
+ true,
+ false,
+ 'K',
+ 'K'
+ )],
dispatchParts: [
'ctrl+shift+alt+K'
]
@@ -124,7 +121,7 @@ describe('Monaco API', async function () {
}
assert.deepStrictEqual({
- label, ariaLabel, electronAccelerator, userSettingsLabel, WYSIWYG, chord, parts, dispatchParts
+ label, ariaLabel, electronAccelerator, userSettingsLabel, WYSIWYG, parts, dispatchParts
}, expected);
} else {
assert.fail(`resolvedKeybinding must be of ${MonacoResolvedKeybinding.name} type`);
@@ -136,7 +133,7 @@ describe('Monaco API', async function () {
const didChangeColorMap = new Promise(resolve => {
const toDispose = TokenizationRegistry.onDidChange(() => {
toDispose.dispose();
- resolve();
+ resolve(undefined);
});
});
textmateService['themeService'].setCurrentTheme('light');
@@ -175,15 +172,15 @@ describe('Monaco API', async function () {
});
it('Supports setting contexts using the command registry', async () => {
- const setContext = 'setContext';
+ const setContext = '_setContext';
const key = 'monaco-api-test-context';
const firstValue = 'first setting';
const secondValue = 'second setting';
- assert.isFalse(contextKeys.match(`${key} == ${firstValue}`));
+ assert.isFalse(contextKeys.match(`${key} == '${firstValue}'`));
await commands.executeCommand(setContext, key, firstValue);
- assert.isTrue(contextKeys.match(`${key} == ${firstValue}`));
+ assert.isTrue(contextKeys.match(`${key} == '${firstValue}'`));
await commands.executeCommand(setContext, key, secondValue);
- assert.isTrue(contextKeys.match(`${key} == ${secondValue}`));
+ assert.isTrue(contextKeys.match(`${key} == '${secondValue}'`));
});
it('Supports context key: inQuickOpen', async () => {
diff --git a/examples/api-tests/src/saveable.spec.js b/examples/api-tests/src/saveable.spec.js
index e4cec574174a6..091ae06f14ee0 100644
--- a/examples/api-tests/src/saveable.spec.js
+++ b/examples/api-tests/src/saveable.spec.js
@@ -16,7 +16,7 @@
// @ts-check
describe('Saveable', function () {
- this.timeout(5000);
+ this.timeout(30000);
const { assert } = chai;
@@ -81,13 +81,13 @@ describe('Saveable', function () {
afterEach(async () => {
toTearDown.dispose();
- await preferences.set('files.autoSave', autoSave, undefined, rootUri.toString());
// @ts-ignore
editor = undefined;
// @ts-ignore
widget = undefined;
await editorManager.closeAll({ save: false });
await fileService.delete(fileUri.parent, { fromUserGesture: false, useTrash: false, recursive: true });
+ await preferences.set('files.autoSave', autoSave, undefined, rootUri.toString());
});
it('normal save', async function () {
diff --git a/examples/api-tests/src/scm.spec.js b/examples/api-tests/src/scm.spec.js
index ef759e7187ab8..d1289689aa2b2 100644
--- a/examples/api-tests/src/scm.spec.js
+++ b/examples/api-tests/src/scm.spec.js
@@ -19,12 +19,17 @@ describe('SCM', function () {
const { assert } = chai;
+ const { animationFrame } = require('@theia/core/lib/browser/browser');
+ const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
const Uri = require('@theia/core/lib/common/uri');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
const { ScmService } = require('@theia/scm/lib/browser/scm-service');
const { ScmWidget } = require('@theia/scm/lib/browser/scm-widget');
+ const { CommandRegistry } = require('@theia/core/lib/common');
+ const { PreferenceService } = require('@theia/core/lib/browser');
+
/** @type {import('inversify').Container} */
const container = window['theia'].container;
@@ -32,6 +37,9 @@ describe('SCM', function () {
const scmContribution = container.get(ScmContribution);
const shell = container.get(ApplicationShell);
const service = container.get(ScmService);
+ const commandRegistry = container.get(CommandRegistry);
+ const pluginService = container.get(HostedPluginSupport);
+ const preferences = container.get(PreferenceService);
/** @type {ScmWidget} */
let scmWidget;
@@ -39,10 +47,55 @@ describe('SCM', function () {
/** @type {ScmService} */
let scmService;
+ const gitPluginId = 'vscode.git';
+
+ /**
+ * @param {() => unknown} condition
+ * @param {number | undefined} [timeout]
+ * @param {string | undefined} [message]
+ * @returns {Promise}
+ */
+ function waitForAnimation(condition, timeout, message) {
+ const success = new Promise(async (resolve, reject) => {
+ if (timeout === undefined) {
+ timeout = 100000;
+ }
+
+ let timedOut = false;
+ const handle = setTimeout(() => {
+ console.log(message);
+ timedOut = true;
+ }, timeout);
+
+ do {
+ await animationFrame();
+ } while (!timedOut && !condition());
+ if (timedOut) {
+ reject(new Error(message ?? 'Wait for animation timed out.'));
+ } else {
+ clearTimeout(handle);
+ resolve(undefined);
+ }
+
+ });
+ return success;
+ }
+
+
+ before(async () => {
+ preferences.set('git.autoRepositoryDetection', true);
+ preferences.set('git.openRepositoryInParentFolders', 'always');
+ });
+
beforeEach(async () => {
+ if (!pluginService.getPlugin(gitPluginId)) {
+ throw new Error(gitPluginId + ' should be started');
+ }
+ await pluginService.activatePlugin(gitPluginId);
await shell.leftPanelHandler.collapse();
scmWidget = await scmContribution.openView({ activate: true, reveal: true });
scmService = service;
+ await waitForAnimation(() => scmService.selectedRepository, 10000, 'selected repository is not defined');
});
afterEach(() => {
@@ -53,7 +106,6 @@ describe('SCM', function () {
});
describe('scm-view', () => {
-
it('the view should open and activate successfully', () => {
assert.notEqual(scmWidget, undefined);
assert.strictEqual(scmWidget, shell.activeWidget);
@@ -125,6 +177,9 @@ describe('SCM', function () {
const foundRepository = scmService.findRepository(new Uri.default(rootUri));
assert.notEqual(foundRepository, undefined);
}
+ else {
+ assert.fail('Selected repository is undefined');
+ }
});
it('should not find a repository for an unknown uri', () => {
@@ -150,6 +205,9 @@ describe('SCM', function () {
assert.notEqual(commit, undefined);
}
}
+ else {
+ assert.fail('Selected repository is undefined');
+ }
});
});
diff --git a/examples/api-tests/src/typescript.spec.js b/examples/api-tests/src/typescript.spec.js
index c4fe77e9a098f..5ee12992dd52a 100644
--- a/examples/api-tests/src/typescript.spec.js
+++ b/examples/api-tests/src/typescript.spec.js
@@ -16,9 +16,10 @@
// @ts-check
describe('TypeScript', function () {
- this.timeout(30_000);
+ this.timeout(200_000);
const { assert } = chai;
+ const { timeout } = require('@theia/core/lib/common/promise-util');
const Uri = require('@theia/core/lib/common/uri');
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
@@ -63,7 +64,7 @@ describe('TypeScript', function () {
const rootUri = workspaceService.tryGetRoots()[0].resource;
const demoFileUri = rootUri.resolveToAbsolute('../api-tests/test-ts-workspace/demo-file.ts');
const definitionFileUri = rootUri.resolveToAbsolute('../api-tests/test-ts-workspace/demo-definitions-file.ts');
- let originalAutoSaveValue = preferences.inspect('files.autoSave').globalValue;
+ let originalAutoSaveValue = preferences.get('files.autoSave');
before(async function () {
await pluginService.didStart;
@@ -72,8 +73,9 @@ describe('TypeScript', function () {
throw new Error(pluginId + ' should be started');
}
await pluginService.activatePlugin(pluginId);
- }).concat(preferences.set('files.autoSave', 'off', PreferenceScope.User)));
- await preferences.set('files.refactoring.autoSave', 'off', PreferenceScope.User);
+ }));
+ await preferences.set('files.autoSave', 'off');
+ await preferences.set('files.refactoring.autoSave', 'off');
});
beforeEach(async function () {
@@ -89,9 +91,27 @@ describe('TypeScript', function () {
});
after(async () => {
- await preferences.set('files.autoSave', originalAutoSaveValue, PreferenceScope.User);
+ await preferences.set('files.autoSave', originalAutoSaveValue);
})
+ async function waitLanguageServerReady() {
+ // quite a bit of jitter in the "Initializing LS" status bar entry,
+ // so we want to read a few times in a row that it's done (undefined)
+ const MAX_N = 5
+ let n = MAX_N;
+ while (n > 0) {
+ await timeout(1000);
+ if (progressStatusBarItem.currentProgress) {
+ n = MAX_N;
+ } else {
+ n--;
+ }
+ if (n < 5) {
+ console.debug('n = ' + n);
+ }
+ }
+ }
+
/**
* @param {Uri.default} uri
* @param {boolean} preview
@@ -104,9 +124,8 @@ describe('TypeScript', function () {
// wait till tsserver is running, see:
// https://github.com/microsoft/vscode/blob/93cbbc5cae50e9f5f5046343c751b6d010468200/extensions/typescript-language-features/src/extension.ts#L98-L103
await waitForAnimation(() => contextKeyService.match('typescript.isManagedFile'));
- // wait till projects are loaded, see:
- // https://github.com/microsoft/vscode/blob/4aac84268c6226d23828cc6a1fe45ee3982927f0/extensions/typescript-language-features/src/typescriptServiceClient.ts#L911
- await waitForAnimation(() => !progressStatusBarItem.currentProgress);
+
+ waitLanguageServerReady();
return /** @type {MonacoEditor} */ (editor);
}
@@ -118,19 +137,28 @@ describe('TypeScript', function () {
*/
function waitForAnimation(condition, timeout, message) {
const success = new Promise(async (resolve, reject) => {
+ if (timeout === undefined) {
+ timeout = 100000;
+ }
+
+ let timedOut = false;
+ const handle = setTimeout(() => {
+ console.log(message);
+ timedOut = true;
+ }, timeout);
+
toTearDown.push({ dispose: () => reject(message ?? 'Test terminated before resolution.') });
do {
await animationFrame();
- } while (!condition());
- resolve();
+ } while (!timedOut && !condition());
+ if (timedOut) {
+ reject(new Error(message ?? 'Wait for animation timed out.'));
+ } else {
+ clearTimeout(handle);
+ resolve(undefined);
+ }
+
});
- if (timeout !== undefined) {
- const timedOut = new Promise((_, fail) => {
- const toClear = setTimeout(() => fail(new Error(message ?? 'Wait for animation timed out.')), timeout);
- toTearDown.push({ dispose: () => (fail(new Error(message ?? 'Wait for animation timed out.')), clearTimeout(toClear)) });
- });
- return Promise.race([success, timedOut]);
- }
return success;
}
@@ -200,16 +228,8 @@ describe('TypeScript', function () {
await assertPeekOpened(editor);
console.log('closePeek() - Attempt to close by sending "Escape"');
- keybindings.dispatchKeyDown('Escape');
- await waitForAnimation(() => {
- const isClosed = !contextKeyService.match('listFocus');
- if (!isClosed) {
- console.log('...');
- keybindings.dispatchKeyDown('Escape');
- return false;
- }
- return true;
- });
+ await dismissWithEscape('listFocus');
+
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('referenceSearchVisible'));
assert.isFalse(contextKeyService.match('listFocus'));
@@ -386,6 +406,14 @@ describe('TypeScript', function () {
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));
+
+ const suggestController = editor.getControl().getContribution('editor.contrib.suggestController');
+
+ waitForAnimation(() => {
+ const content = nodeAsString(suggestController ? ['_widget']?.['_value']?.['element']?.['domNode']);
+ return !content.includes('loading');
+ });
+
// May need a couple extra "Enter" being sent for the suggest to be accepted
keybindings.dispatchKeyDown('Enter');
await waitForAnimation(() => {
@@ -396,7 +424,7 @@ describe('TypeScript', function () {
return false;
}
return true;
- }, 5000, 'Suggest widget has not been dismissed despite attempts to accept suggestion');
+ }, 20000, 'Suggest widget has not been dismissed despite attempts to accept suggestion');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
@@ -500,7 +528,23 @@ describe('TypeScript', function () {
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber: 28, column: 1 }).word, 'foo');
});
+ async function dismissWithEscape(contextKey) {
+ keybindings.dispatchKeyDown('Escape');
+ // once in a while, a second "Escape" is needed to dismiss widget
+ return waitForAnimation(() => {
+ const suggestWidgetDismissed = !contextKeyService.match(contextKey);
+ if (!suggestWidgetDismissed) {
+ console.log(`Re-try to dismiss ${contextKey} using "Escape" key`);
+ keybindings.dispatchKeyDown('Escape');
+ return false;
+ }
+ return true;
+ }, 5000, `${contextKey} widget not dismissed`);
+ }
+
it('editor.action.triggerParameterHints', async function () {
+ this.timeout(30000);
+ console.log('start trigger parameter hint');
const editor = await openEditor(demoFileUri);
// const demoInstance = new DemoClass('|demo');
editor.getControl().setPosition({ lineNumber: 24, column: 37 });
@@ -510,19 +554,21 @@ describe('TypeScript', function () {
assert.isFalse(contextKeyService.match('parameterHintsVisible'));
await commands.executeCommand('editor.action.triggerParameterHints');
+ console.log('trigger command');
await waitForAnimation(() => contextKeyService.match('parameterHintsVisible'));
+ console.log('context key matched');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isTrue(contextKeyService.match('parameterHintsVisible'));
- keybindings.dispatchKeyDown('Escape');
- await waitForAnimation(() => !contextKeyService.match('parameterHintsVisible'));
+ await dismissWithEscape('parameterHintsVisible');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('parameterHintsVisible'));
});
it('editor.action.showHover', async function () {
+
const editor = await openEditor(demoFileUri);
// class |DemoClass);
editor.getControl().setPosition({ lineNumber: 8, column: 7 });
@@ -532,12 +578,18 @@ describe('TypeScript', function () {
const hover = editor.getControl().getContribution('editor.contrib.hover');
assert.isTrue(contextKeyService.match('editorTextFocus'));
- assert.isFalse(Boolean(hover['_contentWidget']?.['_widget']?.['_visibleData']));
+ assert.isFalse(contextKeyService.match('editorHoverVisible'));
await commands.executeCommand('editor.action.showHover');
let doLog = true;
- await waitForAnimation(() => hover['_contentWidget']?.['_widget']?.['_visibleData']);
+ await waitForAnimation(() => contextKeyService.match('editorHoverVisible'));
+ assert.isTrue(contextKeyService.match('editorHoverVisible'));
assert.isTrue(contextKeyService.match('editorTextFocus'));
- assert.isTrue(Boolean(hover['_contentWidget']?.['_widget']?.['_visibleData']));
+
+ waitForAnimation(() => {
+ const content = nodeAsString(hover['_contentWidget']?.['_widget']?.['_hover']?.['contentsDomNode']);
+ return !content.includes('loading');
+ });
+
assert.deepEqual(nodeAsString(hover['_contentWidget']?.['_widget']?.['_hover']?.['contentsDomNode']).trim(), `
DIV {
DIV {
@@ -562,8 +614,7 @@ DIV {
}
}
}`.trim());
- keybindings.dispatchKeyDown('Escape');
- await waitForAnimation(() => !hover['_contentWidget']?.['_widget']?.['_visibleData']);
+ await dismissWithEscape('editorHoverVisible');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(Boolean(hover['_contentWidget']?.['_widget']?.['_visibleData']));
});
@@ -688,11 +739,10 @@ SPAN {
editor.getControl().revealPosition({ lineNumber, column });
assert.equal(currentChar(), ';', 'Failed at assert 1');
- /** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codeAction/browser/codeActionCommands').CodeActionController} */
+ /** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codeAction/browser/codeActionController').CodeActionController} */
const codeActionController = editor.getControl().getContribution('editor.contrib.codeActionController');
const lightBulbNode = () => {
- const ui = codeActionController['_ui'].rawValue;
- const lightBulb = ui && ui['_lightBulbWidget'].rawValue;
+ const lightBulb = codeActionController['_lightBulbWidget'].rawValue;
return lightBulb && lightBulb['_domNode'];
};
const lightBulbVisible = () => {
@@ -700,18 +750,16 @@ SPAN {
return !!node && node.style.visibility !== 'hidden';
};
- assert.isFalse(lightBulbVisible(), 'Failed at assert 2');
- await waitForAnimation(() => lightBulbVisible());
-
+ await timeout(1000); // quick fix is always available: need to wait for the error fix to become available.
await commands.executeCommand('editor.action.quickFix');
- const codeActionSelector = '.codeActionWidget';
+ const codeActionSelector = '.action-widget';
assert.isFalse(!!document.querySelector(codeActionSelector), 'Failed at assert 3 - codeActionWidget should not be visible');
console.log('Waiting for Quick Fix widget to be visible');
await waitForAnimation(() => {
const quickFixWidgetVisible = !!document.querySelector(codeActionSelector);
if (!quickFixWidgetVisible) {
- console.log('...');
+ // console.log('...');
return false;
}
return true;
@@ -721,20 +769,9 @@ SPAN {
assert.isTrue(lightBulbVisible(), 'Failed at assert 4');
keybindings.dispatchKeyDown('Enter');
console.log('Waiting for confirmation that QuickFix has taken effect');
- await waitForAnimation(() => {
- const quickFixHasTakenEffect = !lightBulbVisible();
- if (!quickFixHasTakenEffect) {
- console.log('...');
- return false;
- }
- return true;
- }, 5000, 'Quickfix widget has not been dismissed despite attempts to accept suggestion');
- await waitForAnimation(() => currentChar() === 'd', 5000, 'Failed to detect expected selected char: "d"');
+ await waitForAnimation(() => currentChar() === 'd', 10000, 'Failed to detect expected selected char: "d"');
assert.equal(currentChar(), 'd', 'Failed at assert 5');
-
- await waitForAnimation(() => !lightBulbVisible());
- assert.isFalse(lightBulbVisible(), 'Failed at assert 6');
});
it('editor.action.formatDocument', async function () {
@@ -780,27 +817,12 @@ SPAN {
assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLength);
});
- for (const referenceViewCommand of ['references-view.find', 'references-view.findImplementations']) {
- it(referenceViewCommand, async function () {
- let steps = 0;
- const editor = await openEditor(demoFileUri);
- editor.getControl().setPosition({ lineNumber: 24, column: 11 });
- assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoInstance');
- await commands.executeCommand(referenceViewCommand);
- const view = await pluginViewRegistry.openView('references-view.tree', { reveal: true });
- const expectedMessage = referenceViewCommand === 'references-view.find' ? '2 results in 1 file' : '1 result in 1 file';
- const getResultText = () => view.node.getElementsByClassName('theia-TreeViewInfo').item(0)?.textContent;
- await waitForAnimation(() => getResultText() === expectedMessage, 5000);
- assert.equal(getResultText(), expectedMessage);
- });
- }
-
it('Can execute code actions', async function () {
const editor = await openEditor(demoFileUri);
- /** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codeAction/browser/codeActionCommands').CodeActionController} */
+ /** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codeAction/browser/codeActionController').CodeActionController} */
const codeActionController = editor.getControl().getContribution('editor.contrib.codeActionController');
const isActionAvailable = () => {
- const lightbulbVisibility = codeActionController['_ui'].rawValue?.['_lightBulbWidget'].rawValue?.['_domNode'].style.visibility;
+ const lightbulbVisibility = codeActionController['_lightBulbWidget'].rawValue?.['_domNode'].style.visibility;
return lightbulbVisibility !== undefined && lightbulbVisibility !== 'hidden';
}
assert.isFalse(isActionAvailable());
@@ -810,8 +832,16 @@ SPAN {
await waitForAnimation(() => isActionAvailable(), 5000, 'No code action available. (1)');
assert.isTrue(isActionAvailable());
+ try { // for some reason, we need to wait a second here, otherwise, we run into some cancellation.
+ await waitForAnimation(() => false, 1000);
+ } catch (e) {
+ }
+
await commands.executeCommand('editor.action.quickFix');
- await waitForAnimation(() => Boolean(document.querySelector('.context-view-pointerBlock')), 5000, 'No context menu appeared. (1)');
+ await waitForAnimation(() => {
+ const elements = document.querySelector('.action-widget');
+ return !!elements;
+ }, 5000, 'No context menu appeared. (1)');
await animationFrame();
keybindings.dispatchKeyDown('Enter');
@@ -853,4 +883,19 @@ SPAN {
assert.isNotNull(editor.getControl().getModel());
await waitForAnimation(() => editor.getControl().getModel().getLineContent(30) === 'import { DefinedInterface } from "./demo-definitions-file";', 5000, 'The named import did not take effect.');
});
+
+ for (const referenceViewCommand of ['references-view.find', 'references-view.findImplementations']) {
+ it(referenceViewCommand, async function () {
+ let steps = 0;
+ const editor = await openEditor(demoFileUri);
+ editor.getControl().setPosition({ lineNumber: 24, column: 11 });
+ assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoInstance');
+ await commands.executeCommand(referenceViewCommand);
+ const view = await pluginViewRegistry.openView('references-view.tree', { reveal: true });
+ const expectedMessage = referenceViewCommand === 'references-view.find' ? '2 results in 1 file' : '1 result in 1 file';
+ const getResultText = () => view.node.getElementsByClassName('theia-TreeViewInfo').item(0)?.textContent;
+ await waitForAnimation(() => getResultText() === expectedMessage, 5000);
+ assert.equal(getResultText(), expectedMessage);
+ });
+ }
});
diff --git a/examples/api-tests/src/views.spec.js b/examples/api-tests/src/views.spec.js
index cdec58bc2348f..61e57595308cd 100644
--- a/examples/api-tests/src/views.spec.js
+++ b/examples/api-tests/src/views.spec.js
@@ -14,6 +14,8 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
+const { timeout } = require('@theia/core/lib/common/promise-util');
+
// @ts-check
describe('Views', function () {
this.timeout(7500);
@@ -52,24 +54,26 @@ describe('Views', function () {
if (view) {
assert.notEqual(shell.getAreaFor(view), contribution.defaultViewOptions.area);
assert.isFalse(view.isVisible);
- assert.notEqual(view, shell.activeWidget);
+ assert.isTrue(view !== shell.activeWidget, `${contribution.viewLabel} !== shell.activeWidget`);
}
view = await contribution.toggleView();
- assert.notEqual(view, undefined);
+ // we can't use "equals" here because Mocha chokes on the diff for certain widgets
+ assert.isTrue(view !== undefined, `${contribution.viewLabel} !== undefined`);
assert.equal(shell.getAreaFor(view), contribution.defaultViewOptions.area);
assert.isDefined(shell.getTabBarFor(view));
// @ts-ignore
assert.equal(shell.getAreaFor(shell.getTabBarFor(view)), contribution.defaultViewOptions.area);
assert.isTrue(view.isVisible);
- assert.equal(view, shell.activeWidget);
+ assert.isTrue(view === shell.activeWidget, `${contribution.viewLabel} === shell.activeWidget`);
view = await contribution.toggleView();
+ await timeout(0); // seems that the "await" is not enought to guarantee that the panel is hidden
assert.notEqual(view, undefined);
assert.equal(shell.getAreaFor(view), contribution.defaultViewOptions.area);
assert.isDefined(shell.getTabBarFor(view));
assert.isFalse(view.isVisible);
- assert.notEqual(view, shell.activeWidget);
+ assert.isTrue(view !== shell.activeWidget, `${contribution.viewLabel} !== shell.activeWidget`);
});
}
diff --git a/examples/browser-only/package.json b/examples/browser-only/package.json
new file mode 100644
index 0000000000000..dc2bbce1b8f8c
--- /dev/null
+++ b/examples/browser-only/package.json
@@ -0,0 +1,86 @@
+{
+ "private": true,
+ "name": "@theia/example-browser-only",
+ "version": "1.54.0",
+ "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
+ "theia": {
+ "target": "browser-only",
+ "frontend": {
+ "config": {
+ "applicationName": "Theia Browser-Only Example",
+ "preferences": {
+ "files.enableTrash": false
+ }
+ }
+ }
+ },
+ "dependencies": {
+ "@theia/ai-chat": "1.54.0",
+ "@theia/ai-chat-ui": "1.54.0",
+ "@theia/ai-code-completion": "1.54.0",
+ "@theia/ai-core": "1.54.0",
+ "@theia/ai-history": "1.54.0",
+ "@theia/ai-ollama": "1.54.0",
+ "@theia/ai-openai": "1.54.0",
+ "@theia/api-samples": "1.54.0",
+ "@theia/bulk-edit": "1.54.0",
+ "@theia/callhierarchy": "1.54.0",
+ "@theia/collaboration": "1.54.0",
+ "@theia/console": "1.54.0",
+ "@theia/core": "1.54.0",
+ "@theia/debug": "1.54.0",
+ "@theia/editor": "1.54.0",
+ "@theia/editor-preview": "1.54.0",
+ "@theia/file-search": "1.54.0",
+ "@theia/filesystem": "1.54.0",
+ "@theia/getting-started": "1.54.0",
+ "@theia/git": "1.54.0",
+ "@theia/keymaps": "1.54.0",
+ "@theia/markers": "1.54.0",
+ "@theia/memory-inspector": "1.54.0",
+ "@theia/messages": "1.54.0",
+ "@theia/metrics": "1.54.0",
+ "@theia/mini-browser": "1.54.0",
+ "@theia/monaco": "1.54.0",
+ "@theia/navigator": "1.54.0",
+ "@theia/outline-view": "1.54.0",
+ "@theia/output": "1.54.0",
+ "@theia/plugin-dev": "1.54.0",
+ "@theia/plugin-ext": "1.54.0",
+ "@theia/plugin-ext-vscode": "1.54.0",
+ "@theia/plugin-metrics": "1.54.0",
+ "@theia/preferences": "1.54.0",
+ "@theia/preview": "1.54.0",
+ "@theia/process": "1.54.0",
+ "@theia/property-view": "1.54.0",
+ "@theia/scm": "1.54.0",
+ "@theia/scm-extra": "1.54.0",
+ "@theia/search-in-workspace": "1.54.0",
+ "@theia/secondary-window": "1.54.0",
+ "@theia/task": "1.54.0",
+ "@theia/terminal": "1.54.0",
+ "@theia/timeline": "1.54.0",
+ "@theia/toolbar": "1.54.0",
+ "@theia/typehierarchy": "1.54.0",
+ "@theia/userstorage": "1.54.0",
+ "@theia/variable-resolver": "1.54.0",
+ "@theia/vsx-registry": "1.54.0",
+ "@theia/workspace": "1.54.0"
+ },
+ "scripts": {
+ "prepare:no-native": "lerna run prepare --scope=\"@theia/re-exports\" && lerna run generate-theia-re-exports --scope=\"@theia/core\"",
+ "clean": "theia clean",
+ "build": "yarn -s compile && yarn -s bundle",
+ "bundle": "theia build --mode development",
+ "compile": "tsc -b",
+ "start": "theia start",
+ "start:debug": "yarn -s start --log-level=debug",
+ "start:watch": "concurrently --kill-others -n tsc,bundle,run -c red,yellow,green \"tsc -b -w --preserveWatchOutput\" \"yarn -s watch:bundle\" \"yarn -s start\"",
+ "watch": "concurrently --kill-others -n tsc,bundle -c red,yellow \"tsc -b -w --preserveWatchOutput\" \"yarn -s watch:bundle\"",
+ "watch:bundle": "theia build --watch --mode development",
+ "watch:compile": "tsc -b -w"
+ },
+ "devDependencies": {
+ "@theia/cli": "1.54.0"
+ }
+}
diff --git a/examples/browser-only/tsconfig.json b/examples/browser-only/tsconfig.json
new file mode 100644
index 0000000000000..2c35a886f30d0
--- /dev/null
+++ b/examples/browser-only/tsconfig.json
@@ -0,0 +1,165 @@
+{
+ "extends": "../../configs/base.tsconfig",
+ "include": [],
+ "compilerOptions": {
+ "composite": true
+ },
+ "references": [
+ {
+ "path": "../../dev-packages/cli"
+ },
+ {
+ "path": "../../packages/ai-chat"
+ },
+ {
+ "path": "../../packages/ai-chat-ui"
+ },
+ {
+ "path": "../../packages/ai-code-completion"
+ },
+ {
+ "path": "../../packages/ai-core"
+ },
+ {
+ "path": "../../packages/ai-history"
+ },
+ {
+ "path": "../../packages/ai-ollama"
+ },
+ {
+ "path": "../../packages/ai-openai"
+ },
+ {
+ "path": "../../packages/bulk-edit"
+ },
+ {
+ "path": "../../packages/callhierarchy"
+ },
+ {
+ "path": "../../packages/collaboration"
+ },
+ {
+ "path": "../../packages/console"
+ },
+ {
+ "path": "../../packages/core"
+ },
+ {
+ "path": "../../packages/debug"
+ },
+ {
+ "path": "../../packages/editor"
+ },
+ {
+ "path": "../../packages/editor-preview"
+ },
+ {
+ "path": "../../packages/file-search"
+ },
+ {
+ "path": "../../packages/filesystem"
+ },
+ {
+ "path": "../../packages/getting-started"
+ },
+ {
+ "path": "../../packages/git"
+ },
+ {
+ "path": "../../packages/keymaps"
+ },
+ {
+ "path": "../../packages/markers"
+ },
+ {
+ "path": "../../packages/memory-inspector"
+ },
+ {
+ "path": "../../packages/messages"
+ },
+ {
+ "path": "../../packages/metrics"
+ },
+ {
+ "path": "../../packages/mini-browser"
+ },
+ {
+ "path": "../../packages/monaco"
+ },
+ {
+ "path": "../../packages/navigator"
+ },
+ {
+ "path": "../../packages/outline-view"
+ },
+ {
+ "path": "../../packages/output"
+ },
+ {
+ "path": "../../packages/plugin-dev"
+ },
+ {
+ "path": "../../packages/plugin-ext"
+ },
+ {
+ "path": "../../packages/plugin-ext-vscode"
+ },
+ {
+ "path": "../../packages/plugin-metrics"
+ },
+ {
+ "path": "../../packages/preferences"
+ },
+ {
+ "path": "../../packages/preview"
+ },
+ {
+ "path": "../../packages/process"
+ },
+ {
+ "path": "../../packages/property-view"
+ },
+ {
+ "path": "../../packages/scm"
+ },
+ {
+ "path": "../../packages/scm-extra"
+ },
+ {
+ "path": "../../packages/search-in-workspace"
+ },
+ {
+ "path": "../../packages/secondary-window"
+ },
+ {
+ "path": "../../packages/task"
+ },
+ {
+ "path": "../../packages/terminal"
+ },
+ {
+ "path": "../../packages/timeline"
+ },
+ {
+ "path": "../../packages/toolbar"
+ },
+ {
+ "path": "../../packages/typehierarchy"
+ },
+ {
+ "path": "../../packages/userstorage"
+ },
+ {
+ "path": "../../packages/variable-resolver"
+ },
+ {
+ "path": "../../packages/vsx-registry"
+ },
+ {
+ "path": "../../packages/workspace"
+ },
+ {
+ "path": "../api-samples"
+ }
+ ]
+}
diff --git a/examples/browser-only/webpack.config.js b/examples/browser-only/webpack.config.js
new file mode 100644
index 0000000000000..40e4ee963ba9f
--- /dev/null
+++ b/examples/browser-only/webpack.config.js
@@ -0,0 +1,18 @@
+/**
+ * This file can be edited to customize webpack configuration.
+ * To reset delete this file and rerun theia build again.
+ */
+// @ts-check
+const configs = require('./gen-webpack.config.js');
+
+
+/**
+ * Expose bundled modules on window.theia.moduleName namespace, e.g.
+ * window['theia']['@theia/core/lib/common/uri'].
+ * Such syntax can be used by external code, for instance, for testing.
+configs[0].module.rules.push({
+ test: /\.js$/,
+ loader: require.resolve('@theia/application-manager/lib/expose-loader')
+}); */
+
+module.exports = configs;
diff --git a/examples/browser/.theia/settings.json b/examples/browser/.theia/settings.json
new file mode 100644
index 0000000000000..005b2ddfae9ac
--- /dev/null
+++ b/examples/browser/.theia/settings.json
@@ -0,0 +1,7 @@
+{
+ "files.autoSave": "afterDelay",
+ "workbench.editor.closeOnFileDelete": true,
+ "git.autoRepositoryDetection": true,
+ "git.openRepositoryInParentFolders": "always"
+}
+
diff --git a/examples/browser/.theia/tasks.json b/examples/browser/.theia/tasks.json
new file mode 100644
index 0000000000000..b37b3b4607ddf
--- /dev/null
+++ b/examples/browser/.theia/tasks.json
@@ -0,0 +1,3 @@
+{
+ "tasks": []
+}
diff --git a/examples/browser/package.json b/examples/browser/package.json
index ad2289ee0f4c5..5cfdc9277b381 100644
--- a/examples/browser/package.json
+++ b/examples/browser/package.json
@@ -1,65 +1,86 @@
{
"private": true,
"name": "@theia/example-browser",
- "version": "1.44.0",
+ "version": "1.54.0",
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"theia": {
"frontend": {
"config": {
"applicationName": "Theia Browser Example",
"preferences": {
- "files.enableTrash": false
- }
+ "files.enableTrash": false,
+ "security.workspace.trust.enabled": false
+ },
+ "reloadOnReconnect": true
+ }
+ },
+ "backend": {
+ "config": {
+ "frontendConnectionTimeout": 3000
}
}
},
+ "theiaPluginsDir": "../../plugins",
"dependencies": {
- "@theia/api-samples": "1.44.0",
- "@theia/bulk-edit": "1.44.0",
- "@theia/callhierarchy": "1.44.0",
- "@theia/console": "1.44.0",
- "@theia/core": "1.44.0",
- "@theia/debug": "1.44.0",
- "@theia/editor": "1.44.0",
- "@theia/editor-preview": "1.44.0",
- "@theia/file-search": "1.44.0",
- "@theia/filesystem": "1.44.0",
- "@theia/getting-started": "1.44.0",
- "@theia/git": "1.44.0",
- "@theia/keymaps": "1.44.0",
- "@theia/markers": "1.44.0",
- "@theia/memory-inspector": "1.44.0",
- "@theia/messages": "1.44.0",
- "@theia/metrics": "1.44.0",
- "@theia/mini-browser": "1.44.0",
- "@theia/monaco": "1.44.0",
- "@theia/navigator": "1.44.0",
- "@theia/notebook": "1.44.0",
- "@theia/outline-view": "1.44.0",
- "@theia/output": "1.44.0",
- "@theia/plugin-dev": "1.44.0",
- "@theia/plugin-ext": "1.44.0",
- "@theia/plugin-ext-vscode": "1.44.0",
- "@theia/plugin-metrics": "1.44.0",
- "@theia/preferences": "1.44.0",
- "@theia/preview": "1.44.0",
- "@theia/process": "1.44.0",
- "@theia/property-view": "1.44.0",
- "@theia/remote": "1.44.0",
- "@theia/scm": "1.44.0",
- "@theia/scm-extra": "1.44.0",
- "@theia/search-in-workspace": "1.44.0",
- "@theia/secondary-window": "1.44.0",
- "@theia/task": "1.44.0",
- "@theia/terminal": "1.44.0",
- "@theia/test": "1.44.0",
- "@theia/timeline": "1.44.0",
- "@theia/toolbar": "1.44.0",
- "@theia/typehierarchy": "1.44.0",
- "@theia/userstorage": "1.44.0",
- "@theia/variable-resolver": "1.44.0",
- "@theia/vsx-registry": "1.44.0",
- "@theia/workspace": "1.44.0"
+ "@theia/ai-chat": "1.54.0",
+ "@theia/ai-chat-ui": "1.54.0",
+ "@theia/ai-code-completion": "1.54.0",
+ "@theia/ai-core": "1.54.0",
+ "@theia/ai-history": "1.54.0",
+ "@theia/ai-llamafile": "1.54.0",
+ "@theia/ai-ollama": "1.54.0",
+ "@theia/ai-openai": "1.54.0",
+ "@theia/ai-terminal": "1.54.0",
+ "@theia/ai-workspace-agent": "1.54.0",
+ "@theia/api-provider-sample": "1.54.0",
+ "@theia/api-samples": "1.54.0",
+ "@theia/bulk-edit": "1.54.0",
+ "@theia/callhierarchy": "1.54.0",
+ "@theia/collaboration": "1.54.0",
+ "@theia/console": "1.54.0",
+ "@theia/core": "1.54.0",
+ "@theia/debug": "1.54.0",
+ "@theia/dev-container": "1.54.0",
+ "@theia/editor": "1.54.0",
+ "@theia/editor-preview": "1.54.0",
+ "@theia/file-search": "1.54.0",
+ "@theia/filesystem": "1.54.0",
+ "@theia/getting-started": "1.54.0",
+ "@theia/keymaps": "1.54.0",
+ "@theia/markers": "1.54.0",
+ "@theia/memory-inspector": "1.54.0",
+ "@theia/messages": "1.54.0",
+ "@theia/metrics": "1.54.0",
+ "@theia/mini-browser": "1.54.0",
+ "@theia/monaco": "1.54.0",
+ "@theia/navigator": "1.54.0",
+ "@theia/notebook": "1.54.0",
+ "@theia/outline-view": "1.54.0",
+ "@theia/output": "1.54.0",
+ "@theia/plugin-dev": "1.54.0",
+ "@theia/plugin-ext": "1.54.0",
+ "@theia/plugin-ext-headless": "1.54.0",
+ "@theia/plugin-ext-vscode": "1.54.0",
+ "@theia/plugin-metrics": "1.54.0",
+ "@theia/preferences": "1.54.0",
+ "@theia/preview": "1.54.0",
+ "@theia/process": "1.54.0",
+ "@theia/property-view": "1.54.0",
+ "@theia/remote": "1.54.0",
+ "@theia/scm": "1.54.0",
+ "@theia/scm-extra": "1.54.0",
+ "@theia/search-in-workspace": "1.54.0",
+ "@theia/secondary-window": "1.54.0",
+ "@theia/task": "1.54.0",
+ "@theia/terminal": "1.54.0",
+ "@theia/test": "1.54.0",
+ "@theia/timeline": "1.54.0",
+ "@theia/toolbar": "1.54.0",
+ "@theia/typehierarchy": "1.54.0",
+ "@theia/userstorage": "1.54.0",
+ "@theia/variable-resolver": "1.54.0",
+ "@theia/vsx-registry": "1.54.0",
+ "@theia/workspace": "1.54.0"
},
"scripts": {
"clean": "theia clean",
@@ -82,6 +103,6 @@
"watch:compile": "tsc -b -w"
},
"devDependencies": {
- "@theia/cli": "1.44.0"
+ "@theia/cli": "1.54.0"
}
}
diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json
index e3756e4f8c7de..5f39c27826132 100644
--- a/examples/browser/tsconfig.json
+++ b/examples/browser/tsconfig.json
@@ -1,6 +1,6 @@
{
"extends": "../../configs/base.tsconfig",
- "include": [ ],
+ "include": [],
"compilerOptions": {
"composite": true
},
@@ -8,12 +8,45 @@
{
"path": "../../dev-packages/cli"
},
+ {
+ "path": "../../packages/ai-chat"
+ },
+ {
+ "path": "../../packages/ai-chat-ui"
+ },
+ {
+ "path": "../../packages/ai-code-completion"
+ },
+ {
+ "path": "../../packages/ai-core"
+ },
+ {
+ "path": "../../packages/ai-history"
+ },
+ {
+ "path": "../../packages/ai-llamafile"
+ },
+ {
+ "path": "../../packages/ai-ollama"
+ },
+ {
+ "path": "../../packages/ai-openai"
+ },
+ {
+ "path": "../../packages/ai-terminal"
+ },
+ {
+ "path": "../../packages/ai-workspace-agent"
+ },
{
"path": "../../packages/bulk-edit"
},
{
"path": "../../packages/callhierarchy"
},
+ {
+ "path": "../../packages/collaboration"
+ },
{
"path": "../../packages/console"
},
@@ -23,6 +56,9 @@
{
"path": "../../packages/debug"
},
+ {
+ "path": "../../packages/dev-container"
+ },
{
"path": "../../packages/editor"
},
@@ -38,9 +74,6 @@
{
"path": "../../packages/getting-started"
},
- {
- "path": "../../packages/git"
- },
{
"path": "../../packages/keymaps"
},
@@ -80,6 +113,9 @@
{
"path": "../../packages/plugin-ext"
},
+ {
+ "path": "../../packages/plugin-ext-headless"
+ },
{
"path": "../../packages/plugin-ext-vscode"
},
@@ -143,6 +179,9 @@
{
"path": "../../packages/workspace"
},
+ {
+ "path": "../api-provider-sample"
+ },
{
"path": "../api-samples"
}
diff --git a/examples/electron/package.json b/examples/electron/package.json
index 6fd2bffc3ea17..5dcb9b9007a93 100644
--- a/examples/electron/package.json
+++ b/examples/electron/package.json
@@ -2,63 +2,88 @@
"private": true,
"name": "@theia/example-electron",
"productName": "Theia Electron Example",
- "version": "1.44.0",
+ "version": "1.54.0",
"main": "lib/backend/electron-main.js",
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"theia": {
"target": "electron",
"frontend": {
"config": {
- "applicationName": "Theia Electron Example"
+ "applicationName": "Theia Electron Example",
+ "reloadOnReconnect": true,
+ "electron": {
+ "splashScreenOptions": {
+ "content": "resources/theia-logo.svg",
+ "height": 90
+ }
+ }
+ }
+ },
+ "backend": {
+ "config": {
+ "frontendConnectionTimeout": -1
}
}
},
"dependencies": {
- "@theia/api-samples": "1.44.0",
- "@theia/bulk-edit": "1.44.0",
- "@theia/callhierarchy": "1.44.0",
- "@theia/console": "1.44.0",
- "@theia/core": "1.44.0",
- "@theia/debug": "1.44.0",
- "@theia/editor": "1.44.0",
- "@theia/editor-preview": "1.44.0",
- "@theia/electron": "1.44.0",
- "@theia/external-terminal": "1.44.0",
- "@theia/file-search": "1.44.0",
- "@theia/filesystem": "1.44.0",
- "@theia/getting-started": "1.44.0",
- "@theia/git": "1.44.0",
- "@theia/keymaps": "1.44.0",
- "@theia/markers": "1.44.0",
- "@theia/memory-inspector": "1.44.0",
- "@theia/messages": "1.44.0",
- "@theia/metrics": "1.44.0",
- "@theia/mini-browser": "1.44.0",
- "@theia/monaco": "1.44.0",
- "@theia/navigator": "1.44.0",
- "@theia/outline-view": "1.44.0",
- "@theia/output": "1.44.0",
- "@theia/plugin-dev": "1.44.0",
- "@theia/plugin-ext": "1.44.0",
- "@theia/plugin-ext-vscode": "1.44.0",
- "@theia/preferences": "1.44.0",
- "@theia/preview": "1.44.0",
- "@theia/process": "1.44.0",
- "@theia/property-view": "1.44.0",
- "@theia/remote": "1.44.0",
- "@theia/scm": "1.44.0",
- "@theia/scm-extra": "1.44.0",
- "@theia/search-in-workspace": "1.44.0",
- "@theia/secondary-window": "1.44.0",
- "@theia/task": "1.44.0",
- "@theia/terminal": "1.44.0",
- "@theia/timeline": "1.44.0",
- "@theia/toolbar": "1.44.0",
- "@theia/typehierarchy": "1.44.0",
- "@theia/userstorage": "1.44.0",
- "@theia/variable-resolver": "1.44.0",
- "@theia/vsx-registry": "1.44.0",
- "@theia/workspace": "1.44.0"
+ "@theia/ai-chat": "1.54.0",
+ "@theia/ai-chat-ui": "1.54.0",
+ "@theia/ai-code-completion": "1.54.0",
+ "@theia/ai-core": "1.54.0",
+ "@theia/ai-history": "1.54.0",
+ "@theia/ai-llamafile": "1.54.0",
+ "@theia/ai-ollama": "1.54.0",
+ "@theia/ai-openai": "1.54.0",
+ "@theia/ai-terminal": "1.54.0",
+ "@theia/ai-workspace-agent": "1.54.0",
+ "@theia/api-provider-sample": "1.54.0",
+ "@theia/api-samples": "1.54.0",
+ "@theia/bulk-edit": "1.54.0",
+ "@theia/callhierarchy": "1.54.0",
+ "@theia/collaboration": "1.54.0",
+ "@theia/console": "1.54.0",
+ "@theia/core": "1.54.0",
+ "@theia/debug": "1.54.0",
+ "@theia/dev-container": "1.54.0",
+ "@theia/editor": "1.54.0",
+ "@theia/editor-preview": "1.54.0",
+ "@theia/electron": "1.54.0",
+ "@theia/external-terminal": "1.54.0",
+ "@theia/file-search": "1.54.0",
+ "@theia/filesystem": "1.54.0",
+ "@theia/getting-started": "1.54.0",
+ "@theia/keymaps": "1.54.0",
+ "@theia/markers": "1.54.0",
+ "@theia/memory-inspector": "1.54.0",
+ "@theia/messages": "1.54.0",
+ "@theia/metrics": "1.54.0",
+ "@theia/mini-browser": "1.54.0",
+ "@theia/monaco": "1.54.0",
+ "@theia/navigator": "1.54.0",
+ "@theia/outline-view": "1.54.0",
+ "@theia/output": "1.54.0",
+ "@theia/plugin-dev": "1.54.0",
+ "@theia/plugin-ext": "1.54.0",
+ "@theia/plugin-ext-headless": "1.54.0",
+ "@theia/plugin-ext-vscode": "1.54.0",
+ "@theia/preferences": "1.54.0",
+ "@theia/preview": "1.54.0",
+ "@theia/process": "1.54.0",
+ "@theia/property-view": "1.54.0",
+ "@theia/remote": "1.54.0",
+ "@theia/scm": "1.54.0",
+ "@theia/scm-extra": "1.54.0",
+ "@theia/search-in-workspace": "1.54.0",
+ "@theia/secondary-window": "1.54.0",
+ "@theia/task": "1.54.0",
+ "@theia/terminal": "1.54.0",
+ "@theia/timeline": "1.54.0",
+ "@theia/toolbar": "1.54.0",
+ "@theia/typehierarchy": "1.54.0",
+ "@theia/userstorage": "1.54.0",
+ "@theia/variable-resolver": "1.54.0",
+ "@theia/vsx-registry": "1.54.0",
+ "@theia/workspace": "1.54.0"
},
"scripts": {
"build": "yarn -s compile && yarn -s bundle",
@@ -76,7 +101,7 @@
"watch:compile": "tsc -b -w"
},
"devDependencies": {
- "@theia/cli": "1.44.0",
- "electron": "^23.2.4"
+ "@theia/cli": "1.54.0",
+ "electron": "^30.1.2"
}
}
diff --git a/examples/electron/resources/theia-logo.svg b/examples/electron/resources/theia-logo.svg
new file mode 100644
index 0000000000000..8d6b150a81d6b
--- /dev/null
+++ b/examples/electron/resources/theia-logo.svg
@@ -0,0 +1,32 @@
+
+
+
+
diff --git a/examples/electron/tsconfig.json b/examples/electron/tsconfig.json
index edac6071a7d07..9199410a96828 100644
--- a/examples/electron/tsconfig.json
+++ b/examples/electron/tsconfig.json
@@ -11,12 +11,45 @@
{
"path": "../../dev-packages/cli"
},
+ {
+ "path": "../../packages/ai-chat"
+ },
+ {
+ "path": "../../packages/ai-chat-ui"
+ },
+ {
+ "path": "../../packages/ai-code-completion"
+ },
+ {
+ "path": "../../packages/ai-core"
+ },
+ {
+ "path": "../../packages/ai-history"
+ },
+ {
+ "path": "../../packages/ai-llamafile"
+ },
+ {
+ "path": "../../packages/ai-ollama"
+ },
+ {
+ "path": "../../packages/ai-openai"
+ },
+ {
+ "path": "../../packages/ai-terminal"
+ },
+ {
+ "path": "../../packages/ai-workspace-agent"
+ },
{
"path": "../../packages/bulk-edit"
},
{
"path": "../../packages/callhierarchy"
},
+ {
+ "path": "../../packages/collaboration"
+ },
{
"path": "../../packages/console"
},
@@ -26,6 +59,9 @@
{
"path": "../../packages/debug"
},
+ {
+ "path": "../../packages/dev-container"
+ },
{
"path": "../../packages/editor"
},
@@ -44,9 +80,6 @@
{
"path": "../../packages/getting-started"
},
- {
- "path": "../../packages/git"
- },
{
"path": "../../packages/keymaps"
},
@@ -83,6 +116,9 @@
{
"path": "../../packages/plugin-ext"
},
+ {
+ "path": "../../packages/plugin-ext-headless"
+ },
{
"path": "../../packages/plugin-ext-vscode"
},
@@ -140,6 +176,9 @@
{
"path": "../../packages/workspace"
},
+ {
+ "path": "../api-provider-sample"
+ },
{
"path": "../api-samples"
}
diff --git a/examples/playwright/.gitignore b/examples/playwright/.gitignore
index 86ac98af48d1c..4fc8f9cdad72e 100644
--- a/examples/playwright/.gitignore
+++ b/examples/playwright/.gitignore
@@ -1,3 +1,4 @@
allure-results
test-results
+playwright-report
.tmp.cfg
diff --git a/examples/playwright/configs/playwright.ci.config.ts b/examples/playwright/configs/playwright.ci.config.ts
index 4fa758b32a3c9..2ba404cb693ad 100644
--- a/examples/playwright/configs/playwright.ci.config.ts
+++ b/examples/playwright/configs/playwright.ci.config.ts
@@ -21,7 +21,13 @@ const ciConfig: PlaywrightTestConfig = {
...baseConfig,
workers: 1,
retries: 2,
- reporter: [['list'], ['allure-playwright'], ['github']]
+ reporter: [
+ ['list'],
+ ['github'],
+ ['html', { open: 'never' }],
+ ],
+ timeout: 30 * 1000, // Overwrite baseConfig timeout
+ preserveOutput: 'always'
};
export default ciConfig;
diff --git a/examples/playwright/docs/EXTENSIBILITY.md b/examples/playwright/docs/EXTENSIBILITY.md
index 63024ca66fe8f..ad90f087a23d2 100644
--- a/examples/playwright/docs/EXTENSIBILITY.md
+++ b/examples/playwright/docs/EXTENSIBILITY.md
@@ -10,11 +10,11 @@ Commands and menu items are handled by their label, so no further customization
Simply interact with them via the menu or quick commands.
```typescript
-const app = await TheiaApp.load(page);
+const app = await TheiaAppLoader.load({ playwright, browser });
const menuBar = app.menuBar;
-const yourMenu = await menuBar.openMenu("Your Menu");
-const yourItem = await mainMenu.menuItemByName("Your Item");
+const yourMenu = await menuBar.openMenu('Your Menu');
+const yourItem = await mainMenu.menuItemByName('Your Item');
expect(await yourItem?.hasSubmenu()).toBe(true);
```
@@ -30,14 +30,14 @@ export class MyTheiaApp extends TheiaApp {
}
export class MyToolbar extends TheiaPageObject {
- selector = "div#myToolbar";
+ selector = 'div#myToolbar';
async clickItem1(): Promise {
await this.page.click(`${this.selector} .item1`);
}
}
-const ws = new TheiaWorkspace(["src/tests/resources/sample-files1"]);
-const app = await MyTheiaApp.loadApp(page, MyTheiaApp, ws);
+const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']);
+const app = await TheiaAppLoader.load({ playwright, browser }, ws, MyTheiaApp);
await app.toolbar.clickItem1();
```
@@ -55,9 +55,9 @@ export class MyView extends TheiaView {
constructor(public app: TheiaApp) {
super(
{
- tabSelector: "#shell-tab-my-view", // the id of the tab
- viewSelector: "#my-view-container", // the id of the view container
- viewName: "My View", // the user visible view name
+ tabSelector: '#shell-tab-my-view', // the id of the tab
+ viewSelector: '#my-view-container', // the id of the view container
+ viewName: 'My View', // the user visible view name
},
app
);
@@ -66,7 +66,7 @@ export class MyView extends TheiaView {
async clickMyButton(): Promise {
await this.activate();
const viewElement = await this.viewElement();
- const button = await viewElement?.waitForSelector("#idOfMyButton");
+ const button = await viewElement?.waitForSelector('#idOfMyButton');
await button?.click();
}
}
@@ -83,7 +83,7 @@ As an example, `MyView` above introduces a method that allows to click a button.
To use this custom page object in a test, we pass our custom page object as a parameter when opening the view with `app.openView`.
```typescript
-const app = await TheiaApp.load(page, ws);
+const app = await TheiaAppLoader.load({ playwright, browser });
const myView = await app.openView(MyView);
await myView.clickMyButton();
```
@@ -94,7 +94,7 @@ As a reference for custom views and editors, please refer to the existing page o
Custom status indicators are supported with the same mechanism. They are accessed via `TheiaApp.statusBar`.
```typescript
-const app = await TheiaApp.load(page);
+const app = await TheiaAppLoader.load({ playwright, browser });
const problemIndicator = await app.statusBar.statusIndicator(
TheiaProblemIndicator
);
diff --git a/examples/playwright/docs/GETTING_STARTED.md b/examples/playwright/docs/GETTING_STARTED.md
index 3e5637057a1b6..ee7ec5225ed8b 100644
--- a/examples/playwright/docs/GETTING_STARTED.md
+++ b/examples/playwright/docs/GETTING_STARTED.md
@@ -43,35 +43,35 @@ Using the `TheiaApp` instance, we open an editor of type `TheiaTextEditor`, whic
At any time, we can also get information from the text editor, such as obtaining dirty state and verify whether this information is what we expect.
```typescript
-test("should undo and redo text changes and correctly update the dirty state", async () => {
+test('should undo and redo text changes and correctly update the dirty state', async ({ playwright, browser }) => {
// 1. set up workspace contents and open Theia app
- const ws = new TheiaWorkspace(["src/tests/resources/sample-files1"]);
- const app = await TheiaApp.load(page, ws);
+ const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']);
+ app = await TheiaAppLoader.load( { playwright, browser }, ws);
// 2. open Theia text editor
const sampleTextEditor = await app.openEditor(
- "sample.txt",
+ 'sample.txt',
TheiaTextEditor
);
// 3. make a change and verify contents and dirty
- await sampleTextEditor.replaceLineWithLineNumber("change", 1);
+ await sampleTextEditor.replaceLineWithLineNumber('change', 1);
expect(await sampleTextEditor.textContentOfLineByLineNumber(1)).toBe(
- "change"
+ 'change'
);
expect(await sampleTextEditor.isDirty()).toBe(true);
// 4. undo and verify contents and dirty state
await sampleTextEditor.undo(2);
expect(await sampleTextEditor.textContentOfLineByLineNumber(1)).toBe(
- "this is just a sample file"
+ 'this is just a sample file'
);
expect(await sampleTextEditor.isDirty()).toBe(false);
// 5. undo and verify contents and dirty state
await sampleTextEditor.redo(2);
expect(await sampleTextEditor.textContentOfLineByLineNumber(1)).toBe(
- "change"
+ 'change'
);
expect(await sampleTextEditor.isDirty()).toBe(true);
@@ -81,9 +81,9 @@ test("should undo and redo text changes and correctly update the dirty state", a
await sampleTextEditor.close();
// 7. reopen editor and verify dirty state
- const reopenedEditor = await app.openEditor("sample.txt", TheiaTextEditor);
+ const reopenedEditor = await app.openEditor('sample.txt', TheiaTextEditor);
expect(await reopenedEditor.textContentOfLineByLineNumber(1)).toBe(
- "change"
+ 'change'
);
await reopenedEditor.close();
diff --git a/examples/playwright/package.json b/examples/playwright/package.json
index d618c6a0c6e38..3d2e3ba098051 100644
--- a/examples/playwright/package.json
+++ b/examples/playwright/package.json
@@ -1,6 +1,6 @@
{
"name": "@theia/playwright",
- "version": "1.44.0",
+ "version": "1.54.0",
"description": "System tests for Theia",
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
@@ -15,12 +15,12 @@
"clean": "rimraf lib *.tsbuildinfo .eslintcache",
"build": "yarn && yarn clean && tsc --incremental && yarn lint && yarn playwright:install",
"watch": "tsc -w --incremental",
- "theia:start": "rimraf .tmp.cfg && THEIA_CONFIG_DIR=$PWD/.tmp.cfg yarn --cwd ../browser start",
+ "theia:start": "rimraf .tmp.cfg && cross-env THEIA_CONFIG_DIR=$PWD/.tmp.cfg yarn --cwd ../browser start",
"lint": "eslint -c ./.eslintrc.js --ext .ts ./src",
"lint:fix": "eslint -c ./.eslintrc.js --ext .ts ./src --fix",
"playwright:install": "playwright install chromium",
"ui-tests": "yarn build && playwright test --config=./configs/playwright.config.ts",
- "ui-tests-electron": "yarn build && USE_ELECTRON=true playwright test --config=./configs/playwright.config.ts",
+ "ui-tests-electron": "yarn build && cross-env USE_ELECTRON=true playwright test --config=./configs/playwright.config.ts",
"ui-tests-ci": "yarn build && playwright test --config=./configs/playwright.ci.config.ts",
"ui-tests-headful": "yarn build && playwright test --config=./configs/playwright.headful.config.ts",
"ui-tests-report-generate": "allure generate ./allure-results --clean -o allure-results/allure-report",
@@ -38,8 +38,9 @@
"@types/fs-extra": "^9.0.8",
"allure-commandline": "^2.23.1",
"allure-playwright": "^2.5.0",
- "rimraf": "^2.6.1",
- "typescript": "~4.5.5"
+ "cross-env": "^7.0.3",
+ "rimraf": "^5.0.0",
+ "typescript": "~5.4.5"
},
"publishConfig": {
"access": "public"
diff --git a/examples/playwright/src/index.ts b/examples/playwright/src/index.ts
index 5dd9ebe6a9a75..68ba7d9d673f2 100644
--- a/examples/playwright/src/index.ts
+++ b/examples/playwright/src/index.ts
@@ -26,6 +26,9 @@ export * from './theia-menu-item';
export * from './theia-menu';
export * from './theia-notification-indicator';
export * from './theia-notification-overlay';
+export * from './theia-notebook-cell';
+export * from './theia-notebook-editor';
+export * from './theia-notebook-toolbar';
export * from './theia-output-channel';
export * from './theia-output-view';
export * from './theia-page-object';
diff --git a/examples/playwright/src/tests/resources/notebook-files/.theia/settings.json b/examples/playwright/src/tests/resources/notebook-files/.theia/settings.json
new file mode 100644
index 0000000000000..8bb69fd4f5289
--- /dev/null
+++ b/examples/playwright/src/tests/resources/notebook-files/.theia/settings.json
@@ -0,0 +1,3 @@
+{
+ "files.autoSave": "off"
+}
diff --git a/examples/playwright/src/tests/resources/notebook-files/sample.ipynb b/examples/playwright/src/tests/resources/notebook-files/sample.ipynb
new file mode 100644
index 0000000000000..709d82cff5441
--- /dev/null
+++ b/examples/playwright/src/tests/resources/notebook-files/sample.ipynb
@@ -0,0 +1,18 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/examples/playwright/src/tests/theia-application-shell.test.ts b/examples/playwright/src/tests/theia-application-shell.test.ts
new file mode 100644
index 0000000000000..779fc5358c18f
--- /dev/null
+++ b/examples/playwright/src/tests/theia-application-shell.test.ts
@@ -0,0 +1,67 @@
+// *****************************************************************************
+// Copyright (C) 2023 Toro Cloud Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+
+import { test } from '@playwright/test';
+import { TheiaApp } from '../theia-app';
+import { TheiaAppLoader } from '../theia-app-loader';
+import { TheiaExplorerView } from '../theia-explorer-view';
+import { TheiaTextEditor } from '../theia-text-editor';
+import { TheiaWelcomeView } from '../theia-welcome-view';
+import { TheiaWorkspace } from '../theia-workspace';
+
+test.describe('Theia Application Shell', () => {
+ test.describe.configure({
+ timeout: 120000
+ });
+
+ let app: TheiaApp;
+
+ test.beforeAll(async ({ playwright, browser }) => {
+ const ws = new TheiaWorkspace(['src/tests/resources/sample-files1']);
+ app = await TheiaAppLoader.load({ playwright, browser }, ws);
+
+ // The welcome view must be closed because the memory leak only occurs when there are
+ // no tabs left open.
+ const welcomeView = new TheiaWelcomeView(app);
+
+ if (await welcomeView.isTabVisible()) {
+ await welcomeView.close();
+ }
+ });
+
+ test.afterAll(async () => {
+ await app.page.close();
+ });
+
+ /**
+ * The aim of this test is to detect memory leaks when opening and closing editors many times.
+ * Remove the skip and run the test, check the logs for any memory leak warnings.
+ * It should take less than 2min to run, if it takes longer than that, just increase the timeout.
+ */
+ test.skip('should open and close a text editor many times', async () => {
+ for (let i = 0; i < 200; i++) {
+ const explorer = await app.openView(TheiaExplorerView);
+
+ const fileStatNode = await explorer.getFileStatNodeByLabel('sample.txt');
+ const contextMenu = await fileStatNode.openContextMenu();
+ await contextMenu.clickMenuItem('Open');
+
+ const textEditor = new TheiaTextEditor('sample.txt', app);
+ await textEditor.waitForVisible();
+
+ await textEditor.close();
+ }
+ });
+});
diff --git a/examples/playwright/src/tests/theia-explorer-view.test.ts b/examples/playwright/src/tests/theia-explorer-view.test.ts
index 1705244609988..f3362f3f212b3 100644
--- a/examples/playwright/src/tests/theia-explorer-view.test.ts
+++ b/examples/playwright/src/tests/theia-explorer-view.test.ts
@@ -183,7 +183,8 @@ test.describe('Theia Explorer View', () => {
expect(await explorer.existsDirectoryNode('sampleDirectoryCompact/nestedFolder1/nestedFolder2', true /* compact */)).toBe(true);
});
- test('should delete nested folder "sampleDirectoryCompact/nestedFolder1/nestedFolder2"', async () => {
+ // TODO These tests only seems to fail on Ubuntu - it's not clear why
+ test.skip('should delete nested folder "sampleDirectoryCompact/nestedFolder1/nestedFolder2"', async () => {
const fileStatElements = await explorer.visibleFileStatNodes();
expect(await explorer.existsDirectoryNode('sampleDirectoryCompact/nestedFolder1/nestedFolder2', true /* compact */)).toBe(true);
await explorer.deleteNode('sampleDirectoryCompact/nestedFolder1/nestedFolder2', true /* confirm */, 'nestedFolder2' /* nodeSegmentLabel */);
@@ -192,7 +193,7 @@ test.describe('Theia Explorer View', () => {
expect(updatedFileStatElements.length).toBe(fileStatElements.length - 1);
});
- test('should delete compact folder "sampleDirectoryCompact/nestedFolder1"', async () => {
+ test.skip('should delete compact folder "sampleDirectoryCompact/nestedFolder1"', async () => {
const fileStatElements = await explorer.visibleFileStatNodes();
expect(await explorer.existsDirectoryNode('sampleDirectoryCompact/nestedFolder1', true /* compact */)).toBe(true);
await explorer.deleteNode('sampleDirectoryCompact/nestedFolder1', true /* confirm */, 'sampleDirectoryCompact' /* nodeSegmentLabel */);
diff --git a/examples/playwright/src/tests/theia-getting-started.test.ts b/examples/playwright/src/tests/theia-getting-started.test.ts
new file mode 100644
index 0000000000000..e938e71beb265
--- /dev/null
+++ b/examples/playwright/src/tests/theia-getting-started.test.ts
@@ -0,0 +1,50 @@
+// *****************************************************************************
+// Copyright (C) 2024 TypeFox and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { expect, test } from '@playwright/test';
+import { TheiaApp } from '../theia-app';
+import { TheiaAppLoader } from '../theia-app-loader';
+import { TheiaExplorerView } from '../theia-explorer-view';
+
+/**
+ * Test the Theia welcome page from the getting-started package.
+ */
+test.describe('Theia Welcome Page', () => {
+ let app: TheiaApp;
+
+ test.beforeAll(async ({ playwright, browser }) => {
+ app = await TheiaAppLoader.load({ playwright, browser });
+ await app.isMainContentPanelVisible();
+ });
+
+ test.afterAll(async () => {
+ await app.page.close();
+ });
+
+ test('New File... entry should create a new file.', async () => {
+ await app.page.getByRole('button', { name: 'New File...' }).click();
+ const quickPicker = app.page.getByPlaceholder('Select File Type or Enter');
+ await quickPicker.fill('testfile.txt');
+ await quickPicker.press('Enter');
+ await app.page.getByRole('button', { name: 'Create File' }).click();
+
+ // check file in workspace exists
+ const explorer = await app.openView(TheiaExplorerView);
+ await explorer.refresh();
+ await explorer.waitForVisibleFileNodes();
+ expect(await explorer.existsFileNode('testfile.txt')).toBe(true);
+ });
+});
diff --git a/examples/playwright/src/tests/theia-main-menu.test.ts b/examples/playwright/src/tests/theia-main-menu.test.ts
index f00fcb6727270..919cc0400b05f 100644
--- a/examples/playwright/src/tests/theia-main-menu.test.ts
+++ b/examples/playwright/src/tests/theia-main-menu.test.ts
@@ -20,6 +20,7 @@ import { TheiaAppLoader } from '../theia-app-loader';
import { TheiaAboutDialog } from '../theia-about-dialog';
import { TheiaMenuBar } from '../theia-main-menu';
import { OSUtil } from '../util';
+import { TheiaExplorerView } from '../theia-explorer-view';
test.describe('Theia Main Menu', () => {
@@ -96,7 +97,7 @@ test.describe('Theia Main Menu', () => {
await (await menuBar.openMenu('Help')).clickMenuItem('About');
const aboutDialog = new TheiaAboutDialog(app);
expect(await aboutDialog.isVisible()).toBe(true);
- await aboutDialog.page.getByRole('button', { name: 'OK' }).click();
+ await aboutDialog.page.locator('#theia-dialog-shell').getByRole('button', { name: 'OK' }).click();
expect(await aboutDialog.isVisible()).toBe(false);
});
@@ -109,4 +110,24 @@ test.describe('Theia Main Menu', () => {
expect(await fileDialog.isVisible()).toBe(false);
});
+ test('Create file via New File menu and cancel', async () => {
+ const openFileEntry = 'New File...';
+ await (await menuBar.openMenu('File')).clickMenuItem(openFileEntry);
+ const quickPick = app.page.getByPlaceholder('Select File Type or Enter');
+ // type file name and press enter
+ await quickPick.fill('test.txt');
+ await quickPick.press('Enter');
+
+ // check file dialog is opened and accept with "Create File" button
+ const fileDialog = await app.page.waitForSelector('div[class="dialogBlock"]');
+ expect(await fileDialog.isVisible()).toBe(true);
+ await app.page.locator('#theia-dialog-shell').getByRole('button', { name: 'Create File' }).click();
+ expect(await fileDialog.isVisible()).toBe(false);
+
+ // check file in workspace exists
+ const explorer = await app.openView(TheiaExplorerView);
+ await explorer.refresh();
+ await explorer.waitForVisibleFileNodes();
+ expect(await explorer.existsFileNode('test.txt')).toBe(true);
+ });
});
diff --git a/examples/playwright/src/tests/theia-notebook-editor.test.ts b/examples/playwright/src/tests/theia-notebook-editor.test.ts
new file mode 100644
index 0000000000000..3045d25421b87
--- /dev/null
+++ b/examples/playwright/src/tests/theia-notebook-editor.test.ts
@@ -0,0 +1,322 @@
+// *****************************************************************************
+// Copyright (C) 2024 TypeFox GmbH and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { Locator, PlaywrightWorkerArgs, expect, test } from '@playwright/test';
+import { TheiaApp } from '../theia-app';
+import { TheiaAppLoader, TheiaPlaywrightTestConfig } from '../theia-app-loader';
+import { TheiaNotebookCell } from '../theia-notebook-cell';
+import { TheiaNotebookEditor } from '../theia-notebook-editor';
+import { TheiaWorkspace } from '../theia-workspace';
+import path = require('path');
+
+// See .github/workflows/playwright.yml for preferred python version
+const preferredKernel = process.env.CI ? 'Python 3.11' : 'Python 3';
+
+test.describe('Theia Notebook Editor interaction', () => {
+
+ let app: TheiaApp;
+ let editor: TheiaNotebookEditor;
+
+ test.beforeAll(async ({ playwright, browser }) => {
+ app = await loadApp({ playwright, browser });
+ });
+
+ test.beforeEach(async ({ playwright, browser }) => {
+ editor = await app.openEditor('sample.ipynb', TheiaNotebookEditor);
+ });
+
+ test.afterAll(async () => {
+ await app.page.close();
+ });
+
+ test.afterEach(async () => {
+ if (editor) {
+ await editor.closeWithoutSave();
+ }
+ });
+
+ test('kernels are installed', async () => {
+ const kernels = await editor.availableKernels();
+ const msg = `Available kernels:\n ${kernels.join('\n')}`;
+ console.log(msg); // Print available kernels, useful when running in CI.
+ expect(kernels.length, msg).toBeGreaterThan(0);
+
+ const py3kernel = kernels.filter(kernel => kernel.match(new RegExp(`^${preferredKernel}`)));
+ expect(py3kernel.length, msg).toBeGreaterThan(0);
+ });
+
+ test('should select a kernel', async () => {
+ await editor.selectKernel(preferredKernel);
+ const selectedKernel = await editor.selectedKernel();
+ expect(selectedKernel).toMatch(new RegExp(`^${preferredKernel}`));
+ });
+
+ test('should add a new code cell', async () => {
+ await editor.addCodeCell();
+ const cells = await editor.cells();
+ expect(cells.length).toBe(2);
+ expect(await cells[1].mode()).toBe('python');
+ });
+
+ test('should add a new markdown cell', async () => {
+ await editor.addMarkdownCell();
+ await (await editor.cells())[1].addEditorText('print("markdown")');
+
+ const cells = await editor.cells();
+ expect(cells.length).toBe(2);
+ expect(await cells[1].mode()).toBe('markdown');
+ expect(await cells[1].editorText()).toBe('print("markdown")');
+ });
+
+ test('should execute all cells', async () => {
+ const cell = await firstCell(editor);
+ await cell.addEditorText('print("Hallo Notebook!")');
+
+ await editor.addCodeCell();
+ const secondCell = (await editor.cells())[1];
+ await secondCell.addEditorText('print("Bye Notebook!")');
+
+ await editor.executeAllCells();
+
+ expect(await cell.outputText()).toBe('Hallo Notebook!');
+ expect(await secondCell.outputText()).toBe('Bye Notebook!');
+ });
+
+ test('should split cell', async () => {
+ const cell = await firstCell(editor);
+ /*
+ Add cell text:
+ print("Line-1")
+ print("Line-2")
+ */
+ await cell.addEditorText('print("Line-1")\nprint("Line-2")');
+
+ /*
+ Set cursor:
+ print("Line-1")
+ <|>print("Line-2")
+ */
+ const line = await cell.editor.lineByLineNumber(1);
+ await line?.waitForElementState('visible');
+ await line?.click();
+ await line?.press('ArrowRight');
+
+ // split cell
+ await cell.splitCell();
+
+ // expect two cells with text "print("Line-1")" and "print("Line-2")"
+ expect(await editor.cells()).toHaveLength(2);
+ expect(await (await editor.cells())[0].editorText()).toBe('print("Line-1")');
+ expect(await (await editor.cells())[1].editorText()).toBe('print("Line-2")');
+ });
+});
+
+test.describe('Theia Notebook Cell interaction', () => {
+
+ let app: TheiaApp;
+ let editor: TheiaNotebookEditor;
+
+ test.beforeAll(async ({ playwright, browser }) => {
+ app = await loadApp({ playwright, browser });
+ });
+
+ test.afterAll(async () => {
+ await app.page.close();
+ });
+
+ test.beforeEach(async () => {
+ editor = await app.openEditor('sample.ipynb', TheiaNotebookEditor);
+ const selectedKernel = await editor.selectedKernel();
+ if (selectedKernel?.match(new RegExp(`^${preferredKernel}`)) === null) {
+ await editor.selectKernel(preferredKernel);
+ }
+ });
+
+ test.afterEach(async () => {
+ if (editor) {
+ await editor.closeWithoutSave();
+ }
+ });
+
+ test('should write text in a code cell', async () => {
+ const cell = await firstCell(editor);
+ // assume the first cell is a code cell
+ expect(await cell.isCodeCell()).toBe(true);
+
+ await cell.addEditorText('print("Hallo")');
+ const cellText = await cell.editorText();
+ expect(cellText).toBe('print("Hallo")');
+ });
+
+ test('should write multi-line text in a code cell', async () => {
+ const cell = await firstCell(editor);
+ await cell.addEditorText('print("Hallo")\nprint("Notebook")');
+
+ const cellText = await cell.editorText();
+ expect(cellText).toBe('print("Hallo")\nprint("Notebook")');
+ });
+
+ test('Execute code cell and read output', async () => {
+ const cell = await firstCell(editor);
+ await cell.addEditorText('print("Hallo Notebook!")');
+ await cell.execute();
+
+ const cellOutput = await cell.outputText();
+ expect(cellOutput).toBe('Hallo Notebook!');
+ });
+
+ test('Check execution count matches', async () => {
+ const cell = await firstCell(editor);
+ await cell.addEditorText('print("Hallo Notebook!")');
+ await cell.execute();
+ await cell.execute();
+ await cell.execute();
+
+ expect(await cell.executionCount()).toBe('3');
+ });
+
+ test('Check arrow up and down works', async () => {
+ const cell = await firstCell(editor);
+ await editor.addCodeCell();
+ const secondCell = (await editor.cells())[1];
+ // second cell is selected after creation
+ expect(await secondCell.isSelected()).toBe(true);
+ // select cell above
+ await editor.page.keyboard.type('second cell');
+ await secondCell.editor.page.keyboard.press('ArrowUp');
+ expect(await cell.isSelected()).toBe(true);
+
+ // select cell below
+ await cell.app.page.keyboard.press('ArrowDown');
+ expect(await secondCell.isSelected()).toBe(true);
+ });
+
+ test('Check k(up)/j(down) selection works', async () => {
+ const cell = await firstCell(editor);
+ await editor.addCodeCell();
+ const secondCell = (await editor.cells())[1];
+ // second cell is selected after creation
+ expect(await secondCell.isSelected()).toBe(true);
+ await secondCell.selectCell(); // deselect editor focus
+
+ // select cell above
+ await secondCell.editor.page.keyboard.press('k');
+ expect(await cell.isSelected()).toBe(true);
+
+ // select cell below
+ await cell.app.page.keyboard.press('j');
+ expect(await secondCell.isSelected()).toBe(true);
+ });
+
+ test('Check x/c/v works', async () => {
+ const cell = await firstCell(editor);
+ await cell.addEditorText('print("First cell")');
+ await editor.addCodeCell();
+ const secondCell = (await editor.cells())[1];
+ await secondCell.addEditorText('print("Second cell")');
+ await secondCell.selectCell(); // deselect editor focus
+
+ // cut second cell
+ await secondCell.page.keyboard.press('x');
+ await editor.waitForCellCountChanged(2);
+ expect((await editor.cells()).length).toBe(1);
+
+ // paste second cell
+ await cell.selectCell();
+ await cell.page.keyboard.press('v');
+ await editor.waitForCellCountChanged(1);
+ expect((await editor.cells()).length).toBe(2);
+ const pastedCell = (await editor.cells())[1];
+ expect(await pastedCell.isSelected()).toBe(true);
+
+ // copy first cell
+ await cell.selectCell(); // deselect editor focus
+ await cell.page.keyboard.press('c');
+ // paste copied cell
+ await cell.page.keyboard.press('v');
+ await editor.waitForCellCountChanged(2);
+ expect((await editor.cells()).length).toBe(3);
+ expect(await (await editor.cells())[0].editorText()).toBe('print("First cell")');
+ expect(await (await editor.cells())[1].editorText()).toBe('print("First cell")');
+ expect(await (await editor.cells())[2].editorText()).toBe('print("Second cell")');
+ });
+
+ test('Check LineNumber switch `l` works', async () => {
+ const cell = await firstCell(editor);
+ await cell.addEditorText('print("First cell")');
+ await cell.selectCell();
+ await cell.page.keyboard.press('l');
+ // NOTE: div.line-numbers is not visible
+ await cell.editor.locator.locator('.overflow-guard > div.line-numbers').waitFor({ state: 'attached' });
+ });
+
+ test('Check Collapse output switch `o` works', async () => {
+ const cell = await firstCell(editor);
+ await cell.addEditorText('print("Check output collapse")');
+ await cell.selectCell();
+ await cell.execute(); // produce output
+ expect(await cell.outputText()).toBe('Check output collapse');
+
+ await cell.page.keyboard.press('o');
+ await (await cell.outputContainer()).waitFor({ state: 'hidden' });
+ await cell.page.keyboard.press('o');
+ await (await cell.outputContainer()).waitFor({ state: 'visible' });
+
+ expect(await cell.outputText()).toBe('Check output collapse');
+ });
+
+ test('Check arrow-up/arrow-down/escape with code completion', async () => {
+ await editor.addMarkdownCell();
+ const mdCell = (await editor.cells())[1];
+ await mdCell.addEditorText('h');
+
+ await editor.page.keyboard.press('Control+Space'); // call CC (suggestWidgetVisible=true)
+ await ensureCodeCompletionVisible(mdCell.editor.locator);
+ await editor.page.keyboard.press('Escape'); // close CC
+ // check the same cell still selected and not lose the edit mode
+ expect(await mdCell.editor.isFocused()).toBe(true);
+
+ await editor.page.keyboard.press('Control+Space'); // call CC (suggestWidgetVisible=true)
+ await ensureCodeCompletionVisible(mdCell.editor.locator);
+ await editor.page.keyboard.press('ArrowUp'); // select next entry in CC list
+ await editor.page.keyboard.press('Enter'); // apply completion
+ // check the same cell still selected and not the second one due to 'ArrowDown' being pressed
+ expect(await mdCell.isSelected()).toBe(true);
+
+ });
+});
+
+async function ensureCodeCompletionVisible(parent: Locator): Promise {
+ await parent.locator('div.monaco-editor div.suggest-widget').waitFor({ timeout: 5000 });
+}
+
+async function firstCell(editor: TheiaNotebookEditor): Promise {
+ return (await editor.cells())[0];
+}
+
+async function loadApp(args: TheiaPlaywrightTestConfig & PlaywrightWorkerArgs): Promise {
+ const workingDir = path.resolve();
+ // correct WS path. When running from IDE the path is playwright/configs with CLI it's playwright/
+ const prefix = workingDir.endsWith('playwright/configs') ? '../' : '';
+ const ws = new TheiaWorkspace([prefix + 'src/tests/resources/notebook-files']);
+ const app = await TheiaAppLoader.load(args, ws);
+ // auto-save are disabled using settings.json file
+ // see examples/playwright/src/tests/resources/notebook-files/.theia/settings.json
+
+ // NOTE: Workspace trust is disabled in examples/browser/package.json using default preferences.
+ // If workspace trust check is on, python extension will not be able to explore Python installations.
+ return app;
+}
diff --git a/examples/playwright/src/tests/theia-quick-command.test.ts b/examples/playwright/src/tests/theia-quick-command.test.ts
index 4f9b8352cc4b6..f24ee87522df0 100644
--- a/examples/playwright/src/tests/theia-quick-command.test.ts
+++ b/examples/playwright/src/tests/theia-quick-command.test.ts
@@ -61,8 +61,8 @@ test.describe('Theia Quick Command', () => {
});
test('should trigger \'Toggle Explorer View\' command after typing', async () => {
- await quickCommand.type('Toggle Explorer');
- await quickCommand.trigger('Toggle Explorer View');
+ await quickCommand.type('Toggle Exp');
+ await quickCommand.trigger('View: Toggle Explorer');
expect(await quickCommand.isOpen()).toBe(false);
const explorerView = new TheiaExplorerView(app);
expect(await explorerView.isDisplayed()).toBe(true);
@@ -77,4 +77,10 @@ test.describe('Theia Quick Command', () => {
expect(await notification.isEntryVisible('Positive Integer: 6')).toBe(true);
});
+ test('retrieve and check visible items', async () => {
+ await quickCommand.type('close all tabs', false);
+ const listItems = await Promise.all((await quickCommand.visibleItems()).map(async item => item.textContent()));
+ expect(listItems).toContain('View: Close All Tabs in Main Area');
+ });
+
});
diff --git a/examples/playwright/src/theia-app-loader.ts b/examples/playwright/src/theia-app-loader.ts
index 08b9084c5d3ba..f420d95c329a2 100644
--- a/examples/playwright/src/theia-app-loader.ts
+++ b/examples/playwright/src/theia-app-loader.ts
@@ -22,7 +22,7 @@ import { TheiaWorkspace } from './theia-workspace';
import { OSUtil } from './util';
export interface TheiaAppFactory {
- new(page: Page, initialWorkspace?: TheiaWorkspace, isElectron?: boolean): T;
+ new(page: Page, initialWorkspace: TheiaWorkspace, isElectron?: boolean): T;
}
// TODO this is just a sketch, we need a proper way to configure tests and pass this configuration to the `TheiaAppLoader`:
diff --git a/examples/playwright/src/theia-app.ts b/examples/playwright/src/theia-app.ts
index ab7aeba5e71e9..43b102df4f156 100644
--- a/examples/playwright/src/theia-app.ts
+++ b/examples/playwright/src/theia-app.ts
@@ -100,7 +100,8 @@ export class TheiaApp {
return view;
}
- async openEditor(filePath: string, editorFactory: { new(filePath: string, app: TheiaApp): T },
+ async openEditor(filePath: string,
+ editorFactory: { new(fp: string, app: TheiaApp): T },
editorName?: string, expectFileNodes = true): Promise {
const explorer = await this.openView(TheiaExplorerView);
if (!explorer) {
@@ -135,7 +136,7 @@ export class TheiaApp {
return editor;
}
- async activateExistingEditor(filePath: string, editorFactory: { new(filePath: string, app: TheiaApp): T }): Promise {
+ async activateExistingEditor(filePath: string, editorFactory: { new(fp: string, app: TheiaApp): T }): Promise {
const editor = new editorFactory(filePath, this);
if (!await editor.isTabVisible()) {
throw new Error(`Could not find opened editor for file ${filePath}`);
diff --git a/examples/playwright/src/theia-monaco-editor.ts b/examples/playwright/src/theia-monaco-editor.ts
index ac0fd5290453b..7cfbf4b492b65 100644
--- a/examples/playwright/src/theia-monaco-editor.ts
+++ b/examples/playwright/src/theia-monaco-editor.ts
@@ -27,7 +27,7 @@ export class TheiaMonacoEditor extends TheiaPageObject {
await this.page.waitForSelector(this.selector, { state: 'visible' });
}
- protected viewElement(): Promise | null> {
+ protected async viewElement(): Promise | null> {
return this.page.$(this.selector);
}
@@ -74,6 +74,49 @@ export class TheiaMonacoEditor extends TheiaPageObject {
return viewElement?.waitForSelector(`.view-lines .view-line:has-text("${text}")`);
}
+ /**
+ * @returns The text content of the editor.
+ */
+ async editorText(): Promise {
+ const lines: string[] = [];
+ const linesCount = await this.numberOfLines();
+ if (linesCount === undefined) {
+ return undefined;
+ }
+ for (let line = 1; line <= linesCount; line++) {
+ const lineText = await this.textContentOfLineByLineNumber(line);
+ if (lineText === undefined) {
+ break;
+ }
+ lines.push(lineText);
+ }
+ return lines.join('\n');
+ }
+
+ /**
+ * Adds text to the editor.
+ * @param text The text to add to the editor.
+ * @param lineNumber The line number where to add the text. Default is 1.
+ */
+ async addEditorText(text: string, lineNumber: number = 1): Promise {
+ const line = await this.lineByLineNumber(lineNumber);
+ await line?.click();
+ await this.page.keyboard.type(text);
+ }
+
+ /**
+ * @returns `true` if the editor is focused, `false` otherwise.
+ */
+ async isFocused(): Promise {
+ const viewElement = await this.viewElement();
+ const monacoEditor = await viewElement?.$('div.monaco-editor');
+ if (!monacoEditor) {
+ throw new Error('Couldn\'t retrieve monaco editor element.');
+ }
+ const editorClass = await monacoEditor.getAttribute('class');
+ return editorClass?.includes('focused') ?? false;
+ }
+
protected replaceEditorSymbolsWithSpace(content: string): string | Promise {
// [ ] => \u00a0 -- NO-BREAK SPACE
// [·] · => \u00b7 -- MIDDLE DOT
diff --git a/examples/playwright/src/theia-notebook-cell.ts b/examples/playwright/src/theia-notebook-cell.ts
new file mode 100644
index 0000000000000..0478a680ac8c4
--- /dev/null
+++ b/examples/playwright/src/theia-notebook-cell.ts
@@ -0,0 +1,247 @@
+// *****************************************************************************
+// Copyright (C) 2024 TypeFox GmbH and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+import { ElementHandle, FrameLocator, Locator } from '@playwright/test';
+import { TheiaApp } from './theia-app';
+import { TheiaMonacoEditor } from './theia-monaco-editor';
+import { TheiaPageObject } from './theia-page-object';
+
+export type CellStatus = 'success' | 'error' | 'waiting';
+
+/**
+ * Page object for a Theia notebook cell.
+ */
+export class TheiaNotebookCell extends TheiaPageObject {
+
+ protected monacoEditor: TheiaEmbeddedMonacoEditor;
+
+ constructor(protected readonly locator: Locator, protected readonly notebookEditorLocator: Locator, app: TheiaApp) {
+ super(app);
+ const editorLocator = locator.locator('div.theia-notebook-cell-editor');
+ this.monacoEditor = new TheiaEmbeddedMonacoEditor(editorLocator, app);
+ }
+
+ /**
+ * @returns The monaco editor page object of the cell.
+ */
+ get editor(): TheiaEmbeddedMonacoEditor {
+ return this.monacoEditor;
+ }
+
+ /**
+ * @returns Locator for the sidebar (left) of the cell.
+ */
+ sidebar(): Locator {
+ return this.locator.locator('div.theia-notebook-cell-sidebar');
+ }
+
+ /**
+ * @returns Locator for the toolbar (top) of the cell.
+ */
+ toolbar(): Locator {
+ return this.locator.locator('div.theia-notebook-cell-toolbar');
+ }
+ /**
+ * @returns Locator for the statusbar (bottom) of the cell.
+ */
+ statusbar(): Locator {
+ return this.locator.locator('div.notebook-cell-status');
+ }
+
+ /**
+ * @returns Locator for the status icon inside the statusbar of the cell.
+ */
+ statusIcon(): Locator {
+ return this.statusbar().locator('span.notebook-cell-status-item');
+ }
+
+ /**
+ * @returns `true` id the cell is a code cell, `false` otherwise.
+ */
+ async isCodeCell(): Promise {
+ const classAttribute = await this.mode();
+ return classAttribute !== 'markdown';
+ }
+
+ /**
+ * @returns The mode of the cell, e.g. 'python', 'markdown', etc.
+ */
+ async mode(): Promise {
+ await this.locator.waitFor({ state: 'visible' });
+ const editorElement = await this.editor.locator.elementHandle();
+ if (editorElement === null) {
+ throw new Error('Could not find editor element for the notebook cell.');
+ }
+ const classAttribute = await editorElement.getAttribute('data-mode-id');
+ if (classAttribute === null) {
+ throw new Error('Could not find mode attribute for the notebook cell.');
+ }
+ return classAttribute;
+ }
+
+ /**
+ * @returns The text content of the cell editor.
+ */
+ async editorText(): Promise {
+ return this.editor.editorText();
+ }
+
+ /**
+ * Adds text to the editor of the cell.
+ * @param text The text to add to the editor.
+ * @param lineNumber The line number where to add the text. Default is 1.
+ */
+ async addEditorText(text: string, lineNumber: number = 1): Promise {
+ await this.editor.addEditorText(text, lineNumber);
+ }
+
+ /**
+ * @param wait If `true` waits for the cell to finish execution, otherwise returns immediately.
+ */
+ async execute(wait = true): Promise {
+ const execButton = this.sidebar().locator('[id="notebook.cell.execute-cell"]');
+ await execButton.waitFor({ state: 'visible' });
+ await execButton.click();
+ if (wait) {
+ // wait for the cell to finish execution
+ await this.waitForCellStatus('success', 'error');
+ }
+ }
+
+ /**
+ * Splits the cell into two cells by dividing the cell text on current cursor position.
+ */
+ async splitCell(): Promise {
+ const execButton = this.toolbar().locator('[id="notebook.cell.split"]');
+ await execButton.waitFor({ state: 'visible' });
+ await execButton.click();
+ }
+
+ /**
+ * Waits for the cell to reach a specific status.
+ * @param status The status to wait for. Possible values are 'success', 'error', 'waiting'.
+ */
+ async waitForCellStatus(...status: CellStatus[]): Promise {
+ await this.statusIcon().waitFor({ state: 'visible' });
+ await this.statusIcon().evaluate(
+ (element, expect) => {
+ if (expect.length === 0) {
+ return true;
+ }
+ const classes = element.getAttribute('class');
+ if (classes !== null) {
+ const cellStatus = classes.includes('codicon-check') ? 'success'
+ : classes.includes('codicon-error') ? 'error'
+ : 'waiting';
+ return expect.includes(cellStatus);
+ }
+ return false;
+ }, status);
+ }
+
+ /**
+ * @returns The status of the cell. Possible values are 'success', 'error', 'waiting'.
+ */
+ async status(): Promise {
+ const statusLocator = this.statusIcon();
+ const status = this.toCellStatus(await (await statusLocator.elementHandle())?.getAttribute('class') ?? '');
+ return status;
+ }
+
+ protected toCellStatus(classes: string): CellStatus {
+ return classes.includes('codicon-check') ? 'success'
+ : classes.includes('codicon-error') ? 'error'
+ : 'waiting';
+ }
+
+ /**
+ * @returns The execution count of the cell.
+ */
+ async executionCount(): Promise {
+ const countNode = this.sidebar().locator('span.theia-notebook-code-cell-execution-order');
+ await countNode.waitFor({ state: 'visible' });
+ await this.waitForCellStatus('success', 'error');
+ const text = await countNode.textContent();
+ return text?.substring(1, text.length - 1);
+ }
+
+ /**
+ * @returns `true` if the cell is selected (blue vertical line), `false` otherwise.
+ */
+ async isSelected(): Promise {
+ const markerClass = await this.locator.locator('div.theia-notebook-cell-marker').getAttribute('class');
+ return markerClass?.includes('theia-notebook-cell-marker-selected') ?? false;
+ }
+
+ /**
+ * @returns The output text of the cell.
+ */
+ async outputText(): Promise {
+ const outputContainer = await this.outputContainer();
+ await outputContainer.waitFor({ state: 'visible' });
+ // By default just collect all spans text.
+ const spansLocator: Locator = outputContainer.locator('span:not(:has(*))'); // ignore nested spans
+ const spanTexts = await spansLocator.evaluateAll(spans => spans.map(span => span.textContent?.trim())
+ .filter(text => text !== undefined && text.length > 0));
+ return spanTexts.join('');
+ }
+
+ /**
+ * Selects the cell itself not it's editor. Important for shortcut usage like copy-, cut-, paste-cell.
+ */
+ async selectCell(): Promise {
+ await this.sidebar().click();
+ }
+
+ async outputContainer(): Promise {
+ const outFrame = await this.outputFrame();
+ // each cell has it's own output div with a unique id = cellHandle
+ const cellOutput = outFrame.locator(`div#cellHandle${await this.cellHandle()}`);
+ return cellOutput.locator('div.output-container');
+ }
+
+ protected async cellHandle(): Promise {
+ const handle = await this.locator.getAttribute('data-cell-handle');
+ if (handle === null) {
+ throw new Error('Could not find cell handle attribute `data-cell-handle` for the notebook cell.');
+ }
+ return handle;
+ }
+
+ protected async outputFrame(): Promise {
+ const containerDiv = this.notebookEditorLocator.locator('div.theia-notebook-cell-output-webview');
+ const webViewFrame = containerDiv.frameLocator('iframe.webview');
+ await webViewFrame.locator('iframe').waitFor({ state: 'attached' });
+ return webViewFrame.frameLocator('iframe');
+ }
+
+}
+
+export class TheiaEmbeddedMonacoEditor extends TheiaMonacoEditor {
+
+ constructor(public readonly locator: Locator, app: TheiaApp) {
+ super('', app);
+ }
+
+ override async waitForVisible(): Promise {
+ // Use locator instead of page to find the editor element.
+ await this.locator.waitFor({ state: 'visible' });
+ }
+
+ protected override viewElement(): Promise | null> {
+ // Use locator instead of page to find the editor element.
+ return this.locator.elementHandle();
+ }
+}
diff --git a/examples/playwright/src/theia-notebook-editor.ts b/examples/playwright/src/theia-notebook-editor.ts
new file mode 100644
index 0000000000000..31ddbd7c3ef42
--- /dev/null
+++ b/examples/playwright/src/theia-notebook-editor.ts
@@ -0,0 +1,171 @@
+// *****************************************************************************
+// Copyright (C) 2024 TypeFox GmbH and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { Locator } from '@playwright/test';
+import { join } from 'path';
+import { TheiaApp } from './theia-app';
+import { TheiaEditor } from './theia-editor';
+import { TheiaNotebookCell } from './theia-notebook-cell';
+import { TheiaNotebookToolbar } from './theia-notebook-toolbar';
+import { TheiaQuickCommandPalette } from './theia-quick-command-palette';
+import { TheiaToolbarItem } from './theia-toolbar-item';
+import { OSUtil, normalizeId, urlEncodePath } from './util';
+
+export namespace NotebookCommands {
+ export const SELECT_KERNEL_COMMAND = 'notebook.selectKernel';
+ export const ADD_NEW_CELL_COMMAND = 'notebook.add-new-code-cell';
+ export const ADD_NEW_MARKDOWN_CELL_COMMAND = 'notebook.add-new-markdown-cell';
+ export const EXECUTE_NOTEBOOK_COMMAND = 'notebook.execute';
+ export const CLEAR_ALL_OUTPUTS_COMMAND = 'notebook.clear-all-outputs';
+ export const EXPORT_COMMAND = 'jupyter.notebookeditor.export';
+}
+
+export class TheiaNotebookEditor extends TheiaEditor {
+
+ constructor(filePath: string, app: TheiaApp) {
+ // shell-tab-notebook::file://
+ // notebook:file://
+ super({
+ tabSelector: normalizeId(`#shell-tab-notebook:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`),
+ viewSelector: normalizeId(`#notebook:file://${urlEncodePath(join(app.workspace.escapedPath, OSUtil.fileSeparator, filePath))}`)
+ }, app);
+ }
+
+ protected viewLocator(): Locator {
+ return this.page.locator(this.data.viewSelector);
+ }
+
+ tabLocator(): Locator {
+ return this.page.locator(this.data.viewSelector);
+ }
+
+ override async waitForVisible(): Promise {
+ await super.waitForVisible();
+ // wait for toolbar being rendered as it takes some time to load the kernel data.
+ await this.notebookToolbar().waitForVisible();
+ }
+
+ /**
+ * @returns The main toolbar of the notebook editor.
+ */
+ notebookToolbar(): TheiaNotebookToolbar {
+ return new TheiaNotebookToolbar(this.viewLocator(), this.app);
+ }
+
+ /**
+ * @returns The name of the selected kernel.
+ */
+ async selectedKernel(): Promise {
+ const kernelItem = await this.toolbarItem(NotebookCommands.SELECT_KERNEL_COMMAND);
+ if (!kernelItem) {
+ throw new Error('Select kernel toolbar item not found.');
+ }
+ return this.notebookToolbar().locator.locator('#kernel-text').innerText();
+ }
+
+ /**
+ * Allows to select a kernel using toolbar item.
+ * @param kernelName The name of the kernel to select.
+ */
+ async selectKernel(kernelName: string): Promise {
+ await this.triggerToolbarItem(NotebookCommands.SELECT_KERNEL_COMMAND);
+ const qInput = new TheiaQuickCommandPalette(this.app);
+ const widget = await this.page.waitForSelector(qInput.selector, { timeout: 5000 });
+ if (widget && !await qInput.isOpen()) {
+ throw new Error('Failed to trigger kernel selection');
+ }
+ await qInput.type(kernelName, true);
+ await qInput.hide();
+ }
+
+ async availableKernels(): Promise {
+ await this.triggerToolbarItem(NotebookCommands.SELECT_KERNEL_COMMAND);
+ const qInput = new TheiaQuickCommandPalette(this.app);
+ const widget = await this.page.waitForSelector(qInput.selector, { timeout: 5000 });
+ if (widget && !await qInput.isOpen()) {
+ throw new Error('Failed to trigger kernel selection');
+ }
+ await qInput.type('Python', false);
+ try {
+ const listItems = await Promise.all((await qInput.visibleItems()).map(async item => item.textContent()));
+ await this.page.keyboard.press('Enter');
+ await qInput.hide();
+ return listItems.filter(item => item !== null) as string[];
+ } finally {
+ await qInput.hide();
+ }
+ }
+
+ /**
+ * Adds a new code cell to the notebook.
+ */
+ async addCodeCell(): Promise {
+ const currentCellsCount = (await this.cells()).length;
+ await this.triggerToolbarItem(NotebookCommands.ADD_NEW_CELL_COMMAND);
+ await this.waitForCellCountChanged(currentCellsCount);
+ }
+
+ /**
+ * Adds a new markdown cell to the notebook.
+ */
+ async addMarkdownCell(): Promise {
+ const currentCellsCount = (await this.cells()).length;
+ await this.triggerToolbarItem(NotebookCommands.ADD_NEW_MARKDOWN_CELL_COMMAND);
+ await this.waitForCellCountChanged(currentCellsCount);
+ }
+
+ async waitForCellCountChanged(prevCount: number): Promise {
+ await this.viewLocator().locator('li.theia-notebook-cell').evaluateAll(
+ (elements, currentCount) => elements.length !== currentCount, prevCount
+ );
+ }
+
+ async executeAllCells(): Promise {
+ await this.triggerToolbarItem(NotebookCommands.EXECUTE_NOTEBOOK_COMMAND);
+ }
+
+ async clearAllOutputs(): Promise {
+ await this.triggerToolbarItem(NotebookCommands.CLEAR_ALL_OUTPUTS_COMMAND);
+ }
+
+ async exportAs(): Promise {
+ await this.triggerToolbarItem(NotebookCommands.EXPORT_COMMAND);
+ }
+
+ async cells(): Promise {
+ const cellsLocator = this.viewLocator().locator('li.theia-notebook-cell');
+ const cells: Array = [];
+ for (const cellLocator of await cellsLocator.all()) {
+ await cellLocator.waitFor({ state: 'visible' });
+ cells.push(new TheiaNotebookCell(cellLocator, this.viewLocator(), this.app));
+ }
+ return cells;
+ }
+
+ protected async triggerToolbarItem(id: string): Promise {
+ const item = await this.toolbarItem(id);
+ if (!item) {
+ throw new Error(`Toolbar item with id ${id} not found`);
+ }
+ await item.trigger();
+ }
+
+ protected async toolbarItem(id: string): Promise {
+ const toolBar = this.notebookToolbar();
+ await toolBar.waitForVisible();
+ return toolBar.toolBarItem(id);
+ }
+}
diff --git a/examples/playwright/src/theia-notebook-toolbar.ts b/examples/playwright/src/theia-notebook-toolbar.ts
new file mode 100644
index 0000000000000..3349e2267b6e4
--- /dev/null
+++ b/examples/playwright/src/theia-notebook-toolbar.ts
@@ -0,0 +1,53 @@
+// *****************************************************************************
+// Copyright (C) 2024 TypeFox GmbH and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { ElementHandle, Locator } from '@playwright/test';
+import { TheiaApp } from './theia-app';
+import { TheiaToolbar } from './theia-toolbar';
+
+export class TheiaNotebookToolbar extends TheiaToolbar {
+ public readonly locator: Locator;
+
+ constructor(parentLocator: Locator, app: TheiaApp) {
+ super(app);
+ this.selector = 'div#notebook-main-toolbar';
+ this.locator = parentLocator.locator(this.selector);
+ }
+
+ protected override toolBarItemSelector(toolbarItemId = ''): string {
+ return `div.theia-notebook-main-toolbar-item${toolbarItemId ? `[id="${toolbarItemId}"]` : ''}`;
+ }
+
+ protected override async toolbarElementHandle(): Promise | null> {
+ // Use locator instead of page to find the toolbar element.
+ return this.locator.elementHandle();
+ }
+
+ override async waitForVisible(): Promise {
+ // Use locator instead of page to find the toolbar element.
+ await this.locator.waitFor({ state: 'visible' });
+ }
+
+ override async waitUntilHidden(): Promise {
+ // Use locator instead of page to find the toolbar element.
+ await this.locator.waitFor({ state: 'hidden' });
+ }
+
+ override async waitUntilShown(): Promise {
+ // Use locator instead of page to find the toolbar element.
+ await this.locator.waitFor({ state: 'visible' });
+ }
+}
diff --git a/examples/playwright/src/theia-preference-view.ts b/examples/playwright/src/theia-preference-view.ts
index a1f701048b17b..1af41fdb55a21 100644
--- a/examples/playwright/src/theia-preference-view.ts
+++ b/examples/playwright/src/theia-preference-view.ts
@@ -86,8 +86,20 @@ export class TheiaPreferenceView extends TheiaView {
super(TheiaSettingsViewData, app);
}
- override async open(preferenceScope = TheiaPreferenceScope.Workspace): Promise {
- await this.app.quickCommandPalette.trigger('Preferences: Open Settings (UI)');
+ /**
+ * @param preferenceScope The preference scope (Workspace or User) to open the view for. Default is Workspace.
+ * @param useMenu If true, the view will be opened via the main menu. If false,
+ * the view will be opened via the quick command palette. Default is using the main menu.
+ * @returns The TheiaPreferenceView page object instance.
+ */
+ override async open(preferenceScope = TheiaPreferenceScope.Workspace, useMenu: boolean = true): Promise {
+ if (useMenu) {
+ const mainMenu = await this.app.menuBar.openMenu('File');
+ await (await mainMenu.menuItemByNamePath('Preferences', 'Settings'))?.click();
+ } else {
+ await this.app.quickCommandPalette.type('Preferences:');
+ await this.app.quickCommandPalette.trigger('Preferences: Open Settings (UI)');
+ }
await this.waitForVisible();
await this.openPreferenceScope(preferenceScope);
return this;
diff --git a/examples/playwright/src/theia-quick-command-palette.ts b/examples/playwright/src/theia-quick-command-palette.ts
index 2728bb0b00d21..bf9ce8c00e4a1 100644
--- a/examples/playwright/src/theia-quick-command-palette.ts
+++ b/examples/playwright/src/theia-quick-command-palette.ts
@@ -63,13 +63,11 @@ export class TheiaQuickCommandPalette extends TheiaPageObject {
if (!await this.isOpen()) {
this.open();
}
- const input = await this.page.waitForSelector(`${this.selector} .monaco-inputbox .input`);
- if (input != null) {
- await input.focus();
- await input.type(value, { delay: USER_KEY_TYPING_DELAY });
- if (confirm) {
- await this.page.keyboard.press('Enter');
- }
+ const input = this.page.locator(`${this.selector} .monaco-inputbox .input`);
+ await input.focus();
+ await input.pressSequentially(value, { delay: USER_KEY_TYPING_DELAY });
+ if (confirm) {
+ await this.page.keyboard.press('Enter');
}
}
@@ -80,4 +78,14 @@ export class TheiaQuickCommandPalette extends TheiaPageObject {
}
return command.$('.monaco-list-row.focused .monaco-highlighted-label');
}
+
+ async visibleItems(): Promise[]> {
+ // FIXME rewrite with locators
+ const command = await this.page.waitForSelector(this.selector);
+ if (!command) {
+ throw new Error('No selected command found!');
+ }
+ return command.$$('.monaco-highlighted-label');
+ }
+
}
diff --git a/examples/playwright/src/theia-rename-dialog.ts b/examples/playwright/src/theia-rename-dialog.ts
index 22114444342b5..66281c0cc3d89 100644
--- a/examples/playwright/src/theia-rename-dialog.ts
+++ b/examples/playwright/src/theia-rename-dialog.ts
@@ -15,15 +15,14 @@
// *****************************************************************************
import { TheiaDialog } from './theia-dialog';
-import { OSUtil, USER_KEY_TYPING_DELAY } from './util';
+import { USER_KEY_TYPING_DELAY } from './util';
export class TheiaRenameDialog extends TheiaDialog {
async enterNewName(newName: string): Promise {
- const inputField = await this.page.waitForSelector(`${this.blockSelector} .theia-input`);
- await inputField.press(OSUtil.isMacOS ? 'Meta+a' : 'Control+a');
- await inputField.type(newName, { delay: USER_KEY_TYPING_DELAY });
- await this.page.waitForTimeout(USER_KEY_TYPING_DELAY);
+ const inputField = this.page.locator(`${this.blockSelector} .theia-input`);
+ await inputField.selectText();
+ await inputField.pressSequentially(newName, { delay: USER_KEY_TYPING_DELAY });
}
async confirm(): Promise {
diff --git a/examples/playwright/src/theia-terminal.ts b/examples/playwright/src/theia-terminal.ts
index 053cc63a46524..72fd695e06fe9 100644
--- a/examples/playwright/src/theia-terminal.ts
+++ b/examples/playwright/src/theia-terminal.ts
@@ -38,7 +38,7 @@ export class TheiaTerminal extends TheiaView {
async write(text: string): Promise {
await this.activate();
const input = await this.waitForInputArea();
- await input.type(text);
+ await input.fill(text);
}
async contents(): Promise {
diff --git a/examples/playwright/src/theia-welcome-view.ts b/examples/playwright/src/theia-welcome-view.ts
new file mode 100644
index 0000000000000..ce68a7f02f2df
--- /dev/null
+++ b/examples/playwright/src/theia-welcome-view.ts
@@ -0,0 +1,31 @@
+// *****************************************************************************
+// Copyright (C) 2023 Toro Cloud Pty Ltd and others.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+
+import { TheiaApp } from './theia-app';
+import { TheiaView } from './theia-view';
+import { normalizeId } from './util';
+
+const TheiaWelcomeViewData = {
+ tabSelector: normalizeId('#shell-tab-getting.started.widget'),
+ viewSelector: normalizeId('#getting.started.widget'),
+ viewName: 'Welcome'
+};
+
+export class TheiaWelcomeView extends TheiaView {
+
+ constructor(app: TheiaApp) {
+ super(TheiaWelcomeViewData, app);
+ }
+}
diff --git a/lerna.json b/lerna.json
index 4e84189a3d20a..63463ee9ae474 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/lerna.json",
"npmClient": "yarn",
- "version": "1.44.0",
+ "version": "1.54.0",
"command": {
"run": {
"stream": true
diff --git a/package.json b/package.json
index eedb0306c48a5..04f0c7ae15d84 100644
--- a/package.json
+++ b/package.json
@@ -4,23 +4,23 @@
"version": "0.0.0",
"engines": {
"yarn": ">=1.7.0 <2",
- "node": ">=16"
+ "node": ">=18"
},
"resolutions": {
- "**/@types/node": "18"
+ "**/@types/node": "18",
+ "**/nan": "2.20.0"
},
"devDependencies": {
"@types/chai": "4.3.0",
"@types/chai-spies": "1.0.3",
"@types/chai-string": "^1.4.0",
- "@types/jsdom": "^21.1.1",
+ "@types/jsdom": "^21.1.7",
"@types/node": "18",
"@types/sinon": "^10.0.6",
"@types/temp": "^0.9.1",
- "@types/uuid": "^7.0.3",
- "@typescript-eslint/eslint-plugin": "^4.8.1",
- "@typescript-eslint/eslint-plugin-tslint": "^4.8.1",
- "@typescript-eslint/parser": "^4.8.1",
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
+ "@typescript-eslint/eslint-plugin-tslint": "^6.21.0",
+ "@typescript-eslint/parser": "^6.21.0",
"@vscode/vsce": "^2.15.0",
"archiver": "^5.3.1",
"chai": "4.3.10",
@@ -29,7 +29,7 @@
"chalk": "4.0.0",
"concurrently": "^3.5.0",
"debug": "^4.3.2",
- "electron-mocha": "^11.0.2",
+ "electron-mocha": "^12.3.0",
"eslint": "7",
"eslint-plugin-deprecation": "~1.2.1",
"eslint-plugin-import": "^2.27.5",
@@ -40,30 +40,29 @@
"if-env": "^1.0.4",
"ignore-styles": "^5.0.1",
"improved-yarn-audit": "^3.0.0",
- "jsdom": "^21.1.1",
+ "jsdom": "^22.1.0",
"lerna": "^7.1.1",
"mkdirp": "^0.5.0",
"node-gyp": "^9.0.0",
- "nsfw": "^2.2.4",
"nyc": "^15.0.0",
- "puppeteer": "19.7.2",
- "puppeteer-core": "19.7.2",
+ "puppeteer": "23.1.0",
+ "puppeteer-core": "23.1.0",
"puppeteer-to-istanbul": "1.4.0",
- "rimraf": "^2.6.1",
+ "rimraf": "^5.0.0",
"sinon": "^12.0.0",
"temp": "^0.9.1",
"tslint": "^5.12.0",
"typedoc": "^0.22.11",
"typedoc-plugin-external-module-map": "1.3.2",
- "typescript": "~4.5.5",
- "uuid": "^8.0.0",
+ "typescript": "~5.4.5",
"yargs": "^15.3.1"
},
"scripts": {
"all": "yarn -s install && yarn -s lint && yarn -s build",
"browser": "yarn -s --cwd examples/browser",
+ "browser-only": "yarn -s --cwd examples/browser-only",
"build": "yarn -s compile && yarn -s build:examples",
- "build:examples": "yarn browser build && yarn electron build",
+ "build:examples": "yarn browser build && yarn electron build && yarn browser-only build",
"clean": "yarn -s rebuild:clean && yarn -s lint:clean && node scripts/run-reverse-topo.js yarn -s clean",
"compile": "echo Compiling TypeScript sources... && yarn -s compile:clean && yarn -s compile:tsc",
"compile:clean": "ts-clean dev-packages/* packages/*",
@@ -78,8 +77,9 @@
"lint:clean": "rimraf .eslintcache",
"lint:oneshot": "node --max-old-space-size=4096 node_modules/eslint/bin/eslint.js --cache=true \"{dev-packages,packages,examples}/**/*.{ts,tsx}\"",
"preinstall": "node-gyp install",
+ "postinstall": "theia-patch",
"prepare": "yarn -s compile:references && lerna run prepare && yarn -s compile",
- "publish:latest": "lerna publish --exact --yes --no-push && yarn -s publish:check",
+ "publish:latest": "lerna publish --exact --yes --no-push",
"publish:next": "lerna publish preminor --exact --canary --preid next --dist-tag next --no-git-reset --no-git-tag-version --no-push --yes && yarn -s publish:check",
"publish:check": "node scripts/check-publish.js",
"rebuild:clean": "rimraf .browser_modules",
@@ -102,20 +102,19 @@
],
"theiaPluginsDir": "plugins",
"theiaPlugins": {
- "eclipse-theia.builtin-extension-pack": "https://open-vsx.org/api/eclipse-theia/builtin-extension-pack/1.79.0/file/eclipse-theia.builtin-extension-pack-1.79.0.vsix",
+ "eclipse-theia.builtin-extension-pack": "https://open-vsx.org/api/eclipse-theia/builtin-extension-pack/1.88.1/file/eclipse-theia.builtin-extension-pack-1.88.1.vsix",
"EditorConfig.EditorConfig": "https://open-vsx.org/api/EditorConfig/EditorConfig/0.16.6/file/EditorConfig.EditorConfig-0.16.6.vsix",
"dbaeumer.vscode-eslint": "https://open-vsx.org/api/dbaeumer/vscode-eslint/2.4.2/file/dbaeumer.vscode-eslint-2.4.2.vsix",
- "ms-vscode.js-debug": "https://open-vsx.org/api/ms-vscode/js-debug/1.78.0/file/ms-vscode.js-debug-1.78.0.vsix",
- "ms-vscode.js-debug-companion": "https://open-vsx.org/api/ms-vscode/js-debug-companion/1.1.2/file/ms-vscode.js-debug-companion-1.1.2.vsix"
+ "ms-toolsai.jupyter": "https://open-vsx.org/api/ms-toolsai/jupyter/2024.6.0/file/ms-toolsai.jupyter-2024.6.0.vsix",
+ "ms-python.python": "https://open-vsx.org/api/ms-python/python/2024.12.3/file/ms-python.python-2024.12.3.vsix"
},
"theiaPluginsExcludeIds": [
"ms-vscode.js-debug-companion",
"vscode.extension-editing",
- "vscode.git",
- "vscode.git-base",
"vscode.github",
"vscode.github-authentication",
"vscode.microsoft-authentication",
- "ms-vscode.references-view"
+ "ms-vscode.references-view",
+ "ms-python.vscode-pylance"
]
}
diff --git a/packages/ai-chat-ui/.eslintrc.js b/packages/ai-chat-ui/.eslintrc.js
new file mode 100644
index 0000000000000..13089943582b6
--- /dev/null
+++ b/packages/ai-chat-ui/.eslintrc.js
@@ -0,0 +1,10 @@
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ extends: [
+ '../../configs/build.eslintrc.json'
+ ],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: 'tsconfig.json'
+ }
+};
diff --git a/packages/ai-chat-ui/README.md b/packages/ai-chat-ui/README.md
new file mode 100644
index 0000000000000..3638d69df5491
--- /dev/null
+++ b/packages/ai-chat-ui/README.md
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
ECLIPSE THEIA - AI Chat UI EXTENSION
+
+
+
+
+
+## Description
+
+The `@theia/ai-chat-ui` extension contributes the `AI Chat` view.\
+The `AI Chat view` can be used to easily communicate with a language model.
+
+It is based on `@theia/ai-chat`.
+
+## Additional Information
+
+- [Theia - GitHub](https://github.com/eclipse-theia/theia)
+- [Theia - Website](https://theia-ide.org/)
+
+## License
+
+- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
+- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
+
+## Trademark
+"Theia" is a trademark of the Eclipse Foundation
+https://www.eclipse.org/theia
diff --git a/packages/ai-chat-ui/package.json b/packages/ai-chat-ui/package.json
new file mode 100644
index 0000000000000..0edf7837f7294
--- /dev/null
+++ b/packages/ai-chat-ui/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "@theia/ai-chat-ui",
+ "version": "1.54.0",
+ "description": "Theia - AI Chat UI Extension",
+ "dependencies": {
+ "@theia/ai-core": "1.54.0",
+ "@theia/ai-chat": "1.54.0",
+ "@theia/core": "1.54.0",
+ "@theia/editor": "1.54.0",
+ "@theia/filesystem": "1.54.0",
+ "@theia/monaco": "1.54.0",
+ "@theia/monaco-editor-core": "1.83.101",
+ "@theia/editor-preview": "1.54.0",
+ "@theia/workspace": "1.54.0",
+ "minimatch": "^5.1.0",
+ "tslib": "^2.6.2",
+ "uuid": "^9.0.1"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "theiaExtensions": [
+ {
+ "frontend": "lib/browser/ai-chat-ui-frontend-module",
+ "secondaryWindow": "lib/browser/ai-chat-ui-frontend-module"
+ }
+ ],
+ "keywords": [
+ "theia-extension"
+ ],
+ "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/eclipse-theia/theia.git"
+ },
+ "bugs": {
+ "url": "https://github.com/eclipse-theia/theia/issues"
+ },
+ "homepage": "https://github.com/eclipse-theia/theia",
+ "files": [
+ "lib",
+ "src"
+ ],
+ "scripts": {
+ "build": "theiaext build",
+ "clean": "theiaext clean",
+ "compile": "theiaext compile",
+ "lint": "theiaext lint",
+ "test": "theiaext test",
+ "watch": "theiaext watch"
+ },
+ "devDependencies": {
+ "@theia/ext-scripts": "1.54.0"
+ },
+ "nyc": {
+ "extends": "../../configs/nyc.json"
+ }
+}
diff --git a/packages/ai-chat-ui/src/browser/ai-chat-ui-contribution.ts b/packages/ai-chat-ui/src/browser/ai-chat-ui-contribution.ts
new file mode 100644
index 0000000000000..3c7c25eac7472
--- /dev/null
+++ b/packages/ai-chat-ui/src/browser/ai-chat-ui-contribution.ts
@@ -0,0 +1,171 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource GmbH.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { inject, injectable } from '@theia/core/shared/inversify';
+import { CommandRegistry, QuickInputButton, QuickInputService, QuickPickItem } from '@theia/core';
+import { Widget } from '@theia/core/lib/browser';
+import { AI_CHAT_NEW_CHAT_WINDOW_COMMAND, AI_CHAT_SHOW_CHATS_COMMAND, ChatCommands } from './chat-view-commands';
+import { ChatAgentLocation, ChatService } from '@theia/ai-chat';
+import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
+import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
+import { ChatViewWidget } from './chat-view-widget';
+import { Deferred } from '@theia/core/lib/common/promise-util';
+import { SecondaryWindowHandler } from '@theia/core/lib/browser/secondary-window-handler';
+
+export const AI_CHAT_TOGGLE_COMMAND_ID = 'aiChat:toggle';
+
+@injectable()
+export class AIChatContribution extends AbstractViewContribution implements TabBarToolbarContribution {
+
+ @inject(ChatService)
+ protected readonly chatService: ChatService;
+ @inject(QuickInputService)
+ protected readonly quickInputService: QuickInputService;
+
+ protected static readonly REMOVE_CHAT_BUTTON: QuickInputButton = {
+ iconClass: 'codicon-remove-close',
+ tooltip: 'Remove Chat',
+ };
+
+ @inject(SecondaryWindowHandler)
+ protected readonly secondaryWindowHandler: SecondaryWindowHandler;
+
+ constructor() {
+ super({
+ widgetId: ChatViewWidget.ID,
+ widgetName: ChatViewWidget.LABEL,
+ defaultWidgetOptions: {
+ area: 'left',
+ rank: 100
+ },
+ toggleCommandId: AI_CHAT_TOGGLE_COMMAND_ID,
+ toggleKeybinding: 'ctrlcmd+shift+e'
+ });
+ }
+
+ override registerCommands(registry: CommandRegistry): void {
+ super.registerCommands(registry);
+ registry.registerCommand(ChatCommands.SCROLL_LOCK_WIDGET, {
+ isEnabled: widget => this.withWidget(widget, chatWidget => !chatWidget.isLocked),
+ isVisible: widget => this.withWidget(widget, chatWidget => !chatWidget.isLocked),
+ execute: widget => this.withWidget(widget, chatWidget => {
+ chatWidget.lock();
+ return true;
+ })
+ });
+ registry.registerCommand(ChatCommands.SCROLL_UNLOCK_WIDGET, {
+ isEnabled: widget => this.withWidget(widget, chatWidget => chatWidget.isLocked),
+ isVisible: widget => this.withWidget(widget, chatWidget => chatWidget.isLocked),
+ execute: widget => this.withWidget(widget, chatWidget => {
+ chatWidget.unlock();
+ return true;
+ })
+ });
+ registry.registerCommand(AI_CHAT_NEW_CHAT_WINDOW_COMMAND, {
+ execute: () => this.chatService.createSession(ChatAgentLocation.Panel, { focus: true }),
+ isEnabled: widget => this.withWidget(widget, () => true),
+ isVisible: widget => this.withWidget(widget, () => true),
+ });
+ registry.registerCommand(AI_CHAT_SHOW_CHATS_COMMAND, {
+ execute: () => this.selectChat(),
+ isEnabled: widget => this.withWidget(widget, () => true) && this.chatService.getSessions().length > 1,
+ isVisible: widget => this.withWidget(widget, () => true)
+ });
+ }
+
+ registerToolbarItems(registry: TabBarToolbarRegistry): void {
+ registry.registerItem({
+ id: AI_CHAT_NEW_CHAT_WINDOW_COMMAND.id,
+ command: AI_CHAT_NEW_CHAT_WINDOW_COMMAND.id,
+ tooltip: 'New Chat',
+ isVisible: widget => this.isChatViewWidget(widget)
+ });
+ registry.registerItem({
+ id: AI_CHAT_SHOW_CHATS_COMMAND.id,
+ command: AI_CHAT_SHOW_CHATS_COMMAND.id,
+ tooltip: 'Show Chats...',
+ isVisible: widget => this.isChatViewWidget(widget),
+ });
+ }
+
+ protected isChatViewWidget(widget?: Widget): boolean {
+ return !!widget && ChatViewWidget.ID === widget.id;
+ }
+
+ protected async selectChat(sessionId?: string): Promise {
+ let activeSessionId = sessionId;
+
+ if (!activeSessionId) {
+ const item = await this.askForChatSession();
+ if (item === undefined) {
+ return;
+ }
+ activeSessionId = item.id;
+ }
+
+ this.chatService.setActiveSession(activeSessionId!, { focus: true });
+ }
+
+ protected askForChatSession(): Promise {
+ const getItems = () =>
+ this.chatService.getSessions().filter(session => !session.isActive).map(session => ({
+ label: session.title ?? 'New Chat',
+ id: session.id,
+ buttons: [AIChatContribution.REMOVE_CHAT_BUTTON]
+ })).reverse();
+
+ const defer = new Deferred();
+ const quickPick = this.quickInputService.createQuickPick();
+ quickPick.placeholder = 'Select chat';
+ quickPick.canSelectMany = false;
+ quickPick.items = getItems();
+
+ quickPick.onDidTriggerItemButton(async context => {
+ this.chatService.deleteSession(context.item.id!);
+ quickPick.items = getItems();
+ if (this.chatService.getSessions().length <= 1) {
+ quickPick.hide();
+ }
+ });
+
+ quickPick.onDidAccept(() => {
+ const selectedItem = quickPick.selectedItems[0];
+ defer.resolve(selectedItem);
+ quickPick.hide();
+ });
+
+ quickPick.onDidHide(() => defer.resolve(undefined));
+
+ quickPick.show();
+
+ return defer.promise;
+ }
+
+ protected withWidget(
+ widget: Widget | undefined = this.tryGetWidget(),
+ predicate: (output: ChatViewWidget) => boolean = () => true
+ ): boolean | false {
+ return widget instanceof ChatViewWidget ? predicate(widget) : false;
+ }
+
+ protected extractChatView(chatView: ChatViewWidget): void {
+ this.secondaryWindowHandler.moveWidgetToSecondaryWindow(chatView);
+ }
+
+ canExtractChatView(chatView: ChatViewWidget): boolean {
+ return !chatView.secondaryWindow;
+ }
+}
diff --git a/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts b/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts
new file mode 100644
index 0000000000000..285f2cadd42fc
--- /dev/null
+++ b/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts
@@ -0,0 +1,103 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource GmbH.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { bindContributionProvider, CommandContribution, MenuContribution } from '@theia/core';
+import { bindViewContribution, FrontendApplicationContribution, WidgetFactory } from '@theia/core/lib/browser';
+import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
+import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
+import { EditorManager } from '@theia/editor/lib/browser';
+import '../../src/browser/style/index.css';
+import { AIChatContribution } from './ai-chat-ui-contribution';
+import { AIChatInputWidget } from './chat-input-widget';
+import { ChatNodeToolbarActionContribution } from './chat-node-toolbar-action-contribution';
+import { ChatResponsePartRenderer } from './chat-response-part-renderer';
+import { CodePartRenderer, CommandPartRenderer, ErrorPartRenderer, HorizontalLayoutPartRenderer, MarkdownPartRenderer, ToolCallPartRenderer } from './chat-response-renderer';
+import {
+ AIEditorManager, AIEditorSelectionResolver,
+ GitHubSelectionResolver, TextFragmentSelectionResolver, TypeDocSymbolSelectionResolver
+} from './chat-response-renderer/ai-editor-manager';
+import { createChatViewTreeWidget } from './chat-tree-view';
+import { ChatViewTreeWidget } from './chat-tree-view/chat-view-tree-widget';
+import { ChatViewMenuContribution } from './chat-view-contribution';
+import { ChatViewLanguageContribution } from './chat-view-language-contribution';
+import { ChatViewWidget } from './chat-view-widget';
+import { ChatViewWidgetToolbarContribution } from './chat-view-widget-toolbar-contribution';
+import { EditorPreviewManager } from '@theia/editor-preview/lib/browser/editor-preview-manager';
+
+export default new ContainerModule((bind, _unbind, _isBound, rebind) => {
+ bindViewContribution(bind, AIChatContribution);
+ bind(TabBarToolbarContribution).toService(AIChatContribution);
+
+ bindContributionProvider(bind, ChatResponsePartRenderer);
+
+ bindChatViewWidget(bind);
+
+ bind(AIChatInputWidget).toSelf();
+ bind(WidgetFactory).toDynamicValue(({ container }) => ({
+ id: AIChatInputWidget.ID,
+ createWidget: () => container.get(AIChatInputWidget)
+ })).inSingletonScope();
+
+ bind(ChatViewTreeWidget).toDynamicValue(ctx =>
+ createChatViewTreeWidget(ctx.container)
+ );
+ bind(WidgetFactory).toDynamicValue(({ container }) => ({
+ id: ChatViewTreeWidget.ID,
+ createWidget: () => container.get(ChatViewTreeWidget)
+ })).inSingletonScope();
+
+ bind(ChatResponsePartRenderer).to(HorizontalLayoutPartRenderer).inSingletonScope();
+ bind(ChatResponsePartRenderer).to(ErrorPartRenderer).inSingletonScope();
+ bind(ChatResponsePartRenderer).to(MarkdownPartRenderer).inSingletonScope();
+ bind(ChatResponsePartRenderer).to(CodePartRenderer).inSingletonScope();
+ bind(ChatResponsePartRenderer).to(CommandPartRenderer).inSingletonScope();
+ bind(ChatResponsePartRenderer).to(ToolCallPartRenderer).inSingletonScope();
+ bind(ChatResponsePartRenderer).to(ErrorPartRenderer).inSingletonScope();
+ [CommandContribution, MenuContribution].forEach(serviceIdentifier =>
+ bind(serviceIdentifier).to(ChatViewMenuContribution).inSingletonScope()
+ );
+
+ bind(AIEditorManager).toSelf().inSingletonScope();
+ rebind(EditorManager).toService(AIEditorManager);
+ rebind(EditorPreviewManager).toService(AIEditorManager);
+
+ bindContributionProvider(bind, AIEditorSelectionResolver);
+ bind(AIEditorSelectionResolver).to(GitHubSelectionResolver).inSingletonScope();
+ bind(AIEditorSelectionResolver).to(TypeDocSymbolSelectionResolver).inSingletonScope();
+ bind(AIEditorSelectionResolver).to(TextFragmentSelectionResolver).inSingletonScope();
+
+ bind(ChatViewWidgetToolbarContribution).toSelf().inSingletonScope();
+ bind(TabBarToolbarContribution).toService(ChatViewWidgetToolbarContribution);
+
+ bind(FrontendApplicationContribution).to(ChatViewLanguageContribution).inSingletonScope();
+
+ bindContributionProvider(bind, ChatNodeToolbarActionContribution);
+});
+
+function bindChatViewWidget(bind: interfaces.Bind): void {
+ let chatViewWidget: ChatViewWidget | undefined;
+ bind(ChatViewWidget).toSelf();
+
+ bind(WidgetFactory).toDynamicValue(context => ({
+ id: ChatViewWidget.ID,
+ createWidget: () => {
+ if (chatViewWidget?.isDisposed !== false) {
+ chatViewWidget = context.container.get(ChatViewWidget);
+ }
+ return chatViewWidget;
+ }
+ })).inSingletonScope();
+}
diff --git a/packages/ai-chat-ui/src/browser/chat-input-widget.tsx b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx
new file mode 100644
index 0000000000000..332c5b3b04232
--- /dev/null
+++ b/packages/ai-chat-ui/src/browser/chat-input-widget.tsx
@@ -0,0 +1,247 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource GmbH.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+import { ChatAgent, ChatAgentService, ChatModel, ChatRequestModel } from '@theia/ai-chat';
+import { UntitledResourceResolver } from '@theia/core';
+import { ContextMenuRenderer, Message, ReactWidget } from '@theia/core/lib/browser';
+import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
+import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
+import * as React from '@theia/core/shared/react';
+import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
+import { CHAT_VIEW_LANGUAGE_EXTENSION } from './chat-view-language-contribution';
+import { IMouseEvent } from '@theia/monaco-editor-core';
+
+type Query = (query: string) => Promise;
+type Cancel = (requestModel: ChatRequestModel) => void;
+
+@injectable()
+export class AIChatInputWidget extends ReactWidget {
+ public static ID = 'chat-input-widget';
+ static readonly CONTEXT_MENU = ['chat-input-context-menu'];
+
+ @inject(ChatAgentService)
+ protected readonly agentService: ChatAgentService;
+
+ @inject(MonacoEditorProvider)
+ protected readonly editorProvider: MonacoEditorProvider;
+
+ @inject(UntitledResourceResolver)
+ protected readonly untitledResourceResolver: UntitledResourceResolver;
+
+ @inject(ContextMenuRenderer)
+ protected readonly contextMenuRenderer: ContextMenuRenderer;
+
+ protected isEnabled = false;
+
+ private _onQuery: Query;
+ set onQuery(query: Query) {
+ this._onQuery = query;
+ }
+ private _onCancel: Cancel;
+ set onCancel(cancel: Cancel) {
+ this._onCancel = cancel;
+ }
+ private _chatModel: ChatModel;
+ set chatModel(chatModel: ChatModel) {
+ this._chatModel = chatModel;
+ this.update();
+ }
+
+ @postConstruct()
+ protected init(): void {
+ this.id = AIChatInputWidget.ID;
+ this.title.closable = false;
+ this.update();
+ }
+ protected override onActivateRequest(msg: Message): void {
+ super.onActivateRequest(msg);
+ this.node.focus({ preventScroll: true });
+ }
+
+ protected getChatAgents(): ChatAgent[] {
+ return this.agentService.getAgents();
+ }
+
+ protected render(): React.ReactNode {
+ return (
+
+ );
+ }
+
+ public setEnabled(enabled: boolean): void {
+ this.isEnabled = enabled;
+ this.update();
+ }
+
+ protected handleContextMenu(event: IMouseEvent): void {
+ this.contextMenuRenderer.render({
+ menuPath: AIChatInputWidget.CONTEXT_MENU,
+ anchor: { x: event.posx, y: event.posy },
+ });
+ event.preventDefault();
+ }
+
+}
+
+interface ChatInputProperties {
+ onCancel: (requestModel: ChatRequestModel) => void;
+ onQuery: (query: string) => void;
+ isEnabled?: boolean;
+ chatModel: ChatModel;
+ getChatAgents: () => ChatAgent[];
+ editorProvider: MonacoEditorProvider;
+ untitledResourceResolver: UntitledResourceResolver;
+ contextMenuCallback: (event: IMouseEvent) => void;
+}
+const ChatInput: React.FunctionComponent = (props: ChatInputProperties) => {
+
+ const [inProgress, setInProgress] = React.useState(false);
+ // eslint-disable-next-line no-null/no-null
+ const editorContainerRef = React.useRef(null);
+ // eslint-disable-next-line no-null/no-null
+ const placeholderRef = React.useRef(null);
+ const editorRef = React.useRef(undefined);
+ const allRequests = props.chatModel.getRequests();
+ const lastRequest = allRequests.length === 0 ? undefined : allRequests[allRequests.length - 1];
+
+ const createInputElement = async () => {
+ const resource = await props.untitledResourceResolver.createUntitledResource('', CHAT_VIEW_LANGUAGE_EXTENSION);
+ const editor = await props.editorProvider.createInline(resource.uri, editorContainerRef.current!, {
+ language: CHAT_VIEW_LANGUAGE_EXTENSION,
+ // Disable code lens, inlay hints and hover support to avoid console errors from other contributions
+ codeLens: false,
+ inlayHints: { enabled: 'off' },
+ hover: { enabled: false },
+ autoSizing: true,
+ scrollBeyondLastLine: false,
+ scrollBeyondLastColumn: 0,
+ minHeight: 1,
+ fontFamily: 'var(--theia-ui-font-family)',
+ fontSize: 13,
+ cursorWidth: 1,
+ maxHeight: -1,
+ scrollbar: { horizontal: 'hidden' },
+ automaticLayout: true,
+ lineNumbers: 'off',
+ lineHeight: 20,
+ padding: { top: 8 },
+ suggest: {
+ showIcons: true,
+ showSnippets: false,
+ showWords: false,
+ showStatusBar: false,
+ insertMode: 'replace',
+ },
+ bracketPairColorization: { enabled: false },
+ wrappingStrategy: 'advanced',
+ stickyScroll: { enabled: false },
+ });
+
+ editor.getControl().onDidChangeModelContent(() => {
+ layout();
+ });
+
+ editor.getControl().onContextMenu(e =>
+ props.contextMenuCallback(e.event)
+ );
+
+ editorRef.current = editor;
+ };
+
+ React.useEffect(() => {
+ createInputElement();
+ return () => {
+ if (editorRef.current) {
+ editorRef.current.dispose();
+ }
+ };
+ }, []);
+
+ React.useEffect(() => {
+ const listener = lastRequest?.response.onDidChange(() => {
+ if (lastRequest.response.isCanceled || lastRequest.response.isComplete || lastRequest.response.isError) {
+ setInProgress(false);
+ }
+ });
+ return () => listener?.dispose();
+ }, [lastRequest]);
+
+ function submit(value: string): void {
+ setInProgress(true);
+ props.onQuery(value);
+ if (editorRef.current) {
+ editorRef.current.document.textEditorModel.setValue('');
+ }
+ };
+
+ function layout(): void {
+ if (editorRef.current === undefined) {
+ return;
+ }
+ const hiddenClass = 'hidden';
+ const editor = editorRef.current;
+ if (editor.document.textEditorModel.getValue().length > 0) {
+ placeholderRef.current?.classList.add(hiddenClass);
+ } else {
+ placeholderRef.current?.classList.remove(hiddenClass);
+ }
+ }
+
+ const onKeyDown = React.useCallback((event: React.KeyboardEvent) => {
+ if (!props.isEnabled) {
+ return;
+ }
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+ submit(editorRef.current?.document.textEditorModel.getValue() || '');
+ }
+ }, [props.isEnabled]);
+
+ return
;
+};
diff --git a/packages/ai-chat-ui/src/browser/chat-node-toolbar-action-contribution.ts b/packages/ai-chat-ui/src/browser/chat-node-toolbar-action-contribution.ts
new file mode 100644
index 0000000000000..1a52d4867e185
--- /dev/null
+++ b/packages/ai-chat-ui/src/browser/chat-node-toolbar-action-contribution.ts
@@ -0,0 +1,63 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource GmbH.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+import { RequestNode, ResponseNode } from './chat-tree-view';
+
+export interface ChatNodeToolbarAction {
+ /**
+ * The command to execute when the item is selected. The handler will receive the `RequestNode` or `ResponseNode` as first argument.
+ */
+ commandId: string;
+ /**
+ * Icon class name(s) for the item (e.g. 'codicon codicon-feedback').
+ */
+ icon: string;
+ /**
+ * Priority among the items. Can be negative. The smaller the number the left-most the item will be placed in the toolbar. It is `0` by default.
+ */
+ priority?: number;
+ /**
+ * Optional tooltip for the item.
+ */
+ tooltip?: string;
+}
+
+/**
+ * Clients implement this interface if they want to contribute to the toolbar of chat nodes.
+ *
+ * ### Example
+ * ```ts
+ * bind(ChatNodeToolbarActionContribution).toDynamicValue(context => ({
+ * getToolbarActions: (args: RequestNode | ResponseNode) => {
+ * if (isResponseNode(args)) {
+ * return [{
+ * commandId: 'core.about',
+ * icon: 'codicon codicon-feedback',
+ * tooltip: 'Show about dialog on response nodes'
+ * }];
+ * } else {
+ * return [];
+ * }
+ * }
+ * }));
+ * ```
+ */
+export const ChatNodeToolbarActionContribution = Symbol('ChatNodeToolbarActionContribution');
+export interface ChatNodeToolbarActionContribution {
+ /**
+ * Returns the toolbar actions for the given node.
+ */
+ getToolbarActions(node: RequestNode | ResponseNode): ChatNodeToolbarAction[];
+}
diff --git a/packages/ai-chat-ui/src/browser/chat-response-part-renderer.ts b/packages/ai-chat-ui/src/browser/chat-response-part-renderer.ts
new file mode 100644
index 0000000000000..17743805aa59d
--- /dev/null
+++ b/packages/ai-chat-ui/src/browser/chat-response-part-renderer.ts
@@ -0,0 +1,25 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource GmbH.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { ChatResponseContent } from '@theia/ai-chat/lib/common';
+import { ReactNode } from '@theia/core/shared/react';
+import { ResponseNode } from './chat-tree-view/chat-view-tree-widget';
+
+export const ChatResponsePartRenderer = Symbol('ChatResponsePartRenderer');
+export interface ChatResponsePartRenderer {
+ canHandle(response: ChatResponseContent): number;
+ render(response: T, parentNode: ResponseNode): ReactNode;
+}
diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/ai-editor-manager.ts b/packages/ai-chat-ui/src/browser/chat-response-renderer/ai-editor-manager.ts
new file mode 100644
index 0000000000000..85f0fbeb95824
--- /dev/null
+++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/ai-editor-manager.ts
@@ -0,0 +1,183 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource GmbH.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import { CancellationToken, ContributionProvider, Prioritizeable, RecursivePartial, URI } from '@theia/core';
+import { inject, injectable, named } from '@theia/core/shared/inversify';
+import { EditorOpenerOptions, EditorWidget, Range } from '@theia/editor/lib/browser';
+
+import { EditorPreviewManager } from '@theia/editor-preview/lib/browser/editor-preview-manager';
+import { DocumentSymbol } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
+import { TextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel';
+import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
+import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
+import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
+import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter';
+
+/** Regex to match GitHub-style position and range declaration with line (L) and column (C) */
+export const LOCATION_REGEX = /#L(\d+)?(?:C(\d+))?(?:-L(\d+)?(?:C(\d+))?)?$/;
+
+export const AIEditorSelectionResolver = Symbol('AIEditorSelectionResolver');
+export interface AIEditorSelectionResolver {
+ /**
+ * The priority of the resolver. A higher value resolver will be called before others.
+ */
+ priority?: number;
+ resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise | undefined>
+}
+
+@injectable()
+export class GitHubSelectionResolver implements AIEditorSelectionResolver {
+ priority = 100;
+
+ async resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise | undefined> {
+ if (!uri) {
+ return;
+ }
+ // We allow the GitHub syntax of selecting a range in markdown 'L1', 'L1-L2' 'L1-C1_L2-C2' (starting at line 1 and column 1)
+ const match = uri?.toString().match(LOCATION_REGEX);
+ if (!match) {
+ return;
+ }
+ // we need to adapt the position information from one-based (in GitHub) to zero-based (in Theia)
+ const startLine = match[1] ? parseInt(match[1], 10) - 1 : undefined;
+ // if no start column is given, we assume the start of the line
+ const startColumn = match[2] ? parseInt(match[2], 10) - 1 : 0;
+ const endLine = match[3] ? parseInt(match[3], 10) - 1 : undefined;
+ // if no end column is given, we assume the end of the line
+ const endColumn = match[4] ? parseInt(match[4], 10) - 1 : endLine ? widget.editor.document.getLineMaxColumn(endLine) : undefined;
+
+ return {
+ start: { line: startLine, character: startColumn },
+ end: { line: endLine, character: endColumn }
+ };
+ }
+}
+
+@injectable()
+export class TypeDocSymbolSelectionResolver implements AIEditorSelectionResolver {
+ priority = 50;
+
+ @inject(MonacoToProtocolConverter) protected readonly m2p: MonacoToProtocolConverter;
+
+ async resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise | undefined> {
+ if (!uri) {
+ return;
+ }
+ const editor = MonacoEditor.get(widget);
+ const monacoEditor = editor?.getControl();
+ if (!monacoEditor) {
+ return;
+ }
+ const symbolPath = this.findSymbolPath(uri);
+ if (!symbolPath) {
+ return;
+ }
+ const textModel = monacoEditor.getModel() as unknown as TextModel;
+ if (!textModel) {
+ return;
+ }
+
+ // try to find the symbol through the document symbol provider
+ // support referencing nested symbols by separating a dot path similar to TypeDoc
+ for (const provider of StandaloneServices.get(ILanguageFeaturesService).documentSymbolProvider.ordered(textModel)) {
+ const symbols = await provider.provideDocumentSymbols(textModel, CancellationToken.None);
+ const match = this.findSymbolByPath(symbols ?? [], symbolPath);
+ if (match) {
+ return this.m2p.asRange(match.selectionRange);
+ }
+ }
+ }
+
+ protected findSymbolPath(uri: URI): string[] | undefined {
+ return uri.fragment.split('.');
+ }
+
+ protected findSymbolByPath(symbols: DocumentSymbol[], symbolPath: string[]): DocumentSymbol | undefined {
+ if (!symbols || symbolPath.length === 0) {
+ return undefined;
+ }
+ let matchedSymbol: DocumentSymbol | undefined = undefined;
+ let currentSymbols = symbols;
+ for (const part of symbolPath) {
+ matchedSymbol = currentSymbols.find(symbol => symbol.name === part);
+ if (!matchedSymbol) {
+ return undefined;
+ }
+ currentSymbols = matchedSymbol.children || [];
+ }
+ return matchedSymbol;
+ }
+}
+
+@injectable()
+export class TextFragmentSelectionResolver implements AIEditorSelectionResolver {
+ async resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise | undefined> {
+ if (!uri) {
+ return;
+ }
+ const fragment = this.findFragment(uri);
+ if (!fragment) {
+ return;
+ }
+ const matches = widget.editor.document.findMatches?.({ isRegex: false, matchCase: false, matchWholeWord: false, searchString: fragment }) ?? [];
+ if (matches.length > 0) {
+ return {
+ start: {
+ line: matches[0].range.start.line - 1,
+ character: matches[0].range.start.character - 1
+ },
+ end: {
+ line: matches[0].range.end.line - 1,
+ character: matches[0].range.end.character - 1
+ }
+ };
+ }
+ }
+
+ protected findFragment(uri: URI): string | undefined {
+ return uri.fragment;
+ }
+}
+
+@injectable()
+export class AIEditorManager extends EditorPreviewManager {
+ @inject(ContributionProvider) @named(AIEditorSelectionResolver)
+ protected readonly resolvers: ContributionProvider;
+
+ protected override async revealSelection(widget: EditorWidget, options: EditorOpenerOptions = {}, uri?: URI): Promise {
+ if (!options.selection) {
+ options.selection = await this.resolveSelection(options, widget, uri);
+ }
+ super.revealSelection(widget, options, uri);
+ }
+
+ protected async resolveSelection(options: EditorOpenerOptions, widget: EditorWidget, uri: URI | undefined): Promise | undefined> {
+ if (!options.selection) {
+ const orderedResolvers = Prioritizeable.prioritizeAllSync(this.resolvers.getContributions(), resolver => resolver.priority ?? 1);
+ for (const linkResolver of orderedResolvers) {
+ try {
+ const selection = await linkResolver.value.resolveSelection(widget, options, uri);
+ if (selection) {
+ return selection;
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ }
+ return undefined;
+ }
+}
diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/code-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/code-part-renderer.tsx
new file mode 100644
index 0000000000000..8802158eb236c
--- /dev/null
+++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/code-part-renderer.tsx
@@ -0,0 +1,207 @@
+// *****************************************************************************
+// Copyright (C) 2024 EclipseSource GmbH.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License v. 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0.
+//
+// This Source Code may also be made available under the following Secondary
+// Licenses when the conditions for such availability set forth in the Eclipse
+// Public License v. 2.0 are satisfied: GNU General Public License, version 2
+// with the GNU Classpath Exception which is available at
+// https://www.gnu.org/software/classpath/license.html.
+//
+// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
+// *****************************************************************************
+
+import {
+ ChatResponseContent,
+ CodeChatResponseContent,
+} from '@theia/ai-chat/lib/common';
+import { UntitledResourceResolver, URI } from '@theia/core';
+import { ContextMenuRenderer, TreeNode } from '@theia/core/lib/browser';
+import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import * as React from '@theia/core/shared/react';
+import { ReactNode } from '@theia/core/shared/react';
+import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
+import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
+import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
+import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
+import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
+import { ChatResponsePartRenderer } from '../chat-response-part-renderer';
+import { ChatViewTreeWidget, ResponseNode } from '../chat-tree-view/chat-view-tree-widget';
+import { IMouseEvent } from '@theia/monaco-editor-core';
+
+@injectable()
+export class CodePartRenderer
+ implements ChatResponsePartRenderer {
+
+ @inject(ClipboardService)
+ protected readonly clipboardService: ClipboardService;
+ @inject(EditorManager)
+ protected readonly editorManager: EditorManager;
+ @inject(UntitledResourceResolver)
+ protected readonly untitledResourceResolver: UntitledResourceResolver;
+ @inject(MonacoEditorProvider)
+ protected readonly editorProvider: MonacoEditorProvider;
+ @inject(MonacoLanguages)
+ protected readonly languageService: MonacoLanguages;
+ @inject(ContextMenuRenderer)
+ protected readonly contextMenuRenderer: ContextMenuRenderer;
+
+ canHandle(response: ChatResponseContent): number {
+ if (CodeChatResponseContent.is(response)) {
+ return 10;
+ }
+ return -1;
+ }
+
+ render(response: CodeChatResponseContent, parentNode: ResponseNode): ReactNode {
+ const language = response.language ? this.languageService.getExtension(response.language) : undefined;
+
+ return (
+